retab
uifile-viewersource: ok · refreshed
File Viewer
One viewer for any file: detects the type and lazy-loads the matching renderer (PDF, DOCX, XLSX, PPTX, CSV, images) plus inline, virtualized text/code/JSON, Markdown (sandboxed), and HTML (sandboxed). Only the needed parser is ever downloaded.
npx shadcn add "https://ui.retab.com/r/file-viewer.json"free
Visual style
terminal-mono
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lighticonslucide-react
ui-primitives@radix-ui/react-slot@^1.1.1
Registry dependencies
@retab/utilsbutton@retab/pdf-viewer@retab/docx-viewer@retab/image-viewer@retab/pptx-viewer@retab/xlsx-viewer@retab/csv-viewer@retab/code-viewer@retab/text-viewer-chenglou@retab/markdown-viewer@retab/viewer-controls@retab/skeletonseparator@retab/spinner@retab/email-viewer
Files49 · source cached 10/8/2026, 10:21:13 AM
registry/new-york-v4/ui/file-viewer.tsx
"use client";
export type { ViewerSource } from "./file-viewer-core";
export {
FileViewerContent,
FileViewerInset,
FileViewerLegend,
FileViewerViewport,
type FileViewerContentProps,
type FileViewerInsetProps,
type FileViewerLegendProps,
type FileViewerViewportProps,
} from "./file-viewer-layout";
export {
FileViewerSidebar,
FileViewerSidebarContent,
FileViewerSidebarFooter,
FileViewerSidebarHeader,
FileViewerSidebarRail,
FileViewerSidebarSection,
FileViewerSidebarSectionAction,
FileViewerSidebarSectionContent,
FileViewerSidebarSectionHeader,
FileViewerSidebarSectionTitle,
FileViewerSidebarSeparator,
type FileViewerSidebarProps,
type FileViewerSidebarContentProps,
type FileViewerSidebarFooterProps,
type FileViewerSidebarHeaderProps,
type FileViewerSidebarRailProps,
type FileViewerSidebarSectionActionProps,
type FileViewerSidebarSectionContentProps,
type FileViewerSidebarSectionHeaderProps,
type FileViewerSidebarSectionProps,
type FileViewerSidebarSectionTitleProps,
type FileViewerSidebarSeparatorProps,
} from "./file-viewer-sidebar";
export {
FileViewerDocument,
type FileViewerDocumentProps,
} from "./file-viewer-document";
export {
FileViewerControls,
FileViewerHeader,
FileViewerMeta,
FileViewerSidebarTrigger,
FileViewerTitle,
type FileViewerControlsProps,
type FileViewerHeaderProps,
type FileViewerMetaProps,
type FileViewerSidebarTriggerProps,
type FileViewerTitleProps,
} from "./file-viewer-header";
export {
FileViewerEmptyState,
FileViewerErrorState,
FileViewerLoadingState,
FileViewerUnavailableState,
FileViewerUnsupportedState,
type FileViewerErrorStateProps,
type FileViewerStateProps,
type FileViewerUnsupportedStateProps,
} from "./file-viewer-state";
export { useFileViewerResource } from "./file-viewer-resource-state";
export {
FileViewerPreview,
type FileViewerPreviewProps,
} from "./file-viewer-preview";
export {
useFileViewerSidebar,
type FileViewerHeaderMode,
type FileViewerSidebarValue,
} from "./file-viewer-context";
export {
FileViewer,
type FileViewerLayout,
type FileViewerProps,
} from "./file-viewer-shell";
export {
FileViewerProvider,
type FileViewerProviderProps,
} from "./file-viewer-provider";
registry/new-york-v4/ui/file-viewer-accessibility.ts
"use client";
import type { FileViewerSidebarCollapsible } from "./file-viewer-context";
import type { FileViewerElements } from "./file-viewer-elements";
export type FileViewerSidebarAccessibilityProps = {
"aria-hidden"?: true;
inert?: true;
};
export type FileViewerSidebarTriggerAccessibilityProps = {
"aria-controls"?: string;
"aria-disabled"?: true;
"aria-expanded"?: boolean;
"aria-label": string;
};
const visibleSidebarAccessibilityProps = {};
const hiddenSidebarAccessibilityProps = {
"aria-hidden": true,
inert: true,
} as const;
export function resolveFileViewerSidebarAccessibilityProps({
collapsible,
deferInert,
isSidebarInteractive,
}: {
collapsible: FileViewerSidebarCollapsible;
deferInert?: boolean;
isSidebarInteractive: boolean;
}): FileViewerSidebarAccessibilityProps {
if (collapsible === "none" || isSidebarInteractive) {
return visibleSidebarAccessibilityProps;
}
if (deferInert) {
return {
"aria-hidden": true,
};
}
return hiddenSidebarAccessibilityProps;
}
export function resolveFileViewerSidebarTriggerAccessibilityProps({
ariaLabel,
canToggleSidebar,
isDisabled,
isSidebarInteractive,
sidebarId,
}: {
ariaLabel: string;
canToggleSidebar: boolean;
isDisabled: boolean;
isSidebarInteractive: boolean;
sidebarId: string;
}): FileViewerSidebarTriggerAccessibilityProps {
return {
"aria-controls": canToggleSidebar ? sidebarId : undefined,
"aria-disabled": isDisabled ? true : undefined,
// Deliberately tracks interactivity, not the requested state: while the
// panel slides open it is still inert, and announcing "expanded" would
// point assistive tech at a region that cannot yet be used.
"aria-expanded": canToggleSidebar ? isSidebarInteractive : undefined,
"aria-label": ariaLabel,
};
}
export function restoreFileViewerSidebarFocusOnClose({
elements,
isSidebarInteractive,
previousIsSidebarInteractive,
}: {
elements: FileViewerElements;
isSidebarInteractive: boolean;
previousIsSidebarInteractive: boolean;
}) {
if (isSidebarInteractive || !previousIsSidebarInteractive) return;
if (!elements.sidebarElement || !elements.sidebarTriggerElement) return;
const ownerWindow = elements.sidebarElement.ownerDocument.defaultView;
const activeElement = elements.sidebarElement.ownerDocument.activeElement;
if (
ownerWindow &&
activeElement instanceof ownerWindow.HTMLElement &&
elements.sidebarElement.contains(activeElement)
) {
elements.sidebarTriggerElement.focus({ preventScroll: true });
}
}
registry/new-york-v4/ui/file-viewer-layout.tsx
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
import {
useOptionalFileViewerShellStatic,
useFileViewerShellStatic,
} from "./file-viewer-context";
import {
FileViewerDocumentFrameProvider,
type FileViewerDocumentFrameState,
} from "./file-viewer-renderer-frame";
import { type FileViewerDocumentAlign } from "./file-viewer-renderer-contract";
import { useStableElementSize } from "./viewer-measurement";
const FileViewerContentContext = React.createContext(false);
const FileViewerInsetContext = React.createContext(false);
const FileViewerViewportContext = React.createContext(false);
export function useIsInsideFileViewerContent(): boolean {
return React.useContext(FileViewerContentContext);
}
export function useIsInsideFileViewerInset(): boolean {
return React.useContext(FileViewerInsetContext);
}
export function useIsInsideFileViewerViewport(): boolean {
return React.useContext(FileViewerViewportContext);
}
export type FileViewerContentProps = React.ComponentProps<"div">;
export type FileViewerInsetProps = React.ComponentProps<"div"> & {
align?: FileViewerDocumentAlign;
documentFrameClassName?: string;
documentFrameStyle?: React.CSSProperties;
maxInlineSize?: React.CSSProperties["maxInlineSize"];
};
export type FileViewerLegendProps = React.ComponentProps<"div">;
export type FileViewerViewportProps = React.ComponentProps<"div">;
export function FileViewerContent({
className,
...props
}: FileViewerContentProps) {
const shell = useFileViewerShellStatic("FileViewerContent");
return (
<FileViewerContentContext.Provider value={true}>
<div
data-file-viewer-sidebar-mode={shell.mode}
data-file-viewer-sidebar-side={shell.side}
data-slot="file-viewer-content"
className={cn(
"relative flex min-h-0 min-w-0 flex-1 overflow-hidden",
className,
)}
{...props}
/>
</FileViewerContentContext.Provider>
);
}
export function FileViewerLegend({
className,
...props
}: FileViewerLegendProps) {
return (
<div
data-slot="file-viewer-legend"
className={cn("min-w-0 flex-shrink-0 border-b", className)}
{...props}
/>
);
}
export function FileViewerInset({
align = "start",
children,
className,
documentFrameClassName,
documentFrameStyle,
maxInlineSize,
...props
}: FileViewerInsetProps) {
const shell = useOptionalFileViewerShellStatic();
const hasAnimatedFileViewerWidth =
shell?.mode === "inline" && shell.canToggleSidebar;
const documentFrame = useMeasuredFileViewerDocumentFrame({
enabled: !hasAnimatedFileViewerWidth,
});
const frame = React.useMemo<FileViewerDocumentFrameState>(
() => ({
align,
element: documentFrame.element,
inlineSize: documentFrame.inlineSize,
}),
[align, documentFrame.element, documentFrame.inlineSize],
);
return (
<FileViewerInsetContext.Provider value={true}>
<FileViewerDocumentFrameProvider value={frame}>
<div
data-slot="file-viewer-inset"
className={cn(
"relative flex h-full min-h-0 min-w-0 flex-1 flex-col overflow-hidden",
className,
)}
{...props}
>
<div
ref={documentFrame.setElement}
data-slot="file-viewer-document-frame"
className={cn(
"[container-type:inline-size] relative flex h-full min-h-0 w-full min-w-0 flex-1 flex-col",
getDocumentFrameAlignClass(align),
documentFrameClassName,
)}
style={
{
maxInlineSize,
...documentFrameStyle,
} as React.CSSProperties
}
>
{children}
</div>
</div>
</FileViewerDocumentFrameProvider>
</FileViewerInsetContext.Provider>
);
}
export const FileViewerViewport = React.forwardRef<
HTMLDivElement,
FileViewerViewportProps
>(function FileViewerViewport({ className, style, ...props }, ref) {
const isInsideInset = useIsInsideFileViewerInset();
const setElement = React.useCallback(
(element: HTMLDivElement | null) => {
if (typeof ref === "function") {
ref(element);
} else if (ref) {
ref.current = element;
}
},
[ref],
);
if (process.env.NODE_ENV !== "production" && !isInsideInset) {
throw new Error(
"FileViewerViewport must be rendered inside FileViewerInset.",
);
}
return (
<FileViewerViewportContext.Provider value={true}>
<div
ref={setElement}
data-slot="file-viewer-viewport"
className={cn(
"relative flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden",
className,
)}
style={
{
...style,
} as React.CSSProperties
}
{...props}
/>
</FileViewerViewportContext.Provider>
);
});
function useMeasuredFileViewerDocumentFrame({ enabled }: { enabled: boolean }) {
const size = useStableElementSize<HTMLDivElement>({
enabled,
});
return React.useMemo(
() => ({
element: size.element,
inlineSize: size.width,
setElement: size.setElement,
}),
[size.element, size.setElement, size.width],
);
}
function getDocumentFrameAlignClass(align: FileViewerDocumentAlign) {
switch (align) {
case "center":
return "mx-auto";
case "end":
return "ml-auto";
case "start":
return "mr-auto";
}
}
registry/new-york-v4/ui/file-viewer-context.tsx
"use client";
import * as React from "react";
import type { FileCategory } from "@/lib/viewer-source";
import type { FileViewerElementRegistry } from "./file-viewer-elements";
import type { FileViewerMotionKernel } from "./file-viewer-motion-kernel";
export type FileViewerSidebarMode = "inline" | "overlay";
export type FileViewerSidebarRequestedMode = "auto" | FileViewerSidebarMode;
export type FileViewerSidebarState = "expanded" | "collapsed";
export type FileViewerSidebarSide = "left" | "right";
export type FileViewerSidebarCollapsible = "offcanvas" | "none";
export type FileViewerHeaderMode = "inline" | "outlets";
export const DEFAULT_FILE_VIEWER_SIDEBAR_WIDTH = "10rem";
export type FileViewerSetSidebarOpen = (
value: boolean | ((isSidebarOpen: boolean) => boolean),
) => void;
export type FileViewerSidebarOpenProps = {
defaultOpen?: boolean;
onOpenChange?: (open: boolean) => void;
open?: boolean;
};
export type FileViewerContextValue = {
headerMode: FileViewerHeaderMode;
hasHeaderOutlets: boolean;
isInsideFileViewer: boolean;
resourceCategory: FileCategory;
sidebarOpenProps: FileViewerSidebarOpenProps;
};
export type FileViewerSidebarRegistration = {
collapsible: FileViewerSidebarCollapsible;
id: string;
side: FileViewerSidebarSide;
width: string;
widthPixels: number;
};
export type FileViewerSidebarValue = {
canToggleSidebar: boolean;
isSidebarInteractive: boolean;
isSidebarOpen: boolean;
mode: FileViewerSidebarMode;
side: FileViewerSidebarSide;
sidebarId: string;
sidebarState: FileViewerSidebarState;
setSidebarOpen: FileViewerSetSidebarOpen;
toggleSidebar: () => void;
};
export type FileViewerShellStaticContextValue = {
canToggleSidebar: boolean;
collapsible: FileViewerSidebarCollapsible;
elementRegistry: FileViewerElementRegistry;
mode: FileViewerSidebarMode;
motionDurationMs: number;
motionKernel: FileViewerMotionKernel;
registerSidebar: (registration: FileViewerSidebarRegistration) => () => void;
rootId: string;
setSidebarOpen: FileViewerSetSidebarOpen;
side: FileViewerSidebarSide;
sidebarId: string;
sidebarWidth: string;
toggleSidebar: () => void;
};
export type FileViewerSidebarDynamicContextValue = {
isSidebarInteractive: boolean;
isSidebarOpen: boolean;
isSidebarTransitioning: boolean;
sidebarState: FileViewerSidebarState;
};
export const FileViewerContext = React.createContext<FileViewerContextValue>({
headerMode: "inline",
hasHeaderOutlets: false,
isInsideFileViewer: false,
resourceCategory: "unsupported",
sidebarOpenProps: {},
});
export const FileViewerShellStaticContext =
React.createContext<FileViewerShellStaticContextValue | null>(null);
export const FileViewerSidebarDynamicContext =
React.createContext<FileViewerSidebarDynamicContextValue | null>(null);
export function useFileViewerContext() {
return React.useContext(FileViewerContext);
}
export function useOptionalFileViewerShellStatic() {
return React.useContext(FileViewerShellStaticContext);
}
export function useFileViewerShellStatic(consumer: string) {
const context = React.useContext(FileViewerShellStaticContext);
if (!context) {
throw new Error(`${consumer} must be rendered inside FileViewer.`);
}
return context;
}
export function useOptionalFileViewerShell() {
const staticContext = React.useContext(FileViewerShellStaticContext);
const sidebarContext = React.useContext(FileViewerSidebarDynamicContext);
return React.useMemo(
() =>
staticContext && sidebarContext
? { ...staticContext, ...sidebarContext }
: null,
[sidebarContext, staticContext],
);
}
export function useFileViewerShell(consumer: string) {
const context = useOptionalFileViewerShell();
if (!context) {
throw new Error(`${consumer} must be rendered inside FileViewer.`);
}
return context;
}
export function useFileViewerSidebar(): FileViewerSidebarValue {
const fileViewerContext = React.useContext(FileViewerContext);
const shellContext = useOptionalFileViewerShell();
if (!fileViewerContext.isInsideFileViewer || !shellContext) {
throw new Error("useFileViewerSidebar must be used within FileViewer.");
}
return shellContext;
}
registry/new-york-v4/ui/file-viewer-core.ts
import {
detectCategory,
extensionOf,
extractName,
resolveViewerDescriptor,
textPayloadKey,
type FileCategory,
type ViewerDescriptor,
type ViewerSource,
} from "@/lib/viewer-source";
export type { FileCategory, ViewerSource };
export type FileViewerFallbackSize = { width: number; height: number };
export interface FileViewerCoreProps {
source: ViewerSource;
category?: FileCategory;
className?: string;
/** Intrinsic first-frame size for image/TIFF loading skeletons. */
fallbackFrameSize?: FileViewerFallbackSize;
/** Intrinsic first-slide size for PPTX loading skeletons. */
fallbackSlideSize?: FileViewerFallbackSize;
isolateStyles?: boolean;
}
export type FileDescriptor = ViewerDescriptor;
export function resolveFileDescriptor({
source,
category,
}: FileViewerCoreProps): FileDescriptor {
return resolveViewerDescriptor({
source,
category,
});
}
export function descriptorResetKey(descriptor: FileDescriptor): string {
return [
descriptorIdentityResetKey(descriptor),
descriptor.displayName,
descriptor.mimeType ?? "",
descriptor.category,
].join("\u0000");
}
function descriptorIdentityResetKey(descriptor: FileDescriptor): string {
if (
descriptor.source.kind === "text" &&
descriptor.source.identityKey == null
) {
return textPayloadKey(descriptor.source.text);
}
return descriptor.identityKey;
}
export function isProseTextDescriptor(descriptor: FileDescriptor): boolean {
if (descriptor.category !== "text") return false;
const extension = extensionOf(descriptor.fileName);
if (extension === "txt" || extension === "text") return true;
return !extension && descriptor.mimeType?.toLowerCase() === "text/plain";
}
export { detectCategory, extensionOf, extractName };
registry/new-york-v4/ui/file-viewer-document.tsx
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
import { FileErrorBoundary, ViewerFallback } from "./file-viewer-fallback";
import { useFileViewerRequiredResourceState } from "./file-viewer-resource-state";
import { FileViewerRoute } from "./file-viewer-route";
import { useIsInsideFileViewerViewport } from "./file-viewer-layout";
export type FileViewerDocumentProps = {
className?: string;
controls?: boolean;
};
export function FileViewerDocument({
className,
controls = false,
}: FileViewerDocumentProps) {
const isInsideViewport = useIsInsideFileViewerViewport();
if (process.env.NODE_ENV !== "production" && !isInsideViewport) {
throw new Error(
"FileViewerDocument must be rendered inside FileViewerViewport.",
);
}
return (
<FileViewerDocumentContent
className={cn("h-full", className)}
controls={controls}
/>
);
}
function FileViewerDocumentContent({
className,
controls,
}: Required<Pick<FileViewerDocumentProps, "controls">> &
Pick<FileViewerDocumentProps, "className">) {
const {
descriptor,
descriptorKey,
descriptorSignal,
fallbackFrameSize,
fallbackSlideSize,
isClient,
isolateStyles,
resource,
} = useFileViewerRequiredResourceState();
const fallback = (
<ViewerFallback
resource={resource}
className={className}
bare
controls={controls}
fallbackFrameSize={fallbackFrameSize}
fallbackSlideSize={fallbackSlideSize}
/>
);
if (!isClient) return fallback;
return (
<FileErrorBoundary
key={descriptorKey}
descriptor={descriptor}
resource={resource}
className={className}
resetKey={descriptorKey}
showDownload={controls}
>
<React.Suspense fallback={fallback}>
<FileViewerRoute
className={className}
controls={controls}
descriptor={descriptor}
descriptorSignal={descriptorSignal}
fallbackFrameSize={fallbackFrameSize}
fallbackSlideSize={fallbackSlideSize}
frame="none"
isolateStyles={isolateStyles}
resource={resource}
/>
</React.Suspense>
</FileErrorBoundary>
);
}
registry/new-york-v4/ui/file-viewer-elements.ts
"use client";
import type {
FileViewerDocumentSurface,
FileViewerMotionKernel,
} from "./file-viewer-motion-kernel";
import type { FileViewerMotionFrame } from "./file-viewer-motion-plan";
export const FILE_VIEWER_BEFORE_LAYOUT_MOTION_EVENT =
"file-viewer:before-layout-motion";
// The kernel dispatches the before-layout-motion event as a CustomEvent whose
// detail is its LIVE interactive frame, so renderers can capture pre-commit
// anchors against what is actually on screen (a mid-flight retarget sees the
// settled layout plus the in-flight transform).
export function readFileViewerBeforeLayoutMotionFrame(
event: Event,
): FileViewerMotionFrame | null {
if (!(event instanceof CustomEvent)) return null;
const detail: unknown = event.detail;
return detail != null && typeof detail === "object"
? (detail as FileViewerMotionFrame)
: null;
}
export type FileViewerElements = {
documentSurfaceElement: HTMLElement | null;
getDocumentSurfaceMotionProbeElement: (() => HTMLElement | null) | null;
sidebarElement: HTMLElement | null;
sidebarGapElement: HTMLDivElement | null;
sidebarTriggerElement: HTMLElement | null;
viewerShellElement: HTMLDivElement | null;
};
export type FileViewerElementRegistry = {
getElements: () => FileViewerElements;
registerDocumentSurface: (surface: FileViewerDocumentSurface) => () => void;
registerSidebarElement: (element: HTMLElement | null) => void;
registerSidebarGapElement: (element: HTMLDivElement | null) => void;
registerSidebarTriggerElement: (element: HTMLElement | null) => void;
registerViewerShellElement: (element: HTMLDivElement | null) => void;
};
export function createFileViewerElementRegistry({
motionKernel,
onViewerShellElementChange,
}: {
motionKernel: FileViewerMotionKernel;
onViewerShellElementChange: (element: HTMLDivElement | null) => void;
}): FileViewerElementRegistry {
const elements: FileViewerElements = {
documentSurfaceElement: null,
getDocumentSurfaceMotionProbeElement: null,
sidebarElement: null,
sidebarGapElement: null,
sidebarTriggerElement: null,
viewerShellElement: null,
};
let documentSurfaceRegistration = 0;
return {
getElements: () => elements,
registerDocumentSurface: (surface) => {
documentSurfaceRegistration += 1;
const registration = documentSurfaceRegistration;
elements.documentSurfaceElement = surface.element;
elements.getDocumentSurfaceMotionProbeElement =
surface.getMotionProbeElement ?? null;
motionKernel.setDocumentSurface(surface);
return () => {
if (documentSurfaceRegistration !== registration) return;
elements.documentSurfaceElement = null;
elements.getDocumentSurfaceMotionProbeElement = null;
motionKernel.setDocumentSurface(null);
};
},
registerSidebarElement: (element) => {
if (elements.sidebarElement === element) return;
elements.sidebarElement = element;
},
registerSidebarGapElement: (element) => {
if (elements.sidebarGapElement === element) return;
elements.sidebarGapElement = element;
motionKernel.setSidebarGapElement(element);
},
registerSidebarTriggerElement: (element) => {
elements.sidebarTriggerElement = element;
},
registerViewerShellElement: (element) => {
if (elements.viewerShellElement === element) return;
elements.viewerShellElement = element;
onViewerShellElementChange(element);
},
};
}
registry/new-york-v4/ui/file-viewer-fallback.tsx
"use client";
import * as React from "react";
import {
fileViewerDiagnostic,
summarizeViewerDescriptor,
summarizeViewerError,
summarizeViewerResource,
} from "@/lib/pdf-viewer-diagnostics";
import { cn } from "@/lib/utils";
import type { ViewerResource } from "@/lib/viewer-resource";
import { Skeleton } from "@/components/ui/skeleton";
import { CodeViewerFallback } from "./code-viewer-chrome";
import { DocxViewerFallback } from "./docx-viewer-chrome";
import {
isProseTextDescriptor,
type FileDescriptor,
type FileViewerFallbackSize,
} from "./file-viewer-core";
import {
FileViewerErrorState,
FileViewerLoadingState,
FileViewerUnsupportedState,
} from "./file-viewer-state";
import { ImageViewerFallback } from "./image-viewer-chrome";
import { PdfViewerFallback } from "./pdf-viewer-states";
import { PptxViewerFallback } from "./pptx-viewer-fallback";
import { TextViewerFallback } from "./text-viewer-chrome";
import { ViewerControlsSkeleton } from "./viewer-controls";
import { XlsxViewerFallback } from "./xlsx-viewer-chrome";
export function UnsupportedCard({
resource,
className,
bare,
message,
showDownload = true,
}: {
resource: ViewerResource;
className?: string;
bare?: boolean;
message?: string;
showDownload?: boolean;
}) {
return (
<FileViewerUnsupportedState
className={cn(
bare ? "bg-muted/20 h-full" : "bg-muted/30 min-h-64 rounded-xl border",
className,
)}
description={
message ?? `No preview is available for ${resource.fileName}.`
}
download={resource.originalDownload}
fileName={resource.fileName}
showDownload={showDownload}
/>
);
}
export function ViewerFallback({
resource,
className,
bare = false,
controls = true,
fallbackFrameSize,
fallbackSlideSize,
}: {
resource: ViewerResource;
className?: string;
bare?: boolean;
controls?: boolean;
fallbackFrameSize?: FileViewerFallbackSize;
fallbackSlideSize?: FileViewerFallbackSize;
}) {
const descriptor = resource.descriptor;
const category = descriptor.category;
// Render the exact per-type skeleton each viewer shows while it parses, so the
// SSR + chunk-loading paint is identical to the in-viewer loading state (same
// controls row, same body) — no controls popping in and no geometry shift as one
// skeleton hands off to the next.
switch (category) {
case "pdf":
return (
<PdfViewerFallback
bare={bare}
className={className}
controls={controls}
/>
);
case "docx":
return (
<DocxViewerFallback
bare={bare}
className={className}
controls={controls}
/>
);
case "pptx":
return (
<PptxViewerFallback
bare={bare}
className={className}
controls={controls}
fallbackSlideSize={fallbackSlideSize}
/>
);
case "image":
return (
<ImageViewerFallback
bare={bare}
className={className}
controls={controls}
fallbackFrameSize={fallbackFrameSize}
/>
);
case "xlsx":
return (
<XlsxViewerFallback
bare={bare}
className={className}
controls={controls}
/>
);
case "markdown":
return (
<TextViewerFallback
bare={bare}
className={className}
controls={controls}
/>
);
case "text":
// The "text" category fans out to a prose viewer or a code viewer; match
// whichever the route will pick so the body skeleton (gutter vs none) lines up.
return isProseTextDescriptor(descriptor) ? (
<TextViewerFallback
bare={bare}
className={className}
controls={controls}
/>
) : (
<CodeViewerFallback
bare={bare}
className={className}
controls={controls}
/>
);
case "csv":
return (
<TextFamilyFallbackFrame bare={bare} className={className}>
{controls ? (
<ViewerControlsSkeleton title subtitle zoom download />
) : null}
<TableBodySkeleton />
</TextFamilyFallbackFrame>
);
case "html":
return (
<TextFamilyFallbackFrame bare={bare} className={className}>
{controls ? <ViewerControlsSkeleton zoom /> : null}
<div className="bg-card min-h-0 flex-1 p-4">
<Skeleton className="size-full rounded-md" />
</div>
</TextFamilyFallbackFrame>
);
}
// Unsupported / unknown categories: a neutral page-sheet placeholder.
return (
<FileViewerLoadingState
description={null}
icon={null}
title={null}
className={cn(
"flex min-h-0 flex-col items-stretch justify-start gap-0 overflow-hidden p-0 text-left",
bare ? "bg-muted/20 h-full" : "bg-muted/20 min-h-64",
className,
)}
>
<div
data-slot="file-viewer-document-fallback"
className="min-h-0 flex-1 overflow-hidden"
>
<div className="flex flex-col items-center p-4">
<Skeleton
aria-hidden
className="ring-border w-full rounded-none shadow-sm ring-1"
style={{ aspectRatio: "8.5 / 11" }}
/>
</div>
</div>
</FileViewerLoadingState>
);
}
function TextFamilyFallbackFrame({
bare,
className,
children,
}: {
bare?: boolean;
className?: string;
children: React.ReactNode;
}) {
return (
<div
data-slot="file-viewer-document-fallback"
className={cn(
"bg-card flex min-h-0 flex-1 flex-col overflow-hidden",
bare ? "h-full" : "min-h-64",
className,
)}
>
{children}
</div>
);
}
function TableBodySkeleton() {
const gutter = 52;
const colWidth = 150;
const cols = 6;
const rows = 14;
const widths = [70, 45, 88, 56, 62, 78];
return (
<div
aria-hidden
className="bg-card flex min-h-0 flex-1 flex-col overflow-hidden text-sm"
>
<div className="bg-muted/60 flex border-b">
<div
className="shrink-0 border-r"
style={{ width: gutter, height: 33 }}
/>
{Array.from({ length: cols }, (_, c) => (
<div
key={c}
className="flex shrink-0 items-center border-r px-3"
style={{ width: colWidth, height: 33 }}
>
<Skeleton className="h-3 w-16" />
</div>
))}
</div>
<div className="min-h-0 flex-1 overflow-hidden">
{Array.from({ length: rows }, (_, r) => (
<div key={r} className="flex border-b" style={{ height: 33 }}>
<div
className="flex shrink-0 items-center justify-end border-r px-2"
style={{ width: gutter }}
>
<Skeleton className="h-3 w-4" />
</div>
{Array.from({ length: cols }, (_, c) => (
<div
key={c}
className="flex shrink-0 items-center border-r px-3"
style={{ width: colWidth }}
>
<Skeleton
className="h-3"
style={{ width: `${widths[(r + c) % widths.length]}%` }}
/>
</div>
))}
</div>
))}
</div>
</div>
);
}
export class FileErrorBoundary extends React.Component<
{
children: React.ReactNode;
descriptor: FileDescriptor;
resource: ViewerResource;
className?: string;
resetKey?: unknown;
showDownload?: boolean;
},
{ error: unknown | null }
> {
state: Readonly<{ error: unknown | null }> = { error: null };
componentDidUpdate(prev: { resetKey?: unknown }) {
if (prev.resetKey !== this.props.resetKey && this.state.error) {
fileViewerDiagnostic("debug", "file_viewer_error_boundary_reset", {
descriptor: summarizeViewerDescriptor(this.props.descriptor),
resource: summarizeViewerResource(this.props.resource),
});
this.setState({ error: null });
}
}
static getDerivedStateFromError(error: unknown) {
return { error };
}
componentDidCatch(error: unknown) {
fileViewerDiagnostic("error", "file_viewer_error_boundary_caught", {
descriptor: summarizeViewerDescriptor(this.props.descriptor),
resource: summarizeViewerResource(this.props.resource),
error: summarizeViewerError(error),
});
}
render() {
if (this.state.error != null) {
return (
<FileViewerErrorState
error={this.state.error}
format="file"
sourceKind={this.props.resource.sourceKind}
download={
this.props.showDownload === false
? null
: this.props.resource.originalDownload
}
className={this.props.className}
/>
);
}
return this.props.children;
}
}
registry/new-york-v4/ui/file-viewer-shell.tsx
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
import {
FileViewerShellStaticContext,
FileViewerSidebarDynamicContext,
type FileViewerSidebarRequestedMode,
} from "./file-viewer-context";
import {
FILE_VIEWER_INLINE_BREAKPOINT,
useFileViewerShellController,
} from "./file-viewer-shell-controller";
export type FileViewerLayout = "fill" | "intrinsic";
export type FileViewerProps = React.ComponentProps<"div"> & {
inlineBreakpoint?: number;
layout?: FileViewerLayout;
sidebarMode?: FileViewerSidebarRequestedMode;
};
export function FileViewer({
children,
className,
inlineBreakpoint = FILE_VIEWER_INLINE_BREAKPOINT,
layout = "fill",
sidebarMode = "auto",
style,
...props
}: FileViewerProps) {
const controller = useFileViewerShellController({
inlineBreakpoint,
sidebarMode,
});
return (
<FileViewerShellStaticContext.Provider
value={controller.shellStaticContext}
>
<FileViewerSidebarDynamicContext.Provider
value={controller.sidebarDynamicContext}
>
<div
ref={controller.setRootElement}
data-file-viewer="root"
data-file-viewer-header-mode={controller.headerMode}
data-file-viewer-layout={layout}
data-file-viewer-resource-category={controller.resourceCategory}
data-file-viewer-sidebar-collapsible={controller.effectiveCollapsible}
data-file-viewer-sidebar-mode={controller.mode}
data-file-viewer-sidebar-open={
controller.isSidebarOpen ? "true" : "false"
}
data-file-viewer-sidebar-side={controller.side}
data-file-viewer-sidebar-state={controller.sidebarState}
data-slot="file-viewer-root"
id={controller.rootId}
className={cn(
"relative flex min-h-0 min-w-0 flex-col overflow-hidden",
layout === "fill" && "h-full w-full flex-1",
className,
)}
style={
{
overflowAnchor: "none",
...style,
} as React.CSSProperties
}
{...props}
>
{children}
</div>
</FileViewerSidebarDynamicContext.Provider>
</FileViewerShellStaticContext.Provider>
);
}
registry/new-york-v4/ui/file-viewer-shell-controller.ts
"use client";
import * as React from "react";
import { useKeyedLayoutEffect } from "@/hooks/use-keyed-layout-effect";
import { joinEffectKey } from "@/lib/effect-key";
import { restoreFileViewerSidebarFocusOnClose } from "./file-viewer-accessibility";
import {
DEFAULT_FILE_VIEWER_SIDEBAR_WIDTH,
useFileViewerContext,
type FileViewerContextValue,
type FileViewerSetSidebarOpen,
type FileViewerShellStaticContextValue,
type FileViewerSidebarCollapsible,
type FileViewerSidebarDynamicContextValue,
type FileViewerSidebarMode,
type FileViewerSidebarOpenProps,
type FileViewerSidebarRegistration,
type FileViewerSidebarRequestedMode,
type FileViewerSidebarSide,
type FileViewerSidebarState,
} from "./file-viewer-context";
import { createFileViewerElementRegistry } from "./file-viewer-elements";
import { useFileViewerShellKeyboard } from "./file-viewer-shell-keyboard";
import {
createFileViewerMotionKernel,
useFileViewerMotionFrame,
} from "./file-viewer-motion-kernel";
import { useFileViewerMotionTelemetry } from "./viewer-motion-telemetry";
import {
FILE_VIEWER_MOTION_DURATION_MS,
type FileViewerMotionTarget,
} from "./file-viewer-motion-plan";
import {
readElementRectSnapshot,
useStableElementSize,
} from "./viewer-measurement";
export const FILE_VIEWER_INLINE_BREAKPOINT = 768;
type FileViewerSidebarOpenController = {
getSidebarOpen: () => boolean;
isSidebarOpen: boolean;
setSidebarOpen: (isSidebarOpen: boolean) => void;
};
// Bridges the controlled/uncontrolled `open` prop pair to real React state, so
// every toggle re-renders the tree and the declarative writers (data
// attributes, inert/aria, overlay translate classes) stay current.
function useFileViewerSidebarOpenController({
collapsible,
defaultOpen,
onOpenChange,
open,
}: FileViewerSidebarOpenProps & {
collapsible: FileViewerSidebarCollapsible;
}): FileViewerSidebarOpenController {
const isControlled = open !== undefined;
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(
defaultOpen ?? false,
);
const isSidebarOpen =
collapsible === "none" ? true : (open ?? uncontrolledOpen);
const openRef = React.useRef(isSidebarOpen);
// Not an inline render-time mirror: setSidebarOpen writes this ref eagerly
// before a controlled parent applies the prop, and the kernel's flushSync
// re-renders mid-click with the stale prop value. Assign only when the
// committed value actually changes so the eager write survives that window.
useKeyedLayoutEffect(
joinEffectKey(["file-viewer-open-mirror", isSidebarOpen]),
() => {
openRef.current = isSidebarOpen;
},
);
const getSidebarOpen = React.useCallback(() => openRef.current, []);
const setSidebarOpen = React.useCallback(
(nextIsSidebarOpen: boolean) => {
openRef.current = nextIsSidebarOpen;
if (!isControlled) {
setUncontrolledOpen(nextIsSidebarOpen);
}
onOpenChange?.(nextIsSidebarOpen);
},
[isControlled, onOpenChange],
);
return React.useMemo(
() => ({
getSidebarOpen,
isSidebarOpen,
setSidebarOpen,
}),
[getSidebarOpen, isSidebarOpen, setSidebarOpen],
);
}
type FileViewerSidebarRegistrationController = {
canToggleSidebar: boolean;
effectiveCollapsible: FileViewerSidebarCollapsible;
registerSidebar: (registration: FileViewerSidebarRegistration) => () => void;
side: FileViewerSidebarSide;
sidebarId: string;
sidebarWidth: string;
sidebarWidthPixels: number;
};
// The mounted FileViewerSidebar is the single source of sidebar configuration
// (collapsible, side, width); the shell only holds neutral defaults until one
// registers. No sidebar registered means nothing to toggle and no motion, so
// no fallback width measurement is needed.
function useFileViewerSidebarRegistration({
fallbackSidebarId,
}: {
fallbackSidebarId: string;
}): FileViewerSidebarRegistrationController {
const [sidebarRegistration, setSidebarRegistration] =
React.useState<FileViewerSidebarRegistration | null>(null);
const effectiveCollapsible = sidebarRegistration?.collapsible ?? "offcanvas";
const side = sidebarRegistration?.side ?? "left";
const sidebarId = sidebarRegistration?.id ?? fallbackSidebarId;
const sidebarWidth =
sidebarRegistration?.width ?? DEFAULT_FILE_VIEWER_SIDEBAR_WIDTH;
const sidebarWidthPixels = sidebarRegistration?.widthPixels ?? 0;
const canToggleSidebar =
sidebarRegistration !== null && effectiveCollapsible !== "none";
const registerSidebar = React.useCallback(
(registration: FileViewerSidebarRegistration) => {
setSidebarRegistration(registration);
return () => {
setSidebarRegistration((current) =>
current?.id === registration.id ? null : current,
);
};
},
[],
);
return React.useMemo(
() => ({
canToggleSidebar,
effectiveCollapsible,
registerSidebar,
side,
sidebarId,
sidebarWidth,
sidebarWidthPixels,
}),
[
canToggleSidebar,
effectiveCollapsible,
registerSidebar,
side,
sidebarId,
sidebarWidth,
sidebarWidthPixels,
],
);
}
type FileViewerShellControllerOptions = {
inlineBreakpoint: number;
sidebarMode: FileViewerSidebarRequestedMode;
};
export type FileViewerShellController = {
effectiveCollapsible: FileViewerSidebarCollapsible;
headerMode: FileViewerContextValue["headerMode"];
isSidebarOpen: boolean;
mode: FileViewerSidebarMode;
resourceCategory: FileViewerContextValue["resourceCategory"];
rootId: string;
setRootElement: (element: HTMLDivElement | null) => void;
shellStaticContext: FileViewerShellStaticContextValue;
side: FileViewerSidebarSide;
sidebarDynamicContext: FileViewerSidebarDynamicContextValue;
sidebarState: FileViewerSidebarState;
};
export function useFileViewerShellController({
inlineBreakpoint,
sidebarMode,
}: FileViewerShellControllerOptions): FileViewerShellController {
const context = useFileViewerContext();
if (!context.isInsideFileViewer) {
throw new Error("FileViewer must be rendered within FileViewerProvider.");
}
const rootId = React.useId();
const fallbackSidebarId = `${rootId}-sidebar`;
// The kernel owns time + style writes only; its settle-hold layout reads are
// injected from the quarantined measurement module here at the controller
// layer.
const motionKernel = React.useMemo(
() => createFileViewerMotionKernel({ readElementRectSnapshot }),
[],
);
const size = useStableElementSize<HTMLDivElement>({
retainLastNonZero: true,
});
const elementRegistry = React.useMemo(
() =>
createFileViewerElementRegistry({
motionKernel,
onViewerShellElementChange: size.setElement,
}),
[motionKernel, size.setElement],
);
const {
canToggleSidebar,
effectiveCollapsible,
registerSidebar,
side,
sidebarId,
sidebarWidth,
sidebarWidthPixels,
} = useFileViewerSidebarRegistration({
fallbackSidebarId,
});
const openController = useFileViewerSidebarOpenController({
...context.sidebarOpenProps,
collapsible: effectiveCollapsible,
});
const isSidebarOpen = openController.isSidebarOpen;
const motionFrame = useFileViewerMotionFrame(motionKernel);
const isSidebarTransitioning = motionFrame.phase === "sliding";
const mode = resolveFileViewerSidebarMode({
inlineBreakpoint,
requestedMode: sidebarMode,
width: size.width,
});
const isSidebarInteractive = resolveFileViewerSidebarInteractive({
canToggleSidebar,
collapsible: effectiveCollapsible,
isSidebarOpen,
isSidebarTransitioning,
mode,
});
const sidebarState: FileViewerSidebarState = isSidebarOpen
? "expanded"
: "collapsed";
const motionTarget = React.useMemo<FileViewerMotionTarget>(
() => ({
shellInlineSize: size.width ?? 0,
durationMs: FILE_VIEWER_MOTION_DURATION_MS,
mode,
open: isSidebarOpen,
side,
sidebarWidth: sidebarWidthPixels,
}),
[isSidebarOpen, mode, side, sidebarWidthPixels, size.width],
);
const motionTargetRef = React.useRef(motionTarget);
// Same two-writer shape as openRef above: setSidebarOpen stores the eager
// nextTarget before React commits, so only adopt the rendered target when
// it is a genuinely new object.
useKeyedLayoutEffect(
joinEffectKey(["file-viewer-motion-target-mirror", motionTarget]),
() => {
motionTargetRef.current = motionTarget;
},
);
const setRootElement = React.useCallback(
(element: HTMLDivElement | null) => {
elementRegistry.registerViewerShellElement(element);
},
[elementRegistry],
);
// Adopt idle geometry (mount, resizes, mode flips) without animating. While
// a motion is in flight the kernel owns the target — EXCEPT on a mode flip:
// React renders the new mode immediately, so the kernel must be told (it
// snaps the motion) or an inline slide keeps writing gap widths against
// overlay DOM until settle.
useKeyedLayoutEffect(
joinEffectKey([
"file-viewer-sync-target",
motionFrame.mode,
motionFrame.phase,
motionKernel,
motionTarget,
]),
() => {
if (
motionFrame.phase !== "idle" &&
motionFrame.mode === motionTarget.mode
) {
return;
}
motionKernel.syncTarget(motionTarget);
},
);
// Single owner of focus restoration: when the sidebar stops being
// interactive while focus is inside it, hand focus back to the trigger.
const previousIsSidebarInteractiveRef = React.useRef(isSidebarInteractive);
useKeyedLayoutEffect(
joinEffectKey([
"file-viewer-sidebar-focus-restore",
elementRegistry,
isSidebarInteractive,
]),
() => {
restoreFileViewerSidebarFocusOnClose({
elements: elementRegistry.getElements(),
isSidebarInteractive,
previousIsSidebarInteractive: previousIsSidebarInteractiveRef.current,
});
previousIsSidebarInteractiveRef.current = isSidebarInteractive;
},
);
const setSidebarOpen = React.useCallback<FileViewerSetSidebarOpen>(
(value) => {
// Toggling requires a registered, collapsible sidebar; a non-collapsible
// sidebar is pinned open and an unregistered one has nothing to move.
if (!canToggleSidebar) return;
const currentOpen = openController.getSidebarOpen();
const nextOpen = typeof value === "function" ? value(currentOpen) : value;
if (nextOpen === currentOpen) return;
openController.setSidebarOpen(nextOpen);
const nextTarget: FileViewerMotionTarget = {
...motionTargetRef.current,
open: nextOpen,
};
motionTargetRef.current = nextTarget;
motionKernel.startMotion(nextTarget);
},
[canToggleSidebar, motionKernel, openController],
);
const toggleSidebar = React.useCallback(
() => setSidebarOpen((open) => !open),
[setSidebarOpen],
);
const closeSidebar = React.useCallback(
() => setSidebarOpen(false),
[setSidebarOpen],
);
useFileViewerShellKeyboard({
canToggleSidebar,
closeSidebar,
elementRegistry,
isSidebarInteractive,
isSidebarOverlayDismissible:
mode === "overlay" && canToggleSidebar && isSidebarOpen,
viewerShellElement: size.element,
});
useFileViewerMotionTelemetry({
getElements: elementRegistry.getElements,
getIsSidebarOpen: openController.getSidebarOpen,
motionDurationMs: FILE_VIEWER_MOTION_DURATION_MS,
motionKernel,
toggleSidebar,
});
const shellStaticContext = React.useMemo<FileViewerShellStaticContextValue>(
() => ({
canToggleSidebar,
collapsible: effectiveCollapsible,
elementRegistry,
mode,
motionDurationMs: FILE_VIEWER_MOTION_DURATION_MS,
motionKernel,
registerSidebar,
rootId,
setSidebarOpen,
side,
sidebarId,
sidebarWidth,
toggleSidebar,
}),
[
canToggleSidebar,
effectiveCollapsible,
elementRegistry,
mode,
motionKernel,
registerSidebar,
rootId,
setSidebarOpen,
side,
sidebarId,
sidebarWidth,
toggleSidebar,
],
);
const sidebarDynamicContext =
React.useMemo<FileViewerSidebarDynamicContextValue>(
() => ({
isSidebarInteractive,
isSidebarOpen,
isSidebarTransitioning,
sidebarState,
}),
[
isSidebarInteractive,
isSidebarOpen,
isSidebarTransitioning,
sidebarState,
],
);
return {
effectiveCollapsible,
headerMode: context.headerMode,
isSidebarOpen,
mode,
resourceCategory: context.resourceCategory,
rootId,
setRootElement,
shellStaticContext,
side,
sidebarDynamicContext,
sidebarState,
};
}
// An inline sidebar stays non-interactive while its geometry is in motion:
// closing makes it inert immediately, opening keeps it inert until the
// settle publish leaves the "sliding" phase.
function resolveFileViewerSidebarInteractive({
canToggleSidebar,
collapsible,
isSidebarOpen,
isSidebarTransitioning,
mode,
}: {
canToggleSidebar: boolean;
collapsible: FileViewerSidebarCollapsible;
isSidebarOpen: boolean;
isSidebarTransitioning: boolean;
mode: FileViewerSidebarMode;
}): boolean {
if (collapsible === "none") return true;
if (!isSidebarOpen) return false;
return !(mode === "inline" && canToggleSidebar && isSidebarTransitioning);
}
function resolveFileViewerSidebarMode({
inlineBreakpoint,
requestedMode,
width,
}: {
inlineBreakpoint: number;
requestedMode: FileViewerSidebarRequestedMode;
width: number | null;
}): FileViewerSidebarMode {
if (requestedMode !== "auto") return requestedMode;
if (width === null) return "overlay";
return width >= inlineBreakpoint ? "inline" : "overlay";
}
registry/new-york-v4/ui/file-viewer-shell-keyboard.ts
"use client";
import { useKeyedLayoutEffect } from "@/hooks/use-keyed-layout-effect";
import { joinEffectKey } from "@/lib/effect-key";
import type { FileViewerElementRegistry } from "./file-viewer-elements";
export function shouldCloseFileViewerSidebarOnEscape({
canToggleSidebar,
event,
isSidebarInteractive,
}: {
canToggleSidebar: boolean;
event: KeyboardEvent;
isSidebarInteractive: boolean;
}) {
return (
canToggleSidebar &&
isSidebarInteractive &&
event.key === "Escape" &&
!event.defaultPrevented &&
!event.repeat
);
}
export function isFileViewerActiveElementInsideShell({
activeElement,
viewerShellElement,
}: {
activeElement: Element | null;
viewerShellElement: HTMLElement | null;
}) {
return Boolean(
activeElement &&
viewerShellElement &&
viewerShellElement.contains(activeElement),
);
}
export function useFileViewerShellKeyboard({
canToggleSidebar,
closeSidebar,
elementRegistry,
isSidebarInteractive,
isSidebarOverlayDismissible,
viewerShellElement,
}: {
canToggleSidebar: boolean;
closeSidebar: () => void;
elementRegistry: FileViewerElementRegistry;
isSidebarInteractive: boolean;
isSidebarOverlayDismissible: boolean;
viewerShellElement: HTMLDivElement | null;
}) {
useKeyedLayoutEffect(
joinEffectKey([
"file-viewer-escape-close",
canToggleSidebar,
closeSidebar,
elementRegistry,
isSidebarInteractive,
viewerShellElement,
]),
() => {
const ownerDocument = viewerShellElement?.ownerDocument;
if (!ownerDocument) return;
const handleKeyDown = (event: KeyboardEvent) => {
if (
!shouldCloseFileViewerSidebarOnEscape({
canToggleSidebar,
event,
isSidebarInteractive,
})
) {
return;
}
if (
!isFileViewerActiveElementInsideShell({
activeElement: ownerDocument.activeElement,
viewerShellElement:
elementRegistry.getElements().viewerShellElement,
})
) {
return;
}
event.preventDefault();
closeSidebar();
};
ownerDocument.addEventListener("keydown", handleKeyDown);
return () => ownerDocument.removeEventListener("keydown", handleKeyDown);
},
);
// An overlay sidebar floats above the document, so a pointer press anywhere
// outside the panel (and its trigger, whose own click handles the toggle)
// dismisses it — the same light-dismiss contract as a popover.
useKeyedLayoutEffect(
joinEffectKey([
"file-viewer-overlay-dismiss",
closeSidebar,
elementRegistry,
isSidebarOverlayDismissible,
viewerShellElement,
]),
() => {
const ownerDocument = viewerShellElement?.ownerDocument;
if (!ownerDocument || !isSidebarOverlayDismissible) return;
const handlePointerDown = (event: PointerEvent) => {
const target = event.target;
if (!(target instanceof Node)) return;
const elements = elementRegistry.getElements();
if (elements.sidebarElement?.contains(target)) return;
if (elements.sidebarTriggerElement?.contains(target)) return;
closeSidebar();
};
ownerDocument.addEventListener("pointerdown", handlePointerDown, {
capture: true,
});
return () =>
ownerDocument.removeEventListener("pointerdown", handlePointerDown, {
capture: true,
});
},
);
}
registry/new-york-v4/ui/file-viewer-sidebar-controller.ts
"use client";
import * as React from "react";
import { useKeyedLayoutEffect } from "@/hooks/use-keyed-layout-effect";
import { joinEffectKey } from "@/lib/effect-key";
import { resolveFileViewerSidebarAccessibilityProps } from "./file-viewer-accessibility";
import {
type FileViewerSidebarCollapsible,
type FileViewerSidebarMode,
type FileViewerSidebarSide,
type FileViewerSidebarState,
useFileViewerShell,
} from "./file-viewer-context";
import { useIsInsideFileViewerContent } from "./file-viewer-layout";
import { useStableCssLength, useStableElementSize } from "./viewer-measurement";
export type FileViewerSidebarController = {
accessibilityProps: ReturnType<
typeof resolveFileViewerSidebarAccessibilityProps
>;
customProperties: React.CSSProperties;
isInline: boolean;
isSidebarOpen: boolean;
mode: FileViewerSidebarMode;
panelStyle: React.CSSProperties;
resolvedCollapsible: FileViewerSidebarCollapsible;
resolvedSide: FileViewerSidebarSide;
setSidebarGapElement: (element: HTMLDivElement | null) => void;
setSidebarPanelElement: (element: HTMLElement | null) => void;
sidebarId: string;
sidebarState: FileViewerSidebarState;
};
export function useFileViewerSidebarController({
collapsible,
id,
side,
style,
width,
}: {
collapsible: FileViewerSidebarCollapsible | undefined;
id: string | undefined;
side: FileViewerSidebarSide | undefined;
style: React.CSSProperties | undefined;
width: string;
}): FileViewerSidebarController {
const isInsideContent = useIsInsideFileViewerContent();
const shell = useFileViewerShell("FileViewerSidebar");
const registerSidebar = shell.registerSidebar;
// Observed, not measured-once: the panel's resolved pixel width is the same
// number the motion kernel animates the gap to, so it must stay fresh across
// font-size and container changes or the panel edge and the layout width
// drift apart.
const sidebarSize = useStableElementSize<HTMLElement>({
retainLastNonZero: true,
});
const reactId = React.useId();
const sidebarId = id ?? `${reactId}-file-viewer-sidebar`;
const resolvedCollapsible = collapsible ?? shell.collapsible;
const resolvedSide = side ?? shell.side;
const isSidebarOpen =
resolvedCollapsible === "none" ? true : shell.isSidebarOpen;
const isSidebarInteractive =
resolvedCollapsible === "none" ? true : shell.isSidebarInteractive;
const sidebarState = isSidebarOpen ? "expanded" : "collapsed";
const isInline = shell.mode === "inline";
const declaredWidthPixels = useStableCssLength({
element: sidebarSize.element,
value: width,
});
const widthPixels =
declaredWidthPixels > 0 ? declaredWidthPixels : (sidebarSize.width ?? 0);
const setSidebarGapElement = React.useCallback(
(element: HTMLDivElement | null) => {
shell.elementRegistry.registerSidebarGapElement(element);
},
[shell.elementRegistry],
);
const setSidebarSizeElement = sidebarSize.setElement;
const setSidebarPanelElement = React.useCallback(
(element: HTMLElement | null) => {
setSidebarSizeElement(element);
shell.elementRegistry.registerSidebarElement(element);
},
[setSidebarSizeElement, shell.elementRegistry],
);
if (process.env.NODE_ENV !== "production" && !isInsideContent) {
throw new Error(
"FileViewerSidebar must be rendered inside FileViewerContent.",
);
}
useKeyedLayoutEffect(
joinEffectKey([
"file-viewer-sidebar-registration",
registerSidebar,
resolvedCollapsible,
resolvedSide,
sidebarId,
width,
widthPixels,
]),
() =>
registerSidebar({
collapsible: resolvedCollapsible,
id: sidebarId,
side: resolvedSide,
width,
widthPixels,
}),
);
const accessibilityProps = resolveFileViewerSidebarAccessibilityProps({
collapsible: resolvedCollapsible,
deferInert:
!isSidebarOpen && !isSidebarInteractive && shell.isSidebarTransitioning,
isSidebarInteractive,
});
const customProperties = pickCssCustomProperties(style);
const panelStyle: React.CSSProperties = {
width,
// Overlay slides on a CSS transition; its duration is the same constant
// that drives the kernel clock, so the two timelines cannot drift.
...(isInline
? null
: { transitionDuration: `${shell.motionDurationMs}ms` }),
...style,
...(isSidebarInteractive ? null : { pointerEvents: "none" as const }),
};
// No memo: `panelStyle` and `customProperties` are fresh objects every
// render, so the controller value can never be referentially stable anyway.
return {
accessibilityProps,
customProperties,
isInline,
isSidebarOpen,
mode: shell.mode,
panelStyle,
resolvedCollapsible,
resolvedSide,
setSidebarGapElement,
setSidebarPanelElement,
sidebarId,
sidebarState,
};
}
function pickCssCustomProperties(style: React.CSSProperties | undefined) {
if (!style) return {};
return Object.fromEntries(
Object.entries(style).filter(([name]) => name.startsWith("--")),
) as React.CSSProperties;
}
registry/new-york-v4/ui/file-viewer-motion-kernel.ts
"use client";
import * as React from "react";
import { flushSync } from "react-dom";
import { FILE_VIEWER_BEFORE_LAYOUT_MOTION_EVENT } from "./file-viewer-elements";
import {
easeFileViewerMotion,
FILE_VIEWER_MOTION_DURATION_MS,
areFileViewerMotionFramesEqual,
areFileViewerMotionRestFramesEqual,
createFileViewerIdleMotionFrame,
createFileViewerMotionPlan,
createFileViewerMotionRestFrame,
type FileViewerMotionFrame,
type FileViewerMotionPlan,
type FileViewerMotionRestFrame,
type FileViewerMotionTarget,
} from "./file-viewer-motion-plan";
// The kernel is the single owner of sidebar motion. It holds the clock (one
// rAF loop from slide start through settle), writes the continuous inline
// styles, and publishes to React subscribers only at phase edges
// (idle → sliding → settling → idle). Everything discrete — data attributes,
// inert/aria, the overlay translate classes — is owned by React renders.
//
// Commit-then-relax ordering: the first sliding frame is flushed
// synchronously inside the toggle's own task, so renderers commit their
// TARGET layout and rebase scroll before anything paints; the per-tick style
// writes then only relax the counter-transform to identity. Settle removes a
// no-op transform — it never commits layout, so no flushSync runs inside rAF.
export type FileViewerMotionKernel = {
getFlightRecords: () => readonly FileViewerMotionFlightRecord[];
getInteractiveSnapshot: () => FileViewerMotionFrame;
getSnapshot: () => FileViewerMotionFrame;
setDocumentSurface: (surface: FileViewerDocumentSurface | null) => void;
setSidebarGapElement: (element: HTMLElement | null) => void;
startMotion: (target: FileViewerMotionTarget) => void;
subscribe: (listener: () => void) => () => void;
syncTarget: (target: FileViewerMotionTarget) => void;
};
// Always-on flight recorder: every motion leaves a bounded trace (per-tick
// widths, phase edges, settle holds, inter-frame gaps) so a blink report is
// diagnosable after the fact without re-instrumenting.
export type FileViewerMotionFlightRecord = {
fromInlineSize: number;
id: number;
interrupted: boolean;
maxTickGapMs: number;
open: boolean;
settleHoldFrameCount: number;
startedAt: number;
ticks: FileViewerMotionFlightTick[];
toInlineSize: number;
};
export type FileViewerMotionFlightTick = {
elapsedMs: number;
phase: FileViewerMotionFrame["phase"];
sidebarInlineSize: number;
};
const FILE_VIEWER_FLIGHT_RECORD_LIMIT = 8;
const FILE_VIEWER_FLIGHT_TICK_LIMIT = 240;
export type FileViewerDocumentSurface = {
element: HTMLElement;
getMotionProbeElement?: (() => HTMLElement | null) | null;
readSettleSnapshot?: FileViewerDocumentSurfaceSettleSnapshotReader | null;
resolveMotionStyle?: FileViewerDocumentSurfaceMotionResolver | null;
};
export type FileViewerDocumentSurfaceSettleSnapshotReader = () =>
| readonly number[]
| null
| undefined;
// Layout reads are quarantined outside the kernel (viewer-measurement / the
// frame controller layer). The kernel owns time and style writes only, so the
// settle-hold rect reader is injected by its creator rather than imported.
export type FileViewerElementRectSnapshotReader = (
element: HTMLElement | null,
) => readonly number[];
export type FileViewerMotionKernelOptions = {
readElementRectSnapshot?: FileViewerElementRectSnapshotReader | null;
};
export type FileViewerDocumentSurfaceMotionStyle = {
customProperties?: Readonly<Record<string, string | null>>;
transform: string;
transformOrigin: string;
willChange: string;
};
export type FileViewerDocumentSurfaceMotionResolver = (
frame: FileViewerMotionFrame,
) => FileViewerDocumentSurfaceMotionStyle | null;
type FileViewerActiveMotion = {
durationMs: number;
from: FileViewerMotionRestFrame;
// The clock re-anchors to the first tick's vsync frame time: startedAt is
// stamped inside the toggle's task, but the synchronous slide-start commit
// can burn 10ms+ before anything paints, and an ease anchored at the click
// lands its first painted frame that deep into the curve.
hasFrameClockAnchor: boolean;
id: number;
startedAt: number;
to: FileViewerMotionRestFrame;
};
type FileViewerSettleRelease = {
idleFrame: FileViewerMotionFrame;
lastSnapshot: readonly number[];
remainingFrameCount: number;
settlingFrame: FileViewerMotionFrame;
stableFrameCount: number;
};
const FILE_VIEWER_SETTLE_SCROLL_EPSILON_PX = 0.25;
const FILE_VIEWER_SETTLE_STABLE_FRAME_COUNT = 2;
const FILE_VIEWER_SETTLE_MAX_HOLD_FRAMES = 6;
const FILE_VIEWER_SUBPIXEL_ENDPOINT_EPSILON_PX = 1;
export const DEFAULT_FILE_VIEWER_MOTION_FRAME: FileViewerMotionFrame = {
shellInlineSize: 0,
durationMs: FILE_VIEWER_MOTION_DURATION_MS,
fromInlineSize: 0,
layoutInlineSize: 0,
mode: "overlay",
motionId: null,
motionProgress: 1,
open: false,
phase: "idle",
side: "left",
sidebarInlineSize: 0,
sidebarWidth: 0,
toInlineSize: 0,
};
export function createFileViewerMotionKernel({
readElementRectSnapshot = null,
}: FileViewerMotionKernelOptions = {}): FileViewerMotionKernel {
const listeners = new Set<() => void>();
let contractFrame = DEFAULT_FILE_VIEWER_MOTION_FRAME;
let interactiveFrame = DEFAULT_FILE_VIEWER_MOTION_FRAME;
let target: FileViewerMotionTarget = {
shellInlineSize: 0,
durationMs: DEFAULT_FILE_VIEWER_MOTION_FRAME.durationMs,
mode: DEFAULT_FILE_VIEWER_MOTION_FRAME.mode,
open: DEFAULT_FILE_VIEWER_MOTION_FRAME.open,
side: DEFAULT_FILE_VIEWER_MOTION_FRAME.side,
sidebarWidth: 0,
};
let documentSurface: FileViewerDocumentSurface | null = null;
let documentSurfaceCustomProperties = new Set<string>();
let sidebarGapElement: HTMLElement | null = null;
let activeMotion: FileViewerActiveMotion | null = null;
let settleRelease: FileViewerSettleRelease | null = null;
let rafHandle = 0;
let settleReleaseHandle = 0;
let motionSequence = 0;
const flightRecords: FileViewerMotionFlightRecord[] = [];
let activeFlightRecord: FileViewerMotionFlightRecord | null = null;
let lastFlightTickAt = 0;
const beginFlightRecord = (motion: FileViewerActiveMotion) => {
if (activeFlightRecord && activeFlightRecord.id !== motion.id) {
activeFlightRecord.interrupted = true;
}
activeFlightRecord = {
fromInlineSize: motion.from.layoutInlineSize,
id: motion.id,
interrupted: false,
maxTickGapMs: 0,
open: motion.to.open,
settleHoldFrameCount: 0,
startedAt: motion.startedAt,
ticks: [],
toInlineSize: motion.to.layoutInlineSize,
};
lastFlightTickAt = motion.startedAt;
flightRecords.push(activeFlightRecord);
if (flightRecords.length > FILE_VIEWER_FLIGHT_RECORD_LIMIT) {
flightRecords.splice(
0,
flightRecords.length - FILE_VIEWER_FLIGHT_RECORD_LIMIT,
);
}
};
const recordFlightTick = (frame: FileViewerMotionFrame, now = readNow()) => {
const record = activeFlightRecord;
if (!record || frame.motionId !== record.id) return;
record.maxTickGapMs = Math.max(record.maxTickGapMs, now - lastFlightTickAt);
lastFlightTickAt = now;
if (frame.phase === "settling") record.settleHoldFrameCount += 1;
if (record.ticks.length >= FILE_VIEWER_FLIGHT_TICK_LIMIT) return;
record.ticks.push({
elapsedMs: Math.max(0, now - record.startedAt),
phase: frame.phase,
sidebarInlineSize: frame.sidebarInlineSize,
});
};
const notify = () => {
for (const listener of listeners) listener();
};
const publishContractFrame = (
nextFrame: FileViewerMotionFrame,
{ flushSubscribers = false }: { flushSubscribers?: boolean } = {},
) => {
if (areFileViewerMotionFramesEqual(contractFrame, nextFrame)) return;
contractFrame = nextFrame;
if (flushSubscribers) {
flushSync(notify);
return;
}
notify();
};
// The gap's inline size and the document surface's counter-scale must land
// in the same frame: two independent CSS transitions (width on the gap,
// transform on the surface) can desync under main-thread jank, letting the
// document edge drift off the sidebar edge mid-slide. The kernel therefore
// writes both here, once per tick.
const writeElementStyles = (nextFrame: FileViewerMotionFrame) => {
writeSidebarGapStyle(nextFrame);
writeDocumentSurfaceStyle(nextFrame);
};
const commit = (
nextFrame: FileViewerMotionFrame,
{ publish = true }: { publish?: boolean } = {},
) => {
writeElementStyles(nextFrame);
interactiveFrame = nextFrame;
if (publish) publishContractFrame(nextFrame);
};
const cancelTick = () => {
if (rafHandle === 0) return;
getCancelAnimationFrame()(rafHandle);
rafHandle = 0;
};
const cancelSettleRelease = () => {
settleRelease = null;
if (settleReleaseHandle === 0) return;
getCancelAnimationFrame()(settleReleaseHandle);
settleReleaseHandle = 0;
};
const readMotionSample = (
motion: FileViewerActiveMotion,
now = readNow(),
): FileViewerMotionFrame => {
const rawTimeProgress =
motion.durationMs <= 0
? 1
: clamp((now - motion.startedAt) / motion.durationMs, 0, 1);
const rawMotionProgress = easeFileViewerMotion(rawTimeProgress);
const rawSidebarInlineSize = lerp(
motion.from.sidebarInlineSize,
motion.to.sidebarInlineSize,
rawMotionProgress,
);
const isSubpixelEndpoint =
rawMotionProgress > 0.98 &&
Math.abs(rawSidebarInlineSize - motion.to.sidebarInlineSize) <=
FILE_VIEWER_SUBPIXEL_ENDPOINT_EPSILON_PX;
const motionProgress = isSubpixelEndpoint ? 1 : rawMotionProgress;
const sidebarInlineSize = isSubpixelEndpoint
? motion.to.sidebarInlineSize
: rawSidebarInlineSize;
const layoutInlineSize = Math.max(
0,
motion.to.shellInlineSize - sidebarInlineSize,
);
const fromInlineSize = motion.from.layoutInlineSize;
return {
shellInlineSize: motion.to.shellInlineSize,
durationMs: motion.durationMs,
fromInlineSize,
layoutInlineSize,
mode: motion.to.mode,
motionId: motion.id,
motionProgress,
open: motion.to.open,
phase: motionProgress < 1 ? "sliding" : "settling",
side: motion.to.side,
sidebarInlineSize,
sidebarWidth: motion.to.sidebarWidth,
toInlineSize: motion.to.layoutInlineSize,
};
};
const settle = () => {
if (!activeMotion) return;
const finishedMotion = activeMotion;
activeMotion = null;
cancelTick();
const idleFrame = createFileViewerIdleMotionFrame(finishedMotion.to);
const settlingFrame: FileViewerMotionFrame = {
...idleFrame,
fromInlineSize: finishedMotion.from.layoutInlineSize,
motionId: finishedMotion.id,
phase: "settling",
};
// Layout and scroll were committed at slide start; settling only clears
// the (now identity) counter-transform and holds until shell geometry
// stops moving. Nothing here re-renders geometry, so no flushSync in rAF.
commit(settlingFrame, { publish: false });
recordFlightTick(settlingFrame);
publishContractFrame(settlingFrame);
scheduleSettleRelease(settlingFrame, idleFrame);
};
// Ticks sample the clock at the rAF FRAME timestamp, never the callback's
// execution time: the frame time is the vsync the paint belongs to, and a
// callback running late in a janky frame would otherwise write a position
// ahead of the frame's own time axis — a real paint-side velocity excess
// (the probes' rule 11, applied to the writer). The first tick also
// re-anchors startedAt to its frame time, so the ease starts at the first
// paintable frame rather than at the click that precedes the slide-start
// commit.
const tick = (frameTime: number) => {
rafHandle = 0;
if (!activeMotion) return;
const now = Number.isFinite(frameTime) ? frameTime : readNow();
if (!activeMotion.hasFrameClockAnchor) {
activeMotion.hasFrameClockAnchor = true;
activeMotion.startedAt = now;
}
const sample = readMotionSample(activeMotion, now);
if (sample.motionProgress >= 1) {
settle();
return;
}
commit(sample, { publish: false });
recordFlightTick(sample, now);
scheduleTick();
};
const scheduleTick = () => {
if (rafHandle !== 0) return;
rafHandle = getRequestAnimationFrame()(tick);
};
const scheduleSettleRelease = (
settlingFrame: FileViewerMotionFrame,
idleFrame: FileViewerMotionFrame,
) => {
cancelSettleRelease();
settleRelease = {
idleFrame,
lastSnapshot: readSettleSnapshot(),
remainingFrameCount: FILE_VIEWER_SETTLE_MAX_HOLD_FRAMES,
settlingFrame,
stableFrameCount: 0,
};
scheduleSettleReleaseFrame();
};
const scheduleSettleReleaseFrame = () => {
if (settleReleaseHandle !== 0) return;
settleReleaseHandle = getRequestAnimationFrame()(holdSettleRelease);
};
const holdSettleRelease = () => {
settleReleaseHandle = 0;
if (!settleRelease) return;
commit(settleRelease.settlingFrame, { publish: false });
recordFlightTick(settleRelease.settlingFrame);
const snapshot = readSettleSnapshot();
const stableFrameCount = areSettleSnapshotsEqual(
settleRelease.lastSnapshot,
snapshot,
)
? settleRelease.stableFrameCount + 1
: 0;
const remainingFrameCount = settleRelease.remainingFrameCount - 1;
if (
stableFrameCount >= FILE_VIEWER_SETTLE_STABLE_FRAME_COUNT ||
remainingFrameCount <= 0
) {
const idleFrame = settleRelease.idleFrame;
settleRelease = null;
// Natural completion: close the flight record so the next motion does
// not mark this one interrupted.
activeFlightRecord = null;
commit(idleFrame);
return;
}
settleRelease = {
...settleRelease,
lastSnapshot: snapshot,
remainingFrameCount,
stableFrameCount,
};
scheduleSettleReleaseFrame();
};
// The event carries the kernel's LIVE frame so renderers can capture their
// pre-commit anchor against what is actually on screen — during a mid-flight
// retarget that is the settled layout PLUS the in-flight transform, not the
// settled layout alone.
const dispatchBeforeLayoutMotion = (currentFrame: FileViewerMotionFrame) => {
documentSurface?.element.dispatchEvent(
new CustomEvent<FileViewerMotionFrame>(
FILE_VIEWER_BEFORE_LAYOUT_MOTION_EVENT,
{ detail: currentFrame },
),
);
};
const interruptActiveFlightRecord = () => {
if (!activeFlightRecord) return;
activeFlightRecord.interrupted = true;
activeFlightRecord = null;
};
const retarget = (nextTarget: FileViewerMotionTarget, animate: boolean) => {
cancelSettleRelease();
// Continuity is with what is PAINTED, not with the clock: mid-flight the
// screen shows the last tick's commit (`interactiveFrame`), which can be
// a frame behind a fresh clock sample. Planning (and the before-motion
// capture renderers do off the event detail) from the painted frame keeps
// the retarget hand-off pixel-continuous; the new motion simply re-lerps
// from the painted geometry.
const currentFrame =
interactiveFrame.shellInlineSize > 0
? interactiveFrame
: createFileViewerIdleMotionFrame(
createFileViewerMotionRestFrame(target),
);
const plan = createFileViewerMotionPlan({
animate: animate && !prefersReducedMotion(),
currentFrame,
nextTarget,
});
if (shouldDispatchBeforeLayoutMotion(plan)) {
dispatchBeforeLayoutMotion(currentFrame);
}
target = plan.resolvedTarget;
if (!plan.shouldAnimate) {
if (activeMotion) interruptActiveFlightRecord();
activeMotion = null;
cancelTick();
commit(createFileViewerIdleMotionFrame(plan.nextRestFrame));
return;
}
motionSequence += 1;
activeMotion = {
durationMs: plan.resolvedTarget.durationMs,
from: { ...plan.currentRestFrame, layoutInlineSize: plan.fromInlineSize },
hasFrameClockAnchor: false,
id: motionSequence,
startedAt: readNow(),
to: plan.nextRestFrame,
};
beginFlightRecord(activeMotion);
const startFrame = readMotionSample(activeMotion, activeMotion.startedAt);
// Commit the discontinuity while it cannot be seen: flush the first
// sliding frame synchronously (inside the toggle's own task) so renderers
// lay out at the target width and rebase scroll before first paint, hidden
// behind the counter-transform written above in the same task.
writeElementStyles(startFrame);
interactiveFrame = startFrame;
recordFlightTick(startFrame, activeMotion.startedAt);
publishContractFrame(startFrame, { flushSubscribers: true });
scheduleTick();
};
const syncTarget = (nextTarget: FileViewerMotionTarget) => {
const nextRestFrame = createFileViewerMotionRestFrame(nextTarget);
if (activeMotion) {
target = nextTarget;
if (areFileViewerMotionRestFramesEqual(activeMotion.to, nextRestFrame)) {
return;
}
// A mode flip mid-motion (breakpoint crossing during the slide) cannot
// be animated: React re-renders the new mode immediately, so an inline
// slide continuing against overlay DOM (or vice versa) double-moves the
// surface. Snap to the new rest geometry instead.
if (nextRestFrame.mode !== activeMotion.to.mode) {
interruptActiveFlightRecord();
activeMotion = null;
cancelTick();
cancelSettleRelease();
commit(createFileViewerIdleMotionFrame(nextRestFrame));
return;
}
retarget(nextTarget, true);
return;
}
cancelSettleRelease();
target = nextTarget;
commit(createFileViewerIdleMotionFrame(nextRestFrame));
};
return {
getFlightRecords: () => flightRecords.slice(),
getInteractiveSnapshot: () =>
activeMotion ? readMotionSample(activeMotion) : interactiveFrame,
getSnapshot: () => contractFrame,
setDocumentSurface: (surface) => {
const previousSurface = documentSurface;
if (
previousSurface &&
(!surface || previousSurface.element !== surface.element)
) {
clearDocumentSurfaceStyle(previousSurface.element);
}
documentSurface = surface;
writeDocumentSurfaceStyle(interactiveFrame);
},
setSidebarGapElement: (element) => {
sidebarGapElement = element;
writeSidebarGapStyle(interactiveFrame);
},
startMotion: (nextTarget) => retarget(nextTarget, true),
subscribe: (listener) => {
listeners.add(listener);
return () => {
listeners.delete(listener);
};
},
syncTarget,
};
function writeSidebarGapStyle(nextFrame: FileViewerMotionFrame) {
if (!sidebarGapElement) return;
// Overlay motion is CSS-owned; relinquish the gap so its `w-0` class is
// the only writer outside inline mode.
if (nextFrame.mode !== "inline") {
sidebarGapElement.style.width = "";
sidebarGapElement.style.flexBasis = "";
return;
}
sidebarGapElement.style.width = `${nextFrame.sidebarInlineSize}px`;
sidebarGapElement.style.flexBasis = `${nextFrame.sidebarInlineSize}px`;
}
function writeDocumentSurfaceStyle(nextFrame: FileViewerMotionFrame) {
if (!documentSurface) return;
const { element, resolveMotionStyle } = documentSurface;
const resolvedStyle = resolveMotionStyle?.(nextFrame);
if (resolvedStyle) {
writeDocumentSurfaceCustomProperties(
element,
resolvedStyle.customProperties,
);
element.style.transform = resolvedStyle.transform;
element.style.transformOrigin = resolvedStyle.transformOrigin;
element.style.willChange = resolvedStyle.willChange;
return;
}
// Default (no motion resolver): identity. Fit-width renderers register
// the shared commit-then-relax resolver (file-viewer-fit-width-motion);
// a surface without one either tracks the live DOM width on its own or
// opts out of shell motion entirely, and must not be transformed here.
writeDocumentSurfaceCustomProperties(element, null);
element.style.transform = "";
element.style.transformOrigin = "";
element.style.willChange = "";
}
function writeDocumentSurfaceCustomProperties(
element: HTMLElement,
customProperties:
| Readonly<Record<string, string | null>>
| null
| undefined,
) {
const nextNames = new Set(Object.keys(customProperties ?? {}));
for (const name of documentSurfaceCustomProperties) {
if (!nextNames.has(name)) {
element.style.removeProperty(name);
}
}
for (const [name, value] of Object.entries(customProperties ?? {})) {
if (value == null) {
element.style.removeProperty(name);
} else {
element.style.setProperty(name, value);
}
}
documentSurfaceCustomProperties = nextNames;
}
function clearDocumentSurfaceStyle(element: HTMLElement) {
element.style.transform = "";
element.style.transformOrigin = "";
element.style.willChange = "";
for (const name of documentSurfaceCustomProperties) {
element.style.removeProperty(name);
}
documentSurfaceCustomProperties = new Set();
}
function readSettleSnapshot(): readonly number[] {
const values: number[] = [];
appendElementRectSnapshot(values, sidebarGapElement);
appendElementRectSnapshot(values, documentSurface?.element ?? null);
try {
const surfaceSnapshot = documentSurface?.readSettleSnapshot?.();
if (surfaceSnapshot) {
values.push(...surfaceSnapshot.map(toSettleSnapshotNumber));
}
} catch {
// A renderer snapshot is diagnostic, not correctness-critical. If a
// renderer unmounts while settling, fall back to shell geometry.
}
return values.length > 0 ? values : [0];
}
function appendElementRectSnapshot(
values: number[],
element: HTMLElement | null,
) {
if (!readElementRectSnapshot || !element) return;
for (const value of readElementRectSnapshot(element)) {
values.push(toSettleSnapshotNumber(value));
}
}
}
function areSettleSnapshotsEqual(
previous: readonly number[],
next: readonly number[],
) {
if (previous.length !== next.length) return false;
return previous.every(
(value, index) =>
Math.abs(value - next[index]) <= FILE_VIEWER_SETTLE_SCROLL_EPSILON_PX,
);
}
function toSettleSnapshotNumber(value: number) {
return Number.isFinite(value) ? value : 0;
}
function shouldDispatchBeforeLayoutMotion({
currentRestFrame,
nextRestFrame,
}: FileViewerMotionPlan) {
return (
currentRestFrame.mode === "inline" &&
nextRestFrame.mode === "inline" &&
Math.abs(
currentRestFrame.layoutInlineSize - nextRestFrame.layoutInlineSize,
) > 0.001
);
}
export function useFileViewerMotionFrame(
kernel: FileViewerMotionKernel | null | undefined,
): FileViewerMotionFrame {
const subscribe = React.useCallback(
(listener: () => void) => kernel?.subscribe(listener) ?? (() => {}),
[kernel],
);
const getSnapshot = React.useCallback(
() => kernel?.getSnapshot() ?? DEFAULT_FILE_VIEWER_MOTION_FRAME,
[kernel],
);
return React.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
}
function prefersReducedMotion() {
return (
typeof matchMedia === "function" &&
matchMedia("(prefers-reduced-motion: reduce)").matches
);
}
function readNow() {
return typeof performance !== "undefined" &&
typeof performance.now === "function"
? performance.now()
: Date.now();
}
function getRequestAnimationFrame() {
return (
globalThis.requestAnimationFrame ??
((callback: FrameRequestCallback) =>
window.setTimeout(() => callback(readNow()), 16))
);
}
function getCancelAnimationFrame() {
return (
globalThis.cancelAnimationFrame ??
((id: number) => {
window.clearTimeout(id);
})
);
}
function lerp(from: number, to: number, progress: number) {
return from + (to - from) * progress;
}
function clamp(value: number, min: number, max: number) {
return Math.min(max, Math.max(min, value));
}
registry/new-york-v4/ui/viewer-motion-telemetry.ts
"use client";
import * as React from "react";
import { joinEffectKey } from "@/lib/effect-key";
import { useKeyedMountEffect } from "@/hooks/use-keyed-mount-effect";
import type { FileViewerElementRegistry } from "./file-viewer-elements";
import type {
FileViewerMotionFlightRecord,
FileViewerMotionKernel,
} from "./file-viewer-motion-kernel";
// Shell-level motion telemetry, shipped with the component. It measures the
// FileViewer's own contract — gap monotonicity, surface continuity across the
// sliding → settling edge, scroll identity, DOM churn, layout shift, main
// thread — for ANY renderer hosted in the shell, complementing the renderer
// probes (e.g. __pdfViewerTelemetry's canvas continuity).
type FileViewerMotionTelemetryMetricId =
| "blink"
| "gap-stability"
| "overshoot"
| "content-start-snap"
| "content-overshoot"
| "settle-snap"
| "scroll-identity"
| "dom-mutations"
| "layout-shift"
| "main-thread"
| "cycle-invariance";
export type FileViewerMotionTelemetryMetric = {
budget: string;
detail: string;
id: FileViewerMotionTelemetryMetricId;
label: string;
passed: boolean;
value: string;
};
export type FileViewerMotionTelemetrySample = {
contentHeight: number;
contentLeft: number;
contentTop: number;
contentWidth: number;
elapsedMs: number;
gapWidth: number;
motionPhase: string;
scrollHeight: number;
scrollTop: number;
scrollWidth: number;
sidebarState: string | null;
sidebarWidth: number;
surfaceHeight: number;
surfaceLeft: number;
surfaceTop: number;
surfaceTransform: string;
surfaceWidth: number;
timestamp: number;
visibleCanvasCount: number;
};
export type FileViewerMotionTelemetryRun = {
action: "close" | "open";
addedNodeCount: number;
after: FileViewerMotionTelemetrySample;
before: FileViewerMotionTelemetrySample;
canvasResizeMutationCount: number;
layoutShiftCount: number;
layoutShiftScore: number;
longTaskCount: number;
longTaskDurationMs: number;
mutationCount: number;
removedNodeCount: number;
samples: FileViewerMotionTelemetrySample[];
scrollEventCount: number;
};
export type FileViewerMotionTelemetryResult = {
durationMs: number;
metrics: FileViewerMotionTelemetryMetric[];
runs: FileViewerMotionTelemetryRun[];
sampledFrameCount: number;
status: "failed" | "passed";
};
export type FileViewerMotionTelemetryRuntime = {
getFlightRecords: () => readonly FileViewerMotionFlightRecord[];
getLastResult: () => FileViewerMotionTelemetryResult | null;
readSample: () => FileViewerMotionTelemetrySample | null;
run: (options?: {
settleFrameCount?: number;
}) => Promise<FileViewerMotionTelemetryResult | null>;
};
declare global {
interface Window {
__fileViewerMotionTelemetry?: FileViewerMotionTelemetryRuntime;
}
}
const FILE_VIEWER_TELEMETRY_DEFAULT_SETTLE_FRAMES = 16;
const FILE_VIEWER_TELEMETRY_GAP_REVERSAL_EPSILON_PX = 1;
const FILE_VIEWER_TELEMETRY_OVERSHOOT_BUDGET_PX = 1;
const FILE_VIEWER_TELEMETRY_CONTENT_START_SNAP_BUDGET_PX = 1.5;
const FILE_VIEWER_TELEMETRY_CONTENT_OVERSHOOT_BUDGET_PX = 1;
const FILE_VIEWER_TELEMETRY_SETTLE_SNAP_BUDGET_PX = 1.5;
const FILE_VIEWER_TELEMETRY_SCROLL_IDENTITY_BUDGET_PX = 1;
const FILE_VIEWER_TELEMETRY_CYCLE_BUDGET_PX = 2;
const FILE_VIEWER_TELEMETRY_MAIN_THREAD_P95_BUDGET_MS = 34;
const FILE_VIEWER_TELEMETRY_MAIN_THREAD_MAX_BUDGET_MS = 250;
type FileViewerMotionTelemetryHost = {
getElements: FileViewerElementRegistry["getElements"];
getIsSidebarOpen: () => boolean;
motionDurationMs: number;
motionKernel: FileViewerMotionKernel;
toggleSidebar: () => void;
};
export function useFileViewerMotionTelemetry(
host: FileViewerMotionTelemetryHost,
) {
const hostRef = React.useRef(host);
const lastResultRef = React.useRef<FileViewerMotionTelemetryResult | null>(
null,
);
const isRunningRef = React.useRef(false);
hostRef.current = host;
useKeyedMountEffect(joinEffectKey(["file-viewer-motion-telemetry"]), () => {
if (typeof window === "undefined") return;
const previousRuntime = window.__fileViewerMotionTelemetry;
const runtime: FileViewerMotionTelemetryRuntime = {
getFlightRecords: () => hostRef.current.motionKernel.getFlightRecords(),
getLastResult: () => lastResultRef.current,
readSample: () =>
readFileViewerMotionTelemetrySample(hostRef.current, performance.now()),
run: async (options) => {
// A telemetry run drives real toggles; a concurrent run would
// interleave motions and score garbage. One at a time.
if (isRunningRef.current) return null;
isRunningRef.current = true;
try {
const result = await runFileViewerMotionTelemetry(hostRef, options);
lastResultRef.current = result;
if (result) logFileViewerMotionTelemetryResult(result);
return result;
} finally {
isRunningRef.current = false;
}
},
};
window.__fileViewerMotionTelemetry = runtime;
return () => {
if (window.__fileViewerMotionTelemetry === runtime) {
window.__fileViewerMotionTelemetry = previousRuntime;
}
};
});
}
async function runFileViewerMotionTelemetry(
hostRef: React.MutableRefObject<FileViewerMotionTelemetryHost>,
options?: { settleFrameCount?: number },
): Promise<FileViewerMotionTelemetryResult | null> {
const host = hostRef.current;
const elements = host.getElements();
if (!elements.viewerShellElement || !elements.documentSurfaceElement) {
return null;
}
const startedAt = performance.now();
const settleFrameCount =
options?.settleFrameCount ?? FILE_VIEWER_TELEMETRY_DEFAULT_SETTLE_FRAMES;
const firstAction = host.getIsSidebarOpen() ? "close" : "open";
const secondAction = firstAction === "close" ? "open" : "close";
const runs: FileViewerMotionTelemetryRun[] = [];
runs.push(
await sampleFileViewerMotionRun({
action: firstAction,
hostRef,
settleFrameCount,
}),
);
await waitForFileViewerTelemetryFrames(6);
runs.push(
await sampleFileViewerMotionRun({
action: secondAction,
hostRef,
settleFrameCount,
}),
);
const metrics = collectFileViewerMotionTelemetryMetrics(runs);
return {
durationMs: Math.max(0, performance.now() - startedAt),
metrics,
runs,
sampledFrameCount: runs.reduce(
(count, run) => count + run.samples.length,
0,
),
status: metrics.every((metric) => metric.passed) ? "passed" : "failed",
};
}
async function sampleFileViewerMotionRun({
action,
hostRef,
settleFrameCount,
}: {
action: "close" | "open";
hostRef: React.MutableRefObject<FileViewerMotionTelemetryHost>;
settleFrameCount: number;
}): Promise<FileViewerMotionTelemetryRun> {
const host = hostRef.current;
const startedAt = performance.now();
const before =
readFileViewerMotionTelemetrySample(host, startedAt) ??
createEmptyFileViewerMotionTelemetrySample();
const trackers = createFileViewerMotionTelemetryTrackers(host);
const samples: FileViewerMotionTelemetrySample[] = [];
host.toggleSidebar();
samples.push(readFileViewerMotionTelemetrySample(host, startedAt) ?? before);
const sampleUntil = host.motionDurationMs + settleFrameCount * (1000 / 60);
while (performance.now() - startedAt < sampleUntil) {
await waitForFileViewerTelemetryFrames(1);
const sample = readFileViewerMotionTelemetrySample(
hostRef.current,
startedAt,
);
if (sample) samples.push(sample);
}
await waitForFileViewerTelemetryFrames(2);
const after =
readFileViewerMotionTelemetrySample(hostRef.current, startedAt) ?? before;
trackers.disconnect();
return {
action,
addedNodeCount: trackers.addedNodeCount,
after,
before,
canvasResizeMutationCount: trackers.canvasResizeMutationCount,
layoutShiftCount: trackers.layoutShiftCount,
layoutShiftScore: trackers.layoutShiftScore,
longTaskCount: trackers.longTaskCount,
longTaskDurationMs: trackers.longTaskDurationMs,
mutationCount: trackers.mutationCount,
removedNodeCount: trackers.removedNodeCount,
samples,
scrollEventCount: trackers.scrollEventCount,
};
}
function createEmptyFileViewerMotionTelemetrySample(): FileViewerMotionTelemetrySample {
return {
contentHeight: 0,
contentLeft: 0,
contentTop: 0,
contentWidth: 0,
elapsedMs: 0,
gapWidth: 0,
motionPhase: "idle",
scrollHeight: 0,
scrollTop: 0,
scrollWidth: 0,
sidebarState: null,
sidebarWidth: 0,
surfaceHeight: 0,
surfaceLeft: 0,
surfaceTop: 0,
surfaceTransform: "",
surfaceWidth: 0,
timestamp: 0,
visibleCanvasCount: 0,
};
}
function readFileViewerMotionTelemetrySample(
host: FileViewerMotionTelemetryHost,
startedAt: number,
): FileViewerMotionTelemetrySample | null {
const elements = host.getElements();
const surfaceElement = elements.documentSurfaceElement;
if (!surfaceElement) return null;
const gapRect = elements.sidebarGapElement?.getBoundingClientRect();
const sidebarRect = elements.sidebarElement?.getBoundingClientRect();
const surfaceRect = surfaceElement.getBoundingClientRect();
const contentElement = readFileViewerMotionProbeElement(elements);
const contentRect = contentElement
? readFileViewerMotionProbeRect(contentElement, elements.viewerShellElement)
: null;
const scroller = findFileViewerTelemetryScroller(surfaceElement);
const frame = host.motionKernel.getInteractiveSnapshot();
const visibleCanvasCount = countFileViewerVisibleCanvases(surfaceElement);
return {
contentHeight: contentRect?.height ?? surfaceRect.height,
contentLeft: contentRect?.left ?? surfaceRect.left,
contentTop: contentRect?.top ?? surfaceRect.top,
contentWidth: contentRect?.width ?? surfaceRect.width,
elapsedMs: Math.max(0, performance.now() - startedAt),
gapWidth: gapRect?.width ?? 0,
motionPhase: frame.phase,
scrollHeight: scroller?.scrollHeight ?? 0,
scrollTop: scroller?.scrollTop ?? 0,
scrollWidth: scroller?.scrollWidth ?? 0,
sidebarState:
elements.sidebarElement?.getAttribute("data-file-viewer-sidebar-state") ??
null,
sidebarWidth: sidebarRect?.width ?? 0,
surfaceHeight: surfaceRect.height,
surfaceLeft: surfaceRect.left,
surfaceTop: surfaceRect.top,
surfaceTransform: getComputedStyle(surfaceElement).transform,
surfaceWidth: surfaceRect.width,
timestamp: performance.now(),
visibleCanvasCount,
};
}
function readFileViewerMotionProbeElement(
elements: ReturnType<FileViewerElementRegistry["getElements"]>,
) {
try {
return elements.getDocumentSurfaceMotionProbeElement?.() ?? null;
} catch {
return null;
}
}
function readFileViewerMotionProbeRect(
element: HTMLElement,
boundaryElement: HTMLElement | null,
) {
const rect = element.getBoundingClientRect();
let left = rect.left;
let right = rect.right;
// getBoundingClientRect reports the transformed content box even when an
// ancestor removes part of it at paint. Intersect horizontal clipping
// ancestors so telemetry measures the pixels a sidebar resize can actually
// show, while leaving vertical viewport clipping (ordinary document
// scrolling) out of this horizontal motion signal.
let ancestor = element.parentElement;
while (ancestor) {
const style = getComputedStyle(ancestor);
if (clipsFileViewerMotionProbeInline(style)) {
const ancestorRect = ancestor.getBoundingClientRect();
left = Math.max(left, ancestorRect.left);
right = Math.min(right, ancestorRect.right);
}
if (ancestor === boundaryElement) break;
ancestor = ancestor.parentElement;
}
return {
height: rect.height,
left,
top: rect.top,
width: Math.max(0, right - left),
};
}
function clipsFileViewerMotionProbeInline(style: CSSStyleDeclaration) {
const contain = style.contain.split(/\s+/);
return (
style.overflowX === "auto" ||
style.overflowX === "clip" ||
style.overflowX === "hidden" ||
style.overflowX === "scroll" ||
contain.includes("content") ||
contain.includes("paint") ||
contain.includes("strict")
);
}
function findFileViewerTelemetryScroller(element: HTMLElement) {
let candidate: HTMLElement | null = element.parentElement;
while (candidate) {
const overflowY = getComputedStyle(candidate).overflowY;
if (overflowY === "auto" || overflowY === "scroll") return candidate;
candidate = candidate.parentElement;
}
return null;
}
function countFileViewerVisibleCanvases(surfaceElement: HTMLElement) {
let count = 0;
for (const canvas of surfaceElement.querySelectorAll("canvas")) {
if (!(canvas instanceof HTMLCanvasElement)) continue;
if (canvas.width <= 0 || canvas.height <= 0) continue;
const rect = canvas.getBoundingClientRect();
if (rect.width <= 0 || rect.height <= 0) continue;
count += 1;
}
return count;
}
function createFileViewerMotionTelemetryTrackers(
host: FileViewerMotionTelemetryHost,
) {
const elements = host.getElements();
const rootElement = elements.viewerShellElement;
const scroller = elements.documentSurfaceElement
? findFileViewerTelemetryScroller(elements.documentSurfaceElement)
: null;
const trackers = {
addedNodeCount: 0,
canvasResizeMutationCount: 0,
layoutShiftCount: 0,
layoutShiftScore: 0,
longTaskCount: 0,
longTaskDurationMs: 0,
mutationCount: 0,
removedNodeCount: 0,
scrollEventCount: 0,
disconnect: () => {},
};
const cleanups: (() => void)[] = [];
if (rootElement && typeof MutationObserver === "function") {
const mutationObserver = new MutationObserver((records) => {
for (const record of records) {
trackers.mutationCount += 1;
trackers.addedNodeCount += record.addedNodes.length;
trackers.removedNodeCount += record.removedNodes.length;
if (
record.type === "attributes" &&
record.target instanceof HTMLCanvasElement &&
(record.attributeName === "width" ||
record.attributeName === "height")
) {
trackers.canvasResizeMutationCount += 1;
}
}
});
mutationObserver.observe(rootElement, {
attributeFilter: ["width", "height", "style", "data-pdf-render-status"],
attributes: true,
childList: true,
subtree: true,
});
cleanups.push(() => mutationObserver.disconnect());
}
const layoutShiftObserver = observeFileViewerPerformanceEntries(
"layout-shift",
(entry) => {
const layoutShift = entry as PerformanceEntry & {
hadRecentInput?: boolean;
value?: number;
};
if (layoutShift.hadRecentInput) return;
trackers.layoutShiftCount += 1;
trackers.layoutShiftScore += layoutShift.value ?? 0;
},
);
if (layoutShiftObserver) cleanups.push(layoutShiftObserver);
const longTaskObserver = observeFileViewerPerformanceEntries(
"longtask",
(entry) => {
trackers.longTaskCount += 1;
trackers.longTaskDurationMs += entry.duration;
},
);
if (longTaskObserver) cleanups.push(longTaskObserver);
if (scroller) {
const handleScroll = () => {
trackers.scrollEventCount += 1;
};
scroller.addEventListener("scroll", handleScroll, { passive: true });
cleanups.push(() => scroller.removeEventListener("scroll", handleScroll));
}
trackers.disconnect = () => {
for (const cleanup of cleanups) cleanup();
};
return trackers;
}
function observeFileViewerPerformanceEntries(
type: string,
onEntry: (entry: PerformanceEntry) => void,
) {
if (typeof PerformanceObserver !== "function") return null;
const supported = PerformanceObserver.supportedEntryTypes;
if (Array.isArray(supported) && !supported.includes(type)) return null;
try {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) onEntry(entry);
});
observer.observe({ type, buffered: false });
return () => observer.disconnect();
} catch {
return null;
}
}
function waitForFileViewerTelemetryFrames(frameCount: number) {
return new Promise<void>((resolve) => {
let remaining = Math.max(1, frameCount);
const step = () => {
remaining -= 1;
if (remaining <= 0) {
resolve();
return;
}
requestAnimationFrame(step);
};
requestAnimationFrame(step);
});
}
function collectFileViewerMotionTelemetryMetrics(
runs: readonly FileViewerMotionTelemetryRun[],
): FileViewerMotionTelemetryMetric[] {
return [
collectFileViewerBlinkMetric(runs),
collectFileViewerGapStabilityMetric(runs),
collectFileViewerOvershootMetric(runs),
collectFileViewerContentStartSnapMetric(runs),
collectFileViewerContentOvershootMetric(runs),
collectFileViewerSettleSnapMetric(runs),
collectFileViewerScrollIdentityMetric(runs),
collectFileViewerDomMutationsMetric(runs),
collectFileViewerLayoutShiftMetric(runs),
collectFileViewerMainThreadMetric(runs),
collectFileViewerCycleInvarianceMetric(runs),
];
}
function collectFileViewerContentStartSnapMetric(
runs: readonly FileViewerMotionTelemetryRun[],
): FileViewerMotionTelemetryMetric {
let snapPx = 0;
for (const run of runs) {
const before = getFileViewerTelemetryContentEdges(run.before);
const first = run.samples[0]
? getFileViewerTelemetryContentEdges(run.samples[0])
: null;
if (!before || !first) continue;
for (let index = 0; index < before.length; index += 1) {
snapPx = Math.max(snapPx, Math.abs(first[index]! - before[index]!));
}
}
return {
budget: `<= ${FILE_VIEWER_TELEMETRY_CONTENT_START_SNAP_BUDGET_PX}px`,
detail:
"The synchronous target-layout commit must paint exactly where the pre-toggle content was; any first-sample deviation is an opening snap or ancestor clip.",
id: "content-start-snap",
label: "Content start snap",
passed: snapPx <= FILE_VIEWER_TELEMETRY_CONTENT_START_SNAP_BUDGET_PX,
value: `${formatFileViewerTelemetryNumber(snapPx, 2)}px`,
};
}
function collectFileViewerContentOvershootMetric(
runs: readonly FileViewerMotionTelemetryRun[],
): FileViewerMotionTelemetryMetric {
let overshootPx = 0;
for (const run of runs) {
const before = getFileViewerTelemetryContentEdges(run.before);
const after = getFileViewerTelemetryContentEdges(run.after);
if (!before || !after) continue;
for (const sample of run.samples) {
const edges = getFileViewerTelemetryContentEdges(sample);
if (!edges) continue;
for (let index = 0; index < edges.length; index += 1) {
const low = Math.min(before[index]!, after[index]!);
const high = Math.max(before[index]!, after[index]!);
overshootPx = Math.max(
overshootPx,
low - edges[index]!,
edges[index]! - high,
);
}
}
}
return {
budget: `<= ${FILE_VIEWER_TELEMETRY_CONTENT_OVERSHOOT_BUDGET_PX}px`,
detail:
"The visible content edges must stay inside their endpoint corridor; an excursion outside it is the first-frame back-and-forth wobble.",
id: "content-overshoot",
label: "Content overshoot",
passed: overshootPx <= FILE_VIEWER_TELEMETRY_CONTENT_OVERSHOOT_BUDGET_PX,
value: `${formatFileViewerTelemetryNumber(Math.max(0, overshootPx), 2)}px`,
};
}
function getFileViewerTelemetryContentEdges(
sample: FileViewerMotionTelemetrySample,
): readonly [number, number, number, number] | null {
if (sample.contentWidth <= 0 || sample.contentHeight <= 0) return null;
return [
sample.contentLeft,
sample.contentTop,
sample.contentLeft + sample.contentWidth,
sample.contentTop + sample.contentHeight,
];
}
function collectFileViewerBlinkMetric(
runs: readonly FileViewerMotionTelemetryRun[],
): FileViewerMotionTelemetryMetric {
let blinkFrameCount = 0;
for (const run of runs) {
// Canvas-backed renderers must never lose all visible pixels mid-motion;
// renderers without canvases (text-like) hold both endpoints at 0 and the
// check degrades to surface-box presence.
const endpointCanvasFloor = Math.min(
run.before.visibleCanvasCount,
run.after.visibleCanvasCount,
);
for (const sample of run.samples) {
const surfaceMissing =
sample.surfaceWidth <= 0 || sample.surfaceHeight <= 0;
const canvasDropout =
endpointCanvasFloor > 0 && sample.visibleCanvasCount === 0;
if (surfaceMissing || canvasDropout) blinkFrameCount += 1;
}
}
return {
budget: "0 blink frames",
detail:
"A blink frame has a collapsed document surface or loses every visible canvas while both endpoints have rendered content.",
id: "blink",
label: "Blink",
passed: blinkFrameCount === 0,
value: `${blinkFrameCount} frames`,
};
}
function collectFileViewerGapStabilityMetric(
runs: readonly FileViewerMotionTelemetryRun[],
): FileViewerMotionTelemetryMetric {
let reversalCount = 0;
for (const run of runs) {
const widths = run.samples.map((sample) => sample.gapWidth);
const direction = Math.sign((widths.at(-1) ?? 0) - (widths.at(0) ?? 0));
if (direction === 0) continue;
for (let index = 1; index < widths.length; index += 1) {
const step = widths[index] - widths[index - 1];
if (
Math.sign(step) === -direction &&
Math.abs(step) > FILE_VIEWER_TELEMETRY_GAP_REVERSAL_EPSILON_PX
) {
reversalCount += 1;
}
}
}
return {
budget: `0 reversals over ${FILE_VIEWER_TELEMETRY_GAP_REVERSAL_EPSILON_PX}px`,
detail:
"The sidebar gap width must progress monotonically toward its target.",
id: "gap-stability",
label: "Gap stability",
passed: reversalCount === 0,
value: `${reversalCount} reversals`,
};
}
function collectFileViewerOvershootMetric(
runs: readonly FileViewerMotionTelemetryRun[],
): FileViewerMotionTelemetryMetric {
let overshootPx = 0;
for (const run of runs) {
const endpoints = [
run.samples.at(0)?.gapWidth ?? 0,
run.samples.at(-1)?.gapWidth ?? 0,
];
const low = Math.min(...endpoints);
const high = Math.max(...endpoints);
for (const sample of run.samples) {
overshootPx = Math.max(
overshootPx,
low - sample.gapWidth,
sample.gapWidth - high,
);
}
}
return {
budget: `<= ${FILE_VIEWER_TELEMETRY_OVERSHOOT_BUDGET_PX}px`,
detail:
"The gap width must stay inside the interval spanned by its endpoints.",
id: "overshoot",
label: "Overshoot",
passed: overshootPx <= FILE_VIEWER_TELEMETRY_OVERSHOOT_BUDGET_PX,
value: `${formatFileViewerTelemetryNumber(Math.max(0, overshootPx), 2)}px`,
};
}
// The settle-boundary blink: once the transform leaves the sliding phase it
// is identity over the already-settled layout, so every post-sliding sample's
// surface box must already equal the final rest box. Deviation here is the
// "reach the target, then move again" wobble. (Comparing against the final
// box — not the previous frame — keeps dropped frames, whose mid-flight box
// coordinates legitimately sweep with the transform's anchor term, out of
// this metric; main-thread covers those.)
function collectFileViewerSettleSnapMetric(
runs: readonly FileViewerMotionTelemetryRun[],
): FileViewerMotionTelemetryMetric {
let snapPx = 0;
for (const run of runs) {
let sawSliding = false;
// Content position, not box position: late page-size measurements can
// resize the stage after the motion — vertically the scroll layer
// compensates scrollTop in the same commit, horizontally the pages stay
// centered — so the box moves while the pixels do not. The
// content-invariant observables are surfaceTop + scrollTop (vertical)
// and the stage center (horizontal).
const afterContentTop = run.after.surfaceTop + run.after.scrollTop;
const afterContentCenter =
run.after.surfaceLeft + run.after.surfaceWidth / 2;
for (const sample of run.samples) {
if (sample.motionPhase === "sliding") {
sawSliding = true;
continue;
}
if (!sawSliding) continue;
// A sample can catch the frame where the kernel has advanced to
// settling but the last sliding tick's transform is still applied; its
// box sweeps with the anchor term while the viewport pixels hold. The
// metric asserts rest AFTER the transform clears.
if (
sample.surfaceTransform !== "none" &&
sample.surfaceTransform !== ""
) {
continue;
}
snapPx = Math.max(
snapPx,
Math.abs(
sample.surfaceLeft + sample.surfaceWidth / 2 - afterContentCenter,
),
Math.abs(sample.surfaceTop + sample.scrollTop - afterContentTop),
);
}
}
return {
budget: `<= ${FILE_VIEWER_TELEMETRY_SETTLE_SNAP_BUDGET_PX}px settle deviation`,
detail:
"After the sliding phase ends the surface box must already sit at its final rest position; deviation is the settle-boundary wobble.",
id: "settle-snap",
label: "Settle snap",
passed: snapPx <= FILE_VIEWER_TELEMETRY_SETTLE_SNAP_BUDGET_PX,
value: `${formatFileViewerTelemetryNumber(snapPx, 2)}px`,
};
}
function collectFileViewerScrollIdentityMetric(
runs: readonly FileViewerMotionTelemetryRun[],
): FileViewerMotionTelemetryMetric {
let scrollRangePx = 0;
for (const run of runs) {
// Layout and scroll commit in the toggle's own task, so every sampled
// frame (the first sample runs after the toggle) sees the final scrollTop.
const tops = run.samples.map((sample) => sample.scrollTop);
if (tops.length === 0) continue;
scrollRangePx = Math.max(
scrollRangePx,
Math.max(...tops) - Math.min(...tops),
);
}
return {
budget: `<= ${FILE_VIEWER_TELEMETRY_SCROLL_IDENTITY_BUDGET_PX}px scroll range`,
detail:
"scrollTop is rebased once at slide start and must hold for the rest of the motion.",
id: "scroll-identity",
label: "Scroll identity",
passed: scrollRangePx <= FILE_VIEWER_TELEMETRY_SCROLL_IDENTITY_BUDGET_PX,
value: `${formatFileViewerTelemetryNumber(scrollRangePx, 2)}px`,
};
}
function collectFileViewerDomMutationsMetric(
runs: readonly FileViewerMotionTelemetryRun[],
): FileViewerMotionTelemetryMetric {
const canvasResizeMutationCount = runs.reduce(
(count, run) => count + run.canvasResizeMutationCount,
0,
);
const mutationCount = runs.reduce(
(count, run) => count + run.mutationCount,
0,
);
return {
budget: "0 canvas resizes",
detail:
"Raster headroom is prepared before motion, so no canvas backing store may resize (and wipe) during it. Total mutations are reported for diagnosis.",
id: "dom-mutations",
label: "DOM mutations",
passed: canvasResizeMutationCount === 0,
value: `${canvasResizeMutationCount} canvas resizes / ${mutationCount} total`,
};
}
function collectFileViewerLayoutShiftMetric(
runs: readonly FileViewerMotionTelemetryRun[],
): FileViewerMotionTelemetryMetric {
const score = runs.reduce((total, run) => total + run.layoutShiftScore, 0);
const count = runs.reduce((total, run) => total + run.layoutShiftCount, 0);
return {
budget: "diagnostic",
detail:
"Cumulative layout-shift entries during motion (the intentional sidebar reflow is included; read alongside settle-snap).",
id: "layout-shift",
label: "Layout shift",
passed: true,
value: `${count} shifts / ${formatFileViewerTelemetryNumber(score, 4)}`,
};
}
function collectFileViewerMainThreadMetric(
runs: readonly FileViewerMotionTelemetryRun[],
): FileViewerMotionTelemetryMetric {
const gaps: number[] = [];
for (const run of runs) {
for (let index = 1; index < run.samples.length; index += 1) {
gaps.push(
run.samples[index].timestamp - run.samples[index - 1].timestamp,
);
}
}
const sortedGaps = gaps.slice().sort((left, right) => left - right);
const p95 =
sortedGaps.length > 0
? sortedGaps[
Math.min(sortedGaps.length - 1, Math.floor(sortedGaps.length * 0.95))
]
: 0;
const max = sortedGaps.at(-1) ?? 0;
const longTaskDurationMs = runs.reduce(
(total, run) => total + run.longTaskDurationMs,
0,
);
const passed =
p95 <= FILE_VIEWER_TELEMETRY_MAIN_THREAD_P95_BUDGET_MS &&
max <= FILE_VIEWER_TELEMETRY_MAIN_THREAD_MAX_BUDGET_MS;
return {
budget: `p95 <= ${FILE_VIEWER_TELEMETRY_MAIN_THREAD_P95_BUDGET_MS}ms, max <= ${FILE_VIEWER_TELEMETRY_MAIN_THREAD_MAX_BUDGET_MS}ms`,
detail:
"Frame-to-frame sample gaps during motion; a long gap is a dropped frame the user sees as stutter. Long-task time is reported alongside.",
id: "main-thread",
label: "Main thread",
passed,
value: `p95 ${formatFileViewerTelemetryNumber(p95, 1)}ms / max ${formatFileViewerTelemetryNumber(max, 1)}ms / ${formatFileViewerTelemetryNumber(longTaskDurationMs, 1)}ms long tasks`,
};
}
function collectFileViewerCycleInvarianceMetric(
runs: readonly FileViewerMotionTelemetryRun[],
): FileViewerMotionTelemetryMetric {
if (runs.length < 2) {
return {
budget: `<= ${FILE_VIEWER_TELEMETRY_CYCLE_BUDGET_PX}px`,
detail: "Needs a full toggle cycle.",
id: "cycle-invariance",
label: "Cycle invariance",
passed: true,
value: "n/a",
};
}
const first = runs[0].before;
const last = runs[runs.length - 1].after;
const drift = Math.max(
Math.abs(first.gapWidth - last.gapWidth),
Math.abs(first.surfaceLeft - last.surfaceLeft),
Math.abs(first.surfaceWidth - last.surfaceWidth),
Math.abs(first.scrollTop - last.scrollTop),
);
return {
budget: `<= ${FILE_VIEWER_TELEMETRY_CYCLE_BUDGET_PX}px`,
detail:
"A full open/close cycle must return the gap, surface box, and scroll position to their starting values.",
id: "cycle-invariance",
label: "Cycle invariance",
passed: drift <= FILE_VIEWER_TELEMETRY_CYCLE_BUDGET_PX,
value: `${formatFileViewerTelemetryNumber(drift, 2)}px`,
};
}
function logFileViewerMotionTelemetryResult(
result: FileViewerMotionTelemetryResult,
) {
const summary = {
durationMs: Number(result.durationMs.toFixed(1)),
failedMetricIds: result.metrics
.filter((metric) => !metric.passed)
.map((metric) => metric.id),
metrics: result.metrics,
sampledFrameCount: result.sampledFrameCount,
status: result.status,
};
console.info(
`[file-viewer:motion-telemetry] result ${JSON.stringify(summary)}`,
);
}
function formatFileViewerTelemetryNumber(
value: number,
fractionDigits: number,
) {
return Number.isFinite(value) ? Number(value.toFixed(fractionDigits)) : 0;
}
registry/new-york-v4/ui/file-viewer-header.tsx
"use client";
import * as React from "react";
import { PanelLeft, PanelRight } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "./button";
import { Spinner } from "./spinner";
import { useFileViewerControlsState } from "./file-viewer-provider";
import { useFileViewerRequiredResourceState } from "./file-viewer-resource-state";
import {
useFileViewerContext,
useFileViewerShell,
} from "./file-viewer-context";
import { resolveFileViewerSidebarTriggerAccessibilityProps } from "./file-viewer-accessibility";
import { ViewerControls } from "./viewer-controls";
import { ViewerHeaderOutlet } from "./viewer-header-outlet";
type ButtonProps = React.ComponentProps<typeof Button> & {
loading?: boolean;
};
export type FileViewerHeaderProps = React.ComponentProps<"div">;
export type FileViewerTitleProps = React.ComponentProps<"span">;
export type FileViewerMetaProps = React.ComponentProps<"span"> & {
visibility?: "responsive" | "visible";
};
export type FileViewerControlsProps = Omit<
React.ComponentProps<typeof ViewerControls>,
| "downloads"
| "loading"
| "position"
| "rotate"
| "subtitle"
| "title"
| "zoom"
>;
export type FileViewerSidebarTriggerProps = ButtonProps;
export function FileViewerHeader({
children,
className,
...props
}: FileViewerHeaderProps) {
const { hasHeaderOutlets, headerMode } = useFileViewerContext();
const content = children ?? <FileViewerDefaultHeaderContent />;
if (headerMode === "outlets" && hasHeaderOutlets) {
return <>{content}</>;
}
return (
<div
data-slot="file-viewer-header"
className={cn(
"bg-card flex min-h-10 shrink-0 flex-wrap items-center gap-2 border-b px-2 py-1 sm:flex-nowrap sm:py-0",
className,
)}
{...props}
>
{content}
</div>
);
}
function FileViewerDefaultHeaderContent() {
const { hasHeaderOutlets, headerMode } = useFileViewerContext();
if (headerMode === "outlets" && hasHeaderOutlets) {
return (
<>
<FileViewerTitle />
<FileViewerMeta />
<FileViewerControls />
</>
);
}
return (
<>
<div
data-slot="file-viewer-header-title-group"
className="flex min-w-0 flex-1 items-baseline gap-2"
>
<FileViewerTitle />
<FileViewerMeta />
</div>
<FileViewerControls />
</>
);
}
export function FileViewerTitle({ className, ...props }: FileViewerTitleProps) {
const { hasHeaderOutlets, headerMode } = useFileViewerContext();
const { descriptor } = useFileViewerRequiredResourceState();
const title = (
<span
data-slot="file-viewer-title"
className={cn(
"text-foreground min-w-0 truncate text-[13px] leading-5 font-medium",
className,
)}
title={descriptor.displayName}
{...props}
>
{descriptor.displayName}
</span>
);
if (headerMode === "outlets" && hasHeaderOutlets) {
return <ViewerHeaderOutlet name="titleGroup">{title}</ViewerHeaderOutlet>;
}
return title;
}
export function FileViewerMeta({
className,
visibility = "responsive",
...props
}: FileViewerMetaProps) {
const { hasHeaderOutlets, headerMode } = useFileViewerContext();
const { descriptor, resource } = useFileViewerRequiredResourceState();
const metaText =
resource.mimeType || descriptor.mimeType || descriptor.category;
if (!metaText) return null;
const meta = (
<span
data-slot="file-viewer-meta"
className={cn(
"text-muted-foreground min-w-0 shrink truncate text-xs",
visibility === "responsive" && "hidden sm:inline",
className,
)}
{...props}
>
{metaText}
</span>
);
if (headerMode === "outlets" && hasHeaderOutlets) {
return <ViewerHeaderOutlet name="titleGroup">{meta}</ViewerHeaderOutlet>;
}
return meta;
}
export function FileViewerControls({
className,
extra,
...props
}: FileViewerControlsProps) {
const { resource } = useFileViewerRequiredResourceState();
const controlsState = useFileViewerControlsState();
const downloads = controlsState?.downloads ?? [resource.originalDownload];
const { hasHeaderOutlets, headerMode } = useFileViewerContext();
const controls = (
<ViewerControls
{...props}
data-file-viewer-controls={controlsState ? "ready" : "idle"}
data-slot="file-viewer-controls"
className={cn(
"ms-auto h-8 min-w-0 border-b-0 bg-transparent px-0",
className,
)}
downloads={downloads}
extra={extra ?? controlsState?.extra}
loading={controlsState?.loading ?? false}
position={controlsState?.position ?? null}
rotate={controlsState?.rotate ?? null}
zoom={controlsState?.zoom ?? null}
/>
);
if (headerMode === "outlets" && hasHeaderOutlets) {
return <ViewerHeaderOutlet name="controls">{controls}</ViewerHeaderOutlet>;
}
return controls;
}
export function FileViewerSidebarTrigger({
className,
disabled,
loading,
onClick,
children,
size = "icon",
variant = "ghost",
"aria-label": ariaLabel = "Toggle sidebar",
...props
}: FileViewerSidebarTriggerProps) {
const {
canToggleSidebar,
elementRegistry,
mode,
side,
isSidebarInteractive,
sidebarId,
sidebarState,
toggleSidebar,
} = useFileViewerShell("FileViewerSidebarTrigger");
const isDisabled = Boolean(disabled || loading || !canToggleSidebar);
const Icon = side === "right" ? PanelRight : PanelLeft;
const setTriggerElement = React.useCallback(
(element: HTMLButtonElement | null) => {
elementRegistry.registerSidebarTriggerElement(element);
},
[elementRegistry],
);
const accessibilityProps = resolveFileViewerSidebarTriggerAccessibilityProps({
ariaLabel,
canToggleSidebar,
isDisabled,
isSidebarInteractive,
sidebarId,
});
return (
<Button
ref={setTriggerElement}
{...accessibilityProps}
className={cn("size-8", className)}
data-file-viewer-sidebar-mode={mode}
data-file-viewer-sidebar-side={side}
data-file-viewer-sidebar-state={sidebarState}
data-side={side}
data-slot="file-viewer-sidebar-trigger"
disabled={isDisabled}
onClick={(event) => {
if (isDisabled) {
event.preventDefault();
return;
}
onClick?.(event);
if (!event.defaultPrevented) {
toggleSidebar();
}
}}
size={size}
variant={variant}
{...props}
>
{loading ? <Spinner className="size-4 animate-spin" /> : null}
{children ?? (
<>
{loading ? null : <Icon />}
<span className="sr-only">Toggle sidebar</span>
</>
)}
</Button>
);
}
registry/new-york-v4/ui/file-viewer-html-viewer.tsx
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
import {
viewerContentRenderKey,
type ViewerResource,
} from "@/lib/viewer-resource";
import { ViewerFallback } from "./file-viewer-fallback";
import { getHtmlViewerSrcDoc } from "./file-viewer-html-srcdoc-cache";
import { loadTextResource } from "./file-viewer-text-resource";
import { isAbortError } from "./viewer-abortable-request";
import { useViewerControlsRegistration } from "./viewer-controls";
import { useZoom, ZoomActions } from "./viewer-zoom";
import { useKeyedMountEffect } from "@/hooks/use-keyed-mount-effect";
import { joinEffectKey } from "@/lib/effect-key";
type HtmlLoadState =
| { status: "loading"; key: unknown }
| { status: "loaded"; key: unknown; html: string }
| { status: "error"; key: unknown; error: unknown };
export function FileViewerHtmlContent({
resource,
className,
bare,
controls = true,
descriptorSignal,
}: {
resource: ViewerResource;
className?: string;
bare?: boolean;
controls?: boolean;
descriptorSignal: AbortSignal;
}) {
if (resource.content.payload.kind === "text") {
return (
<FileViewerHtmlContentFrame
key={viewerContentRenderKey(resource.content)}
resource={resource}
html={resource.content.payload.text}
className={className}
bare={bare}
controls={controls}
/>
);
}
return (
<HtmlFileResource
resource={resource}
className={className}
bare={bare}
controls={controls}
descriptorSignal={descriptorSignal}
/>
);
}
function HtmlFileResource({
resource,
className,
bare,
controls,
descriptorSignal,
}: {
resource: ViewerResource;
className?: string;
bare?: boolean;
controls: boolean;
descriptorSignal: AbortSignal;
}) {
const content = resource.content;
const contentKey = content.key;
const [state, setState] = React.useState<HtmlLoadState>({
status: "loading",
key: contentKey,
});
useKeyedMountEffect(
joinEffectKey([content, contentKey, descriptorSignal, resource.fileName]),
() => {
let active = true;
const controller = new AbortController();
const abortLocal = () => controller.abort();
setState({ status: "loading", key: contentKey });
if (descriptorSignal.aborted) {
abortLocal();
} else {
descriptorSignal.addEventListener("abort", abortLocal, { once: true });
}
loadTextResource({
content,
fileName: resource.fileName,
signal: controller.signal,
}).then(
(html) => {
if (active && !controller.signal.aborted) {
setState({ status: "loaded", key: contentKey, html });
}
},
(error: unknown) => {
if (!active || isAbortError(error)) return;
setState({ status: "error", key: contentKey, error });
},
);
return () => {
active = false;
descriptorSignal.removeEventListener("abort", abortLocal);
abortLocal();
};
},
);
if (state.key !== contentKey) {
return (
<ViewerFallback resource={resource} className={className} bare={bare} />
);
}
if (state.status === "error") {
throw state.error;
}
if (state.status === "loading") {
return (
<ViewerFallback resource={resource} className={className} bare={bare} />
);
}
const { html } = state;
return (
<FileViewerHtmlContentFrame
key={contentKey}
resource={resource}
html={html}
className={className}
bare={bare}
controls={controls}
/>
);
}
function FileViewerHtmlContentFrame({
resource,
html,
className,
bare,
controls,
}: {
resource: ViewerResource;
html: string;
className?: string;
bare?: boolean;
controls: boolean;
}) {
const fileName = resource.fileName;
const { scale, zoom, reset } = useZoom();
useHtmlControlsRegistration({ reset, scale, zoom });
return (
<div
data-slot="html-file-viewer-content"
className={cn(
"bg-card flex min-h-0 flex-1 flex-col overflow-hidden",
bare ? "h-full" : "min-h-64",
className,
)}
>
{controls ? (
<HtmlContentToolbar scale={scale} zoom={zoom} reset={reset} />
) : null}
<SandboxedDoc
contentKey={resource.content.key}
html={html}
title={fileName}
scale={scale}
/>
</div>
);
}
function useHtmlControlsRegistration({
reset,
scale,
zoom,
}: {
reset: () => void;
scale: number;
zoom: (factor: number) => void;
}) {
const onControlsChange = useViewerControlsRegistration();
useKeyedMountEffect(
joinEffectKey([onControlsChange, reset, scale, zoom]),
() => {
if (!onControlsChange) return;
onControlsChange({
zoom: {
scale,
onZoomOut: () => zoom(1 / 1.2),
onZoomIn: () => zoom(1.2),
onReset: reset,
},
});
return () => onControlsChange(null);
},
);
}
function HtmlContentToolbar({
scale,
zoom,
reset,
}: {
scale: number;
zoom: (factor: number) => void;
reset: () => void;
}) {
return (
<div className="bg-card flex h-10 shrink-0 items-center justify-end gap-1 border-b px-2">
<ZoomActions scale={scale} zoom={zoom} reset={reset} />
</div>
);
}
function SandboxedDoc({
contentKey,
html,
title,
scale = 1,
}: {
contentKey: string;
html: string;
title: string;
scale?: number;
}) {
const { containerRef, shouldMountIframe } =
useLazySandboxedIframeMount(contentKey);
const srcDoc = React.useMemo(
() =>
shouldMountIframe
? getHtmlViewerSrcDoc({
contentKey,
html,
})
: "",
[contentKey, html, shouldMountIframe],
);
return (
<div
ref={containerRef}
className="bg-document flex min-h-0 flex-1 flex-col overflow-auto"
>
{shouldMountIframe ? (
<iframe
sandbox=""
srcDoc={srcDoc}
title={title}
className="bg-document h-full min-h-0 w-full flex-1 border-0"
style={{ zoom: scale }}
/>
) : null}
</div>
);
}
function useLazySandboxedIframeMount(resetKey: string) {
const containerRef = React.useRef<HTMLDivElement>(null);
const [shouldMountIframe, setShouldMountIframe] = React.useState(false);
const mountEffectKey = shouldMountIframe
? null
: joinEffectKey(["html-iframe-visible", resetKey]);
useKeyedMountEffect(mountEffectKey, () => {
if (typeof IntersectionObserver === "undefined") {
setShouldMountIframe(true);
return;
}
const node = containerRef.current;
if (!node) {
setShouldMountIframe(true);
return;
}
let observer: IntersectionObserver | null = null;
try {
observer = new IntersectionObserver(
(entries) => {
if (
entries.some(
(entry) => entry.isIntersecting || entry.intersectionRatio > 0,
)
) {
setShouldMountIframe(true);
observer?.disconnect();
}
},
{ rootMargin: "512px" },
);
} catch {
setShouldMountIframe(true);
return;
}
observer.observe(node);
return () => observer.disconnect();
});
return { containerRef, shouldMountIframe };
}
registry/new-york-v4/ui/file-viewer-html-srcdoc-cache.ts
import { lruGet, lruSet } from "./viewer-lru-cache";
const HTML_VIEWER_SRC_DOC_IDENTITIES_PER_CONTENT = 2;
const HTML_VIEWER_DOCUMENT_STYLE = `<style data-retab-html-viewer-padding>
html {
background: white;
}
body {
box-sizing: border-box;
margin: 0;
min-width: 0;
padding: 1rem;
}
body > :first-child {
margin-block-start: 0;
}
body > :last-child {
margin-block-end: 0;
}
</style>`;
interface HtmlViewerSrcDocCacheEntry {
readonly html: string;
readonly srcDoc: string;
}
interface HtmlViewerSrcDocCacheBucket {
readonly entries: HtmlViewerSrcDocCacheEntry[];
}
const htmlViewerSrcDocCache = new Map<string, HtmlViewerSrcDocCacheBucket>();
let htmlViewerSrcDocCacheHitsForTests = 0;
let htmlViewerSrcDocCacheMissesForTests = 0;
export function getHtmlViewerSrcDoc({
contentKey,
html,
}: {
contentKey: string;
html: string;
}) {
const bucket = lruGet(htmlViewerSrcDocCache, contentKey);
const cached = bucket?.entries.find((entry) => entry.html === html);
if (cached) {
htmlViewerSrcDocCacheHitsForTests += 1;
return cached.srcDoc;
}
htmlViewerSrcDocCacheMissesForTests += 1;
const srcDoc = createHtmlViewerSrcDoc(html);
const entries = [...(bucket?.entries ?? []), { html, srcDoc }].slice(
-HTML_VIEWER_SRC_DOC_IDENTITIES_PER_CONTENT,
);
lruSet(htmlViewerSrcDocCache, contentKey, { entries });
return srcDoc;
}
export function clearHtmlViewerSrcDocCacheForTests() {
htmlViewerSrcDocCache.clear();
htmlViewerSrcDocCacheHitsForTests = 0;
htmlViewerSrcDocCacheMissesForTests = 0;
}
export function htmlViewerSrcDocCacheStatsForTests() {
let identities = 0;
for (const bucket of htmlViewerSrcDocCache.values()) {
identities += bucket.entries.length;
}
return {
contentKeys: htmlViewerSrcDocCache.size,
hits: htmlViewerSrcDocCacheHitsForTests,
identities,
misses: htmlViewerSrcDocCacheMissesForTests,
};
}
function createHtmlViewerSrcDoc(html: string) {
const headOpenTag = /<head(?:\s[^>]*)?>/i;
const htmlOpenTag = /<html(?:\s[^>]*)?>/i;
if (headOpenTag.test(html)) {
return html.replace(
headOpenTag,
(tag) => `${tag}${HTML_VIEWER_DOCUMENT_STYLE}`,
);
}
if (htmlOpenTag.test(html)) {
return html.replace(
htmlOpenTag,
(tag) => `${tag}<head>${HTML_VIEWER_DOCUMENT_STYLE}</head>`,
);
}
return `${HTML_VIEWER_DOCUMENT_STYLE}${html}`;
}
registry/new-york-v4/ui/file-viewer-preview.tsx
"use client";
import { cn } from "@/lib/utils";
import {
FileViewerContent,
FileViewerInset,
FileViewerViewport,
} from "./file-viewer-layout";
import { FileViewerDocument } from "./file-viewer-document";
import { FileViewer } from "./file-viewer-shell";
import { FileViewerProvider } from "./file-viewer-provider";
import { type FileViewerResourceOptions } from "./file-viewer-resource-state";
export type FileViewerPreviewProps = FileViewerResourceOptions & {
className?: string;
};
export function FileViewerPreview({
category,
className,
fallbackFrameSize,
fallbackSlideSize,
isolateStyles,
source,
}: FileViewerPreviewProps) {
return (
<FileViewerProvider
category={category}
fallbackFrameSize={fallbackFrameSize}
fallbackSlideSize={fallbackSlideSize}
isolateStyles={isolateStyles}
source={source}
>
<FileViewer
data-file-viewer-mode="preview"
className={cn("h-full min-h-0", className)}
>
<FileViewerContent>
<FileViewerInset>
<FileViewerViewport>
<FileViewerDocument controls />
</FileViewerViewport>
</FileViewerInset>
</FileViewerContent>
</FileViewer>
</FileViewerProvider>
);
}
registry/new-york-v4/ui/file-viewer-provider.tsx
"use client";
import * as React from "react";
import { useKeyedMountEffect } from "@/hooks/use-keyed-mount-effect";
import { joinEffectKey } from "@/lib/effect-key";
import type { FileCategory, ViewerSource } from "@/lib/viewer-source";
import {
FileViewerContext,
type FileViewerContextValue,
type FileViewerHeaderMode,
} from "./file-viewer-context";
import type { FileViewerFallbackSize } from "./file-viewer-core";
import {
FileViewerResourceProvider,
useFileViewerResourceState,
} from "./file-viewer-resource-state";
import {
ViewerControlsRegistrationProvider,
type ViewerControlsState,
} from "./viewer-controls";
import { warnViewerDevelopmentOnce } from "./viewer-diagnostics";
import {
useViewerHeaderOutletAvailable,
useViewerHeaderOutletsAvailable,
} from "./viewer-header-outlet";
export type FileViewerControlsState = ViewerControlsState;
type FileViewerControlsRegistration = {
descriptorKey: string;
state: FileViewerControlsState | null;
};
type FileViewerControlsValue = {
controlsState: FileViewerControlsState | null;
};
const FileViewerControlsContext =
React.createContext<FileViewerControlsValue | null>(null);
function useFileViewerControlsController(descriptorKey: string): {
controlsValue: FileViewerControlsValue;
handleControlsChange: (state: FileViewerControlsState | null) => void;
} {
const [controlsRegistration, setControlsRegistration] =
React.useState<FileViewerControlsRegistration>(() => ({
descriptorKey,
state: null,
}));
const controlsState =
controlsRegistration.descriptorKey === descriptorKey
? controlsRegistration.state
: null;
const handleControlsChange = React.useCallback(
(state: FileViewerControlsState | null) => {
setControlsRegistration({ descriptorKey, state });
},
[descriptorKey],
);
const controlsValue = React.useMemo<FileViewerControlsValue>(
() => ({
controlsState,
}),
[controlsState],
);
return {
controlsValue,
handleControlsChange,
};
}
export function useFileViewerControlsState() {
const context = React.useContext(FileViewerControlsContext);
if (!context) {
throw new Error("File viewer controls must be used within FileViewer.");
}
return context.controlsState;
}
export type FileViewerProviderProps = {
children: React.ReactNode;
source: ViewerSource;
category?: FileCategory;
fallbackFrameSize?: FileViewerFallbackSize;
fallbackSlideSize?: FileViewerFallbackSize;
isolateStyles?: boolean;
headerMode?: FileViewerHeaderMode;
defaultSidebarOpen?: boolean;
sidebarOpen?: boolean;
onSidebarOpenChange?: (open: boolean) => void;
};
export function FileViewerProvider({
children,
source,
category,
fallbackFrameSize,
fallbackSlideSize,
isolateStyles,
headerMode,
defaultSidebarOpen,
sidebarOpen,
onSidebarOpenChange,
}: FileViewerProviderProps) {
const diagnosticViewerId = React.useId();
const resourceState = useFileViewerResourceState({
category,
fallbackFrameSize,
fallbackSlideSize,
isolateStyles,
source,
});
const controlsController = useFileViewerControlsController(
resourceState.descriptorKey,
);
const hasTitleGroupOutlet = useViewerHeaderOutletAvailable("titleGroup");
const hasControlsOutlet = useViewerHeaderOutletAvailable("controls");
const hasHeaderOutlets = useViewerHeaderOutletsAvailable();
const resolvedHeaderMode = headerMode ?? "inline";
const contextValue = React.useMemo<FileViewerContextValue>(
() => ({
headerMode: resolvedHeaderMode,
hasHeaderOutlets: resolvedHeaderMode === "outlets" && hasHeaderOutlets,
isInsideFileViewer: true,
resourceCategory: resourceState.descriptor.category,
sidebarOpenProps: {
defaultOpen: defaultSidebarOpen,
onOpenChange: onSidebarOpenChange,
open: sidebarOpen,
},
}),
[
defaultSidebarOpen,
hasHeaderOutlets,
onSidebarOpenChange,
resolvedHeaderMode,
resourceState.descriptor.category,
sidebarOpen,
],
);
const hasExactlyOneHeaderOutlet =
resolvedHeaderMode === "outlets" &&
hasTitleGroupOutlet !== hasControlsOutlet;
useKeyedMountEffect(
hasExactlyOneHeaderOutlet
? joinEffectKey([
"file-viewer-header-outlet-single-mount",
diagnosticViewerId,
hasTitleGroupOutlet,
hasControlsOutlet,
])
: null,
() => {
warnViewerDevelopmentOnce({
code: "file_viewer_header_outlet_single_mount",
message: "file viewer has exactly one header outlet mounted.",
rootId: diagnosticViewerId,
details: {
controlsOutletAvailable: hasControlsOutlet,
titleGroupOutletAvailable: hasTitleGroupOutlet,
},
});
},
);
return (
<FileViewerResourceProvider value={resourceState}>
<FileViewerControlsContext.Provider
value={controlsController.controlsValue}
>
<ViewerControlsRegistrationProvider
onControlsChange={controlsController.handleControlsChange}
>
<FileViewerContext.Provider value={contextValue}>
{children}
</FileViewerContext.Provider>
</ViewerControlsRegistrationProvider>
</FileViewerControlsContext.Provider>
</FileViewerResourceProvider>
);
}
registry/new-york-v4/ui/file-viewer-renderer-contract.ts
"use client";
import {
getFileViewerMotionRasterInlineSize,
type FileViewerMotionFrame,
type FileViewerMotionPhase,
} from "./file-viewer-motion-plan";
import type { ViewerDocumentTransition } from "./viewer-types";
export type FileViewerDocumentAlign = "start" | "center" | "end";
// Physical inline direction of the document frame (computed CSS `direction`).
// The fit-width motion transform works on the physical X axis, so it needs
// the direction to model where auto-margin alignment actually puts the stage.
export type FileViewerInlineDirection = "ltr" | "rtl";
// `phase` is the motion clock's state; `documentTransition` is the single
// spelling of the policies derived from it. Renderers read layout/scroll/
// visual decisions from the transition, never from duplicated top-level
// fields. `isTransitioning` is shorthand for `phase === "sliding"`.
export type FileViewerRendererFrame = {
align: FileViewerDocumentAlign;
canToggleSidebar: boolean;
direction: FileViewerInlineDirection;
documentTransition: ViewerDocumentTransition;
element: HTMLDivElement | null;
fromInlineSize: number | null;
isTransitioning: boolean;
layoutInlineSize: number | null;
motionDurationMs: number;
phase: FileViewerMotionPhase;
rasterInlineSize: number | null;
settledInlineSize: number | null;
shellInlineSize: number | null;
toInlineSize: number | null;
usesShellGeometry: boolean;
};
export function resolveFileViewerRendererLayoutInlineSize({
fallbackInlineSize,
rendererFrame,
}: {
fallbackInlineSize: number | null;
rendererFrame: FileViewerRendererFrame;
}) {
const fallbackSize = resolveMeasuredInlineSize(fallbackInlineSize);
// Commit-then-relax: the renderer lays out at the motion's TARGET width for
// the entire motion (layoutPolicy "target" from the first sliding frame).
// The in-flight visual is the surface motion transform reprojecting that
// settled layout, so settle never commits layout.
if (
rendererFrame.documentTransition.layoutPolicy === "target" &&
rendererFrame.toInlineSize != null
) {
return rendererFrame.toInlineSize;
}
return rendererFrame.layoutInlineSize ?? fallbackSize;
}
export function createFileViewerRendererFrame({
align,
canToggleSidebar,
direction = "ltr",
element,
fallbackInlineSize,
motionFrame,
motionDurationMs,
usesShellGeometry,
}: {
align: FileViewerDocumentAlign;
canToggleSidebar: boolean;
direction?: FileViewerInlineDirection;
element: HTMLDivElement | null;
fallbackInlineSize: number | null;
motionFrame: FileViewerMotionFrame;
motionDurationMs: number;
usesShellGeometry: boolean;
}): FileViewerRendererFrame {
const measuredInlineSize = resolveMeasuredInlineSize(fallbackInlineSize);
const shellInlineSize = usesShellGeometry
? motionFrame.shellInlineSize
: null;
const layoutInlineSize = usesShellGeometry
? motionFrame.layoutInlineSize
: measuredInlineSize;
const settledInlineSize = usesShellGeometry
? motionFrame.toInlineSize
: measuredInlineSize;
const rasterInlineSize = usesShellGeometry
? getFileViewerMotionRasterInlineSize(motionFrame)
: layoutInlineSize;
const fromInlineSize = usesShellGeometry
? motionFrame.fromInlineSize
: settledInlineSize;
const toInlineSize = usesShellGeometry
? motionFrame.toInlineSize
: settledInlineSize;
const documentTransition = createFileViewerRendererTransition({
motionFrame,
usesShellGeometry,
});
const phase = usesShellGeometry ? motionFrame.phase : "idle";
return {
align,
canToggleSidebar,
direction,
documentTransition,
element,
fromInlineSize,
isTransitioning: phase === "sliding",
layoutInlineSize,
motionDurationMs,
phase,
rasterInlineSize,
settledInlineSize: settledInlineSize ?? layoutInlineSize,
shellInlineSize,
toInlineSize: toInlineSize ?? layoutInlineSize,
usesShellGeometry,
};
}
function resolveMeasuredInlineSize(inlineSize: number | null | undefined) {
return inlineSize != null && Number.isFinite(inlineSize) && inlineSize > 0
? inlineSize
: null;
}
function createFileViewerRendererTransition({
motionFrame,
usesShellGeometry,
}: {
motionFrame: FileViewerMotionFrame;
usesShellGeometry: boolean;
}): ViewerDocumentTransition {
if (!usesShellGeometry) {
return {
layoutPolicy: "live",
scrollPolicy: "preserve",
source: "none",
transitionId: null,
visualPolicy: "none",
};
}
switch (motionFrame.phase) {
// Sliding commits the TARGET layout immediately (inside the toggle's own
// task, before first paint) and rebases scroll to the reading anchor in
// the same commit; the shell transform hides the jump. Settling then has
// no layout or scroll work left — it only clears the identity transform.
case "sliding":
return {
layoutPolicy: "target",
scrollPolicy: "rebase",
source: "viewer-shell",
transitionId: motionFrame.motionId,
visualPolicy: "shell-transform",
};
case "settling":
return {
layoutPolicy: "target",
scrollPolicy: "rebase",
source: "viewer-shell",
transitionId: motionFrame.motionId,
visualPolicy: "shell-transform",
};
case "idle":
return {
layoutPolicy: "live",
scrollPolicy: "preserve",
source: "none",
transitionId: null,
visualPolicy: "none",
};
}
}
registry/new-york-v4/ui/file-viewer-renderer-frame.tsx
"use client";
import * as React from "react";
import {
useOptionalFileViewerShell,
useOptionalFileViewerShellStatic,
useFileViewerShellStatic,
} from "./file-viewer-context";
import type { FileViewerDocumentSurface } from "./file-viewer-motion-kernel";
import {
createFileViewerRendererFrame,
type FileViewerDocumentAlign,
type FileViewerRendererFrame,
} from "./file-viewer-renderer-contract";
import { useFileViewerMotionFrame } from "./file-viewer-motion-kernel";
import { useViewerInlineDirection } from "./viewer-measurement";
export type FileViewerRendererEnvironment = {
registerDocumentSurface: (surface: FileViewerDocumentSurface) => () => void;
usesShellGeometry: boolean;
};
export type FileViewerDocumentFrameState = {
align: FileViewerDocumentAlign;
element: HTMLDivElement | null;
inlineSize: number | null;
};
const FileViewerDocumentFrameContext =
React.createContext<FileViewerDocumentFrameState | null>(null);
export function FileViewerDocumentFrameProvider({
children,
value,
}: {
children: React.ReactNode;
value: FileViewerDocumentFrameState;
}) {
return (
<FileViewerDocumentFrameContext.Provider value={value}>
{children}
</FileViewerDocumentFrameContext.Provider>
);
}
export function useOptionalFileViewerDocumentFrame(): FileViewerDocumentFrameState | null {
return React.useContext(FileViewerDocumentFrameContext);
}
export function useOptionalFileViewerRendererEnvironment(): FileViewerRendererEnvironment {
const { elementRegistry, usesShellGeometry } =
useFileViewerRendererEnvironmentState();
const registerDocumentSurface = React.useCallback(
(surface: FileViewerDocumentSurface) =>
elementRegistry?.registerDocumentSurface(surface) ?? (() => {}),
[elementRegistry],
);
return React.useMemo(
() => ({
registerDocumentSurface,
usesShellGeometry,
}),
[registerDocumentSurface, usesShellGeometry],
);
}
export type FileViewerSidebarMotion = {
/** True when the shell animates the sidebar (inline mode with a toggle). */
isMotionManaged: boolean;
isSidebarInteractive: boolean;
isSidebarOpen: boolean;
isSidebarTransitioning: boolean;
};
export function useOptionalFileViewerSidebarMotion(): FileViewerSidebarMotion | null {
const shell = useOptionalFileViewerShell();
return React.useMemo(
() =>
shell
? {
isMotionManaged: shell.mode === "inline" && shell.canToggleSidebar,
isSidebarInteractive: shell.isSidebarInteractive,
isSidebarOpen: shell.isSidebarOpen,
isSidebarTransitioning: shell.isSidebarTransitioning,
}
: null,
[shell],
);
}
export function useFileViewerRendererFrame({
fallbackInlineSize,
}: {
fallbackInlineSize?: number | null;
} = {}): FileViewerRendererFrame {
useFileViewerShellStatic("useFileViewerRendererFrame");
return useResolvedFileViewerRendererFrame({
fallbackInlineSize,
required: true,
});
}
export function useOptionalFileViewerRendererFrame({
fallbackInlineSize,
}: {
fallbackInlineSize?: number | null;
} = {}): FileViewerRendererFrame {
return useResolvedFileViewerRendererFrame({
fallbackInlineSize,
required: false,
});
}
function useResolvedFileViewerRendererFrame({
fallbackInlineSize,
required,
}: {
fallbackInlineSize?: number | null;
required: boolean;
}): FileViewerRendererFrame {
const { motionFrame, shell, usesShellGeometry } =
useFileViewerRendererEnvironmentState();
const documentFrame = useOptionalFileViewerDocumentFrame();
if (required && !documentFrame) {
throw new Error(
"useFileViewerRendererFrame must be used within FileViewerInset.",
);
}
const fallbackSize =
fallbackInlineSize != null && Number.isFinite(fallbackInlineSize)
? fallbackInlineSize
: null;
// The fit-width motion transform is a physical-X computation, so renderers
// need the frame's computed CSS `direction` alongside its logical align.
const direction = useViewerInlineDirection(documentFrame?.element ?? null);
return React.useMemo(
() =>
createFileViewerRendererFrame({
align: documentFrame?.align ?? "center",
canToggleSidebar: shell?.canToggleSidebar ?? false,
direction,
element: documentFrame?.element ?? null,
fallbackInlineSize: documentFrame?.inlineSize ?? fallbackSize,
motionFrame,
motionDurationMs: shell?.motionDurationMs ?? 0,
usesShellGeometry,
}),
[
direction,
documentFrame?.align,
documentFrame?.element,
documentFrame?.inlineSize,
fallbackSize,
shell?.canToggleSidebar,
motionFrame,
shell?.motionDurationMs,
usesShellGeometry,
],
);
}
function useFileViewerRendererEnvironmentState() {
const shell = useOptionalFileViewerShellStatic();
const motionFrame = useFileViewerMotionFrame(shell?.motionKernel);
const usesShellGeometry = Boolean(
shell &&
motionFrame.shellInlineSize > 0 &&
shell.mode === "inline" &&
(shell.canToggleSidebar || shell.collapsible === "none"),
);
return React.useMemo(
() => ({
elementRegistry: shell?.elementRegistry,
motionFrame,
shell,
usesShellGeometry,
}),
[motionFrame, shell, usesShellGeometry],
);
}
registry/new-york-v4/ui/file-viewer-resource-state.ts
"use client";
import * as React from "react";
import { useKeyedLayoutEffect } from "@/hooks/use-keyed-layout-effect";
import { joinEffectKey } from "@/lib/effect-key";
import {
fileViewerDiagnostic,
nextFileViewerSessionId,
summarizeDiagnosticId,
summarizeViewerDescriptor,
summarizeViewerResource,
} from "@/lib/pdf-viewer-diagnostics";
import {
createViewerResource,
type ViewerResource,
} from "@/lib/viewer-resource";
import {
descriptorResetKey,
type FileDescriptor,
type FileViewerFallbackSize,
type FileViewerCoreProps,
} from "./file-viewer-core";
import { useIsClient } from "./use-is-client";
export type FileViewerResourceOptions = Pick<
FileViewerCoreProps,
| "category"
| "fallbackFrameSize"
| "fallbackSlideSize"
| "isolateStyles"
| "source"
>;
export type FileViewerResourceState = {
descriptor: FileDescriptor;
descriptorKey: string;
descriptorSignal: AbortSignal;
fallbackFrameSize?: FileViewerFallbackSize;
fallbackSlideSize?: FileViewerFallbackSize;
isClient: boolean;
isolateStyles: boolean;
resource: ViewerResource;
};
const FileViewerResourceContext =
React.createContext<FileViewerResourceState | null>(null);
// Per-descriptor cancellation signal. Aborting is deferred a macrotask so a
// keyed remount can keep reusing a shared resource request through React
// StrictMode's mount/unmount/mount sequence.
function useDescriptorSignal(descriptorKey: string): AbortSignal {
const controller = React.useMemo(() => {
void descriptorKey;
return new AbortController();
}, [descriptorKey]);
const abortTimerRef = React.useRef<{
controller: AbortController;
timer: ReturnType<typeof setTimeout>;
} | null>(null);
useKeyedLayoutEffect(joinEffectKey([controller]), () => {
const descriptorKeyHash = summarizeDiagnosticId(descriptorKey);
fileViewerDiagnostic("debug", "file_viewer_descriptor_signal_active", {
descriptorKeyHash,
});
if (abortTimerRef.current?.controller === controller) {
clearTimeout(abortTimerRef.current.timer);
abortTimerRef.current = null;
fileViewerDiagnostic(
"debug",
"file_viewer_descriptor_signal_abort_cancelled",
{ descriptorKeyHash },
);
}
return () => {
fileViewerDiagnostic("debug", "file_viewer_descriptor_abort_scheduled", {
descriptorKeyHash,
isSignalAborted: controller.signal.aborted,
});
const abortTimer = {
controller,
timer: setTimeout(() => {
fileViewerDiagnostic("debug", "file_viewer_descriptor_abort_fired", {
descriptorKeyHash,
isSignalAborted: controller.signal.aborted,
});
controller.abort();
if (abortTimerRef.current === abortTimer) {
abortTimerRef.current = null;
}
}, 0),
};
abortTimerRef.current = abortTimer;
};
});
return controller.signal;
}
export function useFileViewerResourceState({
category,
fallbackFrameSize,
fallbackSlideSize,
isolateStyles = false,
source,
}: FileViewerResourceOptions): FileViewerResourceState {
const isClient = useIsClient();
const resource = React.useMemo(
() => createViewerResource(source, category),
[source, category],
);
const descriptor = resource.descriptor;
const descriptorKey = descriptorResetKey(descriptor);
const viewerSessionId = React.useMemo(() => {
void descriptorKey;
return nextFileViewerSessionId("fv");
}, [descriptorKey]);
const descriptorSignal = useDescriptorSignal(descriptorKey);
useKeyedLayoutEffect(joinEffectKey([descriptorKey, viewerSessionId]), () => {
fileViewerDiagnostic("info", "file_viewer_resource_mount", {
descriptor: summarizeViewerDescriptor(descriptor),
descriptorKeyHash: summarizeDiagnosticId(descriptorKey),
isClient,
isolateStyles,
resource: summarizeViewerResource(resource),
viewerSessionId,
});
return () => {
fileViewerDiagnostic("debug", "file_viewer_resource_unmount", {
descriptor: summarizeViewerDescriptor(descriptor),
descriptorKeyHash: summarizeDiagnosticId(descriptorKey),
resource: summarizeViewerResource(resource),
viewerSessionId,
});
};
});
return React.useMemo<FileViewerResourceState>(
() => ({
descriptor,
descriptorKey,
descriptorSignal,
fallbackFrameSize,
fallbackSlideSize,
isClient,
isolateStyles,
resource,
}),
[
descriptor,
descriptorKey,
descriptorSignal,
fallbackFrameSize,
fallbackSlideSize,
isClient,
isolateStyles,
resource,
],
);
}
export function FileViewerResourceProvider({
children,
value,
}: {
children: React.ReactNode;
value: FileViewerResourceState;
}) {
return React.createElement(
FileViewerResourceContext.Provider,
{ value },
children,
);
}
export function useFileViewerRequiredResourceState(): FileViewerResourceState {
const context = React.useContext(FileViewerResourceContext);
if (!context) {
throw new Error("File viewer parts must be used within FileViewer.");
}
return context;
}
export function useOptionalFileViewerResource(): ViewerResource | null {
return React.useContext(FileViewerResourceContext)?.resource ?? null;
}
export function useFileViewerResource(): ViewerResource {
const resource = useOptionalFileViewerResource();
if (!resource) {
throw new Error("useFileViewerResource must be used within FileViewer.");
}
return resource;
}
registry/new-york-v4/ui/file-viewer-route.tsx
"use client";
import * as React from "react";
import { useKeyedMountEffect } from "@/hooks/use-keyed-mount-effect";
import { inferCsvDialect } from "@/lib/csv";
import { joinEffectKey } from "@/lib/effect-key";
import { cn } from "@/lib/utils";
import { type ViewerResource } from "@/lib/viewer-resource";
import {
fileViewerDiagnostic,
summarizeViewerDescriptor,
summarizeViewerSource,
} from "@/lib/pdf-viewer-diagnostics";
import {
isProseTextDescriptor,
type FileCategory,
type FileDescriptor,
type FileViewerFallbackSize,
} from "./file-viewer-core";
import { UnsupportedCard } from "./file-viewer-fallback";
import { FileViewerHtmlContent } from "./file-viewer-html-viewer";
import {
getFileViewerPrewarmTarget,
scheduleFileViewerRendererPrewarm,
} from "./file-viewer-prewarm";
const PdfResourceContent = React.lazy(() =>
import("@/components/ui/pdf-viewer").then((m) => ({
default: m.PdfResourceContent,
})),
);
const DocxResourceContent = React.lazy(() =>
import("@/components/ui/docx-viewer").then((m) => ({
default: m.DocxResourceContent,
})),
);
const ImageResourceContent = React.lazy(() =>
import("@/components/ui/image-viewer").then((m) => ({
default: m.ImageResourceContent,
})),
);
const PptxResourceContent = React.lazy(() =>
import("@/components/ui/pptx-viewer").then((m) => ({
default: m.PptxResourceContent,
})),
);
const XlsxResourceContent = React.lazy(() =>
import("@/components/ui/xlsx-viewer").then((m) => ({
default: m.XlsxResourceContent,
})),
);
const ProseTextViewer = React.lazy(() =>
import("@/components/ui/text-viewer").then((m) => ({
default: m.TextResourceContent,
})),
);
const MarkdownViewer = React.lazy(() =>
import("@/components/ui/markdown-viewer").then((m) => ({
default: m.MarkdownResourceContent,
})),
);
const CodeTextViewer = React.lazy(() =>
import("@/components/ui/code-viewer").then((m) => ({
default: m.CodeResourceContent,
})),
);
const EmailResourceContent = React.lazy(() =>
import("@/components/ui/email-viewer").then((m) => ({
default: m.EmailResourceContent,
})),
);
const CsvResourceContent = React.lazy(() =>
import("@/components/ui/csv-viewer").then((m) => ({
default: m.CsvResourceContent,
})),
);
function FileViewerCsvContent({
resource,
className,
bare,
controls = false,
isolateStyles,
}: {
resource: ViewerResource;
className?: string;
bare?: boolean;
controls?: boolean;
isolateStyles?: boolean;
}) {
const dialect = inferCsvDialect({
src: resource.content.directUrl ?? undefined,
fileName: resource.fileName,
mimeType: resource.mimeType,
});
return (
<div
data-slot="csv-file-viewer-content"
className={cn(
"bg-card flex min-h-0 flex-1 flex-col overflow-hidden",
bare ? "h-full" : "min-h-64",
className,
)}
>
<CsvResourceContent
resource={resource}
dialect={dialect}
fillHeight
frame={!bare}
controls={controls}
isolateStyles={isolateStyles}
/>
</div>
);
}
export type FileViewerRouteProps = {
className?: string;
descriptor: FileDescriptor;
descriptorSignal: AbortSignal;
fallbackFrameSize?: FileViewerFallbackSize;
fallbackSlideSize?: FileViewerFallbackSize;
frame?: "contained" | "none";
isolateStyles: boolean;
resource: ViewerResource;
controls: boolean;
};
type RenderContext = {
descriptor: FileDescriptor;
resource: ViewerResource;
className?: string;
bare: boolean;
controls: boolean;
fallbackFrameSize?: FileViewerFallbackSize;
fallbackSlideSize?: FileViewerFallbackSize;
isolateStyles: boolean;
descriptorSignal: AbortSignal;
};
// Categories a text-kind source (raw text, no blob/url backing) can render.
const TEXT_SOURCE_CATEGORIES = new Set<FileCategory>([
"csv",
"markdown",
"html",
"email",
"text",
]);
const RENDERERS: Partial<
Record<FileCategory, (ctx: RenderContext) => React.ReactNode>
> = {
pdf: ({ resource, className, bare, controls }) => (
<PdfResourceContent
resource={resource}
className={className}
bare={bare}
controls={controls}
download
/>
),
docx: ({ resource, className, bare, controls }) => (
<DocxResourceContent
resource={resource}
className={className}
bare={bare}
controls={controls}
download
/>
),
image: ({ resource, className, bare, controls, fallbackFrameSize }) => (
<ImageResourceContent
resource={resource}
className={className}
bare={bare}
controls={controls}
download
fallbackFrameSize={fallbackFrameSize}
/>
),
pptx: ({ resource, className, bare, controls, fallbackSlideSize }) => (
<PptxResourceContent
resource={resource}
className={className}
bare={bare}
controls={controls}
download
fallbackSlideSize={fallbackSlideSize}
/>
),
xlsx: ({ resource, className, bare, controls, isolateStyles }) => (
<XlsxResourceContent
resource={resource}
className={className}
bare={bare}
controls={controls}
download
isolateStyles={isolateStyles}
/>
),
csv: ({ resource, className, bare, controls, isolateStyles }) => (
<FileViewerCsvContent
resource={resource}
className={className}
bare={bare}
controls={controls}
isolateStyles={isolateStyles}
/>
),
html: ({ resource, className, bare, controls, descriptorSignal }) => (
<FileViewerHtmlContent
resource={resource}
className={className}
bare={bare}
controls={controls}
descriptorSignal={descriptorSignal}
/>
),
email: ({ resource, className, bare, controls, descriptorSignal }) => (
<EmailResourceContent
resource={resource}
className={className}
bare={bare}
controls={controls}
download
descriptorSignal={descriptorSignal}
/>
),
markdown: ({ resource, className, bare, controls }) => (
<MarkdownViewer
resource={resource}
className={className}
controls={controls}
download
bare={bare}
urlFragmentNavigation={false}
/>
),
text: renderTextViewer,
};
export function FileViewerRoute({
descriptor,
className,
frame = "contained",
isolateStyles,
descriptorSignal,
fallbackFrameSize,
fallbackSlideSize,
resource,
controls,
}: FileViewerRouteProps) {
const { category } = descriptor;
const bare = frame === "none";
const isTextSource = descriptor.source.kind === "text";
const renderer = RENDERERS[category];
const textSourceAllowed =
!isTextSource || TEXT_SOURCE_CATEGORIES.has(category);
const isRouteRenderable = Boolean(renderer) && textSourceAllowed;
const prewarmTarget = getFileViewerPrewarmTarget({
descriptor,
isRouteRenderable,
});
useKeyedMountEffect(
prewarmTarget
? joinEffectKey(["file-viewer-renderer-prewarm", prewarmTarget])
: null,
() => {
if (!prewarmTarget) return;
return scheduleFileViewerRendererPrewarm(prewarmTarget);
},
);
const routeDetails = {
bare,
category,
hasRenderer: Boolean(renderer),
isTextSource,
isolateStyles,
source: summarizeViewerSource(descriptor.source),
descriptor: summarizeViewerDescriptor(descriptor),
controls,
};
if (renderer && textSourceAllowed) {
fileViewerDiagnostic("debug", "file_viewer_route_renderer_selected", {
...routeDetails,
textSourceAllowed,
});
return renderer({
descriptor,
resource,
className,
bare,
controls,
fallbackFrameSize,
fallbackSlideSize,
isolateStyles,
descriptorSignal,
});
}
fileViewerDiagnostic("warn", "file_viewer_route_unsupported", {
...routeDetails,
textSourceAllowed,
});
return (
<UnsupportedCard
resource={resource}
className={className}
bare={bare}
showDownload={controls}
/>
);
}
function renderTextViewer({
descriptor,
resource,
className,
bare,
controls,
}: RenderContext): React.ReactNode {
if (isProseTextDescriptor(descriptor)) {
return (
<ProseTextViewer
resource={resource}
className={className}
controls={controls}
download
bare={bare}
mode="text"
/>
);
}
return (
<CodeTextViewer
resource={resource}
className={className}
controls={controls}
download
bare={bare}
/>
);
}
registry/new-york-v4/ui/file-viewer-prewarm.ts
import {
isProseTextDescriptor,
type FileDescriptor,
type FileCategory,
} from "./file-viewer-core";
export type FileViewerPrewarmTarget =
| "pdf"
| "docx"
| "image"
| "pptx"
| "xlsx"
| "markdown"
| "email"
| "text"
| "code";
type FileViewerPrewarmStatus = "scheduled" | "running" | "complete";
type FileViewerIdleWindow = Window &
typeof globalThis & {
cancelIdleCallback?: Window["cancelIdleCallback"];
requestIdleCallback?: Window["requestIdleCallback"];
};
type FileViewerIdleTask =
| { kind: "idle"; id: number }
| { kind: "timeout"; id: ReturnType<typeof setTimeout> };
type FileViewerPrewarmRecord = {
browserWindow: FileViewerIdleWindow;
isCancelled: boolean;
status: FileViewerPrewarmStatus;
subscribers: number;
task: FileViewerIdleTask | null;
};
const CATEGORY_PREWARM_TARGETS: Partial<
Record<FileCategory, FileViewerPrewarmTarget>
> = {
pdf: "pdf",
docx: "docx",
image: "image",
pptx: "pptx",
xlsx: "xlsx",
markdown: "markdown",
email: "email",
};
const PREWARM_LOADERS: Record<FileViewerPrewarmTarget, () => Promise<void>> = {
pdf: () => import("@/components/ui/pdf-viewer").then(() => undefined),
docx: () => import("@/components/ui/docx-viewer").then(() => undefined),
image: () => import("@/components/ui/image-viewer").then(() => undefined),
pptx: () =>
import("@/components/ui/pptx-viewer").then((module) => {
const preloadViewer = (module as { preloadPptxViewer?: () => void })
.preloadPptxViewer;
preloadViewer?.();
}),
xlsx: () => import("@/components/ui/xlsx-viewer").then(() => undefined),
markdown: () =>
import("@/components/ui/markdown-viewer").then(() => undefined),
email: () => import("@/components/ui/email-viewer").then(() => undefined),
text: () =>
import("@/components/ui/text-viewer-chenglou").then(() => undefined),
code: () => import("@/components/ui/code-viewer").then(() => undefined),
};
const prewarmRecords = new Map<
FileViewerPrewarmTarget,
FileViewerPrewarmRecord
>();
export function getFileViewerPrewarmTarget({
descriptor,
isRouteRenderable,
}: {
descriptor: FileDescriptor;
isRouteRenderable: boolean;
}): FileViewerPrewarmTarget | null {
if (!isRouteRenderable) return null;
if (descriptor.category === "text") {
return isProseTextDescriptor(descriptor) ? "text" : "code";
}
return CATEGORY_PREWARM_TARGETS[descriptor.category] ?? null;
}
export function scheduleFileViewerRendererPrewarm(
target: FileViewerPrewarmTarget,
): () => void {
if (typeof window === "undefined") return noop;
const existingRecord = prewarmRecords.get(target);
if (existingRecord) {
if (existingRecord.status !== "scheduled") return noop;
existingRecord.subscribers += 1;
return () => releaseScheduledPrewarm(target, existingRecord);
}
const browserWindow = window as FileViewerIdleWindow;
const record: FileViewerPrewarmRecord = {
browserWindow,
isCancelled: false,
status: "scheduled",
subscribers: 1,
task: null,
};
record.task = scheduleIdleTask(browserWindow, () => {
if (record.isCancelled) return;
record.status = "running";
record.task = null;
void PREWARM_LOADERS[target]()
.then(() => {
if (prewarmRecords.get(target) === record) {
record.status = "complete";
}
})
.catch(() => {
if (prewarmRecords.get(target) === record) {
prewarmRecords.delete(target);
}
});
});
prewarmRecords.set(target, record);
return () => releaseScheduledPrewarm(target, record);
}
export function resetFileViewerRendererPrewarmForTests() {
prewarmRecords.clear();
}
function releaseScheduledPrewarm(
target: FileViewerPrewarmTarget,
record: FileViewerPrewarmRecord,
) {
if (prewarmRecords.get(target) !== record || record.status !== "scheduled") {
return;
}
record.subscribers -= 1;
if (record.subscribers > 0) return;
record.isCancelled = true;
if (record.task) cancelIdleTask(record.browserWindow, record.task);
prewarmRecords.delete(target);
}
function scheduleIdleTask(
browserWindow: FileViewerIdleWindow,
callback: () => void,
): FileViewerIdleTask {
if (browserWindow.requestIdleCallback) {
return {
kind: "idle",
id: browserWindow.requestIdleCallback(callback, { timeout: 600 }),
};
}
return { kind: "timeout", id: setTimeout(callback, 120) };
}
function cancelIdleTask(
browserWindow: FileViewerIdleWindow,
task: FileViewerIdleTask,
) {
if (task.kind === "idle") {
browserWindow.cancelIdleCallback?.(task.id);
return;
}
clearTimeout(task.id);
}
function noop() {}
registry/new-york-v4/ui/file-viewer-state.tsx
"use client";
import * as React from "react";
import {
AlertCircle,
FileQuestion,
Inbox,
Loader2,
WifiOff,
} from "lucide-react";
import { cn } from "@/lib/utils";
import type { ViewerDownloadAction } from "@/lib/viewer-download-actions";
import {
toViewerErrorInfo,
type ViewerErrorContext,
} from "@/lib/viewer-errors";
import { Button } from "./button";
import { Skeleton } from "./skeleton";
import { ViewerDownloadButton } from "./viewer-download";
type FileViewerStateKind =
| "empty"
| "error"
| "loading"
| "unavailable"
| "unsupported";
export type FileViewerStateProps = Omit<
React.ComponentProps<"div">,
"title"
> & {
action?: React.ReactNode;
description?: React.ReactNode;
icon?: React.ReactNode;
title?: React.ReactNode;
};
export type FileViewerErrorStateProps = FileViewerStateProps &
ViewerErrorContext & {
canDownload?: boolean;
download?: ViewerDownloadAction | null;
error?: unknown;
onRetry?: () => void;
};
export type FileViewerUnsupportedStateProps = FileViewerStateProps & {
download?: ViewerDownloadAction | null;
fileName?: string;
showDownload?: boolean;
};
function stateSlot(kind: FileViewerStateKind) {
return `file-viewer-${kind}-state`;
}
function renderErrorDescription(error: unknown): React.ReactNode {
if (error instanceof Error && error.message) return error.message;
if (typeof error === "string" && error) return error;
return undefined;
}
function FileViewerStateFrame({
action,
children,
className,
description,
icon,
kind,
role,
title,
...props
}: FileViewerStateProps & {
kind: FileViewerStateKind;
role?: React.AriaRole;
}) {
return (
<div
data-slot={stateSlot(kind)}
role={role}
className={cn(
"text-muted-foreground flex min-h-48 min-w-0 flex-col items-center justify-center gap-3 p-6 text-center text-sm",
className,
)}
{...props}
>
{icon ? (
<div
aria-hidden
data-slot="file-viewer-state-icon"
className="text-muted-foreground flex size-9 items-center justify-center"
>
{icon}
</div>
) : null}
<div className="flex max-w-md min-w-0 flex-col items-center gap-1">
{title ? (
<p
data-slot="file-viewer-state-title"
className="text-foreground text-sm font-medium"
>
{title}
</p>
) : null}
{description ? (
<p
data-slot="file-viewer-state-description"
className="text-muted-foreground text-sm"
>
{description}
</p>
) : null}
</div>
{children}
{action ? (
<div
data-slot="file-viewer-state-action"
className="flex items-center justify-center gap-2"
>
{action}
</div>
) : null}
</div>
);
}
export function FileViewerLoadingState({
children,
description = "Preparing the preview.",
icon = <Loader2 className="size-5 animate-spin" />,
title = "Loading file",
...props
}: FileViewerStateProps) {
return (
<FileViewerStateFrame
aria-busy="true"
description={description}
icon={icon}
kind="loading"
title={title}
{...props}
>
{children ?? (
<div
aria-hidden
data-slot="file-viewer-state-skeleton"
className="flex w-full max-w-xs flex-col gap-2"
>
<Skeleton className="h-2 w-full" />
<Skeleton className="h-2 w-3/4 self-center" />
</div>
)}
</FileViewerStateFrame>
);
}
export function FileViewerUnavailableState({
description = "The file is not available right now.",
icon = <WifiOff className="size-5" />,
title = "File unavailable",
...props
}: FileViewerStateProps) {
return (
<FileViewerStateFrame
description={description}
icon={icon}
kind="unavailable"
role="status"
title={title}
{...props}
/>
);
}
export function FileViewerEmptyState({
description = "Choose a file to preview.",
icon = <Inbox className="size-5" />,
title = "No file selected",
...props
}: FileViewerStateProps) {
return (
<FileViewerStateFrame
description={description}
icon={icon}
kind="empty"
role="status"
title={title}
{...props}
/>
);
}
export function FileViewerErrorState({
action,
canDownload,
description,
download,
error,
format = "file",
icon = <AlertCircle className="size-5" />,
onRetry,
retry,
sourceKind,
title,
...props
}: FileViewerErrorStateProps) {
const info =
error === undefined
? null
: toViewerErrorInfo(error, {
canDownload: canDownload ?? Boolean(download && !download.isDisabled),
format,
retry,
sourceKind,
});
const showRetry = Boolean(info?.isRetryable && onRetry);
const showDownload = Boolean(
info?.isDownloadUseful && download && !download.isDisabled,
);
const resolvedAction =
action ??
(showRetry || showDownload ? (
<>
{showRetry ? (
<Button variant="outline" size="sm" onClick={onRetry}>
Retry
</Button>
) : null}
{showDownload && download ? (
<ViewerDownloadButton
action={download}
variant={showRetry ? "ghost" : "outline"}
size="sm"
className=""
showLabel
/>
) : null}
</>
) : null);
return (
<FileViewerStateFrame
action={resolvedAction}
data-error-domain={info?.domain}
data-error-format={info?.format}
data-error-kind={info?.kind}
data-error-message={info?.message}
description={
description ?? info?.userMessage ?? renderErrorDescription(error)
}
icon={icon}
kind="error"
role="alert"
title={title ?? "Could not preview file"}
{...props}
/>
);
}
export function FileViewerUnsupportedState({
description,
download,
fileName,
icon = <FileQuestion className="size-5" />,
showDownload = true,
title = "Preview unsupported",
...props
}: FileViewerUnsupportedStateProps) {
const resolvedDescription =
description ??
(fileName
? `No preview is available for ${fileName}.`
: "No preview is available for this file.");
return (
<FileViewerStateFrame
action={
showDownload && download && !download.isDisabled ? (
<ViewerDownloadButton
action={download}
variant="outline"
size="sm"
className=""
showLabel
/>
) : undefined
}
description={resolvedDescription}
icon={icon}
kind="unsupported"
role="status"
title={title}
{...props}
/>
);
}
registry/new-york-v4/ui/file-viewer-sidebar.tsx
"use client";
import * as React from "react";
import { Slot } from "radix-ui";
import { cn } from "@/lib/utils";
import {
DEFAULT_FILE_VIEWER_SIDEBAR_WIDTH,
type FileViewerSidebarCollapsible,
type FileViewerSidebarSide,
useFileViewerShell,
} from "./file-viewer-context";
import { useFileViewerSidebarController } from "./file-viewer-sidebar-controller";
import { Separator } from "./separator";
export type FileViewerSidebarProps = React.ComponentProps<"aside"> & {
collapsible?: FileViewerSidebarCollapsible;
innerClassName?: string;
side?: FileViewerSidebarSide;
width?: string;
};
export type FileViewerSidebarContentProps = React.ComponentProps<"div">;
export type FileViewerSidebarFooterProps = React.ComponentProps<"div">;
export type FileViewerSidebarHeaderProps = React.ComponentProps<"div">;
export type FileViewerSidebarRailProps = React.ComponentProps<"button"> & {
asChild?: boolean;
};
export type FileViewerSidebarSectionProps = React.ComponentProps<"section">;
export type FileViewerSidebarSectionActionProps =
React.ComponentProps<"button"> & {
asChild?: boolean;
};
export type FileViewerSidebarSectionContentProps = React.ComponentProps<"div">;
export type FileViewerSidebarSectionHeaderProps = React.ComponentProps<"div">;
export type FileViewerSidebarSectionTitleProps = React.ComponentProps<"h3">;
export type FileViewerSidebarSeparatorProps = React.ComponentProps<
typeof Separator
>;
export function FileViewerSidebar({
children,
className,
collapsible,
id,
innerClassName,
side,
style,
width = DEFAULT_FILE_VIEWER_SIDEBAR_WIDTH,
...props
}: FileViewerSidebarProps) {
const sidebar = useFileViewerSidebarController({
collapsible,
id,
side,
style,
width,
});
return (
<div
ref={sidebar.setSidebarGapElement}
data-file-viewer-sidebar-collapsible={sidebar.resolvedCollapsible}
data-file-viewer-sidebar-mode={sidebar.mode}
data-file-viewer-sidebar-open={sidebar.isSidebarOpen ? "true" : "false"}
data-file-viewer-sidebar-side={sidebar.resolvedSide}
data-file-viewer-sidebar-state={sidebar.sidebarState}
data-slot="file-viewer-sidebar-gap"
className={cn(
"group/file-viewer-sidebar z-30 min-h-0 min-w-0 shrink-0 overflow-hidden",
sidebar.isInline
? "relative h-full"
: "absolute inset-y-0 w-0 overflow-visible",
sidebar.resolvedSide === "left" ? "order-first" : "order-last",
!sidebar.isInline && sidebar.resolvedSide === "left" && "left-0",
!sidebar.isInline && sidebar.resolvedSide === "right" && "right-0",
)}
style={
{
...sidebar.customProperties,
} as React.CSSProperties
}
>
<aside
ref={sidebar.setSidebarPanelElement}
id={sidebar.sidebarId}
data-file-viewer-sidebar-collapsible={sidebar.resolvedCollapsible}
data-file-viewer-sidebar-mode={sidebar.mode}
data-file-viewer-sidebar-open={sidebar.isSidebarOpen ? "true" : "false"}
data-file-viewer-sidebar-side={sidebar.resolvedSide}
data-file-viewer-sidebar-state={sidebar.sidebarState}
data-slot="file-viewer-sidebar"
className={cn(
"bg-background absolute inset-y-0 flex min-w-0 flex-col overflow-hidden",
sidebar.resolvedSide === "left"
? "right-0 border-r"
: "left-0 border-l",
!sidebar.isInline && "shadow-lg transition-transform ease-out",
!sidebar.isInline &&
!sidebar.isSidebarOpen &&
sidebar.resolvedSide === "left" &&
"-translate-x-full",
!sidebar.isInline &&
!sidebar.isSidebarOpen &&
sidebar.resolvedSide === "right" &&
"translate-x-full",
className,
)}
style={sidebar.panelStyle}
{...props}
{...sidebar.accessibilityProps}
>
<FileViewerSidebarInner className={innerClassName}>
{children}
</FileViewerSidebarInner>
</aside>
</div>
);
}
const FileViewerSidebarInner = React.memo(function FileViewerSidebarInner({
children,
className,
}: React.ComponentProps<"div">) {
return (
<div
data-slot="file-viewer-sidebar-inner"
className={cn(
"flex h-full min-h-0 w-full min-w-0 flex-col overflow-hidden",
className,
)}
>
{children}
</div>
);
});
export function FileViewerSidebarContent({
className,
...props
}: FileViewerSidebarContentProps) {
return (
<div
data-slot="file-viewer-sidebar-content"
className={cn(
"flex min-h-0 min-w-0 flex-1 flex-col overflow-auto",
className,
)}
{...props}
/>
);
}
export function FileViewerSidebarHeader({
className,
...props
}: FileViewerSidebarHeaderProps) {
return (
<div
data-slot="file-viewer-sidebar-header"
className={cn(
"flex min-h-10 min-w-0 shrink-0 items-center gap-2 border-b px-3",
className,
)}
{...props}
/>
);
}
export function FileViewerSidebarFooter({
className,
...props
}: FileViewerSidebarFooterProps) {
return (
<div
data-slot="file-viewer-sidebar-footer"
className={cn(
"flex min-h-10 min-w-0 shrink-0 items-center gap-2 border-t px-3",
className,
)}
{...props}
/>
);
}
export function FileViewerSidebarRail({
asChild = false,
className,
onClick,
type = "button",
...props
}: FileViewerSidebarRailProps) {
const Comp = asChild ? Slot.Root : "button";
const sidebar = useFileViewerShell("FileViewerSidebarRail");
return (
<Comp
aria-disabled={sidebar.canToggleSidebar ? undefined : true}
aria-label="Toggle sidebar"
data-file-viewer-sidebar-mode={sidebar.mode}
data-file-viewer-sidebar-side={sidebar.side}
data-file-viewer-sidebar-state={sidebar.sidebarState}
data-side={sidebar.side}
data-slot="file-viewer-sidebar-rail"
type={asChild ? undefined : type}
onClick={(event) => {
if (!sidebar.canToggleSidebar) {
event.preventDefault();
return;
}
onClick?.(event);
if (!event.defaultPrevented) {
sidebar.toggleSidebar();
}
}}
className={cn(
"hover:bg-muted absolute inset-y-0 z-40 hidden w-2 transition-colors after:absolute after:inset-y-0 after:left-1/2 after:w-4 after:-translate-x-1/2 md:block",
sidebar.side === "left" ? "right-0" : "left-0",
className,
)}
{...props}
/>
);
}
export function FileViewerSidebarSection({
className,
...props
}: FileViewerSidebarSectionProps) {
return (
<section
data-slot="file-viewer-sidebar-section"
className={cn("relative flex w-full min-w-0 flex-col p-3", className)}
{...props}
/>
);
}
export function FileViewerSidebarSectionHeader({
className,
...props
}: FileViewerSidebarSectionHeaderProps) {
return (
<div
data-slot="file-viewer-sidebar-section-header"
className={cn(
"flex min-h-8 min-w-0 shrink-0 items-center gap-2",
className,
)}
{...props}
/>
);
}
export function FileViewerSidebarSectionTitle({
className,
...props
}: FileViewerSidebarSectionTitleProps) {
return (
<h3
data-slot="file-viewer-sidebar-section-title"
className={cn(
"text-muted-foreground min-w-0 flex-1 truncate px-1 text-xs font-medium",
className,
)}
{...props}
/>
);
}
export function FileViewerSidebarSectionAction({
asChild = false,
className,
type = "button",
...props
}: FileViewerSidebarSectionActionProps) {
const Comp = asChild ? Slot.Root : "button";
return (
<Comp
data-slot="file-viewer-sidebar-section-action"
type={asChild ? undefined : type}
className={cn(
"ring-ring text-muted-foreground hover:bg-accent hover:text-accent-foreground flex size-7 shrink-0 items-center justify-center rounded-md outline-hidden transition-[background-color,color,box-shadow] focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
className,
)}
{...props}
/>
);
}
export function FileViewerSidebarSectionContent({
className,
...props
}: FileViewerSidebarSectionContentProps) {
return (
<div
data-slot="file-viewer-sidebar-section-content"
className={cn("flex min-w-0 flex-col gap-2", className)}
{...props}
/>
);
}
export function FileViewerSidebarSeparator({
className,
...props
}: FileViewerSidebarSeparatorProps) {
return (
<Separator
data-slot="file-viewer-sidebar-separator"
className={cn("mx-3 w-auto", className)}
{...props}
/>
);
}
registry/new-york-v4/ui/file-viewer-motion-plan.ts
"use client";
import type {
FileViewerSidebarMode,
FileViewerSidebarSide,
} from "./file-viewer-context";
export const FILE_VIEWER_MOTION_EPSILON = 0.5;
// The one duration for every sidebar motion timeline — the kernel clock, the
// motion targets, and the overlay panel's CSS transition all read this value.
export const FILE_VIEWER_MOTION_DURATION_MS = 150;
// The one easing for every sidebar motion timeline. Cubic ease-out: the slide
// decelerates into rest. Linear progress ends at full velocity, and content
// far from the reading anchor (the bottom of a tall fit-width document)
// travels several pixels per millisecond straight into a hard stop — a
// visible jolt the anchor line never shows.
export function easeFileViewerMotion(timeProgress: number) {
return 1 - (1 - timeProgress) ** 3;
}
export type FileViewerMotionPhase = "idle" | "sliding" | "settling";
export type FileViewerMotionTarget = {
shellInlineSize: number;
durationMs: number;
mode: FileViewerSidebarMode;
open: boolean;
side: FileViewerSidebarSide;
sidebarWidth: number;
};
export type FileViewerMotionFrame = {
shellInlineSize: number;
durationMs: number;
fromInlineSize: number;
layoutInlineSize: number;
mode: FileViewerSidebarMode;
motionId: number | null;
motionProgress: number;
open: boolean;
phase: FileViewerMotionPhase;
side: FileViewerSidebarSide;
sidebarInlineSize: number;
sidebarWidth: number;
toInlineSize: number;
};
export type FileViewerMotionRestFrame = Pick<
FileViewerMotionFrame,
| "shellInlineSize"
| "durationMs"
| "layoutInlineSize"
| "mode"
| "open"
| "side"
| "sidebarInlineSize"
| "sidebarWidth"
>;
export type FileViewerMotionPlan = {
currentRestFrame: FileViewerMotionRestFrame;
fromInlineSize: number;
nextRestFrame: FileViewerMotionRestFrame;
resolvedTarget: FileViewerMotionTarget;
shouldAnimate: boolean;
};
export function createFileViewerMotionRestFrame(
target: FileViewerMotionTarget,
): FileViewerMotionRestFrame {
const shellInlineSize = target.shellInlineSize;
const sidebarInlineSize =
target.mode === "inline" && target.open
? Math.min(target.sidebarWidth, shellInlineSize)
: 0;
return {
shellInlineSize,
durationMs: target.durationMs,
layoutInlineSize: Math.max(0, shellInlineSize - sidebarInlineSize),
mode: target.mode,
open: target.open,
side: target.side,
sidebarInlineSize,
sidebarWidth: target.sidebarWidth,
};
}
export function createFileViewerIdleMotionFrame(
restFrame: FileViewerMotionRestFrame,
): FileViewerMotionFrame {
return {
...restFrame,
fromInlineSize: restFrame.layoutInlineSize,
motionId: null,
motionProgress: 1,
phase: "idle",
toInlineSize: restFrame.layoutInlineSize,
};
}
export function getFileViewerMotionRasterInlineSize(
frame: Pick<
FileViewerMotionFrame,
"fromInlineSize" | "layoutInlineSize" | "toInlineSize"
>,
): number {
return Math.max(
frame.fromInlineSize,
frame.toInlineSize,
frame.layoutInlineSize,
);
}
export function createFileViewerMotionPlan({
animate,
currentFrame,
nextTarget,
}: {
animate: boolean;
currentFrame: FileViewerMotionFrame;
nextTarget: FileViewerMotionTarget;
}): FileViewerMotionPlan {
const resolvedTarget = resolveFileViewerMotionTarget({
currentFrame,
nextTarget,
});
const currentRestFrame = pickFileViewerMotionRestFrame(currentFrame);
const nextRestFrame = createFileViewerMotionRestFrame(resolvedTarget);
// The motion's visual origin is what is on screen RIGHT NOW: for a fresh
// motion that is the rest layout; for a mid-flight retarget it is the live
// interpolated width, so the new motion's first frame (and every renderer's
// anchor solve) continues from the picture the reader is looking at rather
// than the interrupted motion's origin.
const fromInlineSize =
currentFrame.phase === "sliding"
? currentFrame.layoutInlineSize
: currentRestFrame.layoutInlineSize;
const shouldAnimate =
animate &&
resolvedTarget.mode === "inline" &&
currentFrame.shellInlineSize > 0 &&
Math.abs(
currentRestFrame.sidebarInlineSize - nextRestFrame.sidebarInlineSize,
) > FILE_VIEWER_MOTION_EPSILON &&
resolvedTarget.durationMs > 0;
return {
currentRestFrame,
fromInlineSize,
nextRestFrame,
resolvedTarget,
shouldAnimate,
};
}
export function areFileViewerMotionRestFramesEqual(
previous: FileViewerMotionRestFrame,
next: FileViewerMotionRestFrame,
) {
return (
areFileViewerMotionNumbersEqual(
previous.shellInlineSize,
next.shellInlineSize,
) &&
previous.durationMs === next.durationMs &&
areFileViewerMotionNumbersEqual(
previous.layoutInlineSize,
next.layoutInlineSize,
) &&
previous.mode === next.mode &&
previous.open === next.open &&
previous.side === next.side &&
areFileViewerMotionNumbersEqual(
previous.sidebarInlineSize,
next.sidebarInlineSize,
) &&
areFileViewerMotionNumbersEqual(previous.sidebarWidth, next.sidebarWidth)
);
}
export function areFileViewerMotionFramesEqual(
previous: FileViewerMotionFrame,
next: FileViewerMotionFrame,
) {
return (
areFileViewerMotionRestFramesEqual(previous, next) &&
previous.motionId === next.motionId &&
areFileViewerMotionNumbersEqual(
previous.motionProgress,
next.motionProgress,
) &&
previous.phase === next.phase &&
areFileViewerMotionNumbersEqual(
previous.fromInlineSize,
next.fromInlineSize,
) &&
areFileViewerMotionNumbersEqual(previous.toInlineSize, next.toInlineSize)
);
}
function pickFileViewerMotionRestFrame(
frame: FileViewerMotionFrame,
): FileViewerMotionRestFrame {
return {
shellInlineSize: frame.shellInlineSize,
durationMs: frame.durationMs,
layoutInlineSize: frame.layoutInlineSize,
mode: frame.mode,
open: frame.open,
side: frame.side,
sidebarInlineSize: frame.sidebarInlineSize,
sidebarWidth: frame.sidebarWidth,
};
}
function resolveFileViewerMotionTarget({
currentFrame,
nextTarget,
}: {
currentFrame: FileViewerMotionFrame;
nextTarget: FileViewerMotionTarget;
}): FileViewerMotionTarget {
if (
nextTarget.mode !== "overlay" ||
currentFrame.mode !== "inline" ||
currentFrame.shellInlineSize <= 0
) {
return nextTarget;
}
return {
...nextTarget,
shellInlineSize:
nextTarget.shellInlineSize > 0
? nextTarget.shellInlineSize
: currentFrame.shellInlineSize,
mode: "inline",
};
}
function areFileViewerMotionNumbersEqual(previous: number, next: number) {
return Math.abs(previous - next) <= 0.001;
}
registry/new-york-v4/ui/file-viewer-text-resource.ts
import type {
ViewerContentIdentity,
ViewerContentText,
} from "@/lib/viewer-resource";
import {
abortError,
isAbortError,
subscribeToAbortableRequest,
type SharedAbortableRequest,
} from "./viewer-abortable-request";
import { lruGet } from "./viewer-lru-cache";
async function timed<T>(label: string, fn: () => Promise<T>): Promise<T> {
const on =
typeof globalThis !== "undefined" &&
(globalThis as { __FILE_VIEWER_PROFILE__?: boolean })
.__FILE_VIEWER_PROFILE__;
if (!on) return fn();
const t0 = performance.now();
try {
return await fn();
} finally {
console.log(
`[file-viewer] ${label} ${(performance.now() - t0).toFixed(1)}ms`,
);
}
}
export interface TextResourceSubscription {
content: ViewerContentIdentity & ViewerContentText;
fileName: string;
signal: AbortSignal;
}
export interface TextResourceCache {
load(sub: TextResourceSubscription): Promise<string>;
clear(): void;
size(): number;
}
export function createTextResourceCache(maxEntries = 12): TextResourceCache {
const requests = new Map<string, SharedAbortableRequest<string>>();
function remove(contentKey: string) {
requests.delete(contentKey);
}
function pruneInactiveEntries() {
while (requests.size > maxEntries) {
let pruned = false;
for (const [contentKey, entry] of requests) {
if (!entry.settled && entry.subscribers.size > 0) continue;
requests.delete(contentKey);
if (!entry.settled) entry.controller.abort();
pruned = true;
break;
}
if (!pruned) break;
}
}
function entryFor({
content,
fileName,
}: TextResourceSubscription): SharedAbortableRequest<string> {
const contentKey = content.key;
const existingEntry = lruGet(requests, contentKey);
if (existingEntry) return existingEntry;
const controller = new AbortController();
let entry: SharedAbortableRequest<string> | null = null;
const promise = timed(`text:fetch ${fileName}`, () =>
content.readText({ signal: controller.signal }),
)
.catch((error: unknown) => {
if (!isAbortError(error)) requests.delete(contentKey);
throw error;
})
.finally(() => {
if (entry) {
entry.settled = true;
pruneInactiveEntries();
}
});
entry = {
controller,
promise,
subscriberPromises: new WeakMap(),
subscribers: new Set(),
settled: false,
};
requests.set(contentKey, entry);
return entry;
}
return {
load(sub) {
if (sub.signal.aborted) return Promise.reject(abortError());
const contentKey = sub.content.key;
const entry = entryFor(sub);
const promise = subscribeToAbortableRequest(entry, sub.signal, () =>
remove(contentKey),
);
pruneInactiveEntries();
return promise;
},
clear() {
for (const entry of requests.values()) entry.controller.abort();
requests.clear();
},
size() {
return requests.size;
},
};
}
export const textResource = createTextResourceCache();
export function loadTextResource(
sub: TextResourceSubscription,
): Promise<string> {
return textResource.load(sub);
}
registry/new-york-v4/ui/viewer.tsx
"use client";
export { ViewerBody } from "./viewer-body";
export type { ViewerSidebarTriggerProps } from "./viewer-chrome";
export {
ViewerFrame,
ViewerHeader,
ViewerSidebarTrigger,
} from "./viewer-chrome";
export {
ViewerRoot,
useOptionalViewerSidebar,
useViewerSidebar,
} from "./viewer-root";
export { ViewerSidebar } from "./viewer-sidebar";
export { ViewerSurface, ViewerViewport } from "./viewer-surface";
export type {
ViewerBodyProps,
ViewerDataAttributes,
ViewerFrameProps,
ViewerHeaderProps,
ViewerRootProps,
ViewerSidebarCollapsible,
ViewerSidebarStateValue,
ViewerSidebarMode,
ViewerSidebarProps,
ViewerSidebarRequestedMode,
ViewerSidebarSide,
ViewerSidebarSlotNames,
ViewerSidebarState,
ViewerStateAttributeNamespace,
ViewerStateAttributeSlot,
ViewerStateAttributeValues,
ViewerSurfaceProps,
ViewerViewportProps,
} from "./viewer-types";
registry/new-york-v4/ui/viewer-body.tsx
"use client";
import * as React from "react";
import { useKeyedLayoutEffect } from "@/hooks/use-keyed-layout-effect";
import { joinEffectKey } from "@/lib/effect-key";
import { cn } from "@/lib/utils";
import {
createViewerStateAttributes,
readViewerElementSize,
useOptionalViewerSidebarRegistration,
useViewerDevelopmentLayoutWarning,
} from "./viewer-internals";
import { useOptionalViewerSidebar } from "./viewer-root";
import { warnViewerDevelopmentOnce } from "./viewer-diagnostics";
import type { ViewerBodyProps } from "./viewer-types";
export function ViewerBody({ className, ...props }: ViewerBodyProps) {
const bodyRef = React.useRef<HTMLDivElement | null>(null);
const [bodyElement, setBodyElement] = React.useState<HTMLDivElement | null>(
null,
);
const sidebarState = useOptionalViewerSidebar();
const sidebarRegistration = useOptionalViewerSidebarRegistration();
const namespacedBodyStateAttributes = createViewerStateAttributes(
sidebarRegistration?.stateNamespace,
"body",
{
hasSidebar: sidebarRegistration?.hasSidebar === true,
sidebarMode: sidebarState?.mode,
sidebarOpen: sidebarState?.open,
sidebarSide: sidebarRegistration?.sidebarSide,
sidebarState: sidebarState?.state,
},
);
useViewerDevelopmentLayoutWarning({
enabled: Boolean(sidebarRegistration),
elements: () => [
sidebarRegistration?.getRootElement() ?? null,
bodyRef.current,
],
inspect: () => {
const rootElement = sidebarRegistration?.getRootElement() ?? null;
const bodyElement = bodyRef.current;
const rootSize = readViewerElementSize(rootElement);
const bodySize = readViewerElementSize(bodyElement);
if (rootSize.width > 0 && bodySize.width === 0) {
warnViewerDevelopmentOnce({
code: "viewer_body_zero_width",
message:
"viewer root has nonzero width but viewer body width is zero.",
rootId: sidebarRegistration?.rootId ?? "unknown-root",
details: {
bodyHeight: bodySize.height,
bodySlot: bodyElement?.dataset.slot,
bodyWidth: bodySize.width,
rootHeight: rootSize.height,
rootSlot: rootElement?.dataset.slot,
rootWidth: rootSize.width,
},
});
}
},
keyParts: [
"viewer-body-zero-width",
sidebarRegistration?.rootId,
sidebarRegistration?.getRootElement(),
bodyRef.current,
],
});
const setBodyRef = React.useCallback((element: HTMLDivElement | null) => {
bodyRef.current = element;
setBodyElement(element);
}, []);
useKeyedLayoutEffect(
bodyElement && sidebarRegistration
? joinEffectKey([
"viewer-body:register-geometry",
bodyElement,
sidebarRegistration.registerBody,
])
: null,
() => {
if (!bodyElement || !sidebarRegistration) return;
return sidebarRegistration.registerBody(bodyElement);
},
);
return (
<div
ref={setBodyRef}
data-slot="viewer-body"
data-viewer-has-sidebar={
sidebarRegistration?.hasSidebar === true ? "true" : "false"
}
data-viewer-sidebar-mode={sidebarState?.mode}
data-viewer-sidebar-open={
sidebarState ? (sidebarState.open ? "true" : "false") : undefined
}
data-viewer-sidebar-side={sidebarRegistration?.sidebarSide}
data-viewer-sidebar-state={sidebarState?.state}
data-viewer-slot="body"
{...namespacedBodyStateAttributes}
className={cn(
"relative flex min-h-0 min-w-0 flex-1 overflow-hidden",
className,
)}
{...props}
/>
);
}
registry/new-york-v4/ui/viewer-chrome.tsx
"use client";
import * as React from "react";
import { PanelLeft, PanelRight } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "./button";
import { Spinner } from "./spinner";
import {
isAriaDisabled,
useViewerSidebarState,
useViewerSidebarRegistrationContext,
} from "./viewer-internals";
import type { ViewerFrameProps, ViewerHeaderProps } from "./viewer-types";
// Stock Button no longer exports ButtonProps or a `loading` prop. ViewerSidebarTrigger
// keeps its own `loading` API, so we define ButtonProps locally as the stock Button's
// props plus the retab `loading` flag (translated to disabled + Spinner at the leaf).
type ButtonProps = React.ComponentProps<typeof Button> & {
loading?: boolean;
};
export type ViewerSidebarTriggerProps = ButtonProps;
export function ViewerFrame({ className, ...props }: ViewerFrameProps) {
return (
<div
data-slot="viewer-frame"
className={cn(
"bg-background relative min-h-0 overflow-hidden rounded-xl border",
className,
)}
{...props}
/>
);
}
export function ViewerHeader({ className, ...props }: ViewerHeaderProps) {
return (
<div
data-slot="viewer-header"
className={cn("bg-card flex-shrink-0 border-b", className)}
{...props}
/>
);
}
export function ViewerSidebarTrigger({
className,
disabled,
loading,
onClick,
onPointerDown,
children,
size = "icon",
variant = "ghost",
"aria-label": ariaLabel = "Toggle sidebar",
"aria-disabled": ariaDisabled,
...props
}: ViewerSidebarTriggerProps) {
const {
hasSidebar,
rootId,
sidebarId,
sidebarSide,
setLastTriggerElement,
} = useViewerSidebarRegistrationContext("ViewerSidebarTrigger");
const sidebarState = useViewerSidebarState("ViewerSidebarTrigger");
const { canToggleSidebar, open, state, toggleSidebar } = sidebarState;
const isDisabled = Boolean(
disabled || loading || isAriaDisabled(ariaDisabled) || !canToggleSidebar,
);
const Icon = sidebarSide === "right" ? PanelRight : PanelLeft;
return (
<Button
aria-controls={hasSidebar ? sidebarId : undefined}
aria-disabled={isDisabled ? true : ariaDisabled}
aria-expanded={hasSidebar ? open : undefined}
aria-label={ariaLabel}
className={cn("size-8", className)}
data-side={sidebarSide}
data-slot="viewer-sidebar-trigger"
data-state={state}
data-viewer-root-id={rootId}
data-viewer-sidebar-trigger=""
disabled={isDisabled}
onClick={(event) => {
setLastTriggerElement(event.currentTarget);
if (isDisabled) {
event.preventDefault();
return;
}
onClick?.(event);
if (!event.defaultPrevented) {
toggleSidebar();
}
}}
onPointerDown={(event) => {
setLastTriggerElement(event.currentTarget);
onPointerDown?.(event);
}}
size={size}
variant={variant}
{...props}
>
{loading ? <Spinner className="size-4 animate-spin" /> : null}
{children ?? (
<>
{loading ? null : <Icon />}
<span className="sr-only">Toggle sidebar</span>
</>
)}
</Button>
);
}
registry/new-york-v4/ui/viewer-diagnostics.ts
"use client";
const emittedViewerDevelopmentWarnings = new Set<string>();
export type ViewerDevelopmentWarningDetails = Record<string, unknown>;
export function warnViewerDevelopmentOnce({
code,
details,
message,
rootId,
}: {
code: string;
details?: ViewerDevelopmentWarningDetails;
message: string;
rootId: string;
}) {
if (process.env.NODE_ENV === "production") return;
const warningKey = `${rootId}:${code}`;
if (emittedViewerDevelopmentWarnings.has(warningKey)) return;
emittedViewerDevelopmentWarnings.add(warningKey);
console.warn(`[file-viewer] ${message}`, {
code,
rootId,
...details,
});
}
export function resetViewerDevelopmentWarningsForTests() {
emittedViewerDevelopmentWarnings.clear();
}
registry/new-york-v4/ui/viewer-header-outlet.tsx
"use client";
import { createContext, useContext, useMemo, type ReactNode } from "react";
import { createPortal } from "react-dom";
import { useKeyedMountEffect } from "@/hooks/use-keyed-mount-effect";
export type ViewerHeaderOutletName = "titleGroup" | "controls";
type ViewerHeaderOutletContextValue = {
outlets: Partial<Record<ViewerHeaderOutletName, HTMLElement | null>>;
registerContent?: (name: ViewerHeaderOutletName) => () => void;
};
const ViewerHeaderOutletContext =
createContext<ViewerHeaderOutletContextValue | null>(null);
export function useViewerHeaderOutletAvailable(name: ViewerHeaderOutletName) {
const context = useContext(ViewerHeaderOutletContext);
return Boolean(context?.outlets[name]);
}
export function useViewerHeaderOutletsAvailable(
names: ViewerHeaderOutletName[] = ["titleGroup", "controls"],
) {
const context = useContext(ViewerHeaderOutletContext);
if (!context) return false;
return names.every((name) => Boolean(context.outlets[name]));
}
export function useViewerHeaderOutlets() {
return useViewerHeaderOutletsAvailable();
}
export function ViewerHeaderOutlet({
children,
name,
}: {
children: ReactNode;
name: ViewerHeaderOutletName;
}) {
const context = useContext(ViewerHeaderOutletContext);
const target = context?.outlets[name] ?? null;
const outletMountKey = target ? `viewer-header-outlet:${name}` : null;
useKeyedMountEffect(outletMountKey, () =>
target ? context?.registerContent?.(name) : undefined,
);
if (!target) return null;
return createPortal(children, target);
}
export function ViewerHeaderOutletStart({ children }: { children: ReactNode }) {
const hasHeaderOutlets = useViewerHeaderOutlets();
if (!hasHeaderOutlets) return <>{children}</>;
return <ViewerHeaderOutlet name="titleGroup">{children}</ViewerHeaderOutlet>;
}
export function ViewerHeaderOutletEnd({ children }: { children: ReactNode }) {
const hasHeaderOutlets = useViewerHeaderOutlets();
if (!hasHeaderOutlets) return <>{children}</>;
return <ViewerHeaderOutlet name="controls">{children}</ViewerHeaderOutlet>;
}
export function ViewerHeaderOutletProvider({
children,
outlets,
registerContent,
}: {
children: ReactNode;
outlets: Partial<Record<ViewerHeaderOutletName, HTMLElement | null>>;
registerContent?: (name: ViewerHeaderOutletName) => () => void;
}) {
const value = useMemo(
() => ({ outlets, registerContent }),
[outlets, registerContent],
);
return (
<ViewerHeaderOutletContext.Provider value={value}>
{children}
</ViewerHeaderOutletContext.Provider>
);
}
registry/new-york-v4/ui/viewer-internals.tsx
"use client";
import * as React from "react";
import { useKeyedLayoutEffect } from "@/hooks/use-keyed-layout-effect";
import { joinEffectKey } from "@/lib/effect-key";
import type {
ViewerDataAttributes,
ViewerPortalContainmentAttributes,
ViewerSidebarStateValue,
ViewerGeometrySnapshot,
ViewerGeometryStore,
ViewerGeometryTarget,
ViewerSidebarRegistrationState,
ViewerSidebarRequestedMode,
ViewerSidebarMode,
ViewerStateAttributeNamespace,
ViewerStateAttributeSlot,
ViewerStateAttributeValues,
ViewerSurfaceMeasurement,
} from "./viewer-types";
export const VIEWER_SIDEBAR_INLINE_BREAKPOINT = 768;
export const VIEWER_SIDEBAR_WIDTH = "10rem";
export const VIEWER_ROOT_ID_ATTRIBUTE = "data-viewer-root-id";
export const VIEWER_PORTAL_ROOT_ID_ATTRIBUTE = "data-viewer-portal-root-id";
const VIEWER_SIDEBAR_MODE_HYSTERESIS = 16;
const DEFAULT_VIEWER_GEOMETRY_SNAPSHOT: ViewerGeometrySnapshot = {
bodyInlineSize: 0,
documentInlineSize: 0,
hasMeasuredBody: false,
isTransitioning: false,
mode: "overlay",
open: false,
progress: 1,
sidebarGapTransition: "width",
sidebarInlineSize: 0,
sidebarWidth: 0,
side: "left",
state: "collapsed",
transitionPhase: "idle",
};
const ViewerSidebarStateContext =
React.createContext<ViewerSidebarStateValue | null>(null);
const ViewerSidebarRegistrationContext =
React.createContext<ViewerSidebarRegistrationState | null>(null);
const ViewerSurfaceMeasurementContext =
React.createContext<ViewerSurfaceMeasurement | null>(null);
const ViewerViewportPresenceContext = React.createContext(false);
const VIEWER_GEOMETRY_TRANSITION_MS = 150;
type ViewerGeometryBaseSnapshot = Pick<
ViewerGeometrySnapshot,
| "bodyInlineSize"
| "documentInlineSize"
| "hasMeasuredBody"
| "mode"
| "open"
| "sidebarGapTransition"
| "sidebarInlineSize"
| "sidebarWidth"
| "side"
| "state"
>;
export function ViewerSidebarStateProvider({
children,
value,
}: {
children: React.ReactNode;
value: ViewerSidebarStateValue;
}) {
return React.createElement(
ViewerSidebarStateContext.Provider,
{ value },
children,
);
}
export function ViewerSidebarRegistrationProvider({
children,
value,
}: {
children: React.ReactNode;
value: ViewerSidebarRegistrationState;
}) {
return React.createElement(
ViewerSidebarRegistrationContext.Provider,
{ value },
children,
);
}
export function ViewerSurfaceMeasurementProvider({
children,
value,
}: {
children: React.ReactNode;
value: ViewerSurfaceMeasurement;
}) {
return React.createElement(
ViewerSurfaceMeasurementContext.Provider,
{ value },
children,
);
}
export function ViewerViewportPresenceProvider({
children,
}: {
children: React.ReactNode;
}) {
return React.createElement(
ViewerViewportPresenceContext.Provider,
{ value: true },
children,
);
}
export function useOptionalViewerSidebarState(): ViewerSidebarStateValue | null {
return React.useContext(ViewerSidebarStateContext);
}
export function useViewerSidebarState(
consumer: string,
): ViewerSidebarStateValue {
const context = useOptionalViewerSidebarState();
if (!context) {
throw new Error(`${consumer} must be used within a ViewerRoot.`);
}
return context;
}
export function useOptionalViewerSidebarRegistration(): ViewerSidebarRegistrationState | null {
return React.useContext(ViewerSidebarRegistrationContext);
}
export function useOptionalViewerSurfaceMeasurementContext(): ViewerSurfaceMeasurement | null {
return React.useContext(ViewerSurfaceMeasurementContext);
}
export function useViewerViewportPresence(): boolean {
return React.useContext(ViewerViewportPresenceContext);
}
export function useViewerGeometrySnapshot(
geometryStore: ViewerGeometryStore,
): ViewerGeometrySnapshot {
return React.useSyncExternalStore(
geometryStore.subscribe,
geometryStore.getSnapshot,
geometryStore.getSnapshot,
);
}
export function createViewerGeometryStore(): ViewerGeometryStore {
const listeners = new Set<() => void>();
let snapshot = DEFAULT_VIEWER_GEOMETRY_SNAPSHOT;
let transitionTargetSnapshot: ViewerGeometryBaseSnapshot | null = null;
let transitionFrame = 0;
let transitionStartTime: number | null = null;
let transitionStartSidebarInlineSize =
DEFAULT_VIEWER_GEOMETRY_SNAPSHOT.sidebarInlineSize;
const notify = () => {
for (const listener of listeners) listener();
};
const commitSnapshot = (nextSnapshot: ViewerGeometrySnapshot) => {
if (areViewerGeometrySnapshotsEqual(snapshot, nextSnapshot)) return;
snapshot = nextSnapshot;
notify();
};
const cancelTransitionFrame = () => {
if (transitionFrame === 0) return;
const cancelAnimationFrame =
globalThis.cancelAnimationFrame ??
(typeof window === "undefined" ? undefined : window.cancelAnimationFrame);
cancelAnimationFrame?.(transitionFrame);
transitionFrame = 0;
};
const commitGeometryProgress = (progress: number) => {
if (!transitionTargetSnapshot) return;
const sidebarInlineSize = lerp(
transitionStartSidebarInlineSize,
transitionTargetSnapshot.sidebarInlineSize,
progress,
);
commitSnapshot({
...transitionTargetSnapshot,
documentInlineSize: getViewerGeometryDocumentInlineSize({
bodyInlineSize: transitionTargetSnapshot.bodyInlineSize,
mode: transitionTargetSnapshot.mode,
sidebarGapTransition: transitionTargetSnapshot.sidebarGapTransition,
sidebarInlineSize,
}),
isTransitioning: progress < 1,
progress,
sidebarInlineSize,
transitionPhase: progress < 1 ? "sliding" : "idle",
});
if (progress >= 1) {
transitionTargetSnapshot = null;
}
};
const scheduleTransitionFrame = () => {
const requestAnimationFrame =
globalThis.requestAnimationFrame ??
(typeof window === "undefined" ? undefined : window.requestAnimationFrame);
if (typeof requestAnimationFrame !== "function") {
commitGeometryProgress(1);
return;
}
transitionFrame = requestAnimationFrame((timestamp) => {
transitionFrame = 0;
if (!transitionTargetSnapshot) return;
if (transitionStartTime === null) transitionStartTime = timestamp;
const progress = clamp01(
(timestamp - transitionStartTime) / VIEWER_GEOMETRY_TRANSITION_MS,
);
commitGeometryProgress(progress);
if (progress < 1) scheduleTransitionFrame();
});
};
const startTransition = (targetSnapshot: ViewerGeometryBaseSnapshot) => {
cancelTransitionFrame();
transitionTargetSnapshot = targetSnapshot;
transitionStartSidebarInlineSize = snapshot.sidebarInlineSize;
transitionStartTime = readViewerGeometryNow();
commitGeometryProgress(0);
scheduleTransitionFrame();
};
return {
getSnapshot: () => snapshot,
setTarget: (target) => {
const resolvedSidebarWidth =
Number.isFinite(target.sidebarWidth) && target.sidebarWidth > 0
? target.sidebarWidth
: readViewerGeometrySidebarWidth(target);
const sidebarWidth =
resolvedSidebarWidth > 0 ? resolvedSidebarWidth : snapshot.sidebarWidth;
const measuredBodyInlineSize = readViewerGeometryBodyInlineSize(target);
const bodyInlineSize =
measuredBodyInlineSize > 0
? measuredBodyInlineSize
: snapshot.bodyInlineSize;
const hasMeasuredBody =
measuredBodyInlineSize > 0 || snapshot.hasMeasuredBody;
const sidebarInlineSize = getViewerGeometryTargetSidebarInlineSize({
mode: target.mode,
open: target.open,
sidebarGapTransition: target.sidebarGapTransition,
sidebarWidth,
});
const nextBaseSnapshot: ViewerGeometryBaseSnapshot = {
bodyInlineSize,
documentInlineSize: getViewerGeometryDocumentInlineSize({
bodyInlineSize,
mode: target.mode,
sidebarGapTransition: target.sidebarGapTransition,
sidebarInlineSize,
}),
hasMeasuredBody,
mode: target.mode,
open: target.open,
sidebarGapTransition: target.sidebarGapTransition,
sidebarInlineSize,
sidebarWidth,
side: target.side,
state: target.state,
};
if (
snapshot.isTransitioning &&
transitionTargetSnapshot &&
areViewerGeometryTargetsEqual(
transitionTargetSnapshot,
nextBaseSnapshot,
)
) {
return;
}
const shouldAnimate = shouldAnimateViewerGeometry({
previousSnapshot: snapshot,
targetSnapshot: nextBaseSnapshot,
});
if (!shouldAnimate) {
cancelTransitionFrame();
transitionTargetSnapshot = null;
commitSnapshot({
...nextBaseSnapshot,
isTransitioning: false,
progress: 1,
transitionPhase: "idle",
});
return;
}
startTransition(nextBaseSnapshot);
},
subscribe: (listener) => {
listeners.add(listener);
return () => {
listeners.delete(listener);
};
},
};
}
export function resolveSidebarMode({
requestedMode,
width,
inlineBreakpoint,
}: {
requestedMode: ViewerSidebarRequestedMode;
width: number | null;
inlineBreakpoint: number;
}): ViewerSidebarMode {
if (requestedMode !== "auto") return requestedMode;
if (width === null) return "overlay";
return width >= inlineBreakpoint ? "inline" : "overlay";
}
export function resolveMeasuredSidebarMode({
currentMode,
hasMeasured,
inlineBreakpoint,
requestedMode,
width,
}: {
currentMode: ViewerSidebarMode;
hasMeasured: boolean;
inlineBreakpoint: number;
requestedMode: ViewerSidebarRequestedMode;
width: number;
}): ViewerSidebarMode {
if (requestedMode !== "auto") return requestedMode;
if (!hasMeasured) {
return width >= inlineBreakpoint ? "inline" : "overlay";
}
if (currentMode === "inline") {
return width < inlineBreakpoint - VIEWER_SIDEBAR_MODE_HYSTERESIS
? "overlay"
: "inline";
}
return width > inlineBreakpoint + VIEWER_SIDEBAR_MODE_HYSTERESIS
? "inline"
: "overlay";
}
export function isAriaDisabled(value: unknown): boolean {
return value === true || value === "true";
}
export function pickCssCustomProperties(
style: React.CSSProperties | undefined,
): React.CSSProperties {
if (!style) return {};
return Object.fromEntries(
Object.entries(style).filter(([name]) => name.startsWith("--")),
) as React.CSSProperties;
}
function isViewerStateNamespaceEnabled(
namespace: ViewerStateAttributeNamespace | undefined,
slot: ViewerStateAttributeSlot,
): namespace is ViewerStateAttributeNamespace {
if (!namespace) return false;
return namespace.slots?.[slot] ?? true;
}
export function createViewerStateAttributes(
namespace: ViewerStateAttributeNamespace | undefined,
slot: ViewerStateAttributeSlot,
values: ViewerStateAttributeValues,
): ViewerDataAttributes {
if (!isViewerStateNamespaceEnabled(namespace, slot)) return {};
const attributes: ViewerDataAttributes = {};
const prefix = namespace.prefix;
if (values.hasSidebar !== undefined) {
attributes[`data-${prefix}-has-sidebar`] = values.hasSidebar
? "true"
: "false";
}
if (values.sidebarCollapsible !== undefined) {
attributes[`data-${prefix}-sidebar-collapsible`] =
values.sidebarCollapsible;
}
if (values.sidebarMode !== undefined) {
attributes[`data-${prefix}-sidebar-mode`] = values.sidebarMode;
}
if (values.sidebarOpen !== undefined) {
attributes[`data-${prefix}-sidebar-open`] = values.sidebarOpen
? "true"
: "false";
}
if (values.sidebarSide !== undefined) {
attributes[`data-${prefix}-sidebar-side`] = values.sidebarSide;
}
if (values.sidebarState !== undefined) {
attributes[`data-${prefix}-sidebar-state`] = values.sidebarState;
}
return attributes;
}
export function createViewerSlotAttributes(
namespace: ViewerStateAttributeNamespace | undefined,
slot: ViewerStateAttributeSlot,
value: string | undefined,
): ViewerDataAttributes {
if (!value || !isViewerStateNamespaceEnabled(namespace, slot)) return {};
return {
[`data-${namespace.prefix}-slot`]: value,
};
}
export function createViewerPortalContainmentAttributes(
rootId: string,
): ViewerPortalContainmentAttributes {
return {
[VIEWER_PORTAL_ROOT_ID_ATTRIBUTE]: rootId,
};
}
export function readViewerElementSize(element: HTMLElement | null) {
if (!element) return { height: 0, width: 0 };
const rect =
typeof element.getBoundingClientRect === "function"
? element.getBoundingClientRect()
: null;
return {
height: rect?.height || element.clientHeight || 0,
width: rect?.width || element.clientWidth || 0,
};
}
function readViewerGeometrySidebarWidth({
rootElement,
sidebarElement,
}: ViewerGeometryTarget) {
const elementSize = readViewerElementSize(sidebarElement);
if (Number.isFinite(elementSize.width) && elementSize.width > 0) {
return elementSize.width;
}
return readViewerSidebarDeclaredWidth(rootElement, sidebarElement);
}
function readViewerGeometryBodyInlineSize({
bodyElement,
rootElement,
}: ViewerGeometryTarget) {
const bodySize = readViewerElementSize(bodyElement);
if (Number.isFinite(bodySize.width) && bodySize.width > 0) {
return bodySize.width;
}
const rootSize = readViewerElementSize(rootElement);
return Number.isFinite(rootSize.width) && rootSize.width > 0
? rootSize.width
: 0;
}
function readViewerSidebarDeclaredWidth(
rootElement: HTMLElement | null,
sidebarElement: HTMLElement | null,
) {
return (
readViewerCssLength(rootElement, "--viewer-sidebar-width") ??
readViewerCssLength(sidebarElement, "--viewer-sidebar-width") ??
0
);
}
function readViewerCssLength(
element: HTMLElement | null,
propertyName: string,
) {
if (!element || typeof window === "undefined") return null;
const styleValue = element.style.getPropertyValue(propertyName);
const computedValue =
window.getComputedStyle(element).getPropertyValue(propertyName);
return parseViewerCssLength(styleValue) ?? parseViewerCssLength(computedValue);
}
function parseViewerCssLength(value: string) {
const trimmedValue = value.trim();
if (!trimmedValue.endsWith("px")) return null;
const length = Number.parseFloat(trimmedValue);
return Number.isFinite(length) && length > 0 ? length : null;
}
function areViewerGeometrySnapshotsEqual(
previous: ViewerGeometrySnapshot,
next: ViewerGeometrySnapshot,
) {
return (
previous.bodyInlineSize === next.bodyInlineSize &&
previous.documentInlineSize === next.documentInlineSize &&
previous.hasMeasuredBody === next.hasMeasuredBody &&
previous.isTransitioning === next.isTransitioning &&
previous.mode === next.mode &&
previous.open === next.open &&
previous.progress === next.progress &&
previous.sidebarGapTransition === next.sidebarGapTransition &&
previous.sidebarInlineSize === next.sidebarInlineSize &&
previous.sidebarWidth === next.sidebarWidth &&
previous.side === next.side &&
previous.state === next.state &&
previous.transitionPhase === next.transitionPhase
);
}
function areViewerGeometryTargetsEqual(
previous: ViewerGeometryBaseSnapshot,
next: ViewerGeometryBaseSnapshot,
) {
return (
previous.bodyInlineSize === next.bodyInlineSize &&
previous.documentInlineSize === next.documentInlineSize &&
previous.hasMeasuredBody === next.hasMeasuredBody &&
previous.mode === next.mode &&
previous.open === next.open &&
previous.sidebarGapTransition === next.sidebarGapTransition &&
previous.sidebarInlineSize === next.sidebarInlineSize &&
previous.sidebarWidth === next.sidebarWidth &&
previous.side === next.side &&
previous.state === next.state
);
}
function shouldAnimateViewerGeometry({
previousSnapshot,
targetSnapshot,
}: {
previousSnapshot: ViewerGeometrySnapshot;
targetSnapshot: ViewerGeometryBaseSnapshot;
}) {
return (
previousSnapshot.sidebarWidth > 0 &&
targetSnapshot.mode === "inline" &&
targetSnapshot.sidebarGapTransition === "width" &&
targetSnapshot.sidebarWidth > 0 &&
hasViewerGeometryAnimationSupport() &&
!prefersReducedViewerGeometryMotion() &&
!areViewerGeometryInlineSizesEqual(
previousSnapshot.sidebarInlineSize,
targetSnapshot.sidebarInlineSize,
)
);
}
function getViewerGeometryTargetSidebarInlineSize(
snapshot: Pick<
ViewerGeometryBaseSnapshot,
"mode" | "open" | "sidebarGapTransition" | "sidebarWidth"
>,
) {
return snapshot.mode === "inline" && snapshot.open && snapshot.sidebarWidth > 0
? snapshot.sidebarWidth
: 0;
}
function getViewerGeometryDocumentInlineSize({
bodyInlineSize,
mode,
sidebarGapTransition,
sidebarInlineSize,
}: {
bodyInlineSize: number;
mode: ViewerSidebarMode;
sidebarGapTransition: string;
sidebarInlineSize: number;
}) {
if (bodyInlineSize <= 0) return 0;
if (mode !== "inline" || sidebarGapTransition !== "width") {
return bodyInlineSize;
}
return Math.max(1, bodyInlineSize - sidebarInlineSize);
}
function areViewerGeometryInlineSizesEqual(
previousInlineSize: number,
nextInlineSize: number,
) {
return Math.abs(previousInlineSize - nextInlineSize) < 0.5;
}
function hasViewerGeometryAnimationSupport() {
const requestAnimationFrame =
globalThis.requestAnimationFrame ??
(typeof window === "undefined" ? undefined : window.requestAnimationFrame);
return typeof requestAnimationFrame === "function";
}
function prefersReducedViewerGeometryMotion() {
return (
typeof window !== "undefined" &&
typeof window.matchMedia === "function" &&
window.matchMedia("(prefers-reduced-motion: reduce)").matches
);
}
function readViewerGeometryNow() {
const performanceNow =
globalThis.performance?.now ??
(typeof window === "undefined" ? undefined : window.performance?.now);
return typeof performanceNow === "function"
? performanceNow.call(globalThis.performance ?? window.performance)
: Date.now();
}
function lerp(start: number, end: number, progress: number) {
return start + (end - start) * progress;
}
function clamp01(value: number) {
if (!Number.isFinite(value)) return 1;
return Math.min(1, Math.max(0, value));
}
export function elementContainsTarget(
element: HTMLElement | null | undefined,
target: unknown,
): boolean {
if (!element || target === null || target === undefined) return false;
if (element === target) return true;
if (typeof element.contains === "function") {
try {
if (element.contains(target as Node)) return true;
} catch {
// Some test and embedded DOM hosts do not share a Node prototype chain.
}
}
if (typeof target !== "object") return false;
let current: unknown = (target as { parentNode?: unknown }).parentNode;
while (current && typeof current === "object") {
if (current === element) return true;
current = (current as { parentNode?: unknown }).parentNode;
}
return false;
}
export function targetHasViewerPortalContainment({
rootId,
target,
}: {
rootId: string;
target: unknown;
}): boolean {
if (!target || typeof target !== "object") return false;
if (typeof (target as { closest?: unknown }).closest === "function") {
try {
const ownerElement = (
target as {
closest: (selector: string) => {
getAttribute?: (attributeName: string) => string | null;
} | null;
}
).closest(`[${VIEWER_PORTAL_ROOT_ID_ATTRIBUTE}]`);
if (
ownerElement?.getAttribute?.(VIEWER_PORTAL_ROOT_ID_ATTRIBUTE) === rootId
) {
return true;
}
} catch {
// Some test and embedded DOM hosts do not support Element.closest.
}
}
let current: unknown = target;
while (current && typeof current === "object") {
const getAttribute = (current as { getAttribute?: unknown }).getAttribute;
if (
typeof getAttribute === "function" &&
getAttribute.call(current, VIEWER_PORTAL_ROOT_ID_ATTRIBUTE) === rootId
) {
return true;
}
current = (current as { parentNode?: unknown }).parentNode;
}
return false;
}
export function targetViewerRootId(target: unknown): string | null {
if (!target || typeof target !== "object") return null;
if (typeof (target as { closest?: unknown }).closest === "function") {
try {
const rootElement = (
target as {
closest: (selector: string) => {
getAttribute?: (attributeName: string) => string | null;
} | null;
}
).closest(`[${VIEWER_ROOT_ID_ATTRIBUTE}]`);
const rootId = rootElement?.getAttribute?.(VIEWER_ROOT_ID_ATTRIBUTE);
if (rootId) return rootId;
} catch {
// Some test and embedded DOM hosts do not support Element.closest.
}
}
let current: unknown = target;
while (current && typeof current === "object") {
const getAttribute = (current as { getAttribute?: unknown }).getAttribute;
if (typeof getAttribute === "function") {
const rootId = getAttribute.call(current, VIEWER_ROOT_ID_ATTRIBUTE);
if (typeof rootId === "string" && rootId) {
return rootId;
}
}
current = (current as { parentNode?: unknown }).parentNode;
}
return null;
}
export function findClosestViewerBody(
element: HTMLDivElement | null,
): HTMLElement | null {
if (!element || typeof element.closest !== "function") return null;
return element.closest<HTMLElement>('[data-viewer-slot="body"]');
}
export function useViewerDevelopmentLayoutWarning({
enabled = true,
elements,
inspect,
keyParts,
}: {
enabled?: boolean;
elements: () => Array<HTMLElement | null>;
inspect: () => void;
keyParts: unknown[];
}) {
useKeyedLayoutEffect(enabled ? joinEffectKey(keyParts) : null, () => {
if (process.env.NODE_ENV === "production" || !enabled) return;
const observedElements = elements().filter(
(element): element is HTMLElement => Boolean(element),
);
let frame: number | null = null;
const run = () => {
frame = null;
inspect();
};
const schedule = () => {
if (frame !== null) return;
if (typeof requestAnimationFrame === "function") {
frame = requestAnimationFrame(run);
return;
}
run();
};
schedule();
const ResizeObserverConstructor = globalThis.ResizeObserver;
if (
observedElements.length === 0 ||
typeof ResizeObserverConstructor === "undefined"
) {
return () => {
if (frame !== null && typeof cancelAnimationFrame === "function") {
cancelAnimationFrame(frame);
}
};
}
const observer = new ResizeObserverConstructor(schedule);
for (const element of observedElements) {
observer.observe(element);
}
return () => {
if (frame !== null && typeof cancelAnimationFrame === "function") {
cancelAnimationFrame(frame);
}
observer.disconnect();
};
});
}
export function useViewerSidebarRegistrationContext(
consumer: string,
): ViewerSidebarRegistrationState {
const context = useOptionalViewerSidebarRegistration();
if (!context) {
throw new Error(`${consumer} must be used within a ViewerRoot.`);
}
return context;
}
registry/new-york-v4/ui/viewer-measurement.ts
"use client";
import * as React from "react";
import { useKeyedLayoutEffect } from "@/hooks/use-keyed-layout-effect";
import { joinEffectKey } from "@/lib/effect-key";
export type StableElementSize<Element extends HTMLElement = HTMLElement> = {
element: Element | null;
hasMeasured: boolean;
height: number | null;
setElement: React.RefCallback<Element>;
width: number | null;
};
export type StableElementSizeOptions = {
enabled?: boolean;
observe?: boolean;
retainLastNonZero?: boolean;
};
export type StableCssLengthOptions = {
element: HTMLElement | null;
retainLastNonZero?: boolean;
value: string;
};
type MeasuredSize = {
height: number | null;
width: number | null;
};
type RawMeasuredSize = {
height: number;
width: number;
};
// DOM layout reads for viewer chrome are quarantined in this module. The
// file-viewer motion kernel (time + style writes only) receives this reader by
// injection from the frame controller instead of touching layout APIs itself.
export function readElementRectSnapshot(
element: HTMLElement | null,
): readonly number[] {
if (!element) return [];
const rect = element.getBoundingClientRect();
return [rect.left, rect.top, rect.width, rect.height];
}
// Computed CSS inline direction of an element, sampled when it attaches (a
// runtime `dir` flip is picked up on the next mount). The fit-width motion
// transform works on the physical X axis, so the renderer frame needs to
// know which edge auto-margin alignment pins the stage to.
export function useViewerInlineDirection(
element: HTMLElement | null,
): "ltr" | "rtl" {
const [direction, setDirection] = React.useState<"ltr" | "rtl">("ltr");
useKeyedLayoutEffect(element ? joinEffectKey([element]) : null, () => {
if (!element) return;
setDirection(getComputedStyle(element).direction === "rtl" ? "rtl" : "ltr");
});
return direction;
}
function readElementSize(element: HTMLElement): RawMeasuredSize {
const rect =
typeof element.getBoundingClientRect === "function"
? element.getBoundingClientRect()
: null;
return {
height: rect?.height || element.clientHeight,
width: rect?.width || element.clientWidth,
};
}
function resolveMeasuredElementSize({
currentSize,
nextSize,
retainLastNonZero,
}: {
currentSize: MeasuredSize;
nextSize: RawMeasuredSize;
retainLastNonZero: boolean;
}): MeasuredSize {
const width =
Number.isFinite(nextSize.width) &&
(!retainLastNonZero || nextSize.width > 0)
? nextSize.width
: currentSize.width;
const height =
Number.isFinite(nextSize.height) &&
(!retainLastNonZero || nextSize.height > 0)
? nextSize.height
: currentSize.height;
if (currentSize.width === width && currentSize.height === height) {
return currentSize;
}
return { height, width };
}
export function useStableElementSize<Element extends HTMLElement = HTMLElement>(
options: StableElementSizeOptions = {},
): StableElementSize<Element> {
const enabled = options.enabled ?? true;
const observe = options.observe ?? true;
const retainLastNonZero = options.retainLastNonZero ?? false;
const [element, setElementState] = React.useState<Element | null>(null);
const [size, setSize] = React.useState<MeasuredSize>({
height: null,
width: null,
});
const hasMeasured = size.height !== null || size.width !== null;
const setElement = React.useCallback((nextElement: Element | null) => {
setElementState(nextElement);
}, []);
useKeyedLayoutEffect(enabled ? null : "reset", () => {
setSize({ height: null, width: null });
});
useKeyedLayoutEffect(
enabled && element
? joinEffectKey([element, observe, retainLastNonZero])
: null,
() => {
if (!element) return;
setSize((currentSize) =>
resolveMeasuredElementSize({
currentSize,
nextSize: readElementSize(element),
retainLastNonZero,
}),
);
const ResizeObserverConstructor = observe
? globalThis.ResizeObserver
: undefined;
if (typeof ResizeObserverConstructor === "undefined") return;
let frame = 0;
let latestSize = readElementSize(element);
const observer = new ResizeObserverConstructor((entries) => {
for (const entry of entries) {
latestSize = readElementSize(entry.target as HTMLElement);
}
if (frame) return;
frame = requestAnimationFrame(() => {
frame = 0;
setSize((currentSize) =>
resolveMeasuredElementSize({
currentSize,
nextSize: latestSize,
retainLastNonZero,
}),
);
});
});
observer.observe(element);
return () => {
if (frame) cancelAnimationFrame(frame);
observer.disconnect();
};
},
);
return React.useMemo(
() => ({
element,
hasMeasured,
height: size.height,
setElement,
width: size.width,
}),
[element, hasMeasured, setElement, size.height, size.width],
);
}
export function useStableCssLength({
element,
retainLastNonZero = true,
value,
}: StableCssLengthOptions) {
const [resolvedLength, setResolvedLength] = React.useState(0);
useKeyedLayoutEffect(
value ? joinEffectKey([element, retainLastNonZero, value]) : null,
() => {
const nextLength = resolveCssLength(value, element);
setResolvedLength((currentLength) => {
if (retainLastNonZero && nextLength <= 0) return currentLength;
return areCssLengthsEqual(currentLength, nextLength)
? currentLength
: nextLength;
});
},
);
return resolvedLength;
}
function resolveCssLength(value: string, element: HTMLElement | null) {
const trimmedValue = value.trim();
const pixelMatch = trimmedValue.match(/^(-?\d+(?:\.\d+)?)px$/);
if (pixelMatch) return Math.max(0, Number(pixelMatch[1]));
if (typeof window === "undefined") return 0;
const remMatch = trimmedValue.match(/^(-?\d+(?:\.\d+)?)rem$/);
if (remMatch) {
return (
Math.max(0, Number(remMatch[1])) *
readComputedFontSize(window.document.documentElement)
);
}
const emMatch = trimmedValue.match(/^(-?\d+(?:\.\d+)?)em$/);
if (emMatch) {
return Math.max(0, Number(emMatch[1])) * readComputedFontSize(element);
}
const measuringElement = window.document.createElement("div");
measuringElement.style.contain = "strict";
measuringElement.style.inlineSize = trimmedValue;
measuringElement.style.position = "absolute";
measuringElement.style.visibility = "hidden";
(element ?? window.document.body).appendChild(measuringElement);
const width = readElementInlineSize(measuringElement);
measuringElement.remove();
return width;
}
function readElementInlineSize(element: HTMLElement) {
const rect =
typeof element.getBoundingClientRect === "function"
? element.getBoundingClientRect()
: null;
const width = rect?.width || element.clientWidth || 0;
return Number.isFinite(width) && width > 0 ? width : 0;
}
function readComputedFontSize(element: Element | null) {
if (typeof window === "undefined") return 16;
const fontSize = element ? window.getComputedStyle(element).fontSize : "16px";
const value = Number.parseFloat(fontSize);
return Number.isFinite(value) && value > 0 ? value : 16;
}
function areCssLengthsEqual(previous: number, next: number) {
return Math.abs(previous - next) <= 0.001;
}
registry/new-york-v4/ui/viewer-root.tsx
"use client";
import * as React from "react";
import { useKeyedMountEffect } from "@/hooks/use-keyed-mount-effect";
import { useKeyedLayoutEffect } from "@/hooks/use-keyed-layout-effect";
import { joinEffectKey } from "@/lib/effect-key";
import { cn } from "@/lib/utils";
import {
createViewerStateAttributes,
createViewerPortalContainmentAttributes,
createViewerGeometryStore,
elementContainsTarget,
readViewerElementSize,
resolveMeasuredSidebarMode,
resolveSidebarMode,
targetHasViewerPortalContainment,
targetViewerRootId,
useOptionalViewerSidebarRegistration,
useOptionalViewerSidebarState,
useViewerSidebarState,
VIEWER_SIDEBAR_INLINE_BREAKPOINT,
VIEWER_SIDEBAR_WIDTH,
ViewerSidebarRegistrationProvider,
ViewerSidebarStateProvider,
} from "./viewer-internals";
import type {
ViewerRootDiagnostics,
ViewerPortalContainmentAttributes,
ViewerRootProps,
ViewerSidebarStateValue,
ViewerSidebarMode,
ViewerSidebarRegistration,
ViewerSidebarRegistrationState,
ViewerSidebarState,
} from "./viewer-types";
export function useViewerSidebar(): ViewerSidebarStateValue {
return useViewerSidebarState("useViewerSidebar");
}
export function useOptionalViewerSidebar(): ViewerSidebarStateValue | null {
return useOptionalViewerSidebarState();
}
export function useViewerPortalContainmentAttributes(): ViewerPortalContainmentAttributes {
const context = useOptionalViewerSidebarRegistration();
if (!context) {
throw new Error(
"useViewerPortalContainmentAttributes must be used within a ViewerRoot.",
);
}
return React.useMemo(
() => createViewerPortalContainmentAttributes(context.rootId),
[context.rootId],
);
}
export function useOptionalViewerRootDiagnostics(): ViewerRootDiagnostics | null {
const context = useOptionalViewerSidebarRegistration();
if (!context) return null;
const geometry = context.geometryStore.getSnapshot();
return {
getRootElement: context.getRootElement,
layoutSignature: [
context.hasSidebar ? "sidebar" : "no-sidebar",
context.sidebarSide,
geometry.mode,
geometry.open ? "open" : "closed",
geometry.state,
].join(":"),
rootId: context.rootId,
};
}
export function ViewerRoot({
className,
style,
defaultOpen = false,
open: openProp,
onOpenChange,
mode = "auto",
inlineBreakpoint = VIEWER_SIDEBAR_INLINE_BREAKPOINT,
sidebarSide = "left",
sidebarCollapsible = "offcanvas",
sidebarGapTransition = "width",
stateNamespace,
...props
}: ViewerRootProps) {
const rootRef = React.useRef<HTMLDivElement | null>(null);
const reactId = React.useId();
const rootId = `${reactId}-viewer-root`;
const fallbackSidebarId = `${reactId}-viewer-sidebar`;
const isControlled = openProp !== undefined;
const [internalOpen, setInternalOpen] = React.useState(defaultOpen);
const open = openProp ?? internalOpen;
const openRef = React.useRef(open);
openRef.current = open;
const hasMeasuredSidebarWidthRef = React.useRef(false);
const lastTriggerElementRef = React.useRef<HTMLElement | null>(null);
const registeredSidebarRef = React.useRef<ViewerSidebarRegistration | null>(
null,
);
const bodyElementRef = React.useRef<HTMLElement | null>(null);
const geometryStore = React.useMemo(() => createViewerGeometryStore(), []);
const [registeredSidebar, setRegisteredSidebar] =
React.useState<ViewerSidebarRegistration | null>(null);
const [bodyElement, setBodyElement] = React.useState<HTMLElement | null>(
null,
);
const [resolvedSidebarMode, setResolvedSidebarMode] =
React.useState<ViewerSidebarMode>(() =>
resolveSidebarMode({
requestedMode: mode,
width: null,
inlineBreakpoint,
}),
);
const commitViewerGeometryTarget = React.useCallback(
(nextOpen: boolean) => {
const sidebarRegistration = registeredSidebarRef.current;
const nextEffectiveOpen =
sidebarRegistration?.collapsible === "none" ? true : nextOpen;
const nextState: ViewerSidebarState = nextEffectiveOpen
? "expanded"
: "collapsed";
geometryStore.setTarget({
bodyElement: bodyElementRef.current,
mode: resolvedSidebarMode,
open: nextEffectiveOpen,
rootElement: rootRef.current,
sidebarElement: sidebarRegistration?.element ?? null,
sidebarGapTransition,
sidebarWidth: sidebarRegistration?.widthPixels ?? 0,
side: sidebarRegistration?.side ?? sidebarSide,
state: nextState,
});
},
[
geometryStore,
resolvedSidebarMode,
sidebarGapTransition,
sidebarSide,
],
);
useKeyedLayoutEffect(joinEffectKey([inlineBreakpoint, mode]), () => {
if (mode !== "auto") {
setResolvedSidebarMode((currentMode) =>
currentMode === mode ? currentMode : mode,
);
return;
}
const element = rootRef.current;
const ResizeObserverConstructor = globalThis.ResizeObserver;
if (!element || typeof ResizeObserverConstructor === "undefined") return;
const updateMode = () => {
const nextWidth = readViewerElementSize(element).width;
if (nextWidth === 0) return;
setResolvedSidebarMode((currentMode) => {
const nextMode = resolveMeasuredSidebarMode({
currentMode,
hasMeasured: hasMeasuredSidebarWidthRef.current,
requestedMode: mode,
width: nextWidth,
inlineBreakpoint,
});
hasMeasuredSidebarWidthRef.current = true;
return currentMode === nextMode ? currentMode : nextMode;
});
};
updateMode();
const observer = new ResizeObserverConstructor(updateMode);
observer.observe(element);
return () => observer.disconnect();
});
const setOpen = React.useCallback(
(value: boolean | ((open: boolean) => boolean)) => {
const previousOpen = isControlled ? open : openRef.current;
const nextOpen =
typeof value === "function" ? value(previousOpen) : value;
if (nextOpen === previousOpen) {
return;
}
commitViewerGeometryTarget(nextOpen);
if (!isControlled) {
openRef.current = nextOpen;
setInternalOpen(nextOpen);
}
onOpenChange?.(nextOpen);
},
[commitViewerGeometryTarget, isControlled, onOpenChange, open],
);
const registerSidebar = React.useCallback(
(registration: ViewerSidebarRegistration) => {
const currentRegistration = registeredSidebarRef.current;
if (
currentRegistration &&
currentRegistration.instanceId !== registration.instanceId
) {
throw new Error(
"ViewerRoot supports one primary ViewerSidebar. Use a nested ViewerRoot for a complete nested viewer, or put secondary content inside ViewerSurface.",
);
}
registeredSidebarRef.current = registration;
setRegisteredSidebar(registration);
return () => {
if (
registeredSidebarRef.current?.instanceId !== registration.instanceId
) {
return;
}
registeredSidebarRef.current = null;
setRegisteredSidebar(null);
};
},
[],
);
const registerBody = React.useCallback((element: HTMLElement) => {
bodyElementRef.current = element;
setBodyElement(element);
return () => {
if (bodyElementRef.current !== element) return;
bodyElementRef.current = null;
setBodyElement(null);
};
}, []);
const toggleSidebar = React.useCallback(() => {
setOpen((currentOpen) => !currentOpen);
}, [setOpen]);
const hasSidebar = registeredSidebar !== null;
const effectiveOpen = registeredSidebar?.collapsible === "none" ? true : open;
const state: ViewerSidebarState = effectiveOpen ? "expanded" : "collapsed";
const canToggleSidebar =
hasSidebar && registeredSidebar.collapsible !== "none";
const sidebarId = registeredSidebar?.id ?? fallbackSidebarId;
const resolvedSidebarSide = registeredSidebar?.side ?? sidebarSide;
const sidebarWidth = registeredSidebar?.width ?? VIEWER_SIDEBAR_WIDTH;
useKeyedLayoutEffect(
joinEffectKey([
effectiveOpen,
registeredSidebar?.element,
registeredSidebar?.widthPixels,
bodyElement,
resolvedSidebarMode,
resolvedSidebarSide,
rootRef.current,
sidebarGapTransition,
geometryStore,
state,
]),
() => {
geometryStore.setTarget({
bodyElement,
mode: resolvedSidebarMode,
open: effectiveOpen,
rootElement: rootRef.current,
sidebarElement: registeredSidebar?.element ?? null,
sidebarGapTransition,
sidebarWidth: registeredSidebar?.widthPixels ?? 0,
side: resolvedSidebarSide,
state,
});
},
);
useKeyedLayoutEffect(
bodyElement
? joinEffectKey([bodyElement, commitViewerGeometryTarget, effectiveOpen])
: null,
() => {
if (!bodyElement) return;
const ResizeObserverConstructor = globalThis.ResizeObserver;
if (typeof ResizeObserverConstructor === "undefined") {
commitViewerGeometryTarget(effectiveOpen);
return;
}
const observer = new ResizeObserverConstructor(() => {
commitViewerGeometryTarget(effectiveOpen);
});
observer.observe(bodyElement);
commitViewerGeometryTarget(effectiveOpen);
return () => observer.disconnect();
},
);
const namespacedRootStateAttributes = createViewerStateAttributes(
stateNamespace,
"root",
{
hasSidebar,
sidebarCollapsible: registeredSidebar?.collapsible ?? sidebarCollapsible,
sidebarMode: resolvedSidebarMode,
sidebarOpen: effectiveOpen,
sidebarSide: resolvedSidebarSide,
sidebarState: state,
},
);
const setLastTriggerElement = React.useCallback(
(element: HTMLElement | null) => {
lastTriggerElementRef.current = element;
},
[],
);
const getRootElement = React.useCallback(() => rootRef.current, []);
useKeyedMountEffect(
joinEffectKey([
canToggleSidebar,
open,
registeredSidebar,
resolvedSidebarMode,
rootId,
setOpen,
]),
() => {
if (
!open ||
!canToggleSidebar ||
resolvedSidebarMode !== "overlay" ||
typeof document === "undefined"
) {
return;
}
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
if (event.defaultPrevented) {
return;
}
const activeElement = document.activeElement;
if (
activeElement &&
rootRef.current &&
!elementContainsTarget(rootRef.current, activeElement) &&
!elementContainsTarget(registeredSidebar?.element, activeElement) &&
!targetHasViewerPortalContainment({
rootId,
target: activeElement,
})
) {
return;
}
setOpen(false);
const triggerElement =
lastTriggerElementRef.current?.isConnected === true
? lastTriggerElementRef.current
: rootRef.current?.querySelector<HTMLElement>(
"[data-viewer-sidebar-trigger]",
);
if (typeof triggerElement?.focus === "function") {
triggerElement.focus();
}
}
};
const handlePointerDown = (event: PointerEvent) => {
const target = event.target;
if (elementContainsTarget(registeredSidebar?.element, target)) {
return;
}
if (targetHasViewerPortalContainment({ rootId, target })) {
return;
}
const targetRootId = targetViewerRootId(target);
if (targetRootId && targetRootId !== rootId) {
return;
}
if (
target &&
typeof (target as { closest?: unknown }).closest === "function"
) {
const triggerElement = (
target as unknown as {
closest: (selector: string) => HTMLElement | null;
}
).closest("[data-viewer-sidebar-trigger]");
if (triggerElement?.dataset.viewerRootId === rootId) {
return;
}
}
setOpen(false);
};
document.addEventListener("keydown", handleKeyDown);
document.addEventListener("pointerdown", handlePointerDown);
return () => {
document.removeEventListener("keydown", handleKeyDown);
document.removeEventListener("pointerdown", handlePointerDown);
};
},
);
const sidebarStateContext = React.useMemo<ViewerSidebarStateValue>(
() => ({
state,
open: effectiveOpen,
setOpen,
toggleSidebar,
canToggleSidebar,
mode: resolvedSidebarMode,
side: resolvedSidebarSide,
}),
[
state,
effectiveOpen,
setOpen,
toggleSidebar,
canToggleSidebar,
resolvedSidebarMode,
resolvedSidebarSide,
],
);
const sidebarRegistrationContext =
React.useMemo<ViewerSidebarRegistrationState>(
() => ({
defaultSidebarCollapsible: sidebarCollapsible,
defaultSidebarSide: sidebarSide,
geometryStore,
getRootElement,
hasSidebar,
registerBody,
registerSidebar,
rootId,
sidebarId,
sidebarGapTransition,
sidebarSide: resolvedSidebarSide,
setLastTriggerElement,
stateNamespace,
}),
[
geometryStore,
hasSidebar,
sidebarCollapsible,
resolvedSidebarSide,
getRootElement,
registerBody,
registerSidebar,
rootId,
sidebarId,
sidebarGapTransition,
sidebarSide,
setLastTriggerElement,
stateNamespace,
],
);
return (
<ViewerSidebarStateProvider value={sidebarStateContext}>
<ViewerSidebarRegistrationProvider value={sidebarRegistrationContext}>
<div
ref={rootRef}
data-slot="viewer-root"
data-viewer-root-id={rootId}
data-viewer-has-sidebar={hasSidebar ? "true" : "false"}
data-viewer-sidebar-collapsible={
registeredSidebar?.collapsible ?? sidebarCollapsible
}
data-viewer-sidebar-mode={resolvedSidebarMode}
data-viewer-sidebar-open={effectiveOpen ? "true" : "false"}
data-viewer-sidebar-side={resolvedSidebarSide}
data-viewer-sidebar-state={state}
data-viewer-slot="root"
{...namespacedRootStateAttributes}
className={cn(
"relative flex min-h-0 min-w-0 flex-col overflow-hidden",
className,
)}
style={
{
overflowAnchor: "none",
...style,
"--viewer-sidebar-width": sidebarWidth,
} as React.CSSProperties
}
{...props}
/>
</ViewerSidebarRegistrationProvider>
</ViewerSidebarStateProvider>
);
}
registry/new-york-v4/ui/viewer-sidebar.tsx
"use client";
import * as React from "react";
import { useMountEffect } from "@/hooks/use-mount-effect";
import { useKeyedLayoutEffect } from "@/hooks/use-keyed-layout-effect";
import { joinEffectKey } from "@/lib/effect-key";
import { cn } from "@/lib/utils";
import {
createViewerSlotAttributes,
createViewerStateAttributes,
pickCssCustomProperties,
readViewerElementSize,
useViewerDevelopmentLayoutWarning,
useViewerSidebarState,
useViewerGeometrySnapshot,
useViewerSidebarRegistrationContext,
VIEWER_SIDEBAR_WIDTH,
} from "./viewer-internals";
import { warnViewerDevelopmentOnce } from "./viewer-diagnostics";
import type { ViewerSidebarProps, ViewerSidebarState } from "./viewer-types";
export function ViewerSidebar({
children,
className,
innerClassName,
namespacedSlot,
namespacedSlotNames,
side: sideProp,
slotNames,
collapsible: collapsibleProp,
width = VIEWER_SIDEBAR_WIDTH,
style,
id: idProp,
...props
}: ViewerSidebarProps) {
const registrationContext =
useViewerSidebarRegistrationContext("ViewerSidebar");
const sidebarState = useViewerSidebarState("ViewerSidebar");
const {
defaultSidebarCollapsible,
defaultSidebarSide,
getRootElement,
registerSidebar,
rootId,
sidebarGapTransition,
geometryStore,
} = registrationContext;
const geometry = useViewerGeometrySnapshot(geometryStore);
const reactId = React.useId();
const instanceId = `${reactId}-viewer-sidebar-instance`;
const generatedSidebarId = `${reactId}-viewer-sidebar`;
const sidebarId = idProp ?? generatedSidebarId;
const sidebarRef = React.useRef<HTMLElement | null>(null);
const [transitionsReady, setTransitionsReady] = React.useState(false);
const collapsible = collapsibleProp ?? defaultSidebarCollapsible;
const side = sideProp ?? defaultSidebarSide;
const open = collapsible === "none" ? true : sidebarState.open;
const state: ViewerSidebarState = open ? "expanded" : "collapsed";
const mode = sidebarState.mode;
const visualOpen = mode === "inline" ? geometry.open : open;
const visualState: ViewerSidebarState = visualOpen
? "expanded"
: "collapsed";
const isCollapsed = collapsible !== "none" && !open;
const isOpening =
collapsible !== "none" &&
open &&
geometry.isTransitioning;
const frameSlot = props["data-slot"] ?? "viewer-sidebar";
const sidebarProps = { ...props };
delete sidebarProps["data-slot"];
const styleWithoutWidth: React.CSSProperties = { ...style };
delete styleWithoutWidth.width;
const customProperties = pickCssCustomProperties(style);
const frameStyle = {
...customProperties,
"--viewer-sidebar-width": width,
} as React.CSSProperties;
const inlineFrameStyle = {
...frameStyle,
flexBasis: `${geometry.sidebarInlineSize}px`,
maxWidth: `${geometry.sidebarInlineSize}px`,
width: `${geometry.sidebarInlineSize}px`,
} as React.CSSProperties;
const ariaLabel = sidebarProps["aria-label"];
const ariaLabelledBy = sidebarProps["aria-labelledby"];
const hasAccessibleName =
(typeof ariaLabel === "string"
? ariaLabel.trim().length > 0
: Boolean(ariaLabel)) ||
(typeof ariaLabelledBy === "string"
? ariaLabelledBy.trim().length > 0
: Boolean(ariaLabelledBy));
useViewerDevelopmentLayoutWarning({
enabled: mode === "inline" && open,
elements: () => [getRootElement(), sidebarRef.current],
inspect: () => {
const rootElement = getRootElement();
const sidebarElement = sidebarRef.current;
const rootSize = readViewerElementSize(rootElement);
const sidebarSize = readViewerElementSize(sidebarElement);
if (rootSize.width > 0 && sidebarSize.width > rootSize.width) {
warnViewerDevelopmentOnce({
code: "viewer_sidebar_wider_than_root",
message: "inline viewer sidebar is wider than its root.",
rootId,
details: {
rootSlot: rootElement?.dataset.slot,
rootWidth: rootSize.width,
sidebarMode: mode,
sidebarOpen: open,
sidebarSide: side,
sidebarSlot: sidebarElement?.dataset.slot,
sidebarWidth: sidebarSize.width,
},
});
}
},
keyParts: [
"viewer-sidebar-wider-than-root",
rootId,
mode,
open,
side,
width,
getRootElement(),
sidebarRef.current,
],
});
useMountEffect(() => {
if (
typeof window === "undefined" ||
typeof window.requestAnimationFrame !== "function"
) {
setTransitionsReady(true);
return;
}
let secondFrame = 0;
const firstFrame = window.requestAnimationFrame(() => {
secondFrame = window.requestAnimationFrame(() => {
setTransitionsReady(true);
});
});
return () => {
window.cancelAnimationFrame(firstFrame);
window.cancelAnimationFrame(secondFrame);
};
});
useKeyedLayoutEffect(
joinEffectKey([
collapsible,
instanceId,
registerSidebar,
side,
sidebarId,
width,
]),
() => {
const element = sidebarRef.current;
if (!element) {
return;
}
return registerSidebar({
collapsible,
element,
id: sidebarId,
instanceId,
side,
width,
widthPixels: resolveViewerSidebarWidthPixels(element, width),
});
},
);
useKeyedLayoutEffect(
mode === "overlay" && open && !hasAccessibleName
? joinEffectKey([
"viewer-overlay-sidebar-missing-accessible-name",
rootId,
sidebarId,
side,
])
: null,
() => {
warnViewerDevelopmentOnce({
code: "viewer_overlay_sidebar_missing_accessible_name",
message: "overlay viewer sidebar is missing an accessible name.",
rootId,
details: {
sidebarId,
sidebarMode: mode,
sidebarSide: side,
},
});
},
);
useKeyedLayoutEffect(
isCollapsed
? joinEffectKey([
"viewer-sidebar:move-focus-on-close",
getRootElement,
sidebarId,
])
: null,
() => {
moveFocusOutOfViewerSidebar(
sidebarRef.current,
getRootElement(),
sidebarId,
);
},
);
const hiddenProps = isCollapsed || isOpening
? ({
"aria-hidden": true,
inert: true,
} as React.HTMLAttributes<HTMLElement>)
: {};
const namespacedSidebarStateAttributes = createViewerStateAttributes(
registrationContext.stateNamespace,
"sidebar",
{
sidebarCollapsible: collapsible,
sidebarMode: mode,
sidebarOpen: visualOpen,
sidebarSide: side,
sidebarState: visualState,
},
);
const frameAttributes = {
"data-slot": frameSlot,
"data-collapsible": collapsible,
"data-mode": mode,
"data-side": side,
"data-state": visualState,
"data-viewer-sidebar-transitions": transitionsReady ? "ready" : undefined,
"data-viewer-sidebar-mode": mode,
"data-viewer-sidebar-open": visualOpen ? "true" : "false",
"data-viewer-sidebar-state": visualState,
"data-viewer-slot": "sidebar",
...createViewerSlotAttributes(
registrationContext.stateNamespace,
"sidebar",
namespacedSlot,
),
...namespacedSidebarStateAttributes,
};
const sidebarElement = (
<aside
ref={sidebarRef}
id={sidebarId}
data-slot={slotNames?.container ?? "viewer-sidebar-container"}
data-collapsible={collapsible}
data-mode={mode}
data-side={side}
data-state={visualState}
data-viewer-sidebar-transitions={transitionsReady ? "ready" : undefined}
data-viewer-sidebar-mode={mode}
data-viewer-sidebar-open={visualOpen ? "true" : "false"}
data-viewer-sidebar-state={visualState}
data-viewer-slot="sidebar-container"
{...createViewerSlotAttributes(
registrationContext.stateNamespace,
"sidebar",
namespacedSlotNames?.container,
)}
{...namespacedSidebarStateAttributes}
className={cn(
className,
"z-30 flex w-(--viewer-sidebar-width) min-w-0 overflow-hidden",
mode === "inline" &&
"absolute inset-y-0 h-full min-w-(--viewer-sidebar-width) flex-shrink-0 transition-none",
mode === "inline" && side === "left" && "right-0",
mode === "inline" && side === "right" && "left-0",
mode === "overlay" &&
"absolute inset-y-0 transition-none data-[viewer-sidebar-transitions=ready]:transition-[translate,width,border-color] data-[viewer-sidebar-transitions=ready]:duration-150 data-[viewer-sidebar-transitions=ready]:ease-out",
mode === "overlay" && side === "left" && "left-0",
mode === "overlay" && side === "right" && "right-0",
collapsible === "offcanvas" && mode === "overlay" && "shadow-lg",
isCollapsed && "pointer-events-none",
collapsible === "offcanvas" &&
mode === "overlay" &&
!visualOpen &&
side === "left" &&
"pointer-events-none -translate-x-full border-transparent",
collapsible === "offcanvas" &&
mode === "overlay" &&
!visualOpen &&
side === "right" &&
"pointer-events-none translate-x-full border-transparent",
)}
style={
{
...styleWithoutWidth,
"--viewer-sidebar-width": width,
} as React.CSSProperties
}
{...sidebarProps}
{...hiddenProps}
>
<div
data-slot={slotNames?.inner ?? "viewer-sidebar-inner"}
data-viewer-slot="sidebar-inner"
{...createViewerSlotAttributes(
registrationContext.stateNamespace,
"sidebar",
namespacedSlotNames?.inner,
)}
className={cn(
"flex h-full min-h-0 w-full min-w-0 flex-col overflow-hidden",
innerClassName,
)}
>
{children}
</div>
</aside>
);
if (mode === "inline") {
return (
<div
{...frameAttributes}
className={cn(
"group/viewer-sidebar relative z-30 min-h-0 flex-shrink-0 overflow-hidden transition-none",
side === "right" && "order-last",
)}
style={inlineFrameStyle}
>
{sidebarElement}
</div>
);
}
return (
<div
{...frameAttributes}
className={cn(
"group/viewer-sidebar relative z-30 min-h-0 flex-shrink-0",
side === "right" && "order-last",
)}
style={frameStyle}
>
<div
data-slot={slotNames?.gap ?? "viewer-sidebar-gap"}
data-viewer-slot="sidebar-gap"
{...createViewerSlotAttributes(
registrationContext.stateNamespace,
"sidebar",
namespacedSlotNames?.gap,
)}
className={cn(
"relative h-full w-(--viewer-sidebar-width) flex-shrink-0",
sidebarGapTransition === "width" &&
"transition-[width] duration-150 ease-out",
collapsible === "offcanvas" && mode === "overlay" && "w-0",
)}
/>
{sidebarElement}
</div>
);
}
function moveFocusOutOfViewerSidebar(
sidebarElement: HTMLElement | null,
rootElement: HTMLElement | null,
sidebarId: string,
) {
if (typeof document === "undefined" || !sidebarElement) return;
const activeElement = document.activeElement;
if (
!(activeElement instanceof HTMLElement) ||
!sidebarElement.contains(activeElement)
) {
return;
}
const trigger = findViewerSidebarTrigger(rootElement, sidebarId);
if (trigger) {
trigger.focus({ preventScroll: true });
return;
}
activeElement.blur();
}
function findViewerSidebarTrigger(
rootElement: HTMLElement | null,
sidebarId: string,
) {
if (!rootElement) return null;
for (const element of rootElement.querySelectorAll<HTMLElement>(
"[aria-controls]",
)) {
if (element.getAttribute("aria-controls") === sidebarId) {
return element;
}
}
return null;
}
function resolveViewerSidebarWidthPixels(element: HTMLElement, width: string) {
const declaredWidth = parseViewerSidebarCssLength(width, element);
if (declaredWidth !== null) return declaredWidth;
if (typeof window !== "undefined") {
const computedWidth = parseViewerSidebarCssLength(
window.getComputedStyle(element).width,
element,
);
if (computedWidth !== null) return computedWidth;
}
const measuredWidth = readViewerElementSize(element).width;
return Number.isFinite(measuredWidth) && measuredWidth > 0
? measuredWidth
: 0;
}
function parseViewerSidebarCssLength(value: string, element: HTMLElement) {
const trimmedValue = value.trim();
const length = Number.parseFloat(trimmedValue);
if (!Number.isFinite(length) || length <= 0) return null;
if (trimmedValue.endsWith("px")) return length;
if (trimmedValue.endsWith("rem")) {
return length * readRootFontSize();
}
if (trimmedValue.endsWith("em")) {
return length * readElementFontSize(element);
}
return null;
}
function readRootFontSize() {
if (typeof window === "undefined") return 16;
const rootFontSize = Number.parseFloat(
window.getComputedStyle(document.documentElement).fontSize,
);
return Number.isFinite(rootFontSize) && rootFontSize > 0
? rootFontSize
: 16;
}
function readElementFontSize(element: HTMLElement) {
if (typeof window === "undefined") return 16;
const elementFontSize = Number.parseFloat(
window.getComputedStyle(element).fontSize,
);
return Number.isFinite(elementFontSize) && elementFontSize > 0
? elementFontSize
: readRootFontSize();
}
registry/new-york-v4/ui/viewer-surface.tsx
"use client";
import * as React from "react";
import { useKeyedLayoutEffect } from "@/hooks/use-keyed-layout-effect";
import { joinEffectKey } from "@/lib/effect-key";
import { cn } from "@/lib/utils";
import {
findClosestViewerBody,
readViewerElementSize,
useOptionalViewerSidebarRegistration,
useOptionalViewerSurfaceMeasurementContext,
useViewerDevelopmentLayoutWarning,
useViewerViewportPresence,
ViewerSurfaceMeasurementProvider,
ViewerViewportPresenceProvider,
} from "./viewer-internals";
import { useOptionalViewerRootDiagnostics } from "./viewer-root";
import { warnViewerDevelopmentOnce } from "./viewer-diagnostics";
import { useStableElementSize } from "./viewer-measurement";
import type {
ViewerDocumentFrameAlign,
ViewerDocumentFrameProps,
ViewerGeometryStore,
ViewerSurfaceMeasurement,
ViewerSurfaceProps,
ViewerViewportProps,
ViewerGeometrySnapshot,
} from "./viewer-types";
export type ViewerDocumentFrameLayout = {
activeInlineSize: number | null;
isTransitioning: boolean;
maxInlineSize: number | null;
settledInlineSize: number | null;
};
export type ViewerDocumentFrameState = {
align: ViewerDocumentFrameAlign;
element: HTMLDivElement | null;
geometryStore: ViewerGeometryStore | null;
inlineSize: number | null;
};
const ViewerDocumentFrameContext =
React.createContext<ViewerDocumentFrameState | null>(null);
function getDocumentFrameAlignClass(align: ViewerDocumentFrameAlign) {
switch (align) {
case "center":
return "mx-auto";
case "end":
return "ml-auto";
case "start":
return "mr-auto";
}
}
export function useOptionalViewerDocumentFrame(): ViewerDocumentFrameState | null {
return React.useContext(ViewerDocumentFrameContext);
}
export function useViewerDocumentFrameLayout({
documentFrame,
fallbackInlineSize,
}: {
documentFrame: ViewerDocumentFrameState | null;
fallbackInlineSize: number | null;
}): ViewerDocumentFrameLayout {
const element = documentFrame?.element ?? null;
const geometryStore = documentFrame?.geometryStore ?? null;
const snapshotCacheRef = React.useRef<ViewerDocumentFrameLayout | null>(null);
const subscribe = React.useCallback(
(listener: () => void) =>
geometryStore ? geometryStore.subscribe(listener) : () => {},
[geometryStore],
);
const getSnapshot = React.useCallback(() => {
const geometry = geometryStore?.getSnapshot() ?? null;
const previousLayout = snapshotCacheRef.current;
const nextLayout =
geometry && geometry.hasMeasuredBody
? createViewerDocumentFrameLayoutFromGeometry({
element,
fallbackInlineSize,
geometry,
})
: createViewerDocumentFrameFallbackLayout(fallbackInlineSize);
if (
previousLayout &&
areViewerDocumentFrameLayoutsEqual(previousLayout, nextLayout)
) {
return previousLayout;
}
snapshotCacheRef.current = nextLayout;
return nextLayout;
}, [element, fallbackInlineSize, geometryStore]);
const activeLayout = React.useSyncExternalStore(
subscribe,
getSnapshot,
getSnapshot,
);
const [settledInlineSize, setSettledInlineSize] = React.useState<
number | null
>(() => activeLayout.activeInlineSize);
useKeyedLayoutEffect(
joinEffectKey([
"viewer-document-frame-layout:settled-inline-size",
activeLayout.activeInlineSize,
activeLayout.isTransitioning,
geometryStore,
]),
() => {
if (activeLayout.isTransitioning) return;
setSettledInlineSize((currentInlineSize) =>
currentInlineSize === activeLayout.activeInlineSize
? currentInlineSize
: activeLayout.activeInlineSize,
);
},
);
return React.useMemo(
() => ({
...activeLayout,
settledInlineSize:
settledInlineSize ?? activeLayout.activeInlineSize ?? null,
}),
[activeLayout, settledInlineSize],
);
}
export function ViewerDocumentFrame({
align = "start",
children,
className,
maxInlineSize,
style,
...props
}: ViewerDocumentFrameProps) {
const size = useStableElementSize<HTMLDivElement>({
retainLastNonZero: true,
});
const sidebarRegistration = useOptionalViewerSidebarRegistration();
const value = React.useMemo<ViewerDocumentFrameState>(
() => ({
align,
element: size.element,
geometryStore: sidebarRegistration?.geometryStore ?? null,
inlineSize: size.width,
}),
[align, sidebarRegistration?.geometryStore, size.element, size.width],
);
return (
<ViewerDocumentFrameContext.Provider value={value}>
<div
ref={size.setElement}
data-slot="viewer-document-frame"
className={cn(
"[container-type:inline-size] relative flex h-full min-h-0 w-full min-w-0 flex-1 flex-col",
getDocumentFrameAlignClass(align),
className,
)}
style={
{
maxInlineSize,
...style,
} as React.CSSProperties
}
{...props}
>
{children}
</div>
</ViewerDocumentFrameContext.Provider>
);
}
function createViewerDocumentFrameLayoutFromGeometry({
element,
fallbackInlineSize,
geometry,
}: {
element: HTMLDivElement | null;
fallbackInlineSize: number | null;
geometry: ViewerGeometrySnapshot;
}): ViewerDocumentFrameLayout {
const maxCssInlineSize = element
? readViewerDocumentFrameMaxInlineSize(element)
: null;
const measuredFrameInlineSize =
fallbackInlineSize != null &&
Number.isFinite(fallbackInlineSize) &&
fallbackInlineSize > 0
? fallbackInlineSize
: null;
const usesSidebarGeometry =
geometry.sidebarWidth > 0 ||
geometry.sidebarInlineSize > 0 ||
geometry.isTransitioning;
const activeSourceInlineSize = usesSidebarGeometry
? geometry.documentInlineSize
: (measuredFrameInlineSize ?? geometry.documentInlineSize);
const maxSourceInlineSize = usesSidebarGeometry
? Math.max(geometry.bodyInlineSize, activeSourceInlineSize)
: activeSourceInlineSize;
const activeInlineSize = constrainViewerDocumentFrameInlineSize(
activeSourceInlineSize,
maxCssInlineSize,
);
const maxInlineSize = constrainViewerDocumentFrameInlineSize(
maxSourceInlineSize,
maxCssInlineSize,
);
return {
activeInlineSize,
isTransitioning: geometry.isTransitioning,
maxInlineSize,
settledInlineSize: null,
};
}
function createViewerDocumentFrameFallbackLayout(
fallbackInlineSize: number | null,
): ViewerDocumentFrameLayout {
const inlineSize =
fallbackInlineSize != null &&
Number.isFinite(fallbackInlineSize) &&
fallbackInlineSize > 0
? fallbackInlineSize
: null;
return {
activeInlineSize: inlineSize,
isTransitioning: false,
maxInlineSize: inlineSize,
settledInlineSize: inlineSize,
};
}
function areViewerDocumentFrameLayoutsEqual(
previousLayout: ViewerDocumentFrameLayout,
nextLayout: ViewerDocumentFrameLayout,
) {
return (
previousLayout.activeInlineSize === nextLayout.activeInlineSize &&
previousLayout.isTransitioning === nextLayout.isTransitioning &&
previousLayout.maxInlineSize === nextLayout.maxInlineSize
);
}
function readViewerDocumentFrameMaxInlineSize(element: HTMLElement) {
if (typeof window === "undefined") return null;
const value = window.getComputedStyle(element).maxInlineSize;
if (!value || value === "none") return null;
const size = Number.parseFloat(value);
return Number.isFinite(size) && size > 0 ? size : null;
}
function constrainViewerDocumentFrameInlineSize(
inlineSize: number,
maxInlineSize: number | null,
) {
const safeInlineSize =
Number.isFinite(inlineSize) && inlineSize > 0 ? inlineSize : 0;
const constrainedInlineSize =
maxInlineSize == null
? safeInlineSize
: Math.min(safeInlineSize, maxInlineSize);
return constrainedInlineSize > 0 ? constrainedInlineSize : null;
}
function useViewerSurfaceMeasurementValue(): ViewerSurfaceMeasurement {
const size = useStableElementSize<HTMLDivElement>({
retainLastNonZero: true,
});
return React.useMemo(
() => ({
hasMeasured: size.hasMeasured,
setViewportElement: size.setElement,
viewportElement: size.element,
viewportHeight: size.height,
viewportWidth: size.width,
}),
[size.element, size.hasMeasured, size.height, size.setElement, size.width],
);
}
export function useOptionalViewerSurfaceMeasurement(): ViewerSurfaceMeasurement | null {
return useOptionalViewerSurfaceMeasurementContext();
}
export function useViewerSurfaceMeasurement(): ViewerSurfaceMeasurement {
const context = useOptionalViewerSurfaceMeasurement();
if (!context) {
throw new Error(
"useViewerSurfaceMeasurement must be used within a ViewerSurface.",
);
}
return context;
}
export function useIsInsideViewerViewport(): boolean {
return useViewerViewportPresence();
}
export function ViewerSurface({ className, ...props }: ViewerSurfaceProps) {
const measurement = useViewerSurfaceMeasurementValue();
return (
<ViewerSurfaceMeasurementProvider value={measurement}>
<div
data-slot="viewer-surface"
data-viewer-surface="inset"
className={cn(
"flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden",
className,
)}
{...props}
/>
</ViewerSurfaceMeasurementProvider>
);
}
export const ViewerViewport = React.forwardRef<
HTMLDivElement,
ViewerViewportProps
>(function ViewerViewport({ children, className, ...props }, forwardedRef) {
const measurement = useOptionalViewerSurfaceMeasurement();
const rootDiagnostics = useOptionalViewerRootDiagnostics();
const viewportRef = React.useRef<HTMLDivElement | null>(null);
const setViewportElement = React.useCallback(
(element: HTMLDivElement | null) => {
viewportRef.current = element;
measurement?.setViewportElement(element);
if (typeof forwardedRef === "function") {
forwardedRef(element);
return;
}
if (forwardedRef) {
forwardedRef.current = element;
}
},
[forwardedRef, measurement],
);
useViewerDevelopmentLayoutWarning({
enabled: Boolean(rootDiagnostics),
elements: () => [
findClosestViewerBody(viewportRef.current),
viewportRef.current,
],
inspect: () => {
const viewportElement = viewportRef.current;
const bodyElement = findClosestViewerBody(viewportElement);
const bodySize = readViewerElementSize(bodyElement);
const viewportSize = readViewerElementSize(viewportElement);
if (bodySize.width > 0 && viewportSize.width === 0) {
warnViewerDevelopmentOnce({
code: "viewer_viewport_zero_width",
message:
"viewer body has nonzero width but surface viewport width is zero.",
rootId: rootDiagnostics?.rootId ?? "unknown-root",
details: {
bodyHeight: bodySize.height,
bodySlot: bodyElement?.dataset.slot,
bodyWidth: bodySize.width,
viewportHeight: viewportSize.height,
viewportSlot: viewportElement?.dataset.slot,
viewportWidth: viewportSize.width,
},
});
}
},
keyParts: [
"viewer-viewport-zero-width",
rootDiagnostics?.rootId,
viewportRef.current,
],
});
return (
<div
ref={setViewportElement}
data-slot="viewer-viewport"
className={cn(
"relative flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden",
className,
)}
{...props}
>
<ViewerViewportPresenceProvider>
{children}
</ViewerViewportPresenceProvider>
</div>
);
});
registry/new-york-v4/ui/viewer-types.ts
import type * as React from "react";
export type ViewerSidebarMode = "inline" | "overlay";
export type ViewerSidebarRequestedMode = "auto" | ViewerSidebarMode;
export type ViewerSidebarGapTransition = "width" | "none";
export type ViewerSidebarState = "expanded" | "collapsed";
export type ViewerSidebarSide = "left" | "right";
export type ViewerSidebarCollapsible = "offcanvas" | "none";
export type ViewerDocumentFrameAlign = "start" | "center" | "end";
export type ViewerGeometryTransitionPhase = "idle" | "sliding";
export type ViewerDocumentReadingAnchorInput = {
scrollTop: number;
viewportBlockSize: number;
};
export type ViewerDocumentReadingAnchorTarget<Anchor> = {
anchor: Anchor;
viewportBlockSize: number;
};
export type ViewerDocumentTransitionSource =
| "none"
| "viewer-shell"
| "document-layout";
// Commit-then-relax: layout commits its target inside the motion's first
// frame and scroll rebases in the same commit, so there is no frozen layout
// and no deferred scroll left in the vocabulary.
export type ViewerDocumentLayoutPolicy = "live" | "target";
export type ViewerDocumentScrollPolicy = "preserve" | "rebase";
export type ViewerDocumentVisualPolicy =
| "none"
| "document-flip"
| "shell-transform";
export type ViewerDocumentTransition = {
layoutPolicy: ViewerDocumentLayoutPolicy;
scrollPolicy: ViewerDocumentScrollPolicy;
source: ViewerDocumentTransitionSource;
transitionId: number | string | null;
visualPolicy: ViewerDocumentVisualPolicy;
};
export type ViewerDocumentLayoutModel<Anchor> = {
blockSize: number;
captureReadingAnchor: (
input: ViewerDocumentReadingAnchorInput,
) => Anchor | null;
getReadingAnchorScrollTop: (
target: ViewerDocumentReadingAnchorTarget<Anchor>,
) => number | null;
inlineSize: number;
isTransitioning?: boolean;
transition?: ViewerDocumentTransition;
};
export type ViewerDocumentPhysicalScrollPosition = {
physicalScrollTop: number;
scrollPageOffset: number;
};
export type ViewerDocumentResolvedScrollTarget = {
left?: number;
top: number;
};
export type ViewerDocumentScrollMapper = {
getLogicalScrollTop: (input: {
blockSize: number;
physicalScrollTop: number;
scrollPageOffset: number;
viewportBlockSize: number;
}) => number;
getPhysicalScrollSize: (input: {
blockSize: number;
viewportBlockSize: number;
}) => number;
resolvePhysicalScrollPosition: (input: {
blockSize: number;
logicalScrollTop: number;
scrollPageOffset: number;
viewportBlockSize: number;
}) => ViewerDocumentPhysicalScrollPosition;
};
export type ViewerDocumentScrollMetrics = {
physicalScrollSize: number;
physicalScrollTop: number;
scrollPageOffset: number;
scrollTop: number;
viewportBlockSize: number;
};
export type ViewerDocumentScrollTargetResolver<Anchor, Target> = (input: {
layout: ViewerDocumentLayoutModel<Anchor>;
scrollTop: number;
target: Target;
viewportElement: HTMLDivElement;
}) => ViewerDocumentResolvedScrollTarget | null;
// A zoom step is the one geometry change whose intent is "zoom the camera",
// not "keep my reading position": it re-anchors the viewport CENTER on both
// axes and relaxes a FLIP about that fixed point. `capture` runs in the zoom
// gesture's own task against the pre-zoom layout and painted DOM;
// `resolveScrollTarget` and `play` run inside the geometry commit against the
// post-zoom layout (commit-then-relax).
export type ViewerDocumentZoomMotionBypassReason =
| "resolve-failed"
| "shell-transition"
| "stale-intent";
export type ViewerDocumentZoomMotionController<Transaction = unknown> = {
capture: (input: {
scrollTop: number;
viewportElement: HTMLDivElement;
}) => Transaction | null;
/**
* Telemetry tap: a captured zoom intent reached a geometry commit but the
* zoom lane declined it. Without this the bypass is invisible — the commit
* falls back to the reading-anchor restore and no flight is recorded.
*/
noteBypass?: (reason: ViewerDocumentZoomMotionBypassReason) => void;
resolveScrollTarget: (input: {
transaction: Transaction;
viewportElement: HTMLDivElement;
}) => ViewerDocumentResolvedScrollTarget | null;
play: (input: {
transaction: Transaction;
viewportElement: HTMLDivElement;
}) => (() => void) | null;
};
export type ViewerGeometrySnapshot = {
bodyInlineSize: number;
documentInlineSize: number;
hasMeasuredBody: boolean;
isTransitioning: boolean;
mode: ViewerSidebarMode;
open: boolean;
progress: number;
sidebarGapTransition: ViewerSidebarGapTransition;
sidebarInlineSize: number;
sidebarWidth: number;
side: ViewerSidebarSide;
state: ViewerSidebarState;
transitionPhase: ViewerGeometryTransitionPhase;
};
export type ViewerGeometryStore = {
getSnapshot: () => ViewerGeometrySnapshot;
setTarget: (target: ViewerGeometryTarget) => void;
subscribe: (listener: () => void) => () => void;
};
export type ViewerGeometryTarget = {
bodyElement: HTMLElement | null;
mode: ViewerSidebarMode;
open: boolean;
rootElement: HTMLElement | null;
sidebarElement: HTMLElement | null;
sidebarGapTransition: ViewerSidebarGapTransition;
sidebarWidth: number;
side: ViewerSidebarSide;
state: ViewerSidebarState;
};
export type ViewerSidebarStateValue = {
state: ViewerSidebarState;
open: boolean;
setOpen: (value: boolean | ((open: boolean) => boolean)) => void;
toggleSidebar: () => void;
canToggleSidebar: boolean;
mode: ViewerSidebarMode;
side: ViewerSidebarSide;
};
export type ViewerRootProps = React.ComponentProps<"div"> & {
defaultOpen?: boolean;
inlineBreakpoint?: number;
mode?: ViewerSidebarRequestedMode;
onOpenChange?: (open: boolean) => void;
open?: boolean;
sidebarCollapsible?: ViewerSidebarCollapsible;
sidebarGapTransition?: ViewerSidebarGapTransition;
sidebarSide?: ViewerSidebarSide;
stateNamespace?: ViewerStateAttributeNamespace;
};
export type ViewerFrameProps = React.ComponentProps<"div">;
export type ViewerHeaderProps = React.ComponentProps<"div">;
export type ViewerBodyProps = React.ComponentProps<"div">;
export type ViewerSurfaceProps = React.ComponentProps<"div">;
export type ViewerViewportProps = React.ComponentProps<"div">;
export type ViewerDocumentFrameProps = React.ComponentProps<"div"> & {
align?: ViewerDocumentFrameAlign;
maxInlineSize?: React.CSSProperties["maxInlineSize"];
};
export type ViewerStateAttributeNamespace = {
prefix: string;
slots?: {
body?: boolean;
root?: boolean;
sidebar?: boolean;
};
};
export type ViewerSidebarRegistration = {
collapsible: ViewerSidebarCollapsible;
element: HTMLElement;
id: string;
instanceId: string;
side: ViewerSidebarSide;
width: string;
widthPixels: number;
};
export type ViewerPortalContainmentAttributes = {
"data-viewer-portal-root-id": string;
};
export type ViewerSidebarRegistrationState = {
defaultSidebarCollapsible: ViewerSidebarCollapsible;
defaultSidebarSide: ViewerSidebarSide;
geometryStore: ViewerGeometryStore;
getRootElement: () => HTMLElement | null;
hasSidebar: boolean;
registerBody: (element: HTMLElement) => () => void;
registerSidebar: (registration: ViewerSidebarRegistration) => () => void;
rootId: string;
sidebarId: string;
sidebarGapTransition: ViewerSidebarGapTransition;
sidebarSide: ViewerSidebarSide;
setLastTriggerElement: (element: HTMLElement | null) => void;
stateNamespace?: ViewerStateAttributeNamespace;
};
export type ViewerRootDiagnostics = {
getRootElement: () => HTMLElement | null;
layoutSignature: string;
rootId: string;
};
export type ViewerSurfaceMeasurement = {
hasMeasured: boolean;
setViewportElement: React.RefCallback<HTMLDivElement>;
viewportElement: HTMLDivElement | null;
viewportHeight: number | null;
viewportWidth: number | null;
};
export type ViewerSidebarSlotNames = {
container?: string;
gap?: string;
inner?: string;
};
export type ViewerStateAttributeSlot = "body" | "root" | "sidebar";
export type ViewerStateAttributeValues = {
hasSidebar?: boolean;
sidebarCollapsible?: ViewerSidebarCollapsible;
sidebarMode?: ViewerSidebarMode;
sidebarOpen?: boolean;
sidebarSide?: ViewerSidebarSide;
sidebarState?: ViewerSidebarState;
};
export type ViewerDataAttributes = Record<`data-${string}`, string | undefined>;
export type ViewerSidebarProps = React.ComponentProps<"aside"> &
ViewerDataAttributes & {
side?: ViewerSidebarSide;
collapsible?: ViewerSidebarCollapsible;
innerClassName?: string;
namespacedSlot?: string;
namespacedSlotNames?: ViewerSidebarSlotNames;
slotNames?: ViewerSidebarSlotNames;
width?: string;
};
registry/new-york-v4/ui/viewer-zoom.tsx
"use client";
import * as React from "react";
import { Maximize, Minus, Plus } from "lucide-react";
import { Button } from "@/components/ui/button";
function IconButton({
label,
children,
...props
}: React.ComponentProps<typeof Button> & { label: string }) {
return (
<Button
variant="ghost"
size="iconSm"
className="size-7"
aria-label={label}
title={label}
{...props}
>
{children}
</Button>
);
}
function clamp(value: number, min: number, max: number) {
return Math.min(max, Math.max(min, value));
}
export function useZoom() {
const [scale, setScale] = React.useState(1);
const zoom = React.useCallback(
(factor: number) => setScale((s) => clamp(s * factor, 0.1, 5)),
[],
);
const reset = React.useCallback(() => setScale(1), []);
return { scale, zoom, reset };
}
export function ZoomActions({
scale,
zoom,
reset,
}: {
scale: number;
zoom: (factor: number) => void;
reset: () => void;
}) {
return (
<>
<IconButton label="Zoom out" onClick={() => zoom(1 / 1.2)}>
<Minus />
</IconButton>
<span className="text-muted-foreground w-12 text-center text-xs tabular-nums">
{Math.round(scale * 100)}%
</span>
<IconButton label="Zoom in" onClick={() => zoom(1.2)}>
<Plus />
</IconButton>
<IconButton label="Actual size" onClick={reset}>
<Maximize />
</IconButton>
</>
);
}
registry/new-york-v4/ui/viewer-abortable-request.ts
export interface SharedAbortableRequest<T> {
controller: AbortController;
promise: Promise<T>;
subscriberPromises: WeakMap<AbortSignal, Promise<T>>;
subscribers: Set<AbortSignal>;
settled: boolean;
}
export function abortError(): DOMException {
return new DOMException("Aborted", "AbortError");
}
export function isAbortError(error: unknown): boolean {
return (
(error instanceof DOMException && error.name === "AbortError") ||
(error instanceof Error && error.name === "AbortError")
);
}
export function promiseForSignal<T>(
map: WeakMap<AbortSignal, Promise<T>>,
signal: AbortSignal,
create: () => Promise<T>,
): Promise<T> {
if (signal.aborted) return Promise.reject(abortError());
const existingPromise = map.get(signal);
if (existingPromise) return existingPromise;
const promise = create().catch((error: unknown) => {
map.delete(signal);
throw error;
});
map.set(signal, promise);
return promise;
}
export function subscribeToAbortableRequest<T>(
entry: SharedAbortableRequest<T>,
signal: AbortSignal,
onNoSubscribers: () => void,
): Promise<T> {
return promiseForSignal(entry.subscriberPromises, signal, () => {
entry.subscribers.add(signal);
return new Promise<T>((resolve, reject) => {
let done = false;
const cleanup = () => {
signal.removeEventListener("abort", onAbort);
entry.subscribers.delete(signal);
};
const onAbort = () => {
if (done) return;
done = true;
cleanup();
if (!entry.settled && entry.subscribers.size === 0) {
entry.controller.abort();
onNoSubscribers();
}
reject(abortError());
};
signal.addEventListener("abort", onAbort, { once: true });
entry.promise.then(
(value) => {
if (done) return;
done = true;
cleanup();
resolve(value);
},
(error: unknown) => {
if (done) return;
done = true;
cleanup();
reject(error);
},
);
});
});
}
registry/new-york-v4/ui/viewer-lru-cache.ts
export const VIEWER_LRU_CACHE_MAX = 12;
export function lruGet<K, V>(map: Map<K, V>, key: K): V | undefined {
const value = map.get(key);
if (value !== undefined) {
map.delete(key);
map.set(key, value);
}
return value;
}
export function lruSet<K, V>(
map: Map<K, V>,
key: K,
value: V,
onEvict?: (key: K, value: V) => void,
max = VIEWER_LRU_CACHE_MAX,
) {
map.delete(key);
map.set(key, value);
while (map.size > max) {
const oldest = map.keys().next().value as K | undefined;
if (oldest === undefined) break;
const dropped = map.get(oldest);
map.delete(oldest);
if (dropped !== undefined) onEvict?.(oldest, dropped);
}
}
registry/new-york-v4/lib/pdf-viewer-diagnostics.ts
const FILE_VIEWER_DIAGNOSTIC_STORAGE_KEY = "retab-file-viewer-debug";
const PDF_DIAGNOSTIC_STORAGE_KEY = "retab-pdf-debug";
const PDF_HEADER = [0x25, 0x50, 0x44, 0x46, 0x2d];
const T0 = typeof performance !== "undefined" ? performance.now() : 0;
const seqRef = { n: 0 };
const sessionSeqRef = { n: 0 };
export type FileViewerDiagnosticLevel = "debug" | "info" | "warn" | "error";
export type PdfViewerDiagnosticLevel = FileViewerDiagnosticLevel;
export type FileViewerDiagnosticData = Record<string, unknown>;
export type PdfViewerDiagnosticData = FileViewerDiagnosticData;
function isTruthyDiagnosticFlag(value: string | null): boolean {
return value === "1" || value === "true";
}
export function isFileViewerDiagnosticsEnabled(): boolean {
if (typeof window === "undefined") return false;
try {
const explicit = window.localStorage.getItem(
FILE_VIEWER_DIAGNOSTIC_STORAGE_KEY,
);
if (explicit !== null) return isTruthyDiagnosticFlag(explicit);
const legacyPdf = window.localStorage.getItem(PDF_DIAGNOSTIC_STORAGE_KEY);
if (legacyPdf !== null) return isTruthyDiagnosticFlag(legacyPdf);
return false;
} catch {
return false;
}
}
export function isPdfViewerDiagnosticsEnabled(): boolean {
return isFileViewerDiagnosticsEnabled();
}
export function fileViewerDiagnostic(
level: FileViewerDiagnosticLevel,
event: string,
data?: FileViewerDiagnosticData,
): void {
if (!isFileViewerDiagnosticsEnabled()) return;
const seq = ++seqRef.n;
const elapsedMs =
(typeof performance !== "undefined" ? performance.now() : 0) - T0;
const method = console[level] ?? console.log;
method(
`%cfile-viewer%c #${seq} %c${elapsedMs.toFixed(1)}ms%c ${event}`,
"background:#0f766e;color:#fff;padding:0 4px;border-radius:3px;font-weight:bold",
"color:#888",
"color:#06c",
"color:inherit",
data ?? "",
);
}
export function pdfViewerDiagnostic(
level: PdfViewerDiagnosticLevel,
event: string,
data?: PdfViewerDiagnosticData,
): void {
fileViewerDiagnostic(level, event, data);
}
export function viewerDiagnosticNow(): number {
return typeof performance !== "undefined" ? performance.now() : Date.now();
}
export function viewerDiagnosticDurationMs(startedAt: number): number {
return Math.max(0, Math.round(viewerDiagnosticNow() - startedAt));
}
export function nextFileViewerSessionId(prefix = "fv"): string {
sessionSeqRef.n += 1;
return `${prefix}_${sessionSeqRef.n.toString(36)}`;
}
export function fileBufferDiagnostics(buffer: ArrayBuffer | Uint8Array) {
const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
const head = bytes.slice(0, Math.min(16, bytes.byteLength));
return {
byteLength: bytes.byteLength,
headHex: bytesToHex(head),
looksLikeHtml: looksLikeHtml(bytes),
pdfHeaderOffset: pdfHeaderOffset(bytes),
tiffHeader: tiffHeader(bytes),
};
}
export function pdfBufferDiagnostics(buffer: ArrayBuffer | Uint8Array) {
return fileBufferDiagnostics(buffer);
}
export function summarizeViewerError(error: unknown, depth = 0): unknown {
if (!(error instanceof Error)) {
const value = String(error);
return {
type: typeof error,
valueHash: hashString(value),
valuePreview: redactDiagnosticText(value, 160),
};
}
const details: PdfViewerDiagnosticData = {
name: error.name,
message: redactDiagnosticText(error.message, 240),
messageHash: hashString(error.message),
};
const maybeCode = (error as { code?: unknown }).code;
if (maybeCode !== undefined) details.code = maybeCode;
const maybeStatus = (error as { status?: unknown }).status;
if (maybeStatus !== undefined) details.status = maybeStatus;
const maybeReason = (error as { reason?: unknown }).reason;
if (maybeReason !== undefined) details.reason = maybeReason;
const cause = (error as { cause?: unknown }).cause;
if (cause !== undefined && depth < 1) {
details.cause = summarizeViewerError(cause, depth + 1);
}
return details;
}
export function summarizePdfError(error: unknown, depth = 0): unknown {
return summarizeViewerError(error, depth);
}
export function safeUrlSummary(url: string | null | undefined) {
if (!url) return null;
try {
const currentOrigin =
typeof window !== "undefined" && window.location?.origin
? window.location.origin
: "http://local";
const parsed = new URL(url, currentOrigin);
return {
origin:
typeof window !== "undefined" &&
window.location?.origin &&
parsed.origin === window.location.origin
? "same-origin"
: parsed.origin,
path: redactUrlPath(parsed.pathname),
hasSearch: parsed.search.length > 0,
};
} catch {
return {
valueHash: hashString(url),
valuePreview: redactDiagnosticText(url, 160),
};
}
}
export function safePdfUrlSummary(url: string | null | undefined) {
return safeUrlSummary(url);
}
export function responseHeadersDiagnostics(headers: Headers) {
const retabFileId = headers.get("x-retab-file-id");
return {
contentLength: headers.get("content-length"),
contentRange: headers.get("content-range"),
contentType: headers.get("content-type"),
requestId: headers.get("x-request-id"),
retabFileIdHash: summarizeDiagnosticId(retabFileId),
retabPreviewKind: headers.get("x-retab-preview-kind"),
};
}
export function summarizeViewerReadOptions(options?: {
signal?: AbortSignal;
maxBytes?: number;
maxLines?: number;
}) {
return {
hasSignal: Boolean(options?.signal),
isSignalAborted: options?.signal?.aborted ?? false,
maxBytes: options?.maxBytes,
maxLines: options?.maxLines,
};
}
export function summarizeViewerByteRange(range: {
start: number;
end: number;
}) {
return {
start: range.start,
end: range.end,
requestedBytes: range.end >= range.start ? range.end - range.start + 1 : 0,
};
}
export function summarizeViewerContent(content: {
key: string;
sourceKind: string;
directUrl?: string | null;
mimeType?: string;
payload?: {
kind: string;
url?: string;
blob?: Blob;
text?: string;
};
}) {
return {
keyHash: summarizeDiagnosticId(content.key),
sourceKind: content.sourceKind,
mimeType: content.mimeType,
directUrl: safeUrlSummary(content.directUrl),
payload: content.payload
? summarizeViewerPayload(content.payload)
: undefined,
};
}
export function summarizeViewerKeys(keys: {
load: string;
presentation: string;
resource: string;
}) {
return {
loadHash: summarizeDiagnosticId(keys.load),
presentationHash: summarizeDiagnosticId(keys.presentation),
resourceHash: summarizeDiagnosticId(keys.resource),
};
}
export function summarizeViewerResource(resource: {
descriptor?: {
category: string;
displayName: string;
fileName: string;
identityKey: string;
mimeType?: string;
};
fileName: string;
identityKey: string;
mimeType?: string;
sourceKind: string;
keys: {
load: string;
presentation: string;
resource: string;
};
content?: {
key: string;
sourceKind: string;
directUrl?: string | null;
mimeType?: string;
payload?: {
kind: string;
url?: string;
blob?: Blob;
text?: string;
};
};
}) {
return {
fileName: summarizeFileName(resource.fileName),
identityKeyHash: summarizeDiagnosticId(resource.identityKey),
mimeType: resource.mimeType,
sourceKind: resource.sourceKind,
descriptor: resource.descriptor
? summarizeViewerDescriptor(resource.descriptor)
: undefined,
content: resource.content
? summarizeViewerContent(resource.content)
: undefined,
keys: summarizeViewerKeys(resource.keys),
};
}
export function summarizeViewerSource(source: {
kind: string;
fileName?: string;
mimeType?: string;
identityKey?: string;
downloadUrl?: string;
url?: string;
text?: string;
blob?: Blob;
}) {
return {
kind: source.kind,
fileName: summarizeFileName(source.fileName),
extension: extensionOf(source.fileName),
mimeType: source.mimeType ?? source.blob?.type,
identityKeyHash: summarizeDiagnosticId(source.identityKey),
url: safeUrlSummary(source.url),
downloadUrl: safeUrlSummary(source.downloadUrl),
textLength: source.kind === "text" ? (source.text?.length ?? 0) : undefined,
blobSize: source.blob?.size,
blobType: source.blob?.type || undefined,
};
}
export function summarizeViewerDescriptor(descriptor: {
category: string;
displayName: string;
fileName: string;
identityKey: string;
mimeType?: string;
}) {
return {
category: descriptor.category,
displayName: summarizeFileName(descriptor.displayName),
fileName: summarizeFileName(descriptor.fileName),
extension: extensionOf(descriptor.fileName),
mimeType: descriptor.mimeType,
identityKeyHash: summarizeDiagnosticId(descriptor.identityKey),
extensionCategory: categoryFromExtension(extensionOf(descriptor.fileName)),
mimeCategory: categoryFromMime(descriptor.mimeType),
};
}
export function viewerCategoryMismatch(descriptor: {
fileName: string;
mimeType?: string;
}) {
const extensionCategory = categoryFromExtension(
extensionOf(descriptor.fileName),
);
const mimeCategory = categoryFromMime(descriptor.mimeType);
if (
!extensionCategory ||
!mimeCategory ||
extensionCategory === mimeCategory
) {
return null;
}
return {
extension: extensionOf(descriptor.fileName),
extensionCategory,
mimeCategory,
mimeType: descriptor.mimeType,
};
}
export function truncate(value: string, maxLength: number): string {
if (value.length <= maxLength) return value;
return `${value.slice(0, maxLength)}...`;
}
export function summarizeDiagnosticId(value: string | null | undefined) {
if (!value) return null;
return {
hash: hashString(value),
length: value.length,
};
}
function summarizeFileName(name: string | null | undefined) {
if (!name) return null;
return {
extension: extensionOf(name),
hash: hashString(name),
length: name.length,
};
}
function redactUrlPath(pathname: string) {
const segments = pathname.split("/").map((segment, index, parts) => {
if (!segment) return segment;
const previous = parts[index - 1];
if (previous === "files") return ":file_id";
if (previous === "primitive-executions") return ":primitive_execution_id";
if (previous === "runs") return ":run_id";
if (previous === "workflows") return ":workflow_id";
return redactIdentifierSegment(segment);
});
return segments.join("/");
}
function redactIdentifierSegment(segment: string) {
if (
/^(file|pexec|run|wf|doc|artifact|clss|parse|split|part|edit|extr)[_-]/i.test(
segment,
)
) {
return `:${segment.split(/[_-]/, 1)[0].toLowerCase()}_id`;
}
if (segment.length >= 18 && /^[a-z0-9_-]+$/i.test(segment)) {
return ":id";
}
return segment;
}
function redactDiagnosticText(value: string, maxLength: number) {
return truncate(
value
.replace(/https?:\/\/\S+/gi, "[url]")
.replace(/data:[^\s"'<>]+/gi, "[data-url]")
.replace(/[?&][^=\s"'<>]+=[^\s"'<>]+/g, "[query]"),
maxLength,
);
}
function hashString(value: string): string {
let hash = 2166136261;
for (let index = 0; index < value.length; index += 1) {
hash ^= value.charCodeAt(index);
hash = Math.imul(hash, 16777619);
}
return (hash >>> 0).toString(16).padStart(8, "0");
}
function summarizeViewerPayload(payload: {
kind: string;
url?: string;
blob?: Blob;
text?: string;
}) {
if (payload.kind === "url") {
return {
kind: payload.kind,
url: safeUrlSummary(payload.url),
};
}
if (payload.kind === "blob") {
return {
kind: payload.kind,
blobSize: payload.blob?.size,
blobType: payload.blob?.type || undefined,
};
}
if (payload.kind === "text") {
return {
kind: payload.kind,
textLength: payload.text?.length ?? 0,
};
}
return { kind: payload.kind };
}
function pdfHeaderOffset(bytes: Uint8Array): number {
const limit = Math.min(bytes.byteLength, 1024);
for (let i = 0; i <= limit - PDF_HEADER.length; i += 1) {
let matches = true;
for (let offset = 0; offset < PDF_HEADER.length; offset += 1) {
if (bytes[i + offset] !== PDF_HEADER[offset]) {
matches = false;
break;
}
}
if (matches) return i;
}
return -1;
}
function tiffHeader(bytes: Uint8Array): "little_endian" | "big_endian" | null {
if (bytes.byteLength < 4) return null;
if (
bytes[0] === 0x49 &&
bytes[1] === 0x49 &&
bytes[2] === 0x2a &&
bytes[3] === 0x00
) {
return "little_endian";
}
if (
bytes[0] === 0x4d &&
bytes[1] === 0x4d &&
bytes[2] === 0x00 &&
bytes[3] === 0x2a
) {
return "big_endian";
}
return null;
}
function looksLikeHtml(bytes: Uint8Array): boolean {
const sample = new TextDecoder()
.decode(bytes.slice(0, Math.min(bytes.byteLength, 64)))
.trimStart()
.toLowerCase();
return sample.startsWith("<!doctype html") || sample.startsWith("<html");
}
function bytesToHex(bytes: Uint8Array): string {
return Array.from(bytes)
.map((byte) => byte.toString(16).padStart(2, "0"))
.join("");
}
function extensionOf(name: string | null | undefined): string | null {
if (!name) return null;
const clean = name.split(/[?#]/)[0];
const base = clean.split("/").pop() ?? clean;
const dot = base.lastIndexOf(".");
return dot > 0 ? base.slice(dot + 1).toLowerCase() : null;
}
function categoryFromExtension(extension: string | null): string | null {
if (!extension) return null;
if (extension === "pdf") return "pdf";
if (extension === "tif" || extension === "tiff") return "image";
if (
extension === "png" ||
extension === "jpg" ||
extension === "jpeg" ||
extension === "gif" ||
extension === "webp" ||
extension === "avif" ||
extension === "bmp" ||
extension === "svg" ||
extension === "ico"
) {
return "image";
}
if (extension === "docx") return "docx";
if (extension === "xlsx" || extension === "xls" || extension === "xlsm") {
return "xlsx";
}
if (extension === "pptx") return "pptx";
if (extension === "csv" || extension === "tsv") return "csv";
if (extension === "md" || extension === "markdown") return "markdown";
if (extension === "html" || extension === "htm") return "html";
return null;
}
function categoryFromMime(mimeType: string | null | undefined): string | null {
if (!mimeType) return null;
const mime = mimeType.toLowerCase().split(";")[0].trim();
if (mime === "application/pdf") return "pdf";
if (mime.startsWith("image/")) return "image";
if (mime.includes("wordprocessingml")) return "docx";
if (mime.includes("spreadsheet") || mime.includes("ms-excel")) return "xlsx";
if (mime.includes("presentation") || mime.includes("ms-powerpoint")) {
return "pptx";
}
if (mime === "text/csv" || mime === "text/tab-separated-values") return "csv";
if (mime === "text/markdown") return "markdown";
if (mime === "text/html") return "html";
if (mime.startsWith("text/")) return "text";
return null;
}
registry/new-york-v4/lib/viewer-download-actions.ts
export type ViewerDownloadOrigin = "original" | "derived";
export type ViewerDownloadPayload =
| { kind: "href"; href: string }
| { kind: "blob"; blob: Blob }
| { kind: "text"; text: string; mimeType?: string }
| { kind: "none" };
export interface ViewerDownloadAction {
id: string;
label: string;
fileName: string;
origin: ViewerDownloadOrigin;
isDisabled?: boolean;
getPayload: (options?: {
signal?: AbortSignal;
}) => ViewerDownloadPayload | Promise<ViewerDownloadPayload>;
}
export type ViewerDownloadErrorKind =
| "disabled"
| "aborted"
| "payload_failed"
| "unsupported";
export class ViewerDownloadError extends Error {
readonly kind: ViewerDownloadErrorKind;
readonly actionId: string;
override readonly cause?: unknown;
constructor({
actionId,
kind,
message,
cause,
}: {
actionId: string;
kind: ViewerDownloadErrorKind;
message: string;
cause?: unknown;
}) {
super(message);
this.name = "ViewerDownloadError";
this.actionId = actionId;
this.kind = kind;
this.cause = cause;
}
}
export function createHrefDownloadAction({
id,
label = "Download",
href,
fileName,
origin = "original",
}: {
id: string;
label?: string;
href: string;
fileName: string;
origin?: ViewerDownloadOrigin;
}): ViewerDownloadAction {
return {
id,
label,
fileName,
origin,
getPayload: () => ({ kind: "href", href }),
};
}
export function createBlobDownloadAction({
id,
label = "Download",
blob,
fileName,
origin = "original",
}: {
id: string;
label?: string;
blob: Blob;
fileName: string;
origin?: ViewerDownloadOrigin;
}): ViewerDownloadAction {
return {
id,
label,
fileName,
origin,
getPayload: () => ({ kind: "blob", blob }),
};
}
export function createTextDownloadAction({
id,
label = "Download",
text,
fileName,
mimeType,
origin = "original",
}: {
id: string;
label?: string;
text: string;
fileName: string;
mimeType?: string;
origin?: ViewerDownloadOrigin;
}): ViewerDownloadAction {
return {
id,
label,
fileName,
origin,
getPayload: () => ({ kind: "text", text, mimeType }),
};
}
export function createDisabledDownloadAction({
id,
label = "Download",
fileName,
origin = "original",
}: {
id: string;
label?: string;
fileName: string;
origin?: ViewerDownloadOrigin;
}): ViewerDownloadAction {
return {
id,
label,
fileName,
origin,
isDisabled: true,
getPayload: () => ({ kind: "none" }),
};
}
registry/new-york-v4/lib/viewer-errors.ts
export type ViewerFormat =
| "pdf"
| "image"
| "text"
| "csv"
| "docx"
| "xlsx"
| "pptx"
| "file";
export type ViewerErrorDomain =
| "resource"
| "format"
| "state"
| "unsupported"
| "unknown";
export type ResourceErrorKind =
| "fetch_failed"
| "http_error"
| "aborted"
| "invalid_range"
| "partial_content"
| "too_large"
| "unsupported_capability"
| "unknown";
export type ResourceTooLargeReason = "bytes" | "lines";
export class ResourceError extends Error {
readonly domain = "resource";
readonly kind: ResourceErrorKind;
readonly status?: number;
readonly tooLargeReason?: ResourceTooLargeReason;
override readonly cause?: unknown;
constructor({
kind,
message,
status,
tooLargeReason,
cause,
}: {
kind: ResourceErrorKind;
message: string;
status?: number;
tooLargeReason?: ResourceTooLargeReason;
cause?: unknown;
}) {
super(message);
this.name = "ResourceError";
this.kind = kind;
this.status = status;
this.tooLargeReason = tooLargeReason;
this.cause = cause;
}
}
export type ViewerFormatErrorKind =
| "bounds"
| "decode_failed"
| "disposed"
| "index_out_of_range"
| "load_failed"
| "parse_failed"
| "render_failed"
| "worker_failed"
| "unknown";
export interface ViewerFormatErrorMapperOptions {
kind: ViewerFormatErrorKind;
message: string;
}
export class ViewerFormatError extends Error {
readonly domain = "format";
readonly format: ViewerFormat;
readonly kind: ViewerFormatErrorKind;
override readonly cause?: unknown;
constructor({
format,
kind,
message,
cause,
}: {
format: ViewerFormat;
kind: ViewerFormatErrorKind;
message: string;
cause?: unknown;
}) {
super(message);
this.name = "ViewerFormatError";
this.format = format;
this.kind = kind;
this.cause = cause;
}
}
export type ViewerStateErrorKind =
| "invalid_bounds"
| "invalid_target"
| "out_of_range"
| "stale_resource"
| "unknown";
export class ViewerStateError extends Error {
readonly domain = "state";
readonly format?: ViewerFormat;
readonly kind: ViewerStateErrorKind;
override readonly cause?: unknown;
constructor({
format,
kind,
message,
cause,
}: {
format?: ViewerFormat;
kind: ViewerStateErrorKind;
message: string;
cause?: unknown;
}) {
super(message);
this.name = "ViewerStateError";
this.format = format;
this.kind = kind;
this.cause = cause;
}
}
export class ViewerUnsupportedError extends Error {
readonly domain = "unsupported";
readonly format?: ViewerFormat;
readonly sourceKind?: string;
override readonly cause?: unknown;
constructor({
format,
sourceKind,
message,
cause,
}: {
format?: ViewerFormat;
sourceKind?: string;
message: string;
cause?: unknown;
}) {
super(message);
this.name = "ViewerUnsupportedError";
this.format = format;
this.sourceKind = sourceKind;
this.cause = cause;
}
}
export interface ViewerErrorInfo {
domain: ViewerErrorDomain;
format?: ViewerFormat;
kind: string;
message: string;
status?: number;
isRetryable: boolean;
isDownloadUseful: boolean;
userMessage: string;
cause?: unknown;
}
export interface ViewerErrorContext {
format?: ViewerFormat;
sourceKind?: "url" | "blob" | "text";
canDownload?: boolean;
retry?: "auto" | "always" | "never";
}
export function isAbortError(error: unknown): boolean {
return (
(error instanceof DOMException && error.name === "AbortError") ||
(error instanceof Error && error.name === "AbortError")
);
}
export function isResourceError(error: unknown): error is ResourceError {
return (
error instanceof ResourceError ||
isErrorLike(error, "ResourceError", "resource")
);
}
export function isViewerFormatError(
error: unknown,
): error is ViewerFormatError {
return (
error instanceof ViewerFormatError ||
isErrorLike(error, "ViewerFormatError", "format")
);
}
export function isViewerStateError(error: unknown): error is ViewerStateError {
return (
error instanceof ViewerStateError ||
isErrorLike(error, "ViewerStateError", "state")
);
}
export function isViewerUnsupportedError(
error: unknown,
): error is ViewerUnsupportedError {
return (
error instanceof ViewerUnsupportedError ||
isErrorLike(error, "ViewerUnsupportedError", "unsupported")
);
}
export function toViewerErrorInfo(
error: unknown,
context: ViewerErrorContext = {},
): ViewerErrorInfo {
const canDownload = context.canDownload ?? true;
if (isResourceError(error)) {
return {
domain: "resource",
format: context.format,
kind: error.kind,
message: error.message,
status: error.status,
isRetryable: retryable(
context,
resourceErrorDefaultRetry(error, context),
),
isDownloadUseful: canDownload && error.kind !== "aborted",
userMessage: resourceErrorUserMessage(error),
cause: error.cause,
};
}
if (isViewerFormatError(error)) {
const format = error.format ?? context.format;
return {
domain: "format",
format,
kind: error.kind,
message: error.message,
isRetryable: retryable(
context,
formatErrorDefaultRetry(error, context, format),
),
isDownloadUseful: canDownload,
userMessage: formatErrorUserMessage(format, error.kind, error),
cause: error.cause,
};
}
if (isViewerStateError(error)) {
return {
domain: "state",
format: error.format ?? context.format,
kind: error.kind,
message: error.message,
isRetryable: retryable(context, false),
isDownloadUseful: canDownload,
userMessage: stateErrorUserMessage(error.kind),
cause: error.cause,
};
}
if (isViewerUnsupportedError(error)) {
return {
domain: "unsupported",
format: error.format ?? context.format,
kind: "unsupported",
message: error.message,
isRetryable: retryable(context, false),
isDownloadUseful: canDownload,
userMessage: "This file cannot be previewed here.",
cause: error.cause,
};
}
if (isAbortError(error)) {
return {
domain: "resource",
format: context.format,
kind: "aborted",
message: "Loading was cancelled.",
isRetryable: retryable(context, false),
isDownloadUseful: false,
userMessage: "Loading was cancelled.",
cause: error,
};
}
const message = error instanceof Error ? error.message : String(error);
return {
domain: "unknown",
format: context.format,
kind: "unknown",
message,
isRetryable: retryable(context, unknownErrorDefaultRetry(context)),
isDownloadUseful: canDownload,
userMessage: fallbackUserMessage(context.format),
cause: error,
};
}
function isErrorLike(error: unknown, name: string, domain: ViewerErrorDomain) {
if (!error || typeof error !== "object") return false;
const candidate = error as {
name?: unknown;
domain?: unknown;
kind?: unknown;
};
return (
(candidate.name === name || candidate.domain === domain) &&
typeof candidate.kind === "string"
);
}
function retryable(context: ViewerErrorContext, fallback: boolean) {
if (context.retry === "always") return true;
if (context.retry === "never") return false;
return fallback;
}
function resourceErrorDefaultRetry(
error: ResourceError,
context: ViewerErrorContext,
) {
if (error.kind === "aborted") return false;
if (error.kind === "invalid_range") return false;
if (error.kind === "too_large") return false;
if (error.kind === "unsupported_capability") return false;
return context.sourceKind === "url";
}
function formatErrorDefaultRetry(
error: ViewerFormatError,
context: ViewerErrorContext,
format: ViewerFormat | undefined,
) {
if (format === "text" && error.kind === "bounds") return false;
if (error.kind === "disposed") return false;
if (error.kind === "index_out_of_range") return false;
if (format === "docx") return true;
return context.sourceKind === "url";
}
function unknownErrorDefaultRetry(context: ViewerErrorContext) {
if (context.format === "docx") return true;
return context.sourceKind === "url";
}
function resourceErrorUserMessage(error: ResourceError) {
if (error.kind === "http_error") {
return error.status
? `Failed to load file: ${error.status}.`
: "Couldn't load this file.";
}
if (error.kind === "fetch_failed") return "Couldn't load this file.";
if (error.kind === "aborted") return "Loading was cancelled.";
if (error.kind === "invalid_range") return "This source range is invalid.";
if (error.kind === "too_large") {
return error.tooLargeReason === "lines"
? "This file has too many lines to preview."
: "This file is too large to preview.";
}
if (error.kind === "partial_content") {
return "This source returned partial content and cannot be previewed here.";
}
if (error.kind === "unsupported_capability") {
return "This source cannot be previewed here.";
}
return "Couldn't load this file.";
}
function formatErrorUserMessage(
format: ViewerFormat | undefined,
kind: string,
error?: unknown,
) {
if (format === "pdf") return "Couldn't load this PDF.";
if (format === "image") {
if (kind === "index_out_of_range")
return "This image page is out of range.";
if (kind === "decode_failed") return "Couldn't decode this image.";
return "Couldn't load this image.";
}
if (format === "text") {
if (kind === "render_failed") return "Couldn't render this text file.";
if (kind === "bounds") {
const boundsError = error as {
reason?: unknown;
boundName?: unknown;
};
if (boundsError.reason === "lines") {
return "This text file has too many lines to preview.";
}
if (boundsError.reason === "bytes") {
return "This text file is too large to preview.";
}
if (typeof boundsError.boundName === "string") {
return "Text viewer bounds are invalid.";
}
}
return "Couldn't load this text file.";
}
if (format === "csv") return "Couldn't parse this table.";
if (format === "docx") return "Couldn't render this document.";
if (format === "xlsx") return "Couldn't parse this spreadsheet.";
if (format === "pptx") {
if (kind === "render_failed") return "Couldn't render this slide.";
return "Couldn't load this presentation.";
}
return "Couldn't load this file.";
}
function stateErrorUserMessage(kind: ViewerStateErrorKind) {
if (kind === "invalid_bounds") return "Viewer bounds are invalid.";
if (kind === "invalid_target") return "The requested target is invalid.";
if (kind === "out_of_range") return "The requested item is out of range.";
if (kind === "stale_resource") return "This viewer state is no longer valid.";
return "Couldn't load this file.";
}
function fallbackUserMessage(format: ViewerFormat | undefined) {
if (format === "pdf") return "Couldn't load this PDF.";
if (format === "image") return "Couldn't load this image.";
if (format === "text") return "Couldn't load this text file.";
if (format === "csv") return "Couldn't parse this table.";
if (format === "docx") return "Couldn't load this document.";
if (format === "xlsx") return "Couldn't load this spreadsheet.";
if (format === "pptx") return "Couldn't load this presentation.";
return "Couldn't load this file.";
}
registry/new-york-v4/lib/viewer-resource.ts
import {
createBlobDownloadAction,
createHrefDownloadAction,
createTextDownloadAction,
type ViewerDownloadAction,
} from "@/lib/viewer-download-actions";
import {
isAbortError,
ResourceError,
type ResourceTooLargeReason,
} from "@/lib/viewer-errors";
import {
resolveViewerDescriptor,
textPayloadKey,
type BlobViewerSource,
type FileCategory,
type TextSource,
type UrlViewerSource,
type ViewerDescriptor,
type ViewerSource,
} from "@/lib/viewer-source";
export interface ResourceReadOptions {
cache?: RequestCache;
signal?: AbortSignal;
}
export interface TextReadOptions extends ResourceReadOptions {
maxBytes?: number;
maxLines?: number;
}
export interface ByteRange {
start: number;
end: number;
}
export interface ByteRangeResult {
buffer: ArrayBuffer;
contentRange?: {
start: number;
end: number;
total: number | null;
};
isComplete: boolean;
}
export interface ViewerResourceKeys {
readonly load: string;
readonly presentation: string;
readonly resource: string;
}
export type ViewerResourcePayload =
| { kind: "url"; url: string }
| { kind: "blob"; blob: Blob }
| { kind: "text"; text: string };
export interface ViewerResourceContent {
readonly key: string;
readonly sourceKind: ViewerSource["kind"];
readonly directUrl: string | null;
readonly mimeType?: string;
readonly payload: ViewerResourcePayload;
readBlob(options?: ResourceReadOptions): Promise<Blob>;
readBytes(options?: ResourceReadOptions): Promise<ArrayBuffer>;
readText(options?: TextReadOptions): Promise<string>;
readStream(
options?: ResourceReadOptions,
): Promise<ReadableStream<Uint8Array>>;
readRange(
range: ByteRange,
options?: ResourceReadOptions,
): Promise<ByteRangeResult>;
}
export type ViewerContentIdentity = Pick<
ViewerResourceContent,
"key" | "sourceKind"
>;
export type ViewerContentDirectUrl = ViewerContentIdentity &
Pick<ViewerResourceContent, "directUrl">;
export type ViewerContentPayload = ViewerContentIdentity &
Pick<ViewerResourceContent, "payload">;
export type ViewerContentMime = ViewerContentIdentity &
Pick<ViewerResourceContent, "mimeType">;
export type ViewerContentBlob = ViewerContentIdentity &
Pick<ViewerResourceContent, "readBlob">;
export type ViewerContentBytes = ViewerContentIdentity &
Pick<ViewerResourceContent, "readBytes">;
export type ViewerContentText = ViewerContentIdentity &
Pick<ViewerResourceContent, "readText">;
export type ViewerContentStream = ViewerContentIdentity &
Pick<ViewerResourceContent, "readStream">;
export type ViewerContentRange = ViewerContentIdentity &
Pick<ViewerResourceContent, "readRange">;
export interface ViewerResource {
readonly descriptor: ViewerDescriptor;
readonly sourceKind: ViewerSource["kind"];
readonly keys: ViewerResourceKeys;
readonly identityKey: string;
readonly fileName: string;
readonly mimeType?: string;
readonly content: ViewerResourceContent;
readonly originalDownload: ViewerDownloadAction;
}
const URL_RESOURCE_REGISTRY_MAX = 128;
const TEXT_RESOURCE_REGISTRY_MAX = 64;
// LF, CR, CRLF, LINE SEPARATOR (U+2028), and PARAGRAPH SEPARATOR (U+2029) — the
// ECMAScript LineTerminator set, matching what a browser breaks on in a
// `white-space: pre` block. Kept in sync with text-viewer-resource's splitter.
const TEXT_LINE_BREAK_PATTERN = /\r\n|[\n\r\u2028\u2029]/g;
const urlViewerResourceRegistry = new Map<string, ViewerResource>();
const urlViewerResourceContentRegistry = new Map<
string,
ViewerResourceContent
>();
const textViewerResourceRegistry = new Map<string, ViewerResource>();
const textViewerResourceContentRegistry = new Map<
string,
ViewerResourceContent
>();
let blobViewerResourceRegistry = new WeakMap<
Blob,
Map<string, ViewerResource>
>();
let blobViewerResourceContentRegistry = new WeakMap<
Blob,
Map<string, ViewerResourceContent>
>();
const blobObjectKeys = new WeakMap<Blob, string>();
let nextBlobObjectKey = 0;
export function createViewerResource(
source: ViewerSource,
category?: FileCategory,
): ViewerResource {
const descriptor = resolveViewerDescriptor({ source, category });
const keys = viewerResourceKeys(source, descriptor);
if (source.kind === "url") {
return internUrlResource(source, descriptor, keys);
}
if (source.kind === "blob") {
return internBlobResource(source, descriptor, keys);
}
return internTextResource(source, descriptor, keys);
}
export function clearViewerResourceRegistryForTests() {
urlViewerResourceRegistry.clear();
urlViewerResourceContentRegistry.clear();
textViewerResourceRegistry.clear();
textViewerResourceContentRegistry.clear();
blobViewerResourceRegistry = new WeakMap();
blobViewerResourceContentRegistry = new WeakMap();
}
function internUrlResource(
source: UrlViewerSource,
descriptor: ViewerDescriptor,
keys: ViewerResourceKeys,
): ViewerResource {
const cached = urlViewerResourceRegistry.get(keys.resource);
if (cached) return cached;
const resource = createUrlResource(source, descriptor, keys);
urlViewerResourceRegistry.set(keys.resource, resource);
pruneUrlResourceRegistry();
return resource;
}
function internBlobResource(
source: BlobViewerSource,
descriptor: ViewerDescriptor,
keys: ViewerResourceKeys,
): ViewerResource {
let resources = blobViewerResourceRegistry.get(source.blob);
if (!resources) {
resources = new Map();
blobViewerResourceRegistry.set(source.blob, resources);
}
const cached = resources.get(keys.resource);
if (cached) return cached;
const resource = createBlobResource(source, descriptor, keys);
resources.set(keys.resource, resource);
return resource;
}
function internTextResource(
source: TextSource,
descriptor: ViewerDescriptor,
keys: ViewerResourceKeys,
): ViewerResource {
const cached = textViewerResourceRegistry.get(keys.resource);
if (cached) return cached;
const resource = createTextResource(source, descriptor, keys);
textViewerResourceRegistry.set(keys.resource, resource);
pruneTextResourceRegistry();
return resource;
}
function pruneUrlResourceRegistry() {
while (urlViewerResourceRegistry.size > URL_RESOURCE_REGISTRY_MAX) {
const firstKey = urlViewerResourceRegistry.keys().next().value;
if (!firstKey) return;
urlViewerResourceRegistry.delete(firstKey);
}
}
function pruneUrlResourceContentRegistry() {
while (urlViewerResourceContentRegistry.size > URL_RESOURCE_REGISTRY_MAX) {
const firstKey = urlViewerResourceContentRegistry.keys().next().value;
if (!firstKey) return;
urlViewerResourceContentRegistry.delete(firstKey);
}
}
function pruneTextResourceRegistry() {
while (textViewerResourceRegistry.size > TEXT_RESOURCE_REGISTRY_MAX) {
const firstKey = textViewerResourceRegistry.keys().next().value;
if (!firstKey) return;
textViewerResourceRegistry.delete(firstKey);
}
}
function pruneTextResourceContentRegistry() {
while (textViewerResourceContentRegistry.size > TEXT_RESOURCE_REGISTRY_MAX) {
const firstKey = textViewerResourceContentRegistry.keys().next().value;
if (!firstKey) return;
textViewerResourceContentRegistry.delete(firstKey);
}
}
function viewerResourceKeys(
source: ViewerSource,
descriptor: ViewerDescriptor,
): ViewerResourceKeys {
const load = viewerResourceLoadKey(source, descriptor);
const presentation = viewerResourcePresentationKey(source, descriptor);
return {
load,
presentation,
resource: [load, presentation].join("\u0000"),
};
}
function viewerResourceLoadKey(
source: ViewerSource,
descriptor: ViewerDescriptor,
) {
return [
source.kind,
source.identityKey ?? "",
sourceMimeType(source) ?? "",
directLoadCacheKey(source),
payloadCacheKey(source, descriptor),
].join("\u0000");
}
function viewerResourcePresentationKey(
source: ViewerSource,
descriptor: ViewerDescriptor,
) {
return [
descriptor.category,
descriptor.displayName,
descriptor.fileName,
descriptor.mimeType ?? "",
downloadCacheKey(source),
].join("\u0000");
}
function directLoadCacheKey(source: ViewerSource) {
return source.kind === "url" ? source.url : "";
}
function downloadCacheKey(source: ViewerSource) {
if (source.kind === "text") return "";
return source.downloadUrl ?? "";
}
function payloadCacheKey(source: ViewerSource, descriptor: ViewerDescriptor) {
if (source.kind === "url") return "";
if (source.kind === "blob") return blobObjectKey(source.blob);
return source.identityKey ? "" : descriptor.identityKey;
}
export function viewerResourceRenderKey(resource: ViewerResource): string {
const load = [
resource.sourceKind,
resource.identityKey,
resource.mimeType ?? resource.content.mimeType ?? "",
resource.content.directUrl ?? "",
viewerContentRenderKey(resource.content),
].join("\u0000");
return [load, resource.keys.presentation].join("\u0000");
}
export function viewerContentRenderKey(content: ViewerResourceContent): string {
if (content.payload.kind === "text")
return textPayloadKey(content.payload.text);
return content.key;
}
function sourceMimeType(source: ViewerSource) {
if (source.kind === "blob") return source.mimeType ?? source.blob.type;
return source.mimeType;
}
function blobObjectKey(blob: Blob) {
let key = blobObjectKeys.get(blob);
if (!key) {
nextBlobObjectKey += 1;
key = `blob-object:${nextBlobObjectKey}`;
blobObjectKeys.set(blob, key);
}
return key;
}
export function blobSource(
bytes: Blob | ArrayBuffer | Uint8Array,
metadata: {
identityKey: string;
fileName?: string;
mimeType?: string;
downloadUrl?: string;
},
): BlobViewerSource {
const blob =
bytes instanceof Blob
? bytes
: new Blob(
[bytes instanceof ArrayBuffer ? bytes : new Uint8Array(bytes)],
{
type: metadata.mimeType ?? "",
},
);
return {
kind: "blob",
blob,
fileName: metadata.fileName,
mimeType: metadata.mimeType ?? blob.type,
downloadUrl: metadata.downloadUrl,
identityKey: metadata.identityKey,
};
}
function createUrlResource(
source: UrlViewerSource,
descriptor: ViewerDescriptor,
keys: ViewerResourceKeys,
): ViewerResource {
const content = internUrlResourceContent(source, keys);
const originalDownload = createHrefDownloadAction({
id: "download-original",
label: "Download",
href: source.downloadUrl ?? source.url,
fileName: descriptor.fileName,
});
return resourceBase(source, descriptor, keys, { content, originalDownload });
}
function internUrlResourceContent(
source: UrlViewerSource,
keys: ViewerResourceKeys,
): ViewerResourceContent {
const cached = urlViewerResourceContentRegistry.get(keys.load);
if (cached) return cached;
const content = resourceContentBase(source, keys, {
directUrl: source.url,
payload: { kind: "url", url: source.url },
readBlob: async ({ cache, signal } = {}) => {
const response = await fetchResource(
source.url,
cache ? { cache, signal } : { signal },
);
validateFullContentResponse(response);
return readResponseBlob(response);
},
readBytes: async ({ cache, signal } = {}) => {
const response = await fetchResource(
source.url,
cache ? { cache, signal } : { signal },
);
validateFullContentResponse(response);
return readResponseArrayBuffer(response);
},
readText: async ({ cache, signal, maxBytes, maxLines } = {}) => {
const response = await fetchResource(
source.url,
cache ? { cache, signal } : { signal },
);
return readBoundedResponseText(response, { maxBytes, maxLines });
},
readStream: async ({ cache, signal } = {}) => {
const response = await fetchResource(
source.url,
cache ? { cache, signal } : { signal },
);
validateFullContentResponse(response);
if (!response.body) {
if (response.status === 204 || response.status === 205) {
return emptyByteStream();
}
throw new ResourceError({
kind: "unsupported_capability",
message: "This response cannot be streamed.",
});
}
return response.body;
},
readRange: async (range, { cache, signal } = {}) => {
validateByteRange(range);
const { start, end } = range;
const init = {
signal,
headers: { Range: `bytes=${start}-${end}` },
};
const response = await fetchResource(
source.url,
cache ? { ...init, cache } : init,
);
const buffer = await readResponseArrayBuffer(response);
const contentRange = parseContentRange(
response.headers.get("content-range"),
);
validateUrlRangeResponse({
bufferLength: buffer.byteLength,
contentRange,
range,
status: response.status,
});
return {
buffer,
contentRange,
isComplete: isByteRangeComplete({
bufferLength: buffer.byteLength,
contentRange,
requestedLength: end - start + 1,
status: response.status,
}),
};
},
});
urlViewerResourceContentRegistry.set(keys.load, content);
pruneUrlResourceContentRegistry();
return content;
}
function emptyByteStream() {
return new ReadableStream<Uint8Array>({
start(controller) {
controller.close();
},
});
}
function createBlobResource(
source: BlobViewerSource,
descriptor: ViewerDescriptor,
keys: ViewerResourceKeys,
): ViewerResource {
const blob = source.blob;
const content = internBlobResourceContent(source, keys);
const originalDownload = source.downloadUrl
? createHrefDownloadAction({
id: "download-original",
label: "Download",
href: source.downloadUrl,
fileName: descriptor.fileName,
})
: createBlobDownloadAction({
id: "download-original",
label: "Download",
blob,
fileName: descriptor.fileName,
});
return resourceBase(source, descriptor, keys, { content, originalDownload });
}
function internBlobResourceContent(
source: BlobViewerSource,
keys: ViewerResourceKeys,
): ViewerResourceContent {
const blob = source.blob;
let contents = blobViewerResourceContentRegistry.get(blob);
if (!contents) {
contents = new Map();
blobViewerResourceContentRegistry.set(blob, contents);
}
const cached = contents.get(keys.load);
if (cached) return cached;
const content = resourceContentBase(source, keys, {
directUrl: null,
payload: { kind: "blob", blob },
readBlob: async () => blob,
readBytes: async () => blob.arrayBuffer(),
readText: async ({ maxBytes, maxLines } = {}) =>
readBoundedBlobText(blob, { maxBytes, maxLines }),
readStream: async () => blob.stream(),
readRange: async (range) => {
validateByteRange(range);
const { start, end } = range;
validateKnownByteRangeStart(start, blob.size);
const rangeBlob = blob.slice(start, end + 1);
return {
buffer: await rangeBlob.arrayBuffer(),
contentRange: {
start,
end: Math.min(end, blob.size - 1),
total: blob.size,
},
isComplete: end >= blob.size - 1,
};
},
});
contents.set(keys.load, content);
return content;
}
function createTextResource(
source: TextSource,
descriptor: ViewerDescriptor,
keys: ViewerResourceKeys,
): ViewerResource {
const content = internTextResourceContent(source, keys);
const originalDownload = createTextDownloadAction({
id: "download-original",
label: "Download",
text: source.text,
fileName: descriptor.fileName,
mimeType: descriptor.mimeType,
});
return resourceBase(source, descriptor, keys, { content, originalDownload });
}
function internTextResourceContent(
source: TextSource,
keys: ViewerResourceKeys,
): ViewerResourceContent {
const cached = textViewerResourceContentRegistry.get(keys.load);
if (cached) return cached;
const content = resourceContentBase(source, keys, {
directUrl: null,
payload: { kind: "text", text: source.text },
readBlob: async () =>
new Blob([source.text], {
type: "text/plain;charset=utf-8",
}),
readBytes: async () =>
typedArrayBuffer(new TextEncoder().encode(source.text)),
readText: async ({ maxBytes, maxLines } = {}) =>
readBoundedInlineText(source.text, { maxBytes, maxLines }),
readStream: async () => new Blob([source.text]).stream(),
readRange: async (range) => {
validateByteRange(range);
const { start, end } = range;
const buffer = new TextEncoder().encode(source.text);
validateKnownByteRangeStart(start, buffer.byteLength);
const slice = buffer.slice(start, end + 1);
return {
buffer: typedArrayBuffer(slice),
contentRange: {
start,
end: Math.min(end, buffer.byteLength - 1),
total: buffer.byteLength,
},
isComplete: end >= buffer.byteLength - 1,
};
},
});
textViewerResourceContentRegistry.set(keys.load, content);
pruneTextResourceContentRegistry();
return content;
}
function resourceBase(
source: ViewerSource,
descriptor: ViewerDescriptor,
keys: ViewerResourceKeys,
options: {
content: ViewerResourceContent;
originalDownload: ViewerDownloadAction;
},
): ViewerResource {
const { content, originalDownload } = options;
return Object.freeze({
descriptor,
sourceKind: source.kind,
keys,
identityKey: descriptor.identityKey,
fileName: descriptor.fileName,
mimeType: descriptor.mimeType,
content,
originalDownload,
});
}
function resourceContentBase(
source: ViewerSource,
keys: ViewerResourceKeys,
methods: Omit<ViewerResourceContent, "key" | "sourceKind" | "mimeType">,
): ViewerResourceContent {
return Object.freeze({
key: keys.load,
sourceKind: source.kind,
mimeType: sourceMimeType(source),
...methods,
});
}
function typedArrayBuffer(bytes: Uint8Array): ArrayBuffer {
return bytes.buffer.slice(
bytes.byteOffset,
bytes.byteOffset + bytes.byteLength,
) as ArrayBuffer;
}
async function fetchResource(
input: RequestInfo | URL,
init?: RequestInit,
): Promise<Response> {
let response: Response;
try {
response = await fetch(input, init);
} catch (error) {
if (isAbortError(error)) {
throw new ResourceError({
kind: "aborted",
message: "Loading was cancelled.",
cause: error,
});
}
throw new ResourceError({
kind: "fetch_failed",
message: "Could not fetch this resource.",
cause: error,
});
}
if (!response.ok && response.status !== 206) {
throw new ResourceError({
kind: "http_error",
message: `Failed to load resource: ${response.status}`,
status: response.status,
});
}
return response;
}
async function readBoundedResponseText(
response: Response,
bounds: { maxBytes?: number; maxLines?: number },
) {
validateFullContentResponse(response);
const maxBytes = bounds.maxBytes;
if (
isContentLengthOverLimit(response.headers.get("content-length"), maxBytes)
) {
throw tooLarge("bytes");
}
const body = response.body;
if (!body) {
const buffer = await readResponseArrayBuffer(response);
if (maxBytes != null && buffer.byteLength > maxBytes) {
throw tooLarge("bytes");
}
const text = new TextDecoder().decode(buffer);
assertLineLimit(text, bounds.maxLines);
return text;
}
const reader = body.getReader();
const decoder = new TextDecoder();
const lineLimitTracker = createLineLimitTracker(bounds.maxLines);
let receivedBytes = 0;
let text = "";
while (true) {
const { done, value } = await readResponseStreamChunk(reader);
if (done) break;
receivedBytes += value.byteLength;
if (maxBytes != null && receivedBytes > maxBytes) {
await cancelReaderSilently(reader);
throw tooLarge("bytes");
}
const chunkText = decoder.decode(value, { stream: true });
try {
lineLimitTracker.push(chunkText);
} catch (error) {
await cancelReaderSilently(reader);
throw error;
}
text += chunkText;
}
const finalText = decoder.decode();
lineLimitTracker.push(finalText);
text += finalText;
return text;
}
function isContentLengthOverLimit(
contentLength: string | null,
maxBytes: number | undefined,
) {
if (maxBytes == null || contentLength == null) return false;
const normalizedLength = contentLength.trim().replace(/^0+(?=\d)/, "");
if (!/^\d+$/.test(normalizedLength)) return false;
const maxLength = String(maxBytes);
return (
normalizedLength.length > maxLength.length ||
(normalizedLength.length === maxLength.length &&
normalizedLength > maxLength)
);
}
function validateFullContentResponse(response: Response) {
if (response.status !== 206) return;
const contentRange = parseContentRange(response.headers.get("content-range"));
if (
contentRange?.total != null &&
contentRange.start === 0 &&
contentRange.end === contentRange.total - 1
) {
return;
}
throw new ResourceError({
kind: "partial_content",
message: "Full response returned partial content.",
status: response.status,
});
}
async function readResponseStreamChunk(
reader: ReadableStreamDefaultReader<Uint8Array>,
) {
try {
return await reader.read();
} catch (error) {
throw resourceReadError(error);
}
}
async function readResponseArrayBuffer(response: Response) {
try {
return await response.arrayBuffer();
} catch (error) {
throw resourceReadError(error);
}
}
async function readResponseBlob(response: Response) {
try {
return await response.blob();
} catch (error) {
throw resourceReadError(error);
}
}
function resourceReadError(error: unknown) {
if (isAbortError(error)) {
return new ResourceError({
kind: "aborted",
message: "Loading was cancelled.",
cause: error,
});
}
return new ResourceError({
kind: "fetch_failed",
message: "Could not read this resource.",
cause: error,
});
}
async function readBoundedBlobText(
blob: Blob,
bounds: { maxBytes?: number; maxLines?: number },
) {
if (bounds.maxBytes != null && blob.size > bounds.maxBytes) {
throw tooLarge("bytes");
}
const text = await blob.text();
assertLineLimit(text, bounds.maxLines);
return text;
}
function readBoundedInlineText(
text: string,
{ maxBytes, maxLines }: { maxBytes?: number; maxLines?: number },
) {
// For inline sources the string *is* the resource, so its UTF-8 byte length
// is the authoritative size to measure against maxBytes.
if (
maxBytes != null &&
new TextEncoder().encode(text).byteLength > maxBytes
) {
throw tooLarge("bytes");
}
assertLineLimit(text, maxLines);
return text;
}
// Used after a transferred-byte check has already enforced maxBytes (URL/blob).
// Re-encoding the decoded text here would double-count: invalid UTF-8 decodes to
// U+FFFD (3 bytes each), inflating the measured size past the real wire bytes
// and falsely rejecting small resources as "too large".
function assertLineLimit(text: string, maxLines: number | undefined) {
if (
maxLines != null &&
text.split(TEXT_LINE_BREAK_PATTERN).length > maxLines
) {
throw tooLarge("lines");
}
}
function tooLarge(reason: ResourceTooLargeReason) {
return new ResourceError({
kind: "too_large",
tooLargeReason: reason,
message: `Resource exceeds ${reason} limit.`,
});
}
async function cancelReaderSilently(
reader: ReadableStreamDefaultReader<Uint8Array>,
) {
try {
await reader.cancel();
} catch {
// Preserve the user-facing load failure; cancellation is best-effort cleanup.
}
}
function validateByteRange({ start, end }: ByteRange) {
if (
!Number.isSafeInteger(start) ||
!Number.isSafeInteger(end) ||
start < 0 ||
end < start
) {
throw new ResourceError({
kind: "invalid_range",
message: "Byte range must use non-negative integer bounds.",
});
}
}
function validateKnownByteRangeStart(start: number, total: number) {
if (start > 0 && start >= total) {
throw new ResourceError({
kind: "invalid_range",
message: "Byte range starts past the available resource.",
});
}
}
function validateUrlRangeResponse({
bufferLength,
contentRange,
range,
status,
}: {
bufferLength: number;
contentRange: ByteRangeResult["contentRange"];
range: ByteRange;
status: number;
}) {
if (status === 200) {
if (range.start !== 0 || bufferLength > range.end - range.start + 1) {
throw new ResourceError({
kind: "invalid_range",
message: "Full response does not match the requested byte range.",
});
}
return;
}
if (status !== 206) {
throw new ResourceError({
kind: "invalid_range",
message: "Range response must return full or partial content.",
});
}
if (!contentRange) {
throw new ResourceError({
kind: "invalid_range",
message: "Partial content response is missing a valid byte range.",
});
}
const declaredLength = contentRange.end - contentRange.start + 1;
if (
contentRange.start !== range.start ||
contentRange.end < contentRange.start ||
contentRange.end > range.end ||
(contentRange.total != null && contentRange.end >= contentRange.total) ||
declaredLength !== bufferLength
) {
throw new ResourceError({
kind: "invalid_range",
message: "Response byte range does not match the requested range.",
});
}
}
function isByteRangeComplete({
bufferLength,
contentRange,
requestedLength,
status,
}: {
bufferLength: number;
contentRange: ByteRangeResult["contentRange"];
requestedLength: number;
status: number;
}) {
if (status === 200) return true;
if (contentRange?.total != null) {
if (contentRange.total <= 0) return true;
return contentRange.end >= contentRange.total - 1;
}
if (contentRange) return false;
return bufferLength < requestedLength;
}
function isStandaloneLineBreak(character: string) {
const code = character.charCodeAt(0);
return code === 0x0a || code === 0x2028 || code === 0x2029;
}
function createLineLimitTracker(maxLines: number | undefined) {
let lineCount = 1;
let previousWasCR = false;
return {
push(text: string) {
if (maxLines == null || text.length === 0) return;
for (const character of text) {
if (previousWasCR) {
previousWasCR = false;
if (character === "\n") continue;
}
if (character === "\r") {
lineCount += 1;
previousWasCR = true;
} else if (isStandaloneLineBreak(character)) {
// LF, plus LINE/PARAGRAPH SEPARATOR (U+2028/U+2029); none pair with CR.
lineCount += 1;
}
if (lineCount > maxLines) {
throw tooLarge("lines");
}
}
},
};
}
function parseContentRange(value: string | null) {
if (!value) return undefined;
const match = value.match(/^bytes\s+(\d+)-(\d+)\/(\d+|\*)\s*$/i);
if (!match) return undefined;
const start = parseContentRangeNumber(match[1]);
const end = parseContentRangeNumber(match[2]);
const total =
match[3] === "*" ? null : parseContentRangeNumber(match[3] ?? "");
if (start == null || end == null || total === undefined) return undefined;
return {
start,
end,
total,
};
}
function parseContentRangeNumber(value: string) {
const number = Number(value);
return Number.isSafeInteger(number) ? number : undefined;
}
registry/new-york-v4/lib/viewer-source.ts
export type FileCategory =
| "pdf"
| "docx"
| "xlsx"
| "pptx"
| "csv"
| "image"
| "markdown"
| "html"
| "email"
| "text"
| "unsupported";
export type ViewerSource = UrlViewerSource | TextSource | BlobViewerSource;
export interface UrlViewerSource {
kind: "url";
url: string;
fileName?: string;
mimeType?: string;
downloadUrl?: string;
identityKey?: string;
}
export interface TextSource {
kind: "text";
text: string;
fileName?: string;
mimeType?: string;
identityKey?: string;
}
export interface BlobViewerSource {
kind: "blob";
blob: Blob;
identityKey: string;
fileName?: string;
mimeType?: string;
downloadUrl?: string;
}
export interface ViewerDescriptor {
source: ViewerSource;
category: FileCategory;
identityKey: string;
displayName: string;
fileName: string;
mimeType?: string;
}
const EXTENSION_CATEGORY: Record<string, FileCategory> = {
pdf: "pdf",
docx: "docx",
xlsx: "xlsx",
xls: "xlsx",
xlsm: "xlsx",
pptx: "pptx",
csv: "csv",
tsv: "csv",
png: "image",
jpg: "image",
jpeg: "image",
gif: "image",
webp: "image",
avif: "image",
bmp: "image",
svg: "image",
ico: "image",
tif: "image",
tiff: "image",
md: "markdown",
markdown: "markdown",
mdx: "text",
html: "html",
htm: "html",
eml: "email",
txt: "text",
text: "text",
log: "text",
json: "text",
jsonl: "text",
json5: "text",
ndjson: "text",
xml: "text",
yaml: "text",
yml: "text",
toml: "text",
ini: "text",
env: "text",
js: "text",
mjs: "text",
cjs: "text",
jsx: "text",
ts: "text",
tsx: "text",
css: "text",
scss: "text",
less: "text",
py: "text",
rb: "text",
go: "text",
rs: "text",
java: "text",
kt: "text",
c: "text",
h: "text",
cpp: "text",
cc: "text",
cs: "text",
php: "text",
sh: "text",
bash: "text",
zsh: "text",
sql: "text",
graphql: "text",
proto: "text",
lua: "text",
r: "text",
swift: "text",
scala: "text",
pl: "text",
vue: "text",
svelte: "text",
};
export function extensionOf(name: string): string | null {
const clean = name.split(/[?#]/)[0];
const base = clean.split("/").pop() ?? clean;
const dot = base.lastIndexOf(".");
return dot > 0 ? base.slice(dot + 1).toLowerCase() : null;
}
export function extractName(url: string): string {
const clean = url.split(/[?#]/)[0];
return clean.split("/").pop() || "file";
}
export function detectCategory(
fileName: string,
mimeType?: string,
): FileCategory {
const ext = extensionOf(fileName);
if (ext && EXTENSION_CATEGORY[ext]) return EXTENSION_CATEGORY[ext];
if (mimeType) {
const fromMime = categoryFromMime(mimeType);
if (fromMime) return fromMime;
}
return "unsupported";
}
export function resolveViewerDescriptor({
source,
category,
}: {
source: ViewerSource;
category?: FileCategory;
}): ViewerDescriptor {
const resolvedMimeType =
source.mimeType ??
(source.kind === "blob" && source.blob.type ? source.blob.type : undefined);
const displayName = source.fileName ?? defaultDisplayName(source);
const fileName = source.fileName ?? defaultFileName(source);
const resolvedCategory =
category ?? detectCategory(displayName, resolvedMimeType);
return {
source,
category: resolvedCategory,
identityKey: source.identityKey ?? defaultIdentityKey(source),
displayName,
fileName,
mimeType: resolvedMimeType,
};
}
function categoryFromMime(mimeType: string): FileCategory | null {
const mime = mimeType.toLowerCase().split(";")[0].trim();
if (mime === "application/pdf") return "pdf";
if (mime.includes("wordprocessingml")) return "docx";
if (mime.includes("spreadsheet") || mime.includes("ms-excel")) return "xlsx";
if (mime.includes("presentation") || mime.includes("ms-powerpoint")) {
return "pptx";
}
if (mime === "text/csv" || mime === "text/tab-separated-values") return "csv";
if (mime === "text/markdown") return "markdown";
if (mime === "text/html") return "html";
if (mime === "message/rfc822" || mime === "message/global") {
return "email";
}
if (mime.startsWith("image/")) return "image";
if (mime === "application/json" || mime === "application/xml") return "text";
if (mime.startsWith("text/")) return "text";
return null;
}
function defaultDisplayName(source: ViewerSource) {
if (source.kind === "url") return source.url;
if (source.kind === "text") return "text.txt";
return "file";
}
function defaultFileName(source: ViewerSource) {
if (source.kind === "url") return extractName(source.url);
if (source.kind === "text") return "text.txt";
return "file";
}
function defaultIdentityKey(source: ViewerSource) {
if (source.kind === "url") return `url:${source.url}`;
if (source.kind === "text") return textPayloadIdentityKey(source.text);
return source.identityKey;
}
export function textPayloadIdentityKey(text: string) {
return textPayloadKey(text);
}
export function textPayloadKey(text: string) {
return `text:${text.length}:${hashString(text)}`;
}
function hashString(text: string) {
let hash = 0x811c9dc5;
for (let index = 0; index < text.length; index += 1) {
hash ^= text.charCodeAt(index);
hash = Math.imul(hash, 0x01000193);
}
return (hash >>> 0).toString(36);
}
registry/new-york-v4/ui/use-is-client.ts
"use client";
import * as React from "react";
const emptySubscribe = () => () => {};
const getClientSnapshot = () => true;
const getServerSnapshot = () => false;
/**
* SSR gate: `false` on the server (and during hydration's first pass),
* `true` on the client.
*
* This must stay a synchronous external-store read, NOT the
* `useState(false)` + mount-effect flip. The flip pattern makes every
* viewer mount its Suspense boundary in a later update; when two such
* boundaries suspend on pending resources in the same flush as other
* commit-phase updates (viewer sidebar/geometry registration), React 19's
* retry lanes desynchronize and re-attempt each other's boundary on every
* commit — an unbounded synchronous suspend/retry loop that starves the
* event loop (jsdom tests OOM; browsers busy-spin until the resource
* resolves). With the store read, client renders suspend on mount, which
* never enters that loop. Regression-guarded in
* tests/pdf-viewer-thumbnails.test.tsx ("shares one document resource…").
*/
export function useIsClient(): boolean {
return React.useSyncExternalStore(
emptySubscribe,
getClientSnapshot,
getServerSnapshot,
);
}
registry/new-york-v4/ui/file-viewer-fit-width-motion.ts
"use client";
import type { FileViewerDocumentSurfaceMotionResolver } from "./file-viewer-motion-kernel";
import type { FileViewerMotionFrame } from "./file-viewer-motion-plan";
import type {
FileViewerDocumentAlign,
FileViewerInlineDirection,
} from "./file-viewer-renderer-contract";
export const FILE_VIEWER_FIT_WIDTH_ANCHOR_BLOCK_PROPERTY =
"--file-viewer-fit-width-anchor-block";
// Commit-then-relax: the renderer lays out at the motion's TARGET width from
// the first commit, and this resolver reprojects that settled layout to the
// in-flight visual width with one uniform transform. The transform terminates
// on identity, so settle removes a no-op style instead of committing layout.
//
// The anchor custom property is the reading line's block offset in the settled
// stage's own coordinates (post-rebase scrollTop + marker offset). It is read
// live via var(), so the renderer writes it once per motion (in a layout
// effect after the slide-start scroll rebase) without re-entering the kernel.
export function createFileViewerFitWidthSurfaceMotionResolver({
align,
anchorBlockProperty = FILE_VIEWER_FIT_WIDTH_ANCHOR_BLOCK_PROPERTY,
direction = "ltr",
isFitWidth,
stageInlineSize,
stageOuterInlinePadding = 0,
stageInlinePadding = 0,
stageInlineSlope = 1,
stageBlockSlope = stageInlineSlope,
}: {
align: FileViewerDocumentAlign;
anchorBlockProperty?: string;
direction?: FileViewerInlineDirection;
isFitWidth: boolean;
stageInlineSize: number;
/** Constant symmetric padding around the transformed stage. */
stageOuterInlinePadding?: number;
/** Constant symmetric inline padding inside the transformed stage. */
stageInlinePadding?: number;
stageInlineSlope?: number;
/**
* Slope for the BLOCK axis when it differs from the inline one. A stage
* whose inline box carries constant padding while its block stack scales
* with the content (the image viewer: fit subtracts the horizontal
* padding, vertical gaps/padding scale) has two different affine models —
* X tracks the pane 1:1 while Y scales by the content ratio — and a
* uniform scale cannot land both axes exactly. Defaults to the inline
* slope (uniform scale) for fully proportional stages like the PDF.
*/
stageBlockSlope?: number;
}): FileViewerDocumentSurfaceMotionResolver {
return (frame) => {
if (!isFitWidth || frame.phase !== "sliding") {
return {
transform: "",
transformOrigin: "",
willChange: "",
};
}
return {
transform: getFileViewerFitWidthSurfaceMotionTransform({
align,
anchorBlockProperty,
direction,
frame,
stageInlineSize,
stageOuterInlinePadding,
stageInlinePadding,
stageInlineSlope,
stageBlockSlope,
}),
transformOrigin: "0px 0px",
willChange: "transform",
};
};
}
// Commit-then-relax for a CLAMPED reading column rather than a fit-width
// stage: the stage's inline size is min(canvas, maxStageInlineSize), so it
// does not scale with the pane — the only thing a width change moves is the
// align margin. The canvas commits the motion's TARGET width from the first
// sliding frame (minWidth under layoutPolicy "target"), which means a
// widening pane's chunks land at the settled margin synchronously with the
// click; this resolver reprojects them back to the live width's margin with
// a translate that terminates on identity. A narrowing pane never engages it
// (the canvas tracks the live width above its minWidth, so live and settled
// margins agree) — exactly the leg that already glides on layout.
export function createFileViewerAlignTranslateSurfaceMotionResolver({
align,
direction = "ltr",
maxStageInlineSize,
}: {
align: FileViewerDocumentAlign;
direction?: FileViewerInlineDirection;
/** The column's max inline size (the chunk's max-width, in px). */
maxStageInlineSize: number;
}): FileViewerDocumentSurfaceMotionResolver {
return (frame) => {
if (frame.phase !== "sliding") {
return {
transform: "",
transformOrigin: "",
willChange: "",
};
}
return {
transform: getFileViewerAlignTranslateSurfaceMotionTransform({
align,
direction,
frame,
maxStageInlineSize,
}),
transformOrigin: "0px 0px",
willChange: "transform",
};
};
}
function getFileViewerAlignTranslateSurfaceMotionTransform({
align,
direction,
frame,
maxStageInlineSize,
}: {
align: FileViewerDocumentAlign;
direction: FileViewerInlineDirection;
frame: FileViewerMotionFrame;
maxStageInlineSize: number;
}) {
if (
!Number.isFinite(maxStageInlineSize) ||
maxStageInlineSize <= 0 ||
frame.layoutInlineSize <= 0 ||
frame.toInlineSize <= 0
) {
return "";
}
// The canvas lays out at max(live, target): minWidth holds the committed
// target under a still-narrow pane, and a pane wider than the target just
// fills. The stage (reading column) centers/aligns INSIDE the canvas, and
// an overflowing canvas itself pins to the pane's start edge — left in
// LTR, right in RTL — so the stage's pane-space position carries the
// canvas offset too.
const canvasInlineSize = Math.max(frame.layoutInlineSize, frame.toInlineSize);
const stageInlineSize = Math.min(canvasInlineSize, maxStageInlineSize);
const canvasInlineOffset =
direction === "rtl"
? Math.min(0, frame.layoutInlineSize - canvasInlineSize)
: 0;
const settledStageLeft =
canvasInlineOffset +
getFileViewerStageInlineMargin({
align,
availableInlineSize: canvasInlineSize,
direction,
stageInlineSize,
});
const liveStageLeft = getFileViewerStageInlineMargin({
align,
availableInlineSize: frame.layoutInlineSize,
direction,
stageInlineSize,
});
const translateX = liveStageLeft - settledStageLeft;
if (Math.abs(translateX) <= 0.001) return "";
return `translate3d(${formatFileViewerMotionPixel(translateX)}px, 0px, 0)`;
}
export function getFileViewerFitWidthScale({
availableInlineSize,
contentInlineSize,
stageInlinePadding = 0,
}: {
availableInlineSize: number;
contentInlineSize: number;
stageInlinePadding?: number;
}) {
if (availableInlineSize <= 0 || contentInlineSize <= 0) return 1;
const contentAvailableInlineSize = Math.max(
1,
availableInlineSize - stageInlinePadding,
);
return contentAvailableInlineSize / contentInlineSize;
}
// The visual scale the resolver renders for a given live width — the same
// affine reprojection as the transform itself. Renderers use it to reason
// about the on-screen state (anchor capture/solve) without duplicating the
// formula.
//
// stageInlineSlope is how many stage pixels the settled stage grows per pane
// pixel. It is 1 whenever the stage IS the fit-width content (image, docx,
// pptx, uniform-width PDFs: stage = pane − constant padding), but a stage
// that is WIDER than its fit basis grows faster than the pane — a PDF fits
// its FIRST page while the stage spans its WIDEST page, so a mixed-width
// document has slope maxBase/fitBase > 1. A unit-slope assumption there
// under-scales the first frame by (slope − 1)·delta/stage — measured as a
// ~7px content step at the anchor-hold frame of a 355-page prospectus.
export function getFileViewerFitWidthVisualScale({
liveInlineSize,
stageInlineSize,
stageInlineSlope = 1,
targetInlineSize,
}: {
liveInlineSize: number;
stageInlineSize: number;
stageInlineSlope?: number;
targetInlineSize: number;
}) {
if (
stageInlineSize <= 0 ||
!Number.isFinite(liveInlineSize) ||
!Number.isFinite(targetInlineSize)
) {
return 1;
}
const slope =
Number.isFinite(stageInlineSlope) && stageInlineSlope > 0
? stageInlineSlope
: 1;
return (
Math.max(1, stageInlineSize + slope * (liveInlineSize - targetInlineSize)) /
stageInlineSize
);
}
// Capture side of the motion anchor: the probe content line's on-screen block
// offset relative to the scroll box, taken just before a motion (or retarget)
// commits. When a motion is already in flight the DOM is the settled layout
// PLUS the live transform, so the capture applies that transform — otherwise
// a retarget would solve continuity against a picture the reader never saw.
export function captureFileViewerFitWidthAnchorScreenOffset({
lastAnchorBlock,
liveFrame,
probeStageOffset,
scrollTop,
stageInlineSize,
stageInlinePadding = 0,
stageBlockSlope = 1,
}: {
lastAnchorBlock: number | null;
liveFrame: FileViewerMotionFrame | null;
probeStageOffset: number;
scrollTop: number;
stageInlineSize: number;
stageInlinePadding?: number;
/** The BLOCK-axis slope — anchor capture/solve is block-axis math. */
stageBlockSlope?: number;
}) {
const untransformed = probeStageOffset - scrollTop;
if (!liveFrame || liveFrame.phase !== "sliding") return untransformed;
const liveScale = getFileViewerFitWidthVisualScale({
liveInlineSize: liveFrame.layoutInlineSize,
stageInlineSize: getFileViewerFitWidthContentInlineSize({
stageInlinePadding,
stageInlineSize,
}),
stageInlineSlope: stageBlockSlope,
targetInlineSize: liveFrame.toInlineSize,
});
if (Math.abs(1 - liveScale) <= 0.001) return untransformed;
return (
liveScale * probeStageOffset +
(1 - liveScale) * (lastAnchorBlock ?? 0) -
scrollTop
);
}
// Solve side: the anchor block offset that puts the probe content line back on
// its captured screen position under the NEW layout model at the motion's
// first-frame scale. Exact regardless of how the rebase clamped or how the
// old/new layout models relate (measured page sizes, constant gaps/padding).
// Returns null when the motion is degenerate (caller falls back to the live
// reading marker).
export function resolveFileViewerFitWidthMotionAnchorBlock({
fromInlineSize,
probeScreenOffset,
probeStageOffset,
scrollTop,
stageInlineSize,
stageInlinePadding = 0,
stageBlockSlope = 1,
toInlineSize,
}: {
fromInlineSize: number | null;
probeScreenOffset: number;
probeStageOffset: number;
scrollTop: number;
stageInlineSize: number;
stageInlinePadding?: number;
/** The BLOCK-axis slope — anchor capture/solve is block-axis math. */
stageBlockSlope?: number;
toInlineSize: number | null;
}) {
if (fromInlineSize == null || toInlineSize == null) return null;
const startScale = getFileViewerFitWidthVisualScale({
liveInlineSize: fromInlineSize,
stageInlineSize: getFileViewerFitWidthContentInlineSize({
stageInlinePadding,
stageInlineSize,
}),
stageInlineSlope: stageBlockSlope,
targetInlineSize: toInlineSize,
});
if (!Number.isFinite(startScale) || Math.abs(1 - startScale) <= 0.001) {
return null;
}
return (
(probeScreenOffset + scrollTop - startScale * probeStageOffset) /
(1 - startScale)
);
}
function getFileViewerFitWidthSurfaceMotionTransform({
align,
anchorBlockProperty,
direction,
frame,
stageInlineSize,
stageOuterInlinePadding,
stageInlinePadding,
stageInlineSlope,
stageBlockSlope,
}: {
align: FileViewerDocumentAlign;
anchorBlockProperty: string;
direction: FileViewerInlineDirection;
frame: FileViewerMotionFrame;
stageInlineSize: number;
stageOuterInlinePadding: number;
stageInlinePadding: number;
stageInlineSlope: number;
stageBlockSlope: number;
}) {
if (
stageInlineSize <= 0 ||
frame.layoutInlineSize <= 0 ||
frame.toInlineSize <= 0
) {
return "";
}
// Fit-width renderers size their stage as an affine function of the
// available width (stage = slope × width − constant padding), so the
// in-flight visual stage is the settled stage plus the scaled live width
// delta. At the first frame this resolves to exactly the pre-toggle stage
// size, and at the last frame to the settled stage — identity. Each axis
// carries its own slope: they differ when the stage's inline box holds
// constant padding while its block stack scales with the content.
const contentInlineSize = getFileViewerFitWidthContentInlineSize({
stageInlinePadding,
stageInlineSize,
});
const inlineScale = getFileViewerFitWidthVisualScale({
liveInlineSize: frame.layoutInlineSize,
stageInlineSize: contentInlineSize,
stageInlineSlope,
targetInlineSize: frame.toInlineSize,
});
const blockScale = getFileViewerFitWidthVisualScale({
liveInlineSize: frame.layoutInlineSize,
stageInlineSize: contentInlineSize,
stageInlineSlope: stageBlockSlope,
targetInlineSize: frame.toInlineSize,
});
const visualStageInlineSize =
inlineScale * contentInlineSize + stageInlinePadding;
const availableStageInlineSize = Math.max(
1,
frame.layoutInlineSize - Math.max(0, stageOuterInlinePadding),
);
const settledMargin = getFileViewerStageInlineMargin({
align,
availableInlineSize: availableStageInlineSize,
direction,
stageInlineSize,
});
const visualMargin = getFileViewerStageInlineMargin({
align,
availableInlineSize: availableStageInlineSize,
direction,
stageInlineSize: visualStageInlineSize,
});
// The padding is constant in both endpoint layouts. Scaling the outer stage
// would scale that inset too, making the visible page briefly too wide or
// narrow on the first frame. Rebase the symmetric start inset so the inner
// content edge, not the transparent wrapper edge, is pixel-continuous.
const inlinePaddingStart = stageInlinePadding / 2;
const translateX =
visualMargin - settledMargin + (1 - inlineScale) * inlinePaddingStart;
if (Math.abs(frame.layoutInlineSize - frame.toInlineSize) <= 0.001) {
return "";
}
const formattedInlineScale = formatFileViewerMotionScale(inlineScale);
const formattedBlockScale = formatFileViewerMotionScale(blockScale);
const formattedTranslateX = formatFileViewerMotionPixel(translateX);
// Scale about the stage origin; the anchor term keeps the reading line
// fixed on the block axis: y' = s·y + (1 − s)·anchor equals y at
// y = anchor.
const translateY = `calc((1 - ${formattedBlockScale}) * var(${anchorBlockProperty}, 0px))`;
const formattedScale =
formattedInlineScale === formattedBlockScale
? formattedInlineScale
: `${formattedInlineScale}, ${formattedBlockScale}`;
return `translate3d(${formattedTranslateX}px, ${translateY}, 0) scale(${formattedScale})`;
}
function getFileViewerFitWidthContentInlineSize({
stageInlinePadding,
stageInlineSize,
}: {
stageInlinePadding: number;
stageInlineSize: number;
}) {
const padding = Number.isFinite(stageInlinePadding)
? Math.max(0, stageInlinePadding)
: 0;
return Math.max(1, stageInlineSize - padding);
}
// Physical LEFT offset of the stage box inside the available inline size —
// translateX shifts along the physical X axis, so the model must speak
// physical-left in both directions. Stages align with physical auto margins
// (mx-auto for center, ml-auto for end, plain flow for start), so:
// - free space ≥ 0: center splits it; end pins right in both directions
// (ml-auto is physical); start follows flow (left in LTR, right in RTL).
// - free space < 0 (the settled stage overflows the live container — the
// close leg's early frames): auto margins collapse to 0 and CSS resolves
// the over-constraint against the direction's end edge, pinning the box to
// the start edge — left edge at 0 in LTR, at the negative free space in
// RTL. The old unconditional max(0, …) clamp encoded only the LTR half and
// made the RTL close leg overshoot by the overflow amount.
function getFileViewerStageInlineMargin({
align,
availableInlineSize,
direction,
stageInlineSize,
}: {
align: FileViewerDocumentAlign;
availableInlineSize: number;
direction: FileViewerInlineDirection;
stageInlineSize: number;
}) {
const freeInlineSize = availableInlineSize - stageInlineSize;
if (freeInlineSize < 0) return direction === "rtl" ? freeInlineSize : 0;
switch (align) {
case "start":
return direction === "rtl" ? freeInlineSize : 0;
case "end":
return freeInlineSize;
case "center":
return freeInlineSize / 2;
}
}
function formatFileViewerMotionPixel(value: number) {
return Number.isFinite(value) ? Number(value.toFixed(3)) : 0;
}
function formatFileViewerMotionScale(value: number) {
return Number.isFinite(value) ? String(Number(value.toFixed(6))) : "1";
}