@elabs-ai/components-viewer 4.0.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,15 +6,14 @@
6
6
  > FileViewer — any file (image, text, JSON, CSV) via a pluggable adapter registry.
7
7
 
8
8
  Part of **brand-ui**, a source-owned, token-driven React component system.
9
- These packages are **private** and are not published to any registry — they are
10
- consumed from this workspace. See `docs/CONSUMING.md`.
9
+ Published to the **public npm registry** under the `@elabs-ai` scope — it
10
+ installs like any other npm dependency, with no registry configuration and
11
+ no token required. See `docs/CONSUMING.md`.
11
12
 
12
13
  ## Install
13
14
 
14
- Inside this monorepo the packages resolve as workspace dependencies:
15
-
16
- ```json
17
- "@elabs-ai/components-viewer": "workspace:*"
15
+ ```bash
16
+ pnpm add @elabs-ai/components-tokens @elabs-ai/components-viewer
18
17
  ```
19
18
 
20
19
  ## Set up styling (do not skip)
@@ -69,7 +68,7 @@ prompt for migrating an existing project: `docs/CONSUMING.md`.
69
68
 
70
69
  ## License
71
70
 
72
- UNLICENSED — private.
71
+ MIT
73
72
 
74
73
  <!-- brand-ui:gen:readme:end -->
75
74
 
@@ -70,7 +70,7 @@ function SheetTable({
70
70
  tabIndex: 0,
71
71
  role: "group",
72
72
  "aria-label": t("viewer.content"),
73
- className: "focus-visible:ring-ring min-h-0 flex-1 overflow-auto focus-visible:outline-none focus-visible:ring-2",
73
+ className: "focus-ring min-h-0 flex-1 overflow-auto",
74
74
  children: /* @__PURE__ */ jsxs(Table, { children: [
75
75
  /* @__PURE__ */ jsx(TableCaption, { className: "sr-only", children: t("viewer.table.caption", {
76
76
  rows: formatNumber(totalRows ?? rows.length),
@@ -114,4 +114,4 @@ export {
114
114
  gridToText,
115
115
  SheetTable
116
116
  };
117
- //# sourceMappingURL=chunk-NMA57QZ7.js.map
117
+ //# sourceMappingURL=chunk-XBOR2YXS.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/components/grid-text.ts","../src/components/sheet-table.tsx"],"sourcesContent":["/**\n * The one text projection every tabular format is addressed against.\n *\n * CSV and XLSX arrive through different parsers and render into the same\n * `SheetTable`; they should also be *addressable* the same way, or a citation\n * that resolves in a workbook would miss in the CSV export of the same data.\n * So the projection is written once here and both adapters call it.\n *\n * Shape — a sheet's name (when it has one), then its header row, then its body\n * rows; cells joined by a tab, rows by a newline, sheets by a blank line:\n *\n * ```\n * Q3\n * Region\\tRevenue\n * EMEA\\t4.2M\n *\n * Q4\n * …\n * ```\n *\n * **Rows, not cells, are the finest ref** — the same trade the Word adapter\n * makes. A cell-granular index would need a span per cell and a separator per\n * level; a row-granular one needs neither, because a cell's offset is the sum of\n * the cells before it ({@link chunkOffset}). The granularity of the REF is the\n * row; the granularity of the MARK is still the character.\n */\n\nimport { createTextIndexBuilder, type TextIndex } from \"../core/text-index\";\n\n/** Between two cells of the same row. */\nexport const GRID_CELL_SEPARATOR = \"\\t\";\n/** Between two rows of the same sheet. */\nexport const GRID_ROW_SEPARATOR = \"\\n\";\n/** Between two sheets of the same workbook. */\nexport const GRID_SHEET_SEPARATOR = \"\\n\\n\";\n\n/** The `row` of a sheet's own name line. */\nexport const GRID_NAME_ROW = -2;\n/** The `row` of a sheet's header row. */\nexport const GRID_HEAD_ROW = -1;\n\n/** Where a stretch of the projection came from in the grid. */\nexport interface GridRef {\n /** Index into the sheets. `0` for a single-sheet format like CSV. */\n sheet: number;\n /** Body row index, or {@link GRID_HEAD_ROW} / {@link GRID_NAME_ROW}. */\n row: number;\n}\n\n/** What {@link gridToText} needs from a sheet — the shape both adapters already hold. */\nexport interface GridSheetInput {\n /** The tab's name. Absent for a single-sheet format, which has no tab. */\n name?: string;\n columns: readonly string[];\n rows: readonly (readonly string[])[];\n}\n\n/** Project a workbook (or a one-sheet CSV) to text, with the map back to it. */\nexport function gridToText(sheets: readonly GridSheetInput[]): TextIndex<GridRef> {\n const builder = createTextIndexBuilder<GridRef>({ separator: GRID_ROW_SEPARATOR });\n sheets.forEach((sheet, index) => {\n // The blank line goes before whatever this sheet's FIRST line turns out to\n // be — its name, or its header when it has no name.\n let separator = index === 0 ? undefined : GRID_SHEET_SEPARATOR;\n const push = (chunk: string, row: number) => {\n builder.push(chunk, { sheet: index, row }, separator);\n if (chunk.length > 0) separator = undefined;\n };\n\n if (sheet.name) push(sheet.name, GRID_NAME_ROW);\n push(sheet.columns.join(GRID_CELL_SEPARATOR), GRID_HEAD_ROW);\n sheet.rows.forEach((row, rowIndex) => {\n push(row.join(GRID_CELL_SEPARATOR), rowIndex);\n });\n });\n return builder.build();\n}\n","\"use client\";\n\n/**\n * The one grid every tabular format renders into.\n *\n * CSV and XLSX arrive through completely different parsers and end up wanting\n * exactly the same thing: a header row, body rows, a truncation notice and an\n * `sr-only` caption. That is a PATTERN, not a coincidence, so it is named once\n * here rather than copied a second time (`.claude/rules/design-first.md` —\n * patterns over instances).\n *\n * It stays deliberately dumb: no sorting, no filtering, no virtualization. A\n * preview pane is for looking; past a few thousand rows the right component is\n * `DataTable` in `@elabs-ai/components-data`, which virtualizes.\n */\n\nimport {\n cn,\n Table,\n TableBody,\n TableCaption,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n useLocale,\n} from \"@elabs-ai/components-ui\";\nimport { useRef } from \"react\";\n\nimport { type MarkRanges } from \"../core/highlight-marks\";\nimport { chunkOffset } from \"../core/text-index\";\nimport { useScrollActiveHighlightIntoView } from \"../core/use-highlight-scroll\";\nimport { GRID_CELL_SEPARATOR, GRID_HEAD_ROW } from \"./grid-text\";\nimport { MarkedText } from \"./marked-text\";\n\nexport interface SheetTableProps {\n /** Header cells. May be empty — the grid still renders its body. */\n columns: string[];\n /** Body rows, already capped by the caller. */\n rows: string[][];\n /** Total body rows in the file, when more exist than `rows` holds. */\n totalRows?: number;\n className?: string;\n /** Marks to paint, in projection offsets. */\n marks?: MarkRanges;\n /**\n * Where a row's first cell begins in the projection — {@link GRID_HEAD_ROW}\n * for the header. A function rather than an array so a workbook can answer\n * for the sheet being drawn without slicing an index per tab.\n */\n rowStart?: (row: number) => number | undefined;\n /** Scrolls the current mark into this grid's own viewport when it changes. */\n activeHighlightId?: string | null;\n}\n\nexport function SheetTable({\n columns,\n rows,\n totalRows,\n className,\n marks,\n rowStart,\n activeHighlightId,\n}: SheetTableProps) {\n const { t, formatNumber } = useLocale();\n const viewport = useRef<HTMLDivElement>(null);\n\n useScrollActiveHighlightIntoView(viewport, activeHighlightId);\n\n return (\n <div className={cn(\"flex h-full min-h-0 flex-col gap-2\", className)}>\n {totalRows !== undefined && (\n // A status, not an error: the file is fine, we are simply showing part\n // of it (loading-states.md — a capability bound is news, not an alarm).\n <p role=\"status\" className=\"text-meta text-muted-foreground shrink-0\">\n {t(\"viewer.table.truncated\", { count: formatNumber(rows.length) })}\n </p>\n )}\n {/* The grid keeps its OWN viewport, unlike the flowing formats: a workbook\n puts a sheet-tab bar above it that must not scroll away with the rows.\n A table cell is not focusable, so a scrollable region wrapping one is\n unreachable from a keyboard (WCAG 2.1.1) — `tabIndex={0}` makes it a\n real stop that arrow keys and Page Up/Down drive. `group`, not\n `region`, so a sheet does not mint a second landmark inside the\n viewer's content region. */}\n <div\n ref={viewport}\n tabIndex={0}\n role=\"group\"\n aria-label={t(\"viewer.content\")}\n className=\"focus-visible:ring-ring min-h-0 flex-1 overflow-auto focus-visible:outline-none focus-visible:ring-2\"\n >\n <Table>\n <TableCaption className=\"sr-only\">\n {t(\"viewer.table.caption\", {\n rows: formatNumber(totalRows ?? rows.length),\n columns: formatNumber(columns.length),\n })}\n </TableCaption>\n <TableHeader>\n <TableRow>\n {columns.map((column, index) => (\n // The file's own header text is the only identity a column has;\n // an empty one still needs a cell so the grid stays aligned.\n <TableHead key={`${column}-${String(index)}`} scope=\"col\">\n <MarkedText\n text={column}\n marks={marks}\n start={chunkOffset(\n columns,\n index,\n rowStart?.(GRID_HEAD_ROW),\n GRID_CELL_SEPARATOR,\n )}\n />\n </TableHead>\n ))}\n </TableRow>\n </TableHeader>\n <TableBody>\n {rows.map((row, rowIndex) => {\n const start = rowStart?.(rowIndex);\n return (\n <TableRow key={rowIndex}>\n {columns.map((_, columnIndex) => (\n <TableCell key={columnIndex} className=\"whitespace-pre-wrap\">\n <MarkedText\n text={row[columnIndex] ?? \"\"}\n marks={marks}\n start={chunkOffset(row, columnIndex, start, GRID_CELL_SEPARATOR)}\n />\n </TableCell>\n ))}\n </TableRow>\n );\n })}\n </TableBody>\n </Table>\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;AA8BO,IAAM,sBAAsB;AAE5B,IAAM,qBAAqB;AAE3B,IAAM,uBAAuB;AAG7B,IAAM,gBAAgB;AAEtB,IAAM,gBAAgB;AAmBtB,SAAS,WAAW,QAAuD;AAChF,QAAM,UAAU,uBAAgC,EAAE,WAAW,mBAAmB,CAAC;AACjF,SAAO,QAAQ,CAAC,OAAO,UAAU;AAG/B,QAAI,YAAY,UAAU,IAAI,SAAY;AAC1C,UAAM,OAAO,CAAC,OAAe,QAAgB;AAC3C,cAAQ,KAAK,OAAO,EAAE,OAAO,OAAO,IAAI,GAAG,SAAS;AACpD,UAAI,MAAM,SAAS,EAAG,aAAY;AAAA,IACpC;AAEA,QAAI,MAAM,KAAM,MAAK,MAAM,MAAM,aAAa;AAC9C,SAAK,MAAM,QAAQ,KAAK,mBAAmB,GAAG,aAAa;AAC3D,UAAM,KAAK,QAAQ,CAAC,KAAK,aAAa;AACpC,WAAK,IAAI,KAAK,mBAAmB,GAAG,QAAQ;AAAA,IAC9C,CAAC;AAAA,EACH,CAAC;AACD,SAAO,QAAQ,MAAM;AACvB;;;AC5DA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,cAAc;AA+Cf,cAkBA,YAlBA;AAnBD,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAoB;AAClB,QAAM,EAAE,GAAG,aAAa,IAAI,UAAU;AACtC,QAAM,WAAW,OAAuB,IAAI;AAE5C,mCAAiC,UAAU,iBAAiB;AAE5D,SACE,qBAAC,SAAI,WAAW,GAAG,sCAAsC,SAAS,GAC/D;AAAA,kBAAc;AAAA;AAAA,IAGb,oBAAC,OAAE,MAAK,UAAS,WAAU,4CACxB,YAAE,0BAA0B,EAAE,OAAO,aAAa,KAAK,MAAM,EAAE,CAAC,GACnE;AAAA,IASF;AAAA,MAAC;AAAA;AAAA,QACC,KAAK;AAAA,QACL,UAAU;AAAA,QACV,MAAK;AAAA,QACL,cAAY,EAAE,gBAAgB;AAAA,QAC9B,WAAU;AAAA,QAEV,+BAAC,SACC;AAAA,8BAAC,gBAAa,WAAU,WACrB,YAAE,wBAAwB;AAAA,YACzB,MAAM,aAAa,aAAa,KAAK,MAAM;AAAA,YAC3C,SAAS,aAAa,QAAQ,MAAM;AAAA,UACtC,CAAC,GACH;AAAA,UACA,oBAAC,eACC,8BAAC,YACE,kBAAQ,IAAI,CAAC,QAAQ;AAAA;AAAA;AAAA,YAGpB,oBAAC,aAA6C,OAAM,OAClD;AAAA,cAAC;AAAA;AAAA,gBACC,MAAM;AAAA,gBACN;AAAA,gBACA,OAAO;AAAA,kBACL;AAAA,kBACA;AAAA,kBACA,WAAW,aAAa;AAAA,kBACxB;AAAA,gBACF;AAAA;AAAA,YACF,KAVc,GAAG,MAAM,IAAI,OAAO,KAAK,CAAC,EAW1C;AAAA,WACD,GACH,GACF;AAAA,UACA,oBAAC,aACE,eAAK,IAAI,CAAC,KAAK,aAAa;AAC3B,kBAAM,QAAQ,WAAW,QAAQ;AACjC,mBACE,oBAAC,YACE,kBAAQ,IAAI,CAAC,GAAG,gBACf,oBAAC,aAA4B,WAAU,uBACrC;AAAA,cAAC;AAAA;AAAA,gBACC,MAAM,IAAI,WAAW,KAAK;AAAA,gBAC1B;AAAA,gBACA,OAAO,YAAY,KAAK,aAAa,OAAO,mBAAmB;AAAA;AAAA,YACjE,KALc,WAMhB,CACD,KATY,QAUf;AAAA,UAEJ,CAAC,GACH;AAAA,WACF;AAAA;AAAA,IACF;AAAA,KACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../src/components/grid-text.ts","../src/components/sheet-table.tsx"],"sourcesContent":["/**\n * The one text projection every tabular format is addressed against.\n *\n * CSV and XLSX arrive through different parsers and render into the same\n * `SheetTable`; they should also be *addressable* the same way, or a citation\n * that resolves in a workbook would miss in the CSV export of the same data.\n * So the projection is written once here and both adapters call it.\n *\n * Shape — a sheet's name (when it has one), then its header row, then its body\n * rows; cells joined by a tab, rows by a newline, sheets by a blank line:\n *\n * ```\n * Q3\n * Region\\tRevenue\n * EMEA\\t4.2M\n *\n * Q4\n * …\n * ```\n *\n * **Rows, not cells, are the finest ref** — the same trade the Word adapter\n * makes. A cell-granular index would need a span per cell and a separator per\n * level; a row-granular one needs neither, because a cell's offset is the sum of\n * the cells before it ({@link chunkOffset}). The granularity of the REF is the\n * row; the granularity of the MARK is still the character.\n */\n\nimport { createTextIndexBuilder, type TextIndex } from \"../core/text-index\";\n\n/** Between two cells of the same row. */\nexport const GRID_CELL_SEPARATOR = \"\\t\";\n/** Between two rows of the same sheet. */\nexport const GRID_ROW_SEPARATOR = \"\\n\";\n/** Between two sheets of the same workbook. */\nexport const GRID_SHEET_SEPARATOR = \"\\n\\n\";\n\n/** The `row` of a sheet's own name line. */\nexport const GRID_NAME_ROW = -2;\n/** The `row` of a sheet's header row. */\nexport const GRID_HEAD_ROW = -1;\n\n/** Where a stretch of the projection came from in the grid. */\nexport interface GridRef {\n /** Index into the sheets. `0` for a single-sheet format like CSV. */\n sheet: number;\n /** Body row index, or {@link GRID_HEAD_ROW} / {@link GRID_NAME_ROW}. */\n row: number;\n}\n\n/** What {@link gridToText} needs from a sheet — the shape both adapters already hold. */\nexport interface GridSheetInput {\n /** The tab's name. Absent for a single-sheet format, which has no tab. */\n name?: string;\n columns: readonly string[];\n rows: readonly (readonly string[])[];\n}\n\n/** Project a workbook (or a one-sheet CSV) to text, with the map back to it. */\nexport function gridToText(sheets: readonly GridSheetInput[]): TextIndex<GridRef> {\n const builder = createTextIndexBuilder<GridRef>({ separator: GRID_ROW_SEPARATOR });\n sheets.forEach((sheet, index) => {\n // The blank line goes before whatever this sheet's FIRST line turns out to\n // be — its name, or its header when it has no name.\n let separator = index === 0 ? undefined : GRID_SHEET_SEPARATOR;\n const push = (chunk: string, row: number) => {\n builder.push(chunk, { sheet: index, row }, separator);\n if (chunk.length > 0) separator = undefined;\n };\n\n if (sheet.name) push(sheet.name, GRID_NAME_ROW);\n push(sheet.columns.join(GRID_CELL_SEPARATOR), GRID_HEAD_ROW);\n sheet.rows.forEach((row, rowIndex) => {\n push(row.join(GRID_CELL_SEPARATOR), rowIndex);\n });\n });\n return builder.build();\n}\n","\"use client\";\n\n/**\n * The one grid every tabular format renders into.\n *\n * CSV and XLSX arrive through completely different parsers and end up wanting\n * exactly the same thing: a header row, body rows, a truncation notice and an\n * `sr-only` caption. That is a PATTERN, not a coincidence, so it is named once\n * here rather than copied a second time (`.claude/rules/design-first.md` —\n * patterns over instances).\n *\n * It stays deliberately dumb: no sorting, no filtering, no virtualization. A\n * preview pane is for looking; past a few thousand rows the right component is\n * `DataTable` in `@elabs-ai/components-data`, which virtualizes.\n */\n\nimport {\n cn,\n Table,\n TableBody,\n TableCaption,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n useLocale,\n} from \"@elabs-ai/components-ui\";\nimport { useRef } from \"react\";\n\nimport { type MarkRanges } from \"../core/highlight-marks\";\nimport { chunkOffset } from \"../core/text-index\";\nimport { useScrollActiveHighlightIntoView } from \"../core/use-highlight-scroll\";\nimport { GRID_CELL_SEPARATOR, GRID_HEAD_ROW } from \"./grid-text\";\nimport { MarkedText } from \"./marked-text\";\n\nexport interface SheetTableProps {\n /** Header cells. May be empty — the grid still renders its body. */\n columns: string[];\n /** Body rows, already capped by the caller. */\n rows: string[][];\n /** Total body rows in the file, when more exist than `rows` holds. */\n totalRows?: number;\n className?: string;\n /** Marks to paint, in projection offsets. */\n marks?: MarkRanges;\n /**\n * Where a row's first cell begins in the projection — {@link GRID_HEAD_ROW}\n * for the header. A function rather than an array so a workbook can answer\n * for the sheet being drawn without slicing an index per tab.\n */\n rowStart?: (row: number) => number | undefined;\n /** Scrolls the current mark into this grid's own viewport when it changes. */\n activeHighlightId?: string | null;\n}\n\nexport function SheetTable({\n columns,\n rows,\n totalRows,\n className,\n marks,\n rowStart,\n activeHighlightId,\n}: SheetTableProps) {\n const { t, formatNumber } = useLocale();\n const viewport = useRef<HTMLDivElement>(null);\n\n useScrollActiveHighlightIntoView(viewport, activeHighlightId);\n\n return (\n <div className={cn(\"flex h-full min-h-0 flex-col gap-2\", className)}>\n {totalRows !== undefined && (\n // A status, not an error: the file is fine, we are simply showing part\n // of it (loading-states.md — a capability bound is news, not an alarm).\n <p role=\"status\" className=\"text-meta text-muted-foreground shrink-0\">\n {t(\"viewer.table.truncated\", { count: formatNumber(rows.length) })}\n </p>\n )}\n {/* The grid keeps its OWN viewport, unlike the flowing formats: a workbook\n puts a sheet-tab bar above it that must not scroll away with the rows.\n A table cell is not focusable, so a scrollable region wrapping one is\n unreachable from a keyboard (WCAG 2.1.1) — `tabIndex={0}` makes it a\n real stop that arrow keys and Page Up/Down drive. `group`, not\n `region`, so a sheet does not mint a second landmark inside the\n viewer's content region. */}\n <div\n ref={viewport}\n tabIndex={0}\n role=\"group\"\n aria-label={t(\"viewer.content\")}\n className=\"focus-ring min-h-0 flex-1 overflow-auto\"\n >\n <Table>\n <TableCaption className=\"sr-only\">\n {t(\"viewer.table.caption\", {\n rows: formatNumber(totalRows ?? rows.length),\n columns: formatNumber(columns.length),\n })}\n </TableCaption>\n <TableHeader>\n <TableRow>\n {columns.map((column, index) => (\n // The file's own header text is the only identity a column has;\n // an empty one still needs a cell so the grid stays aligned.\n <TableHead key={`${column}-${String(index)}`} scope=\"col\">\n <MarkedText\n text={column}\n marks={marks}\n start={chunkOffset(\n columns,\n index,\n rowStart?.(GRID_HEAD_ROW),\n GRID_CELL_SEPARATOR,\n )}\n />\n </TableHead>\n ))}\n </TableRow>\n </TableHeader>\n <TableBody>\n {rows.map((row, rowIndex) => {\n const start = rowStart?.(rowIndex);\n return (\n <TableRow key={rowIndex}>\n {columns.map((_, columnIndex) => (\n <TableCell key={columnIndex} className=\"whitespace-pre-wrap\">\n <MarkedText\n text={row[columnIndex] ?? \"\"}\n marks={marks}\n start={chunkOffset(row, columnIndex, start, GRID_CELL_SEPARATOR)}\n />\n </TableCell>\n ))}\n </TableRow>\n );\n })}\n </TableBody>\n </Table>\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;AA8BO,IAAM,sBAAsB;AAE5B,IAAM,qBAAqB;AAE3B,IAAM,uBAAuB;AAG7B,IAAM,gBAAgB;AAEtB,IAAM,gBAAgB;AAmBtB,SAAS,WAAW,QAAuD;AAChF,QAAM,UAAU,uBAAgC,EAAE,WAAW,mBAAmB,CAAC;AACjF,SAAO,QAAQ,CAAC,OAAO,UAAU;AAG/B,QAAI,YAAY,UAAU,IAAI,SAAY;AAC1C,UAAM,OAAO,CAAC,OAAe,QAAgB;AAC3C,cAAQ,KAAK,OAAO,EAAE,OAAO,OAAO,IAAI,GAAG,SAAS;AACpD,UAAI,MAAM,SAAS,EAAG,aAAY;AAAA,IACpC;AAEA,QAAI,MAAM,KAAM,MAAK,MAAM,MAAM,aAAa;AAC9C,SAAK,MAAM,QAAQ,KAAK,mBAAmB,GAAG,aAAa;AAC3D,UAAM,KAAK,QAAQ,CAAC,KAAK,aAAa;AACpC,WAAK,IAAI,KAAK,mBAAmB,GAAG,QAAQ;AAAA,IAC9C,CAAC;AAAA,EACH,CAAC;AACD,SAAO,QAAQ,MAAM;AACvB;;;AC5DA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,cAAc;AA+Cf,cAkBA,YAlBA;AAnBD,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAoB;AAClB,QAAM,EAAE,GAAG,aAAa,IAAI,UAAU;AACtC,QAAM,WAAW,OAAuB,IAAI;AAE5C,mCAAiC,UAAU,iBAAiB;AAE5D,SACE,qBAAC,SAAI,WAAW,GAAG,sCAAsC,SAAS,GAC/D;AAAA,kBAAc;AAAA;AAAA,IAGb,oBAAC,OAAE,MAAK,UAAS,WAAU,4CACxB,YAAE,0BAA0B,EAAE,OAAO,aAAa,KAAK,MAAM,EAAE,CAAC,GACnE;AAAA,IASF;AAAA,MAAC;AAAA;AAAA,QACC,KAAK;AAAA,QACL,UAAU;AAAA,QACV,MAAK;AAAA,QACL,cAAY,EAAE,gBAAgB;AAAA,QAC9B,WAAU;AAAA,QAEV,+BAAC,SACC;AAAA,8BAAC,gBAAa,WAAU,WACrB,YAAE,wBAAwB;AAAA,YACzB,MAAM,aAAa,aAAa,KAAK,MAAM;AAAA,YAC3C,SAAS,aAAa,QAAQ,MAAM;AAAA,UACtC,CAAC,GACH;AAAA,UACA,oBAAC,eACC,8BAAC,YACE,kBAAQ,IAAI,CAAC,QAAQ;AAAA;AAAA;AAAA,YAGpB,oBAAC,aAA6C,OAAM,OAClD;AAAA,cAAC;AAAA;AAAA,gBACC,MAAM;AAAA,gBACN;AAAA,gBACA,OAAO;AAAA,kBACL;AAAA,kBACA;AAAA,kBACA,WAAW,aAAa;AAAA,kBACxB;AAAA,gBACF;AAAA;AAAA,YACF,KAVc,GAAG,MAAM,IAAI,OAAO,KAAK,CAAC,EAW1C;AAAA,WACD,GACH,GACF;AAAA,UACA,oBAAC,aACE,eAAK,IAAI,CAAC,KAAK,aAAa;AAC3B,kBAAM,QAAQ,WAAW,QAAQ;AACjC,mBACE,oBAAC,YACE,kBAAQ,IAAI,CAAC,GAAG,gBACf,oBAAC,aAA4B,WAAU,uBACrC;AAAA,cAAC;AAAA;AAAA,gBACC,MAAM,IAAI,WAAW,KAAK;AAAA,gBAC1B;AAAA,gBACA,OAAO,YAAY,KAAK,aAAa,OAAO,mBAAmB;AAAA;AAAA,YACjE,KALc,WAMhB,CACD,KATY,QAUf;AAAA,UAEJ,CAAC,GACH;AAAA,WACF;AAAA;AAAA,IACF;AAAA,KACF;AAEJ;","names":[]}
@@ -6,7 +6,7 @@ import {
6
6
  import {
7
7
  SheetTable,
8
8
  gridToText
9
- } from "./chunk-NMA57QZ7.js";
9
+ } from "./chunk-XBOR2YXS.js";
10
10
  import "./chunk-2NQ4RSJ3.js";
11
11
  import "./chunk-UL43NGUG.js";
12
12
  import {
@@ -97,4 +97,4 @@ export {
97
97
  CSV_ROW_LIMIT,
98
98
  csv_adapter_default as default
99
99
  };
100
- //# sourceMappingURL=csv-adapter-6VU3FFVU.js.map
100
+ //# sourceMappingURL=csv-adapter-7HOFZ5Z6.js.map
package/dist/index.js CHANGED
@@ -185,12 +185,12 @@ function createDefaultRegistry() {
185
185
  const registry = createRegistry();
186
186
  registry.register(imageManifest, () => import("./image-adapter-WOHZR24J.js"));
187
187
  registry.register(jsonManifest, () => import("./json-adapter-ZUW5GQHE.js"));
188
- registry.register(csvManifest, () => import("./csv-adapter-6VU3FFVU.js"));
188
+ registry.register(csvManifest, () => import("./csv-adapter-7HOFZ5Z6.js"));
189
189
  registry.register(pdfManifest, () => import("./pdf-adapter-5PMKEXUD.js"));
190
190
  registry.register(mediaManifest, () => import("./media-adapter-MCTB4GBH.js"));
191
191
  registry.register(docxManifest, () => import("./docx-adapter-5CQDHWTD.js"));
192
- registry.register(xlsxManifest, () => import("./xlsx-adapter-CM2Y6AKQ.js"));
193
- registry.register(pptxManifest, () => import("./pptx-adapter-6GEQLS2Z.js"));
192
+ registry.register(xlsxManifest, () => import("./xlsx-adapter-O3XIVLFC.js"));
193
+ registry.register(pptxManifest, () => import("./pptx-adapter-BONWFHYT.js"));
194
194
  registry.register(markdownManifest, () => import("./markdown-adapter-YC6WTBS4.js"));
195
195
  registry.register(codeManifest, () => import("./code-adapter-ADZ4UOGN.js"));
196
196
  registry.register(textManifest, () => import("./text-adapter-NFNWB5W3.js"));
@@ -1211,10 +1211,7 @@ var FileViewerContent = forwardRef4(
1211
1211
  role: "region",
1212
1212
  "aria-label": t("viewer.content"),
1213
1213
  tabIndex: 0,
1214
- className: cn4(
1215
- "focus-visible:ring-ring min-h-0 flex-1 overflow-auto p-4 focus-visible:outline-none focus-visible:ring-2",
1216
- className
1217
- ),
1214
+ className: cn4("focus-ring min-h-0 flex-1 overflow-auto p-4", className),
1218
1215
  ...props,
1219
1216
  children: [
1220
1217
  state.status === "empty" && /* @__PURE__ */ jsx4(FileViewerEmpty, {}),
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/file-viewer/file-viewer.tsx","../src/core/registry.ts","../src/adapters/index.ts","../src/core/highlight.ts","../src/core/highlight-resolve.ts","../src/file-viewer/file-viewer-find.tsx","../src/file-viewer/file-viewer-context.tsx","../src/file-viewer/file-viewer-pager.tsx","../src/file-viewer/file-viewer-zoom.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * `FileViewer` — the compound shell.\n *\n * Every part is composed from `@elabs-ai/components-ui` primitives;\n * this package contributes file LOGIC (detection, parsing, the page model) and\n * never a parallel widget set. There is no `showToolbar` boolean: compose the\n * parts you want, or render `<FileViewer>` for the batteries-included default.\n *\n * Surface separation (`styling-and-tokens.md`): the frame is one raised `card`\n * with a border, the toolbar is divided from the content by a single `border-b`\n * — the sole structural cue between two same-fill regions, so it takes the\n * strong rung.\n */\n\nimport {\n Button,\n cn,\n downloadBlob,\n fileIconFor,\n IconButton,\n normalizeFileSource,\n normalizeQuoteTextWithOffsets,\n queryToRanges,\n resolveFileKind,\n Separator,\n Skeleton,\n StatePanel,\n Text,\n useLocale,\n type FileSource,\n type ProseHeadingLevel,\n} from \"@elabs-ai/components-ui\";\nimport { DownloadIcon, EyeOffIcon, SearchXIcon } from \"lucide-react\";\nimport {\n forwardRef,\n useCallback,\n useDeferredValue,\n useEffect,\n useMemo,\n useRef,\n useState,\n type HTMLAttributes,\n type KeyboardEvent as ReactKeyboardEvent,\n type ReactNode,\n} from \"react\";\n\nimport { createDefaultRegistry } from \"../adapters\";\nimport {\n ViewerError,\n isAbort,\n isModuleNotFound,\n parserMissingError,\n toViewerError,\n type ViewerErrorCode,\n} from \"../core/errors\";\nimport {\n FIND_MATCH_LIMIT,\n findMatchId,\n type DocumentHighlight,\n type HighlightSupport,\n type ResolvedHighlight,\n} from \"../core/highlight\";\nimport { resolveHighlights } from \"../core/highlight-resolve\";\nimport type { ViewerRegistry } from \"../core/registry\";\nimport type { DocumentRotation, ZoomLevel } from \"../core/types\";\nimport { DEFAULT_ZOOM, stepZoom } from \"../core/zoom\";\nimport { FileViewerFind, isFindShortcut } from \"./file-viewer-find\";\nimport { FileViewerPager } from \"./file-viewer-pager\";\nimport { FileViewerRotate, FileViewerZoom } from \"./file-viewer-zoom\";\nimport {\n FileViewerContext,\n useFileViewer,\n type FileViewerContextValue,\n type FileViewerFindState,\n type FileViewerLoadState,\n} from \"./file-viewer-context\";\n\n/** Stable empties, so a provider with no citations does not re-render on identity. */\nconst NO_HIGHLIGHTS: readonly DocumentHighlight[] = [];\nconst NO_RESOLVED: readonly ResolvedHighlight[] = [];\nconst NO_SUPPORT: HighlightSupport = [];\n\n/* -------------------------------------------------------------------------- */\n/* Provider */\n/* -------------------------------------------------------------------------- */\n\nexport interface FileViewerProviderProps {\n /** The file to show. `undefined` is the empty state, not an error. */\n source?: FileSource;\n /**\n * Adapters available to this viewer. Defaults to the built-ins.\n * Pass your own to add a format, drop one, or override a built-in.\n */\n registry?: ViewerRegistry;\n /**\n * Force the not-ready state while a parent fetches the source itself.\n * ORed with the viewer's own loading — a parent can add loading, never remove it.\n */\n loading?: boolean;\n /**\n * The rung a viewed document's own top-level heading renders at. Default `2`.\n *\n * A file carries its OWN heading tree, and that tree is only correct relative\n * to the page hosting it: a README's `#` rendered as an `<h1>` inside an app\n * that already has one puts two `h1`s in a screen reader's flat heading list,\n * and the frame's `<section aria-label>` does not fix that — most screen\n * readers list headings flat, not per landmark. The default assumes the\n * common case, a viewer embedded BELOW the page's own heading; pass `1` when\n * the viewer genuinely is the page.\n *\n * Adapters that render no headings ignore it. Same seam as\n * `@elabs-ai/components-ai`'s `MarkdownView baseHeadingLevel`.\n */\n baseHeadingLevel?: ProseHeadingLevel;\n /**\n * The parts of the document to point at — an answer's citations, a search\n * result's context, anything the app already knows about the file.\n *\n * A PROP rather than provider-only state because citations originate outside\n * the viewer entirely: the chat pane that produced them usually lives in\n * another route, and it owns which one the reader clicked. Pass\n * `defaultHighlights` instead to let the viewer own them.\n */\n highlights?: readonly DocumentHighlight[];\n /** Uncontrolled initial citations. Ignored when `highlights` is supplied. */\n defaultHighlights?: readonly DocumentHighlight[];\n onHighlightsChange?: (highlights: readonly DocumentHighlight[]) => void;\n /**\n * Which citation the viewer is pointed at. `null` is \"none\" explicitly;\n * `undefined` is what selects uncontrolled mode, so the two are not\n * interchangeable here.\n */\n activeHighlightId?: string | null;\n defaultActiveHighlightId?: string | null;\n onActiveHighlightChange?: (id: string | null) => void;\n /**\n * Which page the viewer is on, 1-based — controlled.\n *\n * A trio (`component-api.md`) rather than provider-only state because the page\n * is routinely something the APP owns: a deep link to page 7, a URL the reader\n * can share, a position restored from a \"continue reading\" record. Clamped to\n * the document on read, so an out-of-range value degrades to the nearest real\n * page instead of blanking the canvas.\n */\n pageNumber?: number;\n /** Uncontrolled initial page. Ignored when `pageNumber` is supplied. */\n defaultPageNumber?: number;\n onPageNumberChange?: (page: number) => void;\n /**\n * The scale to draw at, or a fit mode — controlled. Persisting a reader's\n * preferred zoom across files and sessions is the reason this is a prop.\n */\n zoom?: ZoomLevel;\n /**\n * Uncontrolled initial zoom. Ignored when `zoom` is supplied.\n *\n * Defaults to `\"fit-width\"`, not `1`: a viewer's job on open is to show the\n * document, and a 4000px scan or an A4 page at 100% in a 600px pane shows its\n * top-left corner. Every reader-facing PDF viewer opens fitted for the same\n * reason. Pass `1` for true 100%.\n */\n defaultZoom?: ZoomLevel;\n onZoomChange?: (zoom: ZoomLevel) => void;\n /** Quarter-turns clockwise — controlled. */\n rotation?: DocumentRotation;\n /** Uncontrolled initial rotation. Ignored when `rotation` is supplied. Default `0`. */\n defaultRotation?: DocumentRotation;\n onRotationChange?: (rotation: DocumentRotation) => void;\n children: ReactNode;\n}\n\n/** Matches for the current query, plus whether the cap swallowed any. */\ninterface FindMatches {\n highlights: readonly DocumentHighlight[];\n total: number;\n truncated: boolean;\n}\n\nconst NO_MATCHES: FindMatches = { highlights: NO_HIGHLIGHTS, total: 0, truncated: false };\n\nexport function FileViewerProvider({\n source,\n registry: registryProp,\n loading = false,\n baseHeadingLevel = 2,\n highlights: highlightsProp,\n defaultHighlights,\n onHighlightsChange,\n activeHighlightId: activeHighlightIdProp,\n defaultActiveHighlightId = null,\n onActiveHighlightChange,\n pageNumber: pageNumberProp,\n defaultPageNumber = 1,\n onPageNumberChange,\n zoom: zoomProp,\n defaultZoom = \"fit-width\",\n onZoomChange,\n rotation: rotationProp,\n defaultRotation = 0,\n onRotationChange,\n children,\n}: FileViewerProviderProps) {\n // A default registry per provider, not per module: one screen's `register()`\n // override must not leak into another's.\n const fallbackRegistry = useMemo(() => createDefaultRegistry(), []);\n const registry = registryProp ?? fallbackRegistry;\n\n const [attempt, setAttempt] = useState(0);\n const [state, setState] = useState<FileViewerLoadState>({ status: \"empty\", capabilities: {} });\n\n const resolved = useMemo(() => (source ? normalizeFileSource(source) : undefined), [source]);\n\n useEffect(() => {\n if (!resolved) {\n setState({ status: \"empty\", capabilities: {} });\n return;\n }\n\n const controller = new AbortController();\n let instance: { dispose?: () => void } | undefined;\n let cancelled = false;\n\n const kind = resolveFileKind(resolved.name, resolved.mediaType);\n const manifest = registry.detect(kind);\n\n // Capabilities come from the eager manifest, so the chrome is correct from\n // the first frame — before a single byte of parser is fetched.\n setState({\n status: \"loading\",\n source: resolved,\n capabilities: manifest?.capabilities ?? {},\n });\n\n void (async () => {\n try {\n if (!manifest) {\n throw new ViewerError(\"unsupported-format\", `No adapter can open \"${resolved.name}\".`, {\n fileName: resolved.name,\n });\n }\n const adapter = await registry.load(manifest.id);\n // A fresh instance per document — adapters may hold per-document state.\n const parser = adapter.create();\n instance = parser;\n const document = await parser.load(resolved, { signal: controller.signal });\n if (cancelled) return;\n setState({\n status: \"ready\",\n source: resolved,\n document,\n adapter,\n capabilities: manifest.capabilities ?? {},\n });\n } catch (error) {\n // A cancelled load is a superseded one — reporting it would flash an\n // error every time the user picks a different file.\n if (cancelled || isAbort(error)) return;\n // Every parser engine is reached by a dynamic `import()` INSIDE the\n // adapter's own `load()`, so \"the optional peer is not installed\" lands\n // here, not on `registry.load()` above. Falling through to `parse-failed`\n // would tell the reader their file is damaged and offer a retry that can\n // never succeed.\n const missingPeer =\n isModuleNotFound(error) && manifest\n ? parserMissingError(manifest.id, manifest.requires ?? [], {\n fileName: resolved.name,\n cause: error,\n })\n : undefined;\n setState({\n status: \"error\",\n source: resolved,\n capabilities: manifest?.capabilities ?? {},\n error: missingPeer ?? toViewerError(error, \"parse-failed\", { fileName: resolved.name }),\n });\n }\n })();\n\n return () => {\n cancelled = true;\n controller.abort();\n instance?.dispose?.();\n // Releases any object URL the source minted for this load.\n resolved.revoke();\n };\n }, [resolved, registry, attempt]);\n\n /* ---- Citations: a prop the app may control, or the viewer's own --------- */\n\n const highlightsControlled = highlightsProp !== undefined;\n const [ownHighlights, setOwnHighlights] = useState(defaultHighlights ?? NO_HIGHLIGHTS);\n const highlights = highlightsControlled ? highlightsProp : ownHighlights;\n const setHighlights = useCallback(\n (next: readonly DocumentHighlight[]) => {\n // Mirroring the platform: a controlled value is never written locally, so\n // the component can't drift from the prop that owns it.\n if (!highlightsControlled) setOwnHighlights(next);\n onHighlightsChange?.(next);\n },\n [highlightsControlled, onHighlightsChange],\n );\n\n const activeControlled = activeHighlightIdProp !== undefined;\n const [ownActiveId, setOwnActiveId] = useState<string | null>(defaultActiveHighlightId);\n const activeHighlightId = activeControlled ? activeHighlightIdProp : ownActiveId;\n const setActiveHighlight = useCallback(\n (id: string | null) => {\n if (!activeControlled) setOwnActiveId(id);\n onActiveHighlightChange?.(id);\n },\n [activeControlled, onActiveHighlightChange],\n );\n\n /* ---- View: which page, at what scale, turned which way ------------------ */\n\n const pageCount = state.document?.pageCount ?? 0;\n\n const pageControlled = pageNumberProp !== undefined;\n const [ownPage, setOwnPage] = useState(defaultPageNumber);\n const rawPage = pageControlled ? pageNumberProp : ownPage;\n // Clamped on READ. Writing a clamped value back at a controlled owner would\n // fight it, and a document whose page count shrank (a different file, same\n // provider) must not blank the canvas while the owner catches up.\n const pageNumber =\n pageCount > 0 ? Math.min(Math.max(1, rawPage), pageCount) : Math.max(1, rawPage);\n const goToPage = useCallback(\n (page: number) => {\n const next = Math.max(1, Math.round(page));\n if (!pageControlled) setOwnPage(next);\n onPageNumberChange?.(next);\n },\n [pageControlled, onPageNumberChange],\n );\n\n const zoomControlled = zoomProp !== undefined;\n const [ownZoom, setOwnZoom] = useState<ZoomLevel>(defaultZoom);\n const zoom = zoomControlled ? zoomProp : ownZoom;\n const setZoom = useCallback(\n (next: ZoomLevel) => {\n if (!zoomControlled) setOwnZoom(next);\n onZoomChange?.(next);\n },\n [zoomControlled, onZoomChange],\n );\n\n // What a fit mode actually became. Only the renderer can know it — it is the\n // one measuring its viewport — so this is a report, not a derivation.\n const [reportedZoom, setReportedZoom] = useState(DEFAULT_ZOOM);\n const effectiveZoom = typeof zoom === \"number\" ? zoom : reportedZoom;\n\n const rotationControlled = rotationProp !== undefined;\n const [ownRotation, setOwnRotation] = useState<DocumentRotation>(defaultRotation);\n const rotation = rotationControlled ? rotationProp : ownRotation;\n const setRotation = useCallback(\n (next: DocumentRotation) => {\n if (!rotationControlled) setOwnRotation(next);\n onRotationChange?.(next);\n },\n [rotationControlled, onRotationChange],\n );\n\n // Opening a DIFFERENT file starts at its first page, the right way up. Zoom\n // deliberately survives: a reader who zoomed in is reading at that size, and\n // snapping back to 100% on every file fights them.\n //\n // Compared against the previous source rather than run on mount, so a\n // `defaultPageNumber` (deep link, restored position) is not clobbered by its\n // own first render.\n const previousSource = useRef(resolved);\n useEffect(() => {\n if (previousSource.current === resolved) return;\n previousSource.current = resolved;\n setOwnPage(1);\n setOwnRotation(0);\n }, [resolved]);\n\n /* ---- Find-in-document: entirely the viewer's own ------------------------ */\n\n const [find, setFind] = useState({\n open: false,\n query: \"\",\n caseSensitive: false,\n activeIndex: 0,\n });\n\n // Whether a `FileViewerFind` part is actually composed into this viewer.\n //\n // The frame swallows Ctrl/Cmd+F, and swallowing it without a box to show is\n // strictly worse than not intercepting at all: the reader loses the browser's\n // own find and gets nothing in return. The parts are composable by design, so\n // \"the adapter could paint a match\" is not the same question as \"there is\n // somewhere to type\" — the find part answers the second by registering itself.\n const [findParts, setFindParts] = useState(0);\n const registerFind = useCallback(() => {\n setFindParts((count) => count + 1);\n return () => setFindParts((count) => count - 1);\n }, []);\n\n const text = state.document?.text;\n const capabilities = state.capabilities;\n const support = capabilities.highlight ?? NO_SUPPORT;\n\n // The declared `search` flag is an override in the OFF direction only. Read\n // as the source of truth it would deny the find box to PDF — the format\n // readers expect it on most — purely because that manifest predates the\n // feature. Read as \"can this adapter paint what find produces\" it is right by\n // construction: find emits `range` addresses, so `range` support is the bar.\n const canFind = (capabilities.search ?? true) && text !== undefined && support.includes(\"range\");\n\n // Deferred so a keystroke paints immediately and the (potentially large)\n // match scan lands a frame later, instead of blocking the caret.\n const query = useDeferredValue(find.query);\n const findMatches = useMemo<FindMatches>(() => {\n if (!find.open || !canFind || text === undefined || query.length === 0) return NO_MATCHES;\n // Deliberately NOT run through `normalizeRanges`: it merges ADJACENT\n // ranges, which is right for a fuzzy matcher painting one contiguous mark\n // and wrong here — searching \"l\" in \"hello\" finds two matches, and merging\n // them into one would make the counter disagree with what a reader counts.\n // A single needle's matches never overlap, so there is nothing to merge.\n const ranges = queryToRanges(text, query, find.caseSensitive);\n return {\n highlights: ranges.slice(0, FIND_MATCH_LIMIT).map(([start, end], index) => ({\n id: findMatchId(index),\n address: { kind: \"range\" as const, start, end },\n source: \"search\" as const,\n })),\n total: ranges.length,\n truncated: ranges.length > FIND_MATCH_LIMIT,\n };\n }, [find.open, find.caseSensitive, canFind, text, query]);\n\n /* ---- Locate: the one step that runs outside the adapter ----------------- */\n\n // Folded once per document, not once per keystroke: find re-resolves on every\n // character, and folding a 2 MB projection each time is what turns a search\n // box into a stutter.\n const normalized = useMemo(\n () => (text === undefined ? undefined : normalizeQuoteTextWithOffsets(text)),\n [text],\n );\n\n // Two things can claim to be \"the current one\": a citation the app pointed at,\n // and the match the reader is stepping through. The precedence is stated once,\n // here — while the find box is open and matching, find wins, because it is what\n // the reader's own keystrokes are moving. Leaving both active would paint the\n // reader's match like every other match while a citation kept the active plate.\n const activeFindId =\n find.open && findMatches.highlights.length > 0 ? findMatchId(find.activeIndex) : undefined;\n\n /**\n * The id the RENDERER is pointed at — the effective one, not the citation knob.\n *\n * A renderer that owns a pager or a tab strip navigates on this, and the shared\n * scroll hook fires on it. Handing it `state.activeHighlightId` instead would\n * leave find-in-document unable to scroll to, page to, or switch sheet to its\n * own match whenever no citation happened to be active.\n */\n const currentHighlightId = activeFindId ?? activeHighlightId;\n\n const resolvedHighlights = useMemo<readonly ResolvedHighlight[]>(() => {\n if (highlights.length === 0 && findMatches.highlights.length === 0) return NO_RESOLVED;\n return resolveHighlights([...highlights, ...findMatches.highlights], {\n normalized,\n textLength: text?.length,\n truncated: state.document?.textTruncated,\n supported: support,\n activeId: currentHighlightId,\n });\n }, [\n highlights,\n findMatches.highlights,\n normalized,\n text?.length,\n state.document?.textTruncated,\n support,\n currentHighlightId,\n ]);\n\n /* ---- Stepping ---------------------------------------------------------- */\n\n const stepCitation = useCallback(\n (delta: 1 | -1) => {\n const list = resolvedHighlights.filter(\n (highlight) => highlight.source === \"citation\" && highlight.status === \"resolved\",\n );\n if (list.length === 0) return;\n const at = list.findIndex((highlight) => highlight.id === activeHighlightId);\n // Nothing active yet: \"next\" starts at the top of the document and\n // \"previous\" at the bottom, rather than both landing on the first.\n const to =\n at === -1 ? (delta === 1 ? 0 : list.length - 1) : (at + delta + list.length) % list.length;\n setActiveHighlight((list[to] as ResolvedHighlight).id);\n },\n [resolvedHighlights, activeHighlightId, setActiveHighlight],\n );\n\n const stepFind = useCallback(\n (delta: 1 | -1) => {\n const count = findMatches.highlights.length;\n if (count === 0) return;\n setFind((current) => ({\n ...current,\n activeIndex: (current.activeIndex + delta + count) % count,\n }));\n },\n [findMatches.highlights.length],\n );\n\n const findState = useMemo<FileViewerFindState>(\n () => ({\n open: find.open,\n query: find.query,\n caseSensitive: find.caseSensitive,\n matches: findMatches.total,\n truncated: findMatches.truncated,\n activeIndex: find.activeIndex,\n }),\n [find, findMatches.total, findMatches.truncated],\n );\n\n const actions = useMemo(\n () => ({\n reload: () => setAttempt((n) => n + 1),\n setHighlights,\n setActiveHighlight,\n nextHighlight: () => stepCitation(1),\n previousHighlight: () => stepCitation(-1),\n openFind: () => setFind((current) => ({ ...current, open: true })),\n // The query survives a close, so re-opening resumes where the reader was;\n // only the box goes away.\n closeFind: () => setFind((current) => ({ ...current, open: false })),\n setFindQuery: (next: string) =>\n setFind((current) => ({ ...current, query: next, activeIndex: 0 })),\n setFindCaseSensitive: (next: boolean) =>\n setFind((current) => ({ ...current, caseSensitive: next, activeIndex: 0 })),\n nextFindMatch: () => stepFind(1),\n previousFindMatch: () => stepFind(-1),\n goToPage,\n // Stop at the ends rather than wrap: a document is not a carousel, and a\n // reader who holds \"next\" past the last page expects to stay there.\n nextPage: () => goToPage(Math.min(pageNumber + 1, pageCount || pageNumber + 1)),\n previousPage: () => goToPage(pageNumber - 1),\n setZoom,\n // Stepping from `effectiveZoom`, not from `zoom`: after a fit the ladder\n // has to continue from what is actually on screen.\n zoomIn: () => setZoom(stepZoom(effectiveZoom, 1)),\n zoomOut: () => setZoom(stepZoom(effectiveZoom, -1)),\n setRotation,\n rotate: (quarterTurns: 1 | -1) =>\n setRotation(((((rotation + quarterTurns * 90) % 360) + 360) % 360) as DocumentRotation),\n reportZoom: setReportedZoom,\n registerFind,\n }),\n [\n setHighlights,\n setActiveHighlight,\n stepCitation,\n stepFind,\n registerFind,\n goToPage,\n pageNumber,\n pageCount,\n setZoom,\n effectiveZoom,\n setRotation,\n rotation,\n ],\n );\n\n const value = useMemo<FileViewerContextValue>(\n () => ({\n state: {\n // A parent's `loading` can add the not-ready state but never clear a\n // real error — losing a failure to a stale prop is worse than a late\n // spinner.\n ...(loading && state.status !== \"error\" ? { ...state, status: \"loading\" as const } : state),\n highlights,\n activeHighlightId,\n find: findState,\n pageNumber,\n pageCount,\n zoom,\n effectiveZoom,\n rotation,\n },\n actions,\n registry,\n meta: {\n baseHeadingLevel,\n resolvedHighlights,\n currentHighlightId,\n highlightSupport: support,\n canFind,\n hasFind: findParts > 0,\n },\n }),\n [\n state,\n loading,\n registry,\n baseHeadingLevel,\n highlights,\n activeHighlightId,\n findState,\n actions,\n resolvedHighlights,\n currentHighlightId,\n support,\n canFind,\n findParts,\n pageNumber,\n pageCount,\n zoom,\n effectiveZoom,\n rotation,\n ],\n );\n\n return <FileViewerContext value={value}>{children}</FileViewerContext>;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Frame */\n/* -------------------------------------------------------------------------- */\n\nexport type FileViewerFrameProps = HTMLAttributes<HTMLDivElement>;\n\n/**\n * The bordered surface the toolbar and content sit in, and the scope of the\n * viewer's find shortcut.\n *\n * Ctrl/Cmd+F is handled HERE rather than on `document`: a page may hold several\n * viewers, or a viewer beside an editor that has its own find, and a\n * document-level listener would let whichever mounted last win. Bound to the\n * frame, the shortcut belongs to whichever viewer the reader is actually inside,\n * and the browser's own find is untouched everywhere else on the page.\n */\nexport const FileViewerFrame = forwardRef<HTMLDivElement, FileViewerFrameProps>(\n function FileViewerFrame({ className, children, onKeyDown, ...props }, ref) {\n const { state, actions, meta } = useFileViewer();\n const { t } = useLocale();\n const frame = useRef<HTMLElement | null>(null);\n const wasOpen = useRef(false);\n\n // Closing the box must hand the caret back, or a keyboard reader is left\n // with focus on nothing at the top of the document.\n const findOpen = state.find.open && meta.canFind;\n useEffect(() => {\n if (wasOpen.current && !findOpen) {\n frame.current\n ?.querySelector<HTMLElement>('[data-slot=\"file-viewer-content\"]')\n ?.focus({ preventScroll: true });\n }\n wasOpen.current = findOpen;\n }, [findOpen]);\n\n return (\n <section\n ref={(node) => {\n frame.current = node;\n if (typeof ref === \"function\") ref(node as HTMLDivElement | null);\n else if (ref) ref.current = node as HTMLDivElement | null;\n }}\n data-slot=\"file-viewer\"\n aria-label={t(\"viewer.label\")}\n className={cn(\n \"bg-card text-card-foreground border-border flex h-full min-h-0 flex-col overflow-hidden rounded-lg border shadow-sm\",\n className,\n )}\n onKeyDown={(event) => {\n // The frame renders a `<section>` while its public props type says\n // `HTMLDivElement` — a pre-existing signature. One cast here beats\n // widening the exported ref type and breaking every caller's\n // `useRef<HTMLDivElement>`.\n onKeyDown?.(event as ReactKeyboardEvent<HTMLDivElement>);\n // Only intercept the browser's shortcut when this viewer can actually\n // honour it — otherwise the reader loses their browser find and gets\n // nothing back. Both halves are needed: an adapter that can paint a\n // match (`canFind`) AND a find part composed in to type into\n // (`hasFind`). A hand-composed frame that omits the part keeps the\n // browser's own find, which is the right answer for it.\n if (!event.defaultPrevented && meta.canFind && meta.hasFind && isFindShortcut(event)) {\n event.preventDefault();\n actions.openFind();\n }\n }}\n {...props}\n >\n {children}\n </section>\n );\n },\n);\n\n/* -------------------------------------------------------------------------- */\n/* Toolbar */\n/* -------------------------------------------------------------------------- */\n\nexport interface FileViewerToolbarProps extends HTMLAttributes<HTMLDivElement> {\n /** Extra controls, placed after the built-in actions. */\n actions?: ReactNode;\n}\n\n/**\n * The identity row: glyph, name, actions.\n *\n * No `role=\"toolbar\"` — that role promises roving-tabindex arrow-key navigation,\n * which this row does not implement. The same decision `ViewToolbar` made, and\n * the reason the P1 `Toolbar` primitive exists (ADR 0024 §5a).\n *\n * With no file it renders NOTHING: a row holding a generic glyph and a blank\n * name reads as a broken render, not as chrome. A screen that needs a permanent\n * header composes its own row around `FileViewerFrame` — that is what the parts\n * are for.\n */\nexport const FileViewerToolbar = forwardRef<HTMLDivElement, FileViewerToolbarProps>(\n function FileViewerToolbar({ className, actions, children, ...props }, ref) {\n const { state } = useFileViewer();\n const { t } = useLocale();\n const source = state.source;\n const Glyph = fileIconFor(source?.name ?? \"\", source?.mediaType);\n\n if (!source) return null;\n\n const download = () => {\n void source.bytes().then((bytes) => {\n downloadBlob(new Blob([bytes], { type: source.mediaType }), source.name);\n });\n };\n\n return (\n <div\n ref={ref}\n data-slot=\"file-viewer-toolbar\"\n className={cn(\n // The divider is the ONLY cue between toolbar and content (same fill,\n // no elevation change) — WCAG 1.4.11, so the strong rung.\n \"border-border-strong flex shrink-0 items-center gap-2 border-b px-3 py-2\",\n className,\n )}\n {...props}\n >\n <Glyph aria-hidden=\"true\" className=\"text-muted-foreground size-4 shrink-0\" />\n {/* min-w-0 is what actually lets the name truncate inside a flex row. */}\n <Text className=\"min-w-0 flex-1 truncate\" title={source.name}>\n {source.name}\n </Text>\n {children}\n {actions}\n <Separator orientation=\"vertical\" className=\"h-4\" />\n {/* `label` is IconButton's single source of truth — it becomes both the\n accessible name and the tooltip, so the two cannot drift. */}\n <IconButton\n variant=\"ghost\"\n label={t(\"viewer.download\", { name: source.name })}\n icon={<DownloadIcon aria-hidden=\"true\" />}\n onClick={download}\n />\n </div>\n );\n },\n);\n\n/* -------------------------------------------------------------------------- */\n/* Highlight status */\n/* -------------------------------------------------------------------------- */\n\nexport type FileViewerHighlightStatusProps = HTMLAttributes<HTMLDivElement>;\n\n/**\n * \"We couldn't find that passage.\"\n *\n * A citation that fails to locate must not fail SILENTLY: the reader clicked a\n * source link and got a document that looks untouched, with no way to tell\n * whether the viewer is broken, the passage moved, or they mis-clicked. The\n * request survives resolution precisely so this line has something to say.\n *\n * Three different pieces of news, three sentences:\n * - `not-found` / `absent` — searched the whole projection; it is not in it.\n * - `not-found` / `truncated` — the projection is capped, so it may lie past it.\n * - `unsupported` — a CAPABILITY GAP: this build cannot point at part of that\n * format. Not the reader's fault and not retryable, same call the error panel\n * already makes for `unsupported-format`.\n *\n * Search misses are excluded: the find bar already counts its own matches, and\n * \"No matches\" there says it better than a second line here would.\n */\nexport const FileViewerHighlightStatus = forwardRef<HTMLDivElement, FileViewerHighlightStatusProps>(\n function FileViewerHighlightStatus({ className, ...props }, ref) {\n const { state, meta } = useFileViewer();\n const { t } = useLocale();\n\n const missed = meta.resolvedHighlights.filter(\n (highlight) =>\n highlight.source === \"citation\" &&\n (highlight.status === \"not-found\" || highlight.status === \"unsupported\"),\n );\n if (missed.length === 0) return null;\n\n const first = missed[0] as ResolvedHighlight;\n const message =\n first.status === \"unsupported\"\n ? t(\"viewer.highlight.unsupported\", {\n format: state.source?.extension.toUpperCase() || \"\",\n })\n : first.reason === \"truncated\"\n ? t(\"viewer.highlight.notFoundTruncated\")\n : t(\"viewer.highlight.notFound\");\n\n return (\n <div\n ref={ref}\n data-slot=\"file-viewer-highlight-status\"\n role=\"status\"\n aria-live=\"polite\"\n className={cn(\n // Information, not a failure — a neutral muted row, never the\n // destructive tone. The divider is the sole cue between it and the\n // content below (WCAG 1.4.11, strong rung).\n \"border-border-strong text-muted-foreground flex shrink-0 items-center gap-2 border-b px-3 py-2\",\n className,\n )}\n {...props}\n >\n <SearchXIcon aria-hidden=\"true\" className=\"size-4 shrink-0\" />\n <span className=\"text-meta min-w-0 flex-1 truncate\">{message}</span>\n </div>\n );\n },\n);\n\n/* -------------------------------------------------------------------------- */\n/* States */\n/* -------------------------------------------------------------------------- */\n\n/**\n * A layout-shaped skeleton, not a spinner: it occupies the box the real content\n * will, so nothing shifts when the file arrives (`loading-states.md`).\n * `aria-hidden` because `Skeleton` is decorative — the single live region on the\n * wrapper is what AT hears.\n */\nexport const FileViewerSkeleton = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(\n function FileViewerSkeleton({ className, ...props }, ref) {\n const { state } = useFileViewer();\n const { t } = useLocale();\n return (\n <div\n ref={ref}\n data-slot=\"file-viewer-skeleton\"\n role=\"status\"\n aria-live=\"polite\"\n className={cn(\"flex h-full flex-col gap-3 p-4\", className)}\n {...props}\n >\n <span className=\"sr-only\">{t(\"viewer.loading\", { name: state.source?.name ?? \"\" })}</span>\n <Skeleton aria-hidden=\"true\" className=\"h-4 w-2/5\" />\n <Skeleton aria-hidden=\"true\" className=\"h-4 w-4/5\" />\n <Skeleton aria-hidden=\"true\" className=\"h-4 w-3/5\" />\n <Skeleton aria-hidden=\"true\" className=\"min-h-24 flex-1\" />\n </div>\n );\n },\n);\n\n/** Message keys per failure code — the code is the contract, the prose is not. */\nconst ERROR_MESSAGES: Record<ViewerErrorCode, { title: string; body: string }> = {\n \"unsupported-format\": {\n title: \"viewer.error.unsupportedFormat\",\n body: \"viewer.error.unsupportedFormatBody\",\n },\n \"parser-missing\": {\n title: \"viewer.error.parserMissing\",\n body: \"viewer.error.parserMissingBody\",\n },\n \"read-failed\": { title: \"viewer.error.readFailed\", body: \"viewer.error.readFailedBody\" },\n \"parse-failed\": { title: \"viewer.error.parseFailed\", body: \"viewer.error.parseFailedBody\" },\n // Neither should ever reach the UI: a protocol mismatch throws at registration\n // and an abort is swallowed as a superseded load. Mapped anyway so the panel\n // can never render an empty title.\n \"protocol-mismatch\": { title: \"viewer.error.parseFailed\", body: \"viewer.error.parseFailedBody\" },\n aborted: { title: \"viewer.error.readFailed\", body: \"viewer.error.readFailedBody\" },\n};\n\n/**\n * A gap in what this build can SHOW — the file is fine, we just cannot draw it.\n * Neither is retryable, and neither is the user's fault, so both are presented\n * as information rather than as a failure (see `FileViewerError`).\n */\nconst CAPABILITY_GAPS = new Set<ViewerErrorCode>([\"unsupported-format\", \"parser-missing\"]);\n\nexport const FileViewerError = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(\n function FileViewerError({ className, ...props }, ref) {\n const { state } = useFileViewer();\n const { t } = useLocale();\n const error = state.error;\n if (!error) return null;\n\n const message = ERROR_MESSAGES[error.code];\n const vars = {\n name: state.source?.name ?? \"\",\n format: state.source?.extension.toUpperCase() || t(\"viewer.error.parseFailed\"),\n packages: error.packages?.join(\", \") ?? \"\",\n };\n\n // \"This build can't draw PDFs\" and \"the network dropped\" are different news.\n // A red alarm on the first one blames the reader for a capability we never\n // shipped, so the gap gets a neutral panel and `status`; a real failure keeps\n // the destructive panel and the `alert` StatePanel already sets.\n const isGap = CAPABILITY_GAPS.has(error.code);\n\n return (\n <div\n ref={ref}\n data-slot=\"file-viewer-error\"\n // min-h-full (not h-full) so a tall panel grows and scrolls instead of\n // being clipped at the top by `justify-center`.\n className={cn(\"flex min-h-full flex-col justify-center p-4\", className)}\n {...(isGap ? { role: \"status\" as const, \"aria-live\": \"polite\" as const } : {})}\n {...props}\n >\n <StatePanel\n kind={isGap ? \"empty\" : \"error\"}\n // A dashed edge invites a drop; this panel accepts nothing. Solid.\n className={isGap ? \"border-solid\" : undefined}\n icon={isGap ? <EyeOffIcon aria-hidden=\"true\" /> : undefined}\n title={t(message.title)}\n description={t(message.body, vars)}\n // `unsupported-format` and `parser-missing` are not retryable — the\n // file has not changed and neither has what is installed. Offering\n // \"Try again\" there teaches users the button does nothing.\n actions={\n error.code === \"read-failed\" || error.code === \"parse-failed\" ? (\n <RetryButton />\n ) : undefined\n }\n />\n </div>\n );\n },\n);\n\n/**\n * The real `Button`, not a styled `<button>`.\n *\n * A hand-rolled `text-primary` link measured 4.14:1 against `StatePanel`'s error\n * wash and failed axe — the brand hue is tuned against `--background`/`--card`,\n * not against a tinted panel. `outline` puts the label on ordinary ink over its\n * own surface, which is contrast-safe on any panel and gives the control a\n * visible hit target besides.\n */\nfunction RetryButton() {\n const { actions } = useFileViewer();\n const { t } = useLocale();\n return (\n <Button variant=\"outline\" size=\"sm\" onClick={actions.reload}>\n {t(\"viewer.retry\")}\n </Button>\n );\n}\n\nexport const FileViewerEmpty = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(\n function FileViewerEmpty({ className, ...props }, ref) {\n const { t } = useLocale();\n return (\n <div\n ref={ref}\n data-slot=\"file-viewer-empty\"\n className={cn(\"flex min-h-full flex-col justify-center p-4\", className)}\n {...props}\n >\n <StatePanel kind=\"empty\" title={t(\"viewer.empty\")} description={t(\"viewer.emptyBody\")} />\n </div>\n );\n },\n);\n\n/* -------------------------------------------------------------------------- */\n/* Content */\n/* -------------------------------------------------------------------------- */\n\nexport type FileViewerContentProps = HTMLAttributes<HTMLDivElement>;\n\n/**\n * The state switch: empty · loading · error · the adapter's own renderer.\n *\n * The adapter supplies its `Renderer` alongside its parser, so this component\n * never grows a per-format `switch` — that is what keeps formats additive.\n *\n * **This is THE scroll boundary** for any adapter that does not manage its own\n * viewport. Two nested `overflow-auto` boxes do not compose: the inner one clips\n * while the outer one's padding stays put, so a long document ends flush against\n * a band of whitespace and reads as a failed render rather than as \"scroll for\n * more\". Adapters whose content simply flows (text, code, markdown, Word) let\n * this scroll; the ones with a fixed sub-control and a scrolling body of their\n * own — PDF pages, PowerPoint slides, a sheet under its tab bar — keep theirs\n * and label it the same way.\n *\n * It is also a **focusable, named region**: a pane that scrolls but contains\n * nothing focusable cannot be reached from a keyboard at all (WCAG 2.1.1), and\n * a plain-text file contains nothing focusable by definition.\n */\nexport const FileViewerContent = forwardRef<HTMLDivElement, FileViewerContentProps>(\n function FileViewerContent({ className, ...props }, ref) {\n const { state, actions, meta } = useFileViewer();\n const { t } = useLocale();\n\n return (\n <div\n ref={ref}\n data-slot=\"file-viewer-content\"\n role=\"region\"\n aria-label={t(\"viewer.content\")}\n tabIndex={0}\n className={cn(\n \"focus-visible:ring-ring min-h-0 flex-1 overflow-auto p-4 focus-visible:outline-none focus-visible:ring-2\",\n className,\n )}\n {...props}\n >\n {state.status === \"empty\" && <FileViewerEmpty />}\n {state.status === \"loading\" && <FileViewerSkeleton />}\n {state.status === \"error\" && <FileViewerError />}\n {state.status === \"ready\" && state.adapter && state.document && state.source && (\n <state.adapter.Renderer\n document={state.document}\n source={state.source}\n baseHeadingLevel={meta.baseHeadingLevel}\n highlights={meta.resolvedHighlights}\n // `meta.currentHighlightId`, NOT `state.activeHighlightId`: the\n // renderer must follow the find match too, or find can never scroll,\n // page or switch sheet.\n activeHighlightId={meta.currentHighlightId}\n // The view half (ADR 0026). A renderer that ignores these keeps\n // working — they are optional, and a format with no pages or no\n // scaling never reads them.\n pageNumber={state.pageNumber}\n onPageChange={actions.goToPage}\n zoom={state.zoom}\n onZoomResolved={actions.reportZoom}\n rotation={state.rotation}\n />\n )}\n </div>\n );\n },\n);\n\n/* -------------------------------------------------------------------------- */\n/* Batteries-included */\n/* -------------------------------------------------------------------------- */\n\nexport interface FileViewerProps\n extends\n Omit<FileViewerProviderProps, \"children\">,\n Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n /** Replace the default composition. Rendered inside the provider AND the frame. */\n children?: ReactNode;\n}\n\n/**\n * The default composition — provider + frame + toolbar + content.\n *\n * Reach for the parts when you need a different arrangement; this covers the\n * common case in one element.\n */\nexport const FileViewer = forwardRef<HTMLDivElement, FileViewerProps>(function FileViewer(\n {\n // Every provider prop is destructured BY NAME, and the rest goes to the\n // frame. Peeling off only some of them and spreading the remainder was a\n // silent bug: `baseHeadingLevel` type-checked, never reached the provider,\n // and landed on the `<section>` as an unknown DOM attribute. Adding a\n // provider prop above without adding it here re-creates that exactly.\n source,\n registry,\n loading,\n baseHeadingLevel,\n highlights,\n defaultHighlights,\n onHighlightsChange,\n activeHighlightId,\n defaultActiveHighlightId,\n onActiveHighlightChange,\n pageNumber,\n defaultPageNumber,\n onPageNumberChange,\n zoom,\n defaultZoom,\n onZoomChange,\n rotation,\n defaultRotation,\n onRotationChange,\n children,\n ...props\n },\n ref,\n) {\n return (\n <FileViewerProvider\n source={source}\n registry={registry}\n loading={loading}\n baseHeadingLevel={baseHeadingLevel}\n highlights={highlights}\n defaultHighlights={defaultHighlights}\n onHighlightsChange={onHighlightsChange}\n activeHighlightId={activeHighlightId}\n defaultActiveHighlightId={defaultActiveHighlightId}\n onActiveHighlightChange={onActiveHighlightChange}\n pageNumber={pageNumber}\n defaultPageNumber={defaultPageNumber}\n onPageNumberChange={onPageNumberChange}\n zoom={zoom}\n defaultZoom={defaultZoom}\n onZoomChange={onZoomChange}\n rotation={rotation}\n defaultRotation={defaultRotation}\n onRotationChange={onRotationChange}\n >\n <FileViewerFrame ref={ref} {...props}>\n {children ?? (\n <>\n {/* The page and scale controls live in the identity row, not inside\n the canvas: one row of chrome per viewer, and an app that wants\n them somewhere else composes the parts itself. Each renders\n nothing for a format whose manifest does not claim it. */}\n <FileViewerToolbar>\n <FileViewerPager />\n <FileViewerZoom />\n <FileViewerRotate />\n </FileViewerToolbar>\n {/* Renders nothing until Ctrl/Cmd+F, and nothing at all for a\n format whose adapter cannot paint a range. */}\n <FileViewerFind />\n <FileViewerHighlightStatus />\n <FileViewerContent />\n </>\n )}\n </FileViewerFrame>\n </FileViewerProvider>\n );\n});\n","/**\n * The adapter registry — how a file finds its renderer.\n *\n * Architecture adapted from [anyview](https://github.com/harshpreet931/anyview)\n * (MIT). Four properties are what make it work, and all four are deliberate:\n *\n * 1. **Eager manifests, lazy loaders.** Routing and capability questions are\n * answered from plain data; the parser is fetched only when a file of that\n * kind is actually opened.\n * 2. **Priority override.** A consumer replaces a built-in by registering a\n * higher priority — no fork, no patch.\n * 3. **A fresh adapter per document.** The module (and its parser) is cached;\n * the instance is not, so per-document state cannot leak between files.\n * 4. **A protocol guard.** A mismatched adapter fails with a named error at\n * registration, not with an undefined property at render.\n */\n\nimport { type FileKind, resolveFileKind } from \"@elabs-ai/components-ui\";\n\nimport { ViewerError, isModuleNotFound, parserMissingError, toViewerError } from \"./errors\";\nimport {\n type AdapterLoader,\n type AdapterManifest,\n type AdapterModule,\n PROTOCOL_VERSION,\n} from \"./types\";\n\n/**\n * How specifically a manifest claimed a file. Higher is more specific.\n * A plain object rather than a `const enum` — the repo builds with\n * `isolatedModules`, which cannot inline one.\n */\nconst MatchScore = {\n None: 0,\n Category: 1,\n MediaTypePrefix: 2,\n MediaType: 3,\n Extension: 4,\n} as const;\n\ntype MatchScore = (typeof MatchScore)[keyof typeof MatchScore];\n\ninterface Entry {\n manifest: AdapterManifest;\n loader: AdapterLoader;\n}\n\n/** A registry of file adapters. Create with {@link createRegistry}. */\nexport interface ViewerRegistry {\n /**\n * Add an adapter, or replace one with the same `id`.\n *\n * Replacement is by id and unconditional — the caller asked for it. Priority\n * governs which of several DIFFERENT adapters wins a file, not whether a\n * registration takes effect.\n *\n * @throws {ViewerError} `protocol-mismatch` if the manifest targets another protocol.\n */\n register(manifest: AdapterManifest, loader: AdapterLoader): void;\n /**\n * Every registered manifest, highest priority first. Not the routing order —\n * specificity is per-file, so only {@link detect} can rank for a given file.\n */\n manifests(): AdapterManifest[];\n /** The manifest that should open this file, or `undefined` if none claims it. */\n detect(kind: FileKind): AdapterManifest | undefined;\n /** Convenience over {@link detect} for a name + MIME. */\n detectByName(name: string, mediaType?: string): AdapterManifest | undefined;\n /**\n * Fetch an adapter module by id, caching the MODULE (never an instance).\n *\n * @throws {ViewerError} `unsupported-format` when the id is unknown,\n * `parser-missing` when the module's optional peer is not installed,\n * `protocol-mismatch` when the loaded module disagrees with its manifest.\n */\n load(id: string): Promise<AdapterModule>;\n}\n\n/** How well `manifest` claims `kind`. `MatchScore.None` means \"not mine\". */\nexport function scoreManifest(manifest: AdapterManifest, kind: FileKind): MatchScore {\n if (kind.extension && manifest.extensions?.includes(kind.extension)) {\n return MatchScore.Extension;\n }\n for (const declared of manifest.mediaTypes ?? []) {\n if (declared.endsWith(\"/\")) {\n if (kind.mediaType.startsWith(declared)) return MatchScore.MediaTypePrefix;\n } else if (declared === kind.mediaType) {\n return MatchScore.MediaType;\n }\n }\n if (manifest.categories?.includes(kind.category)) return MatchScore.Category;\n return MatchScore.None;\n}\n\n/** Unwrap `export default` vs named exports, so an adapter can use either. */\nfunction unwrap(loaded: AdapterModule | { default: AdapterModule }): AdapterModule {\n return \"default\" in loaded && loaded.default ? loaded.default : (loaded as AdapterModule);\n}\n\nfunction assertProtocol(manifest: AdapterManifest, where: string): void {\n if (manifest.protocol !== PROTOCOL_VERSION) {\n throw new ViewerError(\n \"protocol-mismatch\",\n `Adapter \"${manifest.id}\" targets viewer protocol ${String(manifest.protocol)}, but this build speaks ${String(PROTOCOL_VERSION)} (${where}).`,\n );\n }\n}\n\n/**\n * Create an empty registry.\n *\n * Empty on purpose: the built-ins live in `createDefaultRegistry()`\n * (`src/adapters`), so a consumer who wants only their own adapters — or only\n * images — never pulls in the rest.\n */\nexport function createRegistry(): ViewerRegistry {\n const entries = new Map<string, Entry>();\n const modules = new Map<string, Promise<AdapterModule>>();\n\n function ordered(): Entry[] {\n return [...entries.values()].sort(\n (a, b) => (b.manifest.priority ?? 0) - (a.manifest.priority ?? 0),\n );\n }\n\n function detect(kind: FileKind): AdapterManifest | undefined {\n let best: { manifest: AdapterManifest; score: MatchScore; priority: number } | undefined;\n for (const { manifest } of entries.values()) {\n const score = scoreManifest(manifest, kind);\n if (score === MatchScore.None) continue;\n const priority = manifest.priority ?? 0;\n // Priority is the consumer's explicit override, so it outranks how\n // specifically an adapter happened to claim the file.\n const wins =\n !best || priority > best.priority || (priority === best.priority && score > best.score);\n if (wins) best = { manifest, score, priority };\n }\n return best?.manifest;\n }\n\n return {\n register(manifest, loader) {\n assertProtocol(manifest, \"registration\");\n entries.set(manifest.id, { manifest, loader });\n // A replaced id must not keep serving the old module.\n modules.delete(manifest.id);\n },\n\n manifests() {\n return ordered().map((entry) => entry.manifest);\n },\n\n detect,\n\n detectByName(name, mediaType) {\n return detect(resolveFileKind(name, mediaType));\n },\n\n async load(id) {\n const entry = entries.get(id);\n if (!entry) {\n throw new ViewerError(\"unsupported-format\", `No adapter is registered for \"${id}\".`);\n }\n\n let pending = modules.get(id);\n if (!pending) {\n pending = entry\n .loader()\n .then((loaded) => {\n const adapterModule = unwrap(loaded);\n assertProtocol(adapterModule.manifest, `module \"${id}\"`);\n return adapterModule;\n })\n .catch((error: unknown) => {\n // Not cached: an optional peer installed later, or a transient\n // chunk-load failure, must be retryable without a page reload.\n modules.delete(id);\n if (isModuleNotFound(error)) {\n throw parserMissingError(id, entry.manifest.requires ?? [], { cause: error });\n }\n throw toViewerError(error, \"parse-failed\");\n });\n modules.set(id, pending);\n }\n return pending;\n },\n };\n}\n","/**\n * The built-in adapters, and the registry that has them all.\n *\n * Every entry is `manifest` (eager, plain data) + `() => import(…)` (lazy). The\n * manifests are imported statically ON PURPOSE — they are a few dozen bytes of\n * data each and answer \"can this be opened, and what controls apply\" with no\n * network. The parsers and renderers behind them are not.\n *\n * `pnpm heavy-deps:check` enforces the split: a static import of `papaparse`\n * (or, from P1, `pdfjs-dist`) fails CI, because those are optional peers — a\n * static edge does not merely bloat a chunk, it makes the package unresolvable\n * for every consumer that did not install that parser.\n */\n\nimport { codeManifest } from \"./code/code-manifest\";\nimport { csvManifest } from \"./csv/csv-manifest\";\nimport { docxManifest } from \"./docx/docx-manifest\";\nimport { markdownManifest } from \"./markdown/markdown-manifest\";\nimport { imageManifest } from \"./image/image-manifest\";\nimport { jsonManifest } from \"./json/json-manifest\";\nimport { mediaManifest } from \"./media/media-manifest\";\nimport { pdfManifest } from \"./pdf/pdf-manifest\";\nimport { pptxManifest } from \"./pptx/pptx-manifest\";\nimport { textManifest } from \"./text/text-manifest\";\nimport { xlsxManifest } from \"./xlsx/xlsx-manifest\";\nimport { createRegistry, type ViewerRegistry } from \"../core/registry\";\n\nexport {\n codeManifest,\n csvManifest,\n docxManifest,\n imageManifest,\n jsonManifest,\n markdownManifest,\n mediaManifest,\n pdfManifest,\n pptxManifest,\n textManifest,\n xlsxManifest,\n};\n\n/**\n * A registry with every built-in adapter registered.\n *\n * Call it per app (or per view) rather than sharing one module-level instance,\n * so one screen's `register()` override cannot leak into another's.\n */\nexport function createDefaultRegistry(): ViewerRegistry {\n const registry = createRegistry();\n registry.register(imageManifest, () => import(\"./image/image-adapter\"));\n registry.register(jsonManifest, () => import(\"./json/json-adapter\"));\n registry.register(csvManifest, () => import(\"./csv/csv-adapter\"));\n registry.register(pdfManifest, () => import(\"./pdf/pdf-adapter\"));\n registry.register(mediaManifest, () => import(\"./media/media-adapter\"));\n registry.register(docxManifest, () => import(\"./docx/docx-adapter\"));\n registry.register(xlsxManifest, () => import(\"./xlsx/xlsx-adapter\"));\n registry.register(pptxManifest, () => import(\"./pptx/pptx-adapter\"));\n registry.register(markdownManifest, () => import(\"./markdown/markdown-adapter\"));\n registry.register(codeManifest, () => import(\"./code/code-adapter\"));\n // Registered last and claiming only broad categories, so anything above wins\n // a file it names specifically. This is the \"readable as text\" backstop.\n registry.register(textManifest, () => import(\"./text/text-adapter\"));\n return registry;\n}\n","/**\n * What the viewer is pointed AT — the vocabulary shared by citations and\n * find-in-document.\n *\n * A {@link DocumentHighlight} is what a caller asks for; a\n * {@link ResolvedHighlight} is what the viewer worked out and what an adapter's\n * `Renderer` is handed. Keeping the two apart is what lets \"we could not find\n * that passage\" be a rendered STATE rather than a silent no-op: the request\n * survives even when the location does not, so the chrome still has something\n * to name.\n *\n * The address vocabulary itself lives in `@elabs-ai/components-ui`\n * (`DocumentAddress`) because the producer of a citation and this consumer are\n * sibling packages that may not import each other. Everything here is\n * adapter-protocol detail and stays in this package.\n */\n\nimport type {\n DocumentAddress,\n DocumentAddressKind,\n DocumentRect,\n MatchRange,\n} from \"@elabs-ai/components-ui\";\n\n/**\n * Where a highlight came from. Both paint identically — only which one is\n * ACTIVE differs — but the origin decides who owns the list: citations are a\n * controlled prop the app supplies, search matches are the viewer's own.\n */\nexport type HighlightSource = \"citation\" | \"search\";\n\n/** A request to point the viewer at part of the open document. */\nexport interface DocumentHighlight {\n /**\n * Stable per highlight. It is what `activeHighlightId` names and what React\n * keys on, so a list that renumbers between renders must not renumber ids.\n */\n id: string;\n /** Which part of the document. */\n address: DocumentAddress;\n /**\n * Short human label — the answer's claim, the source's title. Announced when\n * this highlight becomes active, so prefer something a listener can act on\n * over \"Citation 3\".\n */\n label?: string;\n /** Defaults to `\"citation\"`. */\n source?: HighlightSource;\n}\n\n/**\n * How a request turned out.\n *\n * `unsupported` is a CAPABILITY GAP, not a failure — the same distinction the\n * error panel already draws. \"This build can't locate a rect in a Word file\"\n * is news about what we shipped; it is not the reader's mistake, and it is not\n * retryable.\n */\nexport type HighlightStatus = \"pending\" | \"resolved\" | \"not-found\" | \"unsupported\";\n\n/** Why a passage was not found — the two are genuinely different news. */\nexport type HighlightMissReason =\n /** Searched the whole projection; the passage is not in it. */\n | \"absent\"\n /** The projection is capped, and the passage may lie past the cap. */\n | \"truncated\";\n\n/** A request, plus where it landed. What every adapter `Renderer` receives. */\nexport interface ResolvedHighlight {\n id: string;\n label?: string;\n source: HighlightSource;\n status: HighlightStatus;\n /** The original request, so a renderer can honour a kind the shell cannot. */\n address: DocumentAddress;\n /** Whether the viewer is currently pointed at this one. */\n active: boolean;\n /**\n * 1-based position among the highlights of the same `source` that resolved —\n * the \"3\" in \"3 of 12\". Absent when this one did not resolve, so a miss never\n * silently consumes a number the reader is counting through.\n */\n index?: number;\n /** Offsets into `document.text`, once located. */\n range?: MatchRange;\n /**\n * 1-based page, set only for a `rect` address — the one kind with no range to\n * derive a position from.\n *\n * A `quote` or `range` deliberately leaves this empty even when the caller\n * supplied a page hint: where the passage actually landed is knowable from the\n * adapter's own index, and a stale hint used as an instruction would page the\n * reader somewhere the mark is not.\n */\n page?: number;\n /** Geometry, for a `rect` address. */\n rects?: readonly DocumentRect[];\n /** Only on `not-found`. */\n reason?: HighlightMissReason;\n}\n\n/**\n * The most matches find-in-document will paint.\n *\n * A one-letter query against a 2 MB log matches hundreds of thousands of times;\n * every one of those is a DOM element. The cap keeps typing responsive, and the\n * chrome says so rather than quietly showing a wrong total.\n */\nexport const FIND_MATCH_LIMIT = 2000;\n\n/** Prefix reserved for the viewer's own search matches. */\nconst FIND_ID_PREFIX = \"find:\";\n\n/** The id for the nth (0-based) find match. */\nexport function findMatchId(index: number): string {\n return `${FIND_ID_PREFIX}${String(index)}`;\n}\n\n/** Whether an id belongs to find-in-document rather than to a caller. */\nexport function isFindMatchId(id: string): boolean {\n return id.startsWith(FIND_ID_PREFIX);\n}\n\n/** Which address kinds an adapter honours. Absent means none — the safe default. */\nexport type HighlightSupport = readonly DocumentAddressKind[];\n","/**\n * Turning a request into a location — the LOCATE step, and the only step that\n * runs outside the adapter.\n *\n * The funnel is three stages with three homes. **Locate** (here) answers \"where\n * in the text projection is this?\" and produces character offsets. **Map** (the\n * adapter's renderer) turns those offsets into its own model — block 14, page 3,\n * cell B7. **Paint** (also the renderer) draws it and scrolls to it.\n *\n * Locate lives in the shell rather than in each adapter because its OUTCOME is\n * chrome state, not pixels: \"3 of 12\", \"we couldn't find that passage\", \"this\n * build can't locate a box in a Word file\". Every adapter would otherwise write\n * that logic again, slightly differently, and the shell would have no way to\n * count what it is showing. It is also pure — no DOM, no engine — so it is\n * testable without jsdom.\n */\n\nimport {\n normalizeQuoteText,\n type MatchRange,\n type NormalizedText,\n type QuoteAddress,\n} from \"@elabs-ai/components-ui\";\n\nimport type { DocumentHighlight, HighlightSupport, ResolvedHighlight } from \"./highlight\";\n\n/**\n * Find a quoted passage in a normalized projection and map it back to raw\n * offsets.\n *\n * Both sides are folded first (whitespace, quote glyphs, case), because a\n * citation is re-typed by a model or extracted by a different tool and will\n * essentially never be byte-identical to what our parser produced.\n *\n * Ambiguity is resolved by the caller's own hints and never guessed at: an\n * explicit `occurrence` wins, then proximity to `near.offset`, then the first\n * match. Returning the first match silently would put a citation on the wrong\n * paragraph of a document that repeats a heading.\n */\nexport function locateQuote(\n normalized: NormalizedText,\n address: QuoteAddress,\n): MatchRange | undefined {\n const needle = normalizeQuoteText(address.text);\n if (needle.length === 0) return undefined;\n\n const starts: number[] = [];\n for (let at = normalized.text.indexOf(needle); at !== -1; ) {\n starts.push(at);\n // Step by one, not by the needle's length: overlapping occurrences of a\n // repeated phrase (\"na na na\") are still distinct places in the document.\n at = normalized.text.indexOf(needle, at + 1);\n }\n if (starts.length === 0) return undefined;\n\n const toRange = (start: number): MatchRange => [\n normalized.offsets[start] as number,\n normalized.offsets[start + needle.length] as number,\n ];\n\n if (address.occurrence !== undefined) {\n const chosen = starts[address.occurrence - 1];\n // An out-of-range occurrence is a miss, not a fallback to the first: the\n // caller asked for the fourth of three, and quietly marking the first would\n // be a confidently wrong citation.\n return chosen === undefined ? undefined : toRange(chosen);\n }\n\n const near = address.near?.offset;\n if (near !== undefined) {\n let best = starts[0] as number;\n let bestDistance = Number.POSITIVE_INFINITY;\n for (const start of starts) {\n const distance = Math.abs((normalized.offsets[start] as number) - near);\n if (distance < bestDistance) {\n bestDistance = distance;\n best = start;\n }\n }\n return toRange(best);\n }\n\n return toRange(starts[0] as number);\n}\n\nexport interface HighlightResolveContext {\n /**\n * The document's text projection, folded once with its offset map. Absent\n * when the format has no text projection at all.\n *\n * Pre-folded rather than raw because find-in-document re-resolves on every\n * keystroke, and folding a 2 MB projection per keystroke is what turns a\n * search box into a stutter.\n */\n normalized?: NormalizedText;\n /** Length of the RAW projection, for clamping `range` addresses. */\n textLength?: number;\n /** Whether that projection is capped, which changes what a miss MEANS. */\n truncated?: boolean;\n /** Which address kinds this document's adapter honours. */\n supported: HighlightSupport;\n /** The highlight the viewer is currently pointed at. */\n activeId?: string | null;\n}\n\n/** Sort key: where in the document this landed. Unresolved sorts last. */\nfunction positionOf(highlight: ResolvedHighlight): number {\n if (highlight.range) return highlight.range[0];\n if (highlight.page !== undefined) return highlight.page;\n return Number.POSITIVE_INFINITY;\n}\n\nfunction resolveOne(\n highlight: DocumentHighlight,\n context: HighlightResolveContext,\n): ResolvedHighlight {\n const source = highlight.source ?? \"citation\";\n const address = highlight.address;\n const base = { id: highlight.id, label: highlight.label, source, address, active: false };\n\n // The load-bearing contract: match only the kinds this adapter DECLARED, and\n // report the rest. No exhaustive switch, no `never` fallthrough — that is\n // what lets a fourth address kind be added later without breaking every\n // consumer that predates it.\n if (!context.supported.includes(address.kind)) {\n return { ...base, status: \"unsupported\" };\n }\n\n if (address.kind === \"rect\") {\n // Geometry needs no text and cannot fail to \"locate\" — whether the page\n // exists is the renderer's business, since only it knows the page count.\n return { ...base, status: \"resolved\", page: address.page, rects: address.rects };\n }\n\n const miss = (): ResolvedHighlight => ({\n ...base,\n status: \"not-found\",\n reason: context.truncated ? \"truncated\" : \"absent\",\n });\n\n if (address.kind === \"range\") {\n const length = context.textLength;\n if (length === undefined) return miss();\n const start = Math.max(0, Math.min(address.start, length));\n const end = Math.max(start, Math.min(address.end, length));\n // A range clamped to nothing is a miss, not a zero-width mark: the offsets\n // were computed against a longer projection than the one we have.\n if (end === start) return miss();\n // No `page`. A producer's page number is a hint, and a wrong one used as an\n // instruction turns the pager to a blank page while the mark sits elsewhere.\n // Where the range actually landed is knowable from the adapter's own index,\n // so that is what navigation uses; `page` stays authoritative only for\n // `rect`, which has no range to derive it from.\n return { ...base, status: \"resolved\", range: [start, end] };\n }\n\n if (!context.normalized) return miss();\n const range = locateQuote(context.normalized, address);\n if (!range) return miss();\n return { ...base, status: \"resolved\", range };\n}\n\n/**\n * Resolve every request, in document order, numbered per source.\n *\n * Document order rather than the caller's, because \"next match\" has to mean the\n * next one down the page — an app listing citations in relevance order would\n * otherwise send the reader jumping backwards. Numbering is per `source` so the\n * find box's \"3 of 12\" counts search matches only, and never the citations\n * painted beside them.\n */\nexport function resolveHighlights(\n highlights: readonly DocumentHighlight[],\n context: HighlightResolveContext,\n): ResolvedHighlight[] {\n const resolved = highlights\n .map((highlight) => resolveOne(highlight, context))\n .sort((a, b) => positionOf(a) - positionOf(b));\n\n const counters = new Map<string, number>();\n return resolved.map((highlight) => {\n const next =\n highlight.status === \"resolved\" ? (counters.get(highlight.source) ?? 0) + 1 : undefined;\n if (next !== undefined) counters.set(highlight.source, next);\n return {\n ...highlight,\n index: next,\n active: context.activeId != null && context.activeId === highlight.id,\n };\n });\n}\n","\"use client\";\n\n/**\n * Find-in-document — the viewer's own Ctrl/Cmd+F.\n *\n * It paints through the SAME highlight layer citations use (`source: \"search\"`),\n * so a document never grows two mark systems that disagree about what a mark\n * looks like. Only which one is CURRENT differs, and that lives on its own knob\n * (`find.activeIndex`) so an app controlling citations does not have to honour\n * every keystroke of a search it never asked for.\n *\n * **Not a `role=\"toolbar\"`.** A toolbar is one tab stop with roving arrow keys,\n * and an `<input>` inside one has its Left/Right stolen from the caret. This is\n * `role=\"search\"` — the same call `FileViewerToolbar` already made.\n */\n\nimport {\n cn,\n IconButton,\n InputGroup,\n InputGroupAddon,\n InputGroupButton,\n InputGroupInput,\n Separator,\n useLocale,\n} from \"@elabs-ai/components-ui\";\nimport { CaseSensitiveIcon, ChevronDownIcon, ChevronUpIcon, SearchIcon, XIcon } from \"lucide-react\";\nimport {\n forwardRef,\n useEffect,\n useRef,\n type HTMLAttributes,\n type KeyboardEvent as ReactKeyboardEvent,\n} from \"react\";\n\nimport { FIND_MATCH_LIMIT } from \"../core/highlight\";\nimport { useFileViewer } from \"./file-viewer-context\";\n\n/** Whether a keyboard event is the platform's find shortcut. */\nexport function isFindShortcut(event: {\n key: string;\n metaKey: boolean;\n ctrlKey: boolean;\n}): boolean {\n // Either modifier, not `navigator.platform`: a Mac driven by an external PC\n // keyboard sends Ctrl, and sniffing the platform gets that reader wrong.\n return event.key.toLowerCase() === \"f\" && (event.metaKey || event.ctrlKey);\n}\n\nexport type FileViewerFindProps = HTMLAttributes<HTMLDivElement>;\n\n/**\n * The search row. Renders nothing until `actions.openFind()` (or Ctrl/Cmd+F on\n * the frame) opens it, and nothing at all for a document whose adapter cannot\n * paint a range — an affordance that could never highlight anything is worse\n * than no affordance at all.\n */\nexport const FileViewerFind = forwardRef<HTMLDivElement, FileViewerFindProps>(\n function FileViewerFind({ className, ...props }, ref) {\n const { state, actions, meta } = useFileViewer();\n const { t, formatNumber } = useLocale();\n const input = useRef<HTMLInputElement>(null);\n const find = state.find;\n const open = find.open && meta.canFind;\n const { registerFind } = actions;\n\n // Announce that there IS somewhere to type. The frame only takes Ctrl/Cmd+F\n // away from the browser once this has run — a shortcut swallowed with no box\n // to show leaves a keyboard reader with no find at all. Registered on mount\n // rather than when open, because the box is closed at exactly the moment the\n // shortcut has to be decided.\n useEffect(() => registerFind(), [registerFind]);\n\n // A find box that does not take the caret sends the next keystroke to the\n // document behind it — the one thing every reader expects not to happen.\n useEffect(() => {\n if (open) input.current?.focus();\n }, [open]);\n\n if (!open) return null;\n\n const hasQuery = find.query.length > 0;\n const hasMatches = find.matches > 0;\n\n const onKeyDown = (event: ReactKeyboardEvent<HTMLInputElement>) => {\n if (event.key === \"Escape\") {\n event.preventDefault();\n // Stopped here: an Escape meant for the find box must not also close a\n // Dialog the viewer happens to be sitting in.\n event.stopPropagation();\n actions.closeFind();\n return;\n }\n if (event.key === \"Enter\") {\n event.preventDefault();\n if (event.shiftKey) actions.previousFindMatch();\n else actions.nextFindMatch();\n }\n };\n\n return (\n <div\n ref={ref}\n data-slot=\"file-viewer-find\"\n role=\"search\"\n aria-label={t(\"viewer.find.label\")}\n className={cn(\n // The divider is the only cue between this row and the content below\n // it — same fill, no elevation change (WCAG 1.4.11, strong rung).\n \"border-border-strong flex shrink-0 items-center gap-2 border-b px-3 py-2\",\n className,\n )}\n {...props}\n >\n <InputGroup className=\"h-8 max-w-72\">\n <InputGroupAddon>\n <SearchIcon aria-hidden=\"true\" />\n </InputGroupAddon>\n <InputGroupInput\n ref={input}\n // Not `type=\"search\"`: the browser's own clear affordance is\n // unlabelled and unthemeable, and this row already carries a close\n // control that does something more useful.\n type=\"text\"\n value={find.query}\n aria-label={t(\"viewer.find.label\")}\n placeholder={t(\"viewer.find.placeholder\")}\n autoComplete=\"off\"\n spellCheck={false}\n onChange={(event) => actions.setFindQuery(event.target.value)}\n onKeyDown={onKeyDown}\n />\n <InputGroupAddon align=\"inline-end\">\n {/* A real toggle button, so the state is announced rather than\n inferred from a colour change. */}\n <InputGroupButton\n size=\"icon-xs\"\n aria-pressed={find.caseSensitive}\n aria-label={t(\"viewer.find.caseSensitive\")}\n title={t(\"viewer.find.caseSensitive\")}\n onClick={() => actions.setFindCaseSensitive(!find.caseSensitive)}\n >\n <CaseSensitiveIcon aria-hidden=\"true\" />\n </InputGroupButton>\n </InputGroupAddon>\n </InputGroup>\n\n {/* ONE live region for the whole result state: \"3 of 12\" and \"No\n matches\" replace each other rather than sitting side by side, so a\n fruitless search never reads as \"0 of 0\". */}\n <span\n role=\"status\"\n aria-live=\"polite\"\n className=\"text-meta text-muted-foreground min-w-0 flex-1 truncate tabular-nums\"\n >\n {!hasQuery\n ? null\n : hasMatches\n ? t(\"viewer.find.count\", {\n index: formatNumber(find.activeIndex + 1),\n total: formatNumber(Math.min(find.matches, FIND_MATCH_LIMIT)),\n })\n : t(\"viewer.find.none\")}\n {find.truncated\n ? ` ${t(\"viewer.find.capped\", {\n limit: formatNumber(FIND_MATCH_LIMIT),\n total: formatNumber(find.matches),\n })}`\n : null}\n </span>\n\n {/* `aria-disabled`, never the native attribute: a focused control that\n becomes `disabled` is dropped from the focus order, so a reader who\n tabbed to Next and then cleared the query would have focus silently\n fall to <body>. The actions are the real guard — both no-op with no\n matches (see interaction-guidelines.md). */}\n <IconButton\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-disabled={!hasMatches}\n label={t(\"viewer.find.previous\")}\n icon={<ChevronUpIcon aria-hidden=\"true\" />}\n onClick={actions.previousFindMatch}\n />\n <IconButton\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-disabled={!hasMatches}\n label={t(\"viewer.find.next\")}\n icon={<ChevronDownIcon aria-hidden=\"true\" />}\n onClick={actions.nextFindMatch}\n />\n <Separator orientation=\"vertical\" className=\"h-4\" />\n <IconButton\n variant=\"ghost\"\n size=\"icon-sm\"\n label={t(\"viewer.find.close\")}\n icon={<XIcon aria-hidden=\"true\" />}\n onClick={actions.closeFind}\n />\n </div>\n );\n },\n);\n","\"use client\";\n\n/**\n * The context contract behind every `FileViewer` part.\n *\n * Per `component-api.md` (\"Lift state into the Provider; expose a\n * `state` / `actions` interface\"), `FileViewerProvider` is the only thing that\n * knows HOW a file is loaded. Parts read the interface below, so a sibling\n * control placed outside the visual frame but inside the provider — a download\n * button in a page header, a format badge in a breadcrumb — reads and drives the\n * same state with no prop-drilling.\n *\n * This module is the CONTRACT only; the provider itself lives beside the parts\n * it feeds, in `file-viewer.tsx`.\n */\n\nimport type { ProseHeadingLevel, ResolvedFileSource } from \"@elabs-ai/components-ui\";\nimport { createContext, use } from \"react\";\n\nimport type { ViewerError } from \"../core/errors\";\nimport type { DocumentHighlight, HighlightSupport, ResolvedHighlight } from \"../core/highlight\";\nimport type { ViewerRegistry } from \"../core/registry\";\nimport type {\n AdapterCapabilities,\n AdapterDocument,\n AdapterModule,\n DocumentRotation,\n ZoomLevel,\n} from \"../core/types\";\n\n/**\n * Where a file is in its journey to the screen.\n *\n * `loading` means \"no renderable content yet\" — the canonical signal from\n * `.claude/rules/loading-states.md`, rendered as a layout-shaped skeleton.\n * There is no separate `isStreaming`: a file arrives settled or not at all.\n */\nexport type FileViewerStatus = \"empty\" | \"loading\" | \"ready\" | \"error\";\n\n/**\n * The LOAD half of the state — everything the fetch-and-parse effect owns.\n *\n * Split from the highlight half because the two have different lifetimes: this\n * one is replaced wholesale each time a file is opened, while the citations\n * pointing into it are a prop the app controls and outlive any single parse.\n * Folding them together would mean every `setState` in the load effect had to\n * remember to carry the highlights forward.\n */\nexport interface FileViewerLoadState {\n status: FileViewerStatus;\n /** The resolved source, available as soon as there IS one — before any read. */\n source?: ResolvedFileSource;\n /** The adapter's parsed output. Only in `ready`. */\n document?: AdapterDocument;\n /** The adapter module that produced it, for its `Renderer`. Only in `ready`. */\n adapter?: AdapterModule;\n /** What the chrome may offer. Known from the manifest BEFORE the parser loads. */\n capabilities: AdapterCapabilities;\n /** Only in `error`. Always a `ViewerError`, so `code` can drive the message. */\n error?: ViewerError;\n}\n\n/**\n * How the open document is being LOOKED at — which page, at what scale, turned\n * which way.\n *\n * Separate from the load state because it survives nothing and owns nothing: it\n * is pure view, reset (page, rotation) or carried (zoom) when a new file opens.\n * It lives in the provider rather than inside each adapter's `Renderer` so a\n * control can sit anywhere — the shell toolbar, an app's own page header, a\n * deep link — instead of only inside the canvas (ADR 0026).\n */\nexport interface FileViewerViewState {\n /** 1-based. `1` for a format that does not paginate. */\n pageNumber: number;\n /** `0` until a paginated document is ready, and for formats with no pages. */\n pageCount: number;\n /** What was ASKED for: a fixed scale, or a fit mode the renderer resolves. */\n zoom: ZoomLevel;\n /**\n * What that resolved to, as a number — what the zoom control shows and what\n * `zoomIn`/`zoomOut` step from. Equal to `zoom` whenever `zoom` is a number.\n */\n effectiveZoom: number;\n /** Quarter-turns clockwise. Reset to `0` when a different file is opened. */\n rotation: DocumentRotation;\n}\n\nexport interface FileViewerState extends FileViewerLoadState, FileViewerViewState {\n /**\n * The parts of the document to point at, as REQUESTED. What was actually\n * located is `meta.resolvedHighlights` — the request survives a miss so the\n * chrome can say \"we couldn't find that passage\" instead of showing nothing.\n */\n highlights: readonly DocumentHighlight[];\n /** Which one the viewer is pointed at. `null` is \"none\", explicitly. */\n activeHighlightId: string | null;\n find: FileViewerFindState;\n}\n\n/** What find-in-document is doing right now. */\nexport interface FileViewerFindState {\n /** Whether the search box is showing. */\n open: boolean;\n query: string;\n caseSensitive: boolean;\n /** How many matches the current query has. */\n matches: number;\n /** Whether that count hit `FIND_MATCH_LIMIT` and is therefore a floor. */\n truncated: boolean;\n /**\n * Which match is current, 0-based — the \"3\" in \"3 of 12\", minus one.\n *\n * Deliberately NOT the same knob as `activeHighlightId`. Find is the viewer's\n * own, and an app that controls `activeHighlightId` to drive citations would\n * otherwise have to also honour every keystroke of a search it never asked\n * for, or silently break next/previous.\n */\n activeIndex: number;\n}\n\nexport interface FileViewerActions {\n /** Re-run the load. The retry action on the error state. */\n reload: () => void;\n /**\n * Replace the citations. While `highlights` is controlled this writes no local\n * state — but it still calls `onHighlightsChange`, so the owner can accept the\n * request. Mirroring the platform: a controlled input reports, it does not\n * self-update.\n */\n setHighlights: (highlights: readonly DocumentHighlight[]) => void;\n /** Point the viewer at one highlight, or at none. */\n setActiveHighlight: (id: string | null) => void;\n /** Move to the next/previous CITATION in document order, wrapping around. */\n nextHighlight: () => void;\n previousHighlight: () => void;\n openFind: () => void;\n closeFind: () => void;\n setFindQuery: (query: string) => void;\n setFindCaseSensitive: (caseSensitive: boolean) => void;\n /** Move to the next/previous SEARCH match, wrapping around. */\n nextFindMatch: () => void;\n previousFindMatch: () => void;\n /**\n * Turn to a page, 1-based. Clamped to the document — an out-of-range page is\n * a caller's arithmetic slip, not a reason to blank the canvas.\n */\n goToPage: (page: number) => void;\n /** Turn one page. Both stop at the ends rather than wrapping: a document is not a carousel. */\n nextPage: () => void;\n previousPage: () => void;\n /** Draw at a fixed scale, or hand the renderer a fit mode to resolve. */\n setZoom: (zoom: ZoomLevel) => void;\n /**\n * Step to the next stop above/below what is currently ON SCREEN — so zooming\n * in from a fitted page continues from the fitted scale, not from wherever the\n * fixed ladder was last parked.\n */\n zoomIn: () => void;\n zoomOut: () => void;\n setRotation: (rotation: DocumentRotation) => void;\n /** Turn the document a quarter-turn: `1` clockwise, `-1` counter-clockwise. */\n rotate: (quarterTurns: 1 | -1) => void;\n /**\n * The renderer's report channel for {@link FileViewerViewState.effectiveZoom}\n * — not for app code. `FileViewerContent` wires it to the adapter's\n * `onZoomResolved`, the same way `registerFind` is wired to a part rather than\n * called by a consumer.\n */\n reportZoom: (scale: number) => void;\n /**\n * Tell the viewer a find part is mounted; call the returned function on\n * unmount. `FileViewerFind` does this for you — it exists so the frame knows\n * whether taking Ctrl/Cmd+F off the browser leads anywhere.\n */\n registerFind: () => () => void;\n}\n\nexport interface FileViewerContextValue {\n state: FileViewerState;\n actions: FileViewerActions;\n registry: ViewerRegistry;\n meta: {\n /**\n * The rung a viewed document's own top-level heading renders at. Passed to\n * every adapter `Renderer`; see `AdapterRendererProps.baseHeadingLevel`.\n */\n baseHeadingLevel: ProseHeadingLevel;\n /**\n * Citations and find matches, LOCATED, in document order, numbered — what\n * an adapter `Renderer` is handed and what the chrome counts.\n */\n resolvedHighlights: readonly ResolvedHighlight[];\n /**\n * Which highlight the viewer is EFFECTIVELY pointed at, and what an adapter\n * `Renderer` receives as `activeHighlightId`.\n *\n * Not the same knob as `state.activeHighlightId`: that one is the citation\n * the app controls, while the reader stepping through find matches is also\n * \"current\". Find outranks the citation while its box is open and matching,\n * so navigation and scrolling follow whichever the reader is actually moving.\n */\n currentHighlightId: string | null;\n /** Which address kinds this document's adapter declared it can paint. */\n highlightSupport: HighlightSupport;\n /**\n * Whether find-in-document applies to the open document. Derived, not read\n * straight off the manifest — see `AdapterCapabilities.search`.\n */\n canFind: boolean;\n /**\n * Whether a `FileViewerFind` part is composed into this viewer.\n *\n * Separate from {@link canFind}, which only says the ADAPTER could paint a\n * match. The frame needs both before it takes Ctrl/Cmd+F away from the\n * browser: intercepting the shortcut with nowhere to type leaves a keyboard\n * reader with no find at all.\n */\n hasFind: boolean;\n };\n}\n\n/**\n * Exported for `FileViewerProvider` (which lives with the parts it feeds, in\n * `file-viewer.tsx`) — NOT part of the package's public surface.\n */\nexport const FileViewerContext = createContext<FileViewerContextValue | null>(null);\n\n/**\n * Read the viewer state. Throws outside a provider — a part that silently\n * rendered nothing would be far harder to diagnose than a named error.\n */\nexport function useFileViewer(): FileViewerContextValue {\n const value = use(FileViewerContext);\n if (!value) {\n throw new Error(\"useFileViewer must be used inside a <FileViewerProvider> (or <FileViewer>).\");\n }\n return value;\n}\n","\"use client\";\n\n/**\n * `FileViewerPager` — previous / a page you can type into / next.\n *\n * A PART, not adapter chrome. The page lives in the provider (ADR 0026), so this\n * row can sit in the viewer's own toolbar, in an app's page header, or beside a\n * thumbnail rail, and every copy of it stays in step. While the page was\n * `useState` inside `PdfRenderer` there could only ever be one pager, inside the\n * canvas.\n *\n * ## Why this is not a `role=\"toolbar\"`\n *\n * The role promises roving-tabindex arrow-key navigation, and the page field is\n * a TEXT INPUT — ArrowLeft/ArrowRight there move the caret. A toolbar that\n * swallowed them would make the number unusable to a keyboard reader, so this is\n * a plain named group of ordinary tab stops: the same call `FileViewerToolbar`\n * and `ViewToolbar` make, for the same reason.\n */\n\nimport { cn, IconButton, Input, useLocale } from \"@elabs-ai/components-ui\";\nimport { ChevronLeftIcon, ChevronRightIcon } from \"lucide-react\";\nimport { forwardRef, useState, type HTMLAttributes } from \"react\";\n\nimport { useFileViewer } from \"./file-viewer-context\";\n\nexport type FileViewerPagerProps = HTMLAttributes<HTMLDivElement>;\n\nexport const FileViewerPager = forwardRef<HTMLDivElement, FileViewerPagerProps>(\n function FileViewerPager({ className, ...props }, ref) {\n const { state, actions } = useFileViewer();\n const { t, formatNumber } = useLocale();\n\n // The half-typed value, while the reader is typing it. `null` means \"not\n // editing\", so the field follows the document the rest of the time — a\n // citation that turns the page updates the number under the caret too.\n const [draft, setDraft] = useState<string | null>(null);\n\n const { pageNumber, pageCount } = state;\n\n // No pages, or none known yet: render nothing rather than an inert \"1 of 0\".\n // Chrome with nothing in it reads as a broken render (viewer-components.md).\n if (!state.capabilities.pages || pageCount === 0) return null;\n\n const commit = () => {\n if (draft === null) return;\n const parsed = Number.parseInt(draft, 10);\n setDraft(null);\n // A blank or nonsense entry is a reader changing their mind, not a\n // navigation — snap back to where they are instead of jumping to page 1.\n if (Number.isNaN(parsed)) return;\n actions.goToPage(parsed);\n };\n\n return (\n <div\n ref={ref}\n data-slot=\"file-viewer-pager\"\n role=\"group\"\n aria-label={t(\"viewer.pager.controls\")}\n className={cn(\"flex shrink-0 items-center gap-1\", className)}\n {...props}\n >\n <IconButton\n variant=\"ghost\"\n size=\"icon-sm\"\n label={t(\"viewer.pager.previous\")}\n icon={<ChevronLeftIcon aria-hidden=\"true\" />}\n disabled={pageNumber <= 1}\n onClick={actions.previousPage}\n />\n <Input\n // Explicit: Radix's toolbar slot and several wrappers default an\n // unset `type` to `\"button\"`, which would silently turn the field\n // into a button the day this moves inside one.\n type=\"text\"\n inputMode=\"numeric\"\n autoComplete=\"off\"\n spellCheck={false}\n aria-label={t(\"viewer.pager.pageNumber\")}\n value={draft ?? String(pageNumber)}\n onChange={(event) => setDraft(event.target.value)}\n onBlur={commit}\n onKeyDown={(event) => {\n if (event.key === \"Enter\") {\n event.preventDefault();\n commit();\n } else if (event.key === \"Escape\") {\n // Abandon the edit without navigating; focus stays put, so the\n // reader can try again without re-reaching the field.\n setDraft(null);\n }\n }}\n className=\"h-7 w-12 px-1 text-center tabular-nums\"\n />\n <span className=\"text-meta text-muted-foreground whitespace-nowrap tabular-nums\">\n {t(\"viewer.pager.of\", { total: formatNumber(pageCount) })}\n </span>\n <IconButton\n variant=\"ghost\"\n size=\"icon-sm\"\n label={t(\"viewer.pager.next\")}\n icon={<ChevronRightIcon aria-hidden=\"true\" />}\n disabled={pageNumber >= pageCount}\n onClick={actions.nextPage}\n />\n {/* Paging repaints a canvas, which announces nothing on its own, and the\n field's own value change is not announced either. ONE live region for\n the group carries the whole sentence — `loading-states.md`'s rule that\n a region announces once, not per element. */}\n <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {t(\"viewer.pager.status\", {\n page: formatNumber(pageNumber),\n total: formatNumber(pageCount),\n })}\n </span>\n </div>\n );\n },\n);\n","\"use client\";\n\n/**\n * `FileViewerZoom` and `FileViewerRotate` — how big, and which way up.\n *\n * Parts over the provider's view state (ADR 0026), so an app can put the scale\n * control in its own header without reaching inside the canvas.\n *\n * Each renders NOTHING for a format whose manifest does not claim the capability\n * — an inert zoom control over a CSV is chrome with nothing in it, which reads\n * as a broken render (`viewer-components.md`). That absence is also the fix for\n * the `image` manifest's old lie: it declared `zoom` and `rotate` while its\n * renderer implemented neither, so the claim was invisible either way.\n */\n\nimport {\n cn,\n IconButton,\n Select,\n SelectContent,\n SelectItem,\n SelectSeparator,\n SelectTrigger,\n SelectValue,\n useLocale,\n} from \"@elabs-ai/components-ui\";\nimport { RotateCwIcon, ZoomInIcon, ZoomOutIcon } from \"lucide-react\";\nimport { forwardRef, type HTMLAttributes } from \"react\";\n\nimport type { ZoomLevel } from \"../core/types\";\nimport { canStepZoom, isZoomFit, VIEWER_ZOOM_STEPS } from \"../core/zoom\";\nimport { useFileViewer } from \"./file-viewer-context\";\n\nexport type FileViewerZoomProps = HTMLAttributes<HTMLDivElement>;\n\n/**\n * Zoom out · the current level · zoom in.\n *\n * The middle control is a `Select` rather than a read-out because the stops are\n * the API: a reader who wants 200% should not have to press \"+\" four times, and\n * the two fit modes have no number to press towards at all.\n *\n * Not a `role=\"toolbar\"` — see `FileViewerPager`. It sits in the same row, and\n * one row that is half roving-tabindex and half ordinary tab stops is worse for\n * a keyboard reader than one that is consistently ordinary.\n */\nexport const FileViewerZoom = forwardRef<HTMLDivElement, FileViewerZoomProps>(\n function FileViewerZoom({ className, ...props }, ref) {\n const { state, actions } = useFileViewer();\n const { t, formatNumber } = useLocale();\n\n if (!state.capabilities.zoom) return null;\n\n const { zoom, effectiveZoom } = state;\n const percent = (scale: number) =>\n formatNumber(scale, { style: \"percent\", maximumFractionDigits: 0 });\n\n return (\n <div\n ref={ref}\n data-slot=\"file-viewer-zoom\"\n role=\"group\"\n aria-label={t(\"viewer.zoom.controls\")}\n className={cn(\"flex shrink-0 items-center gap-1\", className)}\n {...props}\n >\n <IconButton\n variant=\"ghost\"\n size=\"icon-sm\"\n label={t(\"viewer.zoom.out\")}\n icon={<ZoomOutIcon aria-hidden=\"true\" />}\n // Stepping is measured against what is ON SCREEN, so a page fitted to\n // 137% still has somewhere to go in both directions.\n disabled={!canStepZoom(effectiveZoom, -1)}\n onClick={actions.zoomOut}\n />\n <Select\n value={zoomToValue(zoom)}\n onValueChange={(value) => actions.setZoom(valueToZoom(value))}\n >\n <SelectTrigger\n size=\"sm\"\n aria-label={t(\"viewer.zoom.level\")}\n className=\"h-7 w-24 gap-1 px-2\"\n >\n {/* Children override the selected item's own text: a fit mode has to\n read as \"Fit width\", but a fixed stop reads better as the number\n than as a row label, and both have to fit one narrow trigger. */}\n <SelectValue>\n {isZoomFit(zoom)\n ? t(zoom === \"fit-width\" ? \"viewer.zoom.fitWidth\" : \"viewer.zoom.fitPage\")\n : percent(zoom)}\n </SelectValue>\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"fit-width\">{t(\"viewer.zoom.fitWidth\")}</SelectItem>\n <SelectItem value=\"fit-page\">{t(\"viewer.zoom.fitPage\")}</SelectItem>\n <SelectSeparator />\n {VIEWER_ZOOM_STEPS.map((step) => (\n <SelectItem key={step} value={String(step)}>\n {percent(step)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <IconButton\n variant=\"ghost\"\n size=\"icon-sm\"\n label={t(\"viewer.zoom.in\")}\n icon={<ZoomInIcon aria-hidden=\"true\" />}\n disabled={!canStepZoom(effectiveZoom, 1)}\n onClick={actions.zoomIn}\n />\n {/* One live region for the group: neither a repainted canvas nor a\n `Select`'s own value change announces the new scale. */}\n <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {t(\"viewer.zoom.status\", { level: percent(effectiveZoom) })}\n </span>\n </div>\n );\n },\n);\n\n/** `Select` speaks strings; the state is a number or a fit mode. */\nfunction zoomToValue(zoom: ZoomLevel): string {\n return isZoomFit(zoom) ? zoom : String(zoom);\n}\n\nfunction valueToZoom(value: string): ZoomLevel {\n if (value === \"fit-width\" || value === \"fit-page\") return value;\n const parsed = Number.parseFloat(value);\n return Number.isNaN(parsed) ? 1 : parsed;\n}\n\nexport type FileViewerRotateProps = HTMLAttributes<HTMLButtonElement>;\n\n/**\n * One button, one quarter-turn clockwise.\n *\n * Document-level and clockwise-only on purpose: a counter-clockwise button is a\n * second control for something three presses of this one already do, and\n * per-page rotation would change the file, which is authoring rather than\n * viewing. `actions.rotate(-1)` is there for an app that wants the other one.\n */\nexport const FileViewerRotate = forwardRef<HTMLButtonElement, FileViewerRotateProps>(\n function FileViewerRotate({ className, ...props }, ref) {\n const { state, actions } = useFileViewer();\n const { t } = useLocale();\n\n if (!state.capabilities.rotate) return null;\n\n return (\n <IconButton\n ref={ref}\n data-slot=\"file-viewer-rotate\"\n variant=\"ghost\"\n size=\"icon-sm\"\n label={t(\"viewer.rotate\")}\n icon={<RotateCwIcon aria-hidden=\"true\" />}\n className={cn(\"shrink-0\", className)}\n onClick={() => actions.rotate(1)}\n {...props}\n />\n );\n },\n);\n"],"mappings":"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","names":["cn","IconButton","resolveFileKind","Separator","useLocale","forwardRef","useEffect","useRef","useState","FileViewerFind","cn","IconButton","useLocale","forwardRef","jsx","jsxs","forwardRef","FileViewerPager","useLocale","cn","IconButton","cn","IconButton","useLocale","forwardRef","jsx","jsxs","forwardRef","FileViewerZoom","useLocale","cn","IconButton","FileViewerRotate","jsx","jsxs","useState","useEffect","resolveFileKind","useRef","forwardRef","FileViewerFrame","useLocale","cn","FileViewerToolbar","Separator","IconButton","FileViewerHighlightStatus","FileViewerSkeleton","FileViewerError","FileViewerEmpty","FileViewerContent","FileViewer"]}
1
+ {"version":3,"sources":["../src/file-viewer/file-viewer.tsx","../src/core/registry.ts","../src/adapters/index.ts","../src/core/highlight.ts","../src/core/highlight-resolve.ts","../src/file-viewer/file-viewer-find.tsx","../src/file-viewer/file-viewer-context.tsx","../src/file-viewer/file-viewer-pager.tsx","../src/file-viewer/file-viewer-zoom.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * `FileViewer` — the compound shell.\n *\n * Every part is composed from `@elabs-ai/components-ui` primitives;\n * this package contributes file LOGIC (detection, parsing, the page model) and\n * never a parallel widget set. There is no `showToolbar` boolean: compose the\n * parts you want, or render `<FileViewer>` for the batteries-included default.\n *\n * Surface separation (`styling-and-tokens.md`): the frame is one raised `card`\n * with a border, the toolbar is divided from the content by a single `border-b`\n * — the sole structural cue between two same-fill regions, so it takes the\n * strong rung.\n */\n\nimport {\n Button,\n cn,\n downloadBlob,\n fileIconFor,\n IconButton,\n normalizeFileSource,\n normalizeQuoteTextWithOffsets,\n queryToRanges,\n resolveFileKind,\n Separator,\n Skeleton,\n StatePanel,\n Text,\n useLocale,\n type FileSource,\n type ProseHeadingLevel,\n} from \"@elabs-ai/components-ui\";\nimport { DownloadIcon, EyeOffIcon, SearchXIcon } from \"lucide-react\";\nimport {\n forwardRef,\n useCallback,\n useDeferredValue,\n useEffect,\n useMemo,\n useRef,\n useState,\n type HTMLAttributes,\n type KeyboardEvent as ReactKeyboardEvent,\n type ReactNode,\n} from \"react\";\n\nimport { createDefaultRegistry } from \"../adapters\";\nimport {\n ViewerError,\n isAbort,\n isModuleNotFound,\n parserMissingError,\n toViewerError,\n type ViewerErrorCode,\n} from \"../core/errors\";\nimport {\n FIND_MATCH_LIMIT,\n findMatchId,\n type DocumentHighlight,\n type HighlightSupport,\n type ResolvedHighlight,\n} from \"../core/highlight\";\nimport { resolveHighlights } from \"../core/highlight-resolve\";\nimport type { ViewerRegistry } from \"../core/registry\";\nimport type { DocumentRotation, ZoomLevel } from \"../core/types\";\nimport { DEFAULT_ZOOM, stepZoom } from \"../core/zoom\";\nimport { FileViewerFind, isFindShortcut } from \"./file-viewer-find\";\nimport { FileViewerPager } from \"./file-viewer-pager\";\nimport { FileViewerRotate, FileViewerZoom } from \"./file-viewer-zoom\";\nimport {\n FileViewerContext,\n useFileViewer,\n type FileViewerContextValue,\n type FileViewerFindState,\n type FileViewerLoadState,\n} from \"./file-viewer-context\";\n\n/** Stable empties, so a provider with no citations does not re-render on identity. */\nconst NO_HIGHLIGHTS: readonly DocumentHighlight[] = [];\nconst NO_RESOLVED: readonly ResolvedHighlight[] = [];\nconst NO_SUPPORT: HighlightSupport = [];\n\n/* -------------------------------------------------------------------------- */\n/* Provider */\n/* -------------------------------------------------------------------------- */\n\nexport interface FileViewerProviderProps {\n /** The file to show. `undefined` is the empty state, not an error. */\n source?: FileSource;\n /**\n * Adapters available to this viewer. Defaults to the built-ins.\n * Pass your own to add a format, drop one, or override a built-in.\n */\n registry?: ViewerRegistry;\n /**\n * Force the not-ready state while a parent fetches the source itself.\n * ORed with the viewer's own loading — a parent can add loading, never remove it.\n */\n loading?: boolean;\n /**\n * The rung a viewed document's own top-level heading renders at. Default `2`.\n *\n * A file carries its OWN heading tree, and that tree is only correct relative\n * to the page hosting it: a README's `#` rendered as an `<h1>` inside an app\n * that already has one puts two `h1`s in a screen reader's flat heading list,\n * and the frame's `<section aria-label>` does not fix that — most screen\n * readers list headings flat, not per landmark. The default assumes the\n * common case, a viewer embedded BELOW the page's own heading; pass `1` when\n * the viewer genuinely is the page.\n *\n * Adapters that render no headings ignore it. Same seam as\n * `@elabs-ai/components-ai`'s `MarkdownView baseHeadingLevel`.\n */\n baseHeadingLevel?: ProseHeadingLevel;\n /**\n * The parts of the document to point at — an answer's citations, a search\n * result's context, anything the app already knows about the file.\n *\n * A PROP rather than provider-only state because citations originate outside\n * the viewer entirely: the chat pane that produced them usually lives in\n * another route, and it owns which one the reader clicked. Pass\n * `defaultHighlights` instead to let the viewer own them.\n */\n highlights?: readonly DocumentHighlight[];\n /** Uncontrolled initial citations. Ignored when `highlights` is supplied. */\n defaultHighlights?: readonly DocumentHighlight[];\n onHighlightsChange?: (highlights: readonly DocumentHighlight[]) => void;\n /**\n * Which citation the viewer is pointed at. `null` is \"none\" explicitly;\n * `undefined` is what selects uncontrolled mode, so the two are not\n * interchangeable here.\n */\n activeHighlightId?: string | null;\n defaultActiveHighlightId?: string | null;\n onActiveHighlightChange?: (id: string | null) => void;\n /**\n * Which page the viewer is on, 1-based — controlled.\n *\n * A trio (`component-api.md`) rather than provider-only state because the page\n * is routinely something the APP owns: a deep link to page 7, a URL the reader\n * can share, a position restored from a \"continue reading\" record. Clamped to\n * the document on read, so an out-of-range value degrades to the nearest real\n * page instead of blanking the canvas.\n */\n pageNumber?: number;\n /** Uncontrolled initial page. Ignored when `pageNumber` is supplied. */\n defaultPageNumber?: number;\n onPageNumberChange?: (page: number) => void;\n /**\n * The scale to draw at, or a fit mode — controlled. Persisting a reader's\n * preferred zoom across files and sessions is the reason this is a prop.\n */\n zoom?: ZoomLevel;\n /**\n * Uncontrolled initial zoom. Ignored when `zoom` is supplied.\n *\n * Defaults to `\"fit-width\"`, not `1`: a viewer's job on open is to show the\n * document, and a 4000px scan or an A4 page at 100% in a 600px pane shows its\n * top-left corner. Every reader-facing PDF viewer opens fitted for the same\n * reason. Pass `1` for true 100%.\n */\n defaultZoom?: ZoomLevel;\n onZoomChange?: (zoom: ZoomLevel) => void;\n /** Quarter-turns clockwise — controlled. */\n rotation?: DocumentRotation;\n /** Uncontrolled initial rotation. Ignored when `rotation` is supplied. Default `0`. */\n defaultRotation?: DocumentRotation;\n onRotationChange?: (rotation: DocumentRotation) => void;\n children: ReactNode;\n}\n\n/** Matches for the current query, plus whether the cap swallowed any. */\ninterface FindMatches {\n highlights: readonly DocumentHighlight[];\n total: number;\n truncated: boolean;\n}\n\nconst NO_MATCHES: FindMatches = { highlights: NO_HIGHLIGHTS, total: 0, truncated: false };\n\nexport function FileViewerProvider({\n source,\n registry: registryProp,\n loading = false,\n baseHeadingLevel = 2,\n highlights: highlightsProp,\n defaultHighlights,\n onHighlightsChange,\n activeHighlightId: activeHighlightIdProp,\n defaultActiveHighlightId = null,\n onActiveHighlightChange,\n pageNumber: pageNumberProp,\n defaultPageNumber = 1,\n onPageNumberChange,\n zoom: zoomProp,\n defaultZoom = \"fit-width\",\n onZoomChange,\n rotation: rotationProp,\n defaultRotation = 0,\n onRotationChange,\n children,\n}: FileViewerProviderProps) {\n // A default registry per provider, not per module: one screen's `register()`\n // override must not leak into another's.\n const fallbackRegistry = useMemo(() => createDefaultRegistry(), []);\n const registry = registryProp ?? fallbackRegistry;\n\n const [attempt, setAttempt] = useState(0);\n const [state, setState] = useState<FileViewerLoadState>({ status: \"empty\", capabilities: {} });\n\n const resolved = useMemo(() => (source ? normalizeFileSource(source) : undefined), [source]);\n\n useEffect(() => {\n if (!resolved) {\n setState({ status: \"empty\", capabilities: {} });\n return;\n }\n\n const controller = new AbortController();\n let instance: { dispose?: () => void } | undefined;\n let cancelled = false;\n\n const kind = resolveFileKind(resolved.name, resolved.mediaType);\n const manifest = registry.detect(kind);\n\n // Capabilities come from the eager manifest, so the chrome is correct from\n // the first frame — before a single byte of parser is fetched.\n setState({\n status: \"loading\",\n source: resolved,\n capabilities: manifest?.capabilities ?? {},\n });\n\n void (async () => {\n try {\n if (!manifest) {\n throw new ViewerError(\"unsupported-format\", `No adapter can open \"${resolved.name}\".`, {\n fileName: resolved.name,\n });\n }\n const adapter = await registry.load(manifest.id);\n // A fresh instance per document — adapters may hold per-document state.\n const parser = adapter.create();\n instance = parser;\n const document = await parser.load(resolved, { signal: controller.signal });\n if (cancelled) return;\n setState({\n status: \"ready\",\n source: resolved,\n document,\n adapter,\n capabilities: manifest.capabilities ?? {},\n });\n } catch (error) {\n // A cancelled load is a superseded one — reporting it would flash an\n // error every time the user picks a different file.\n if (cancelled || isAbort(error)) return;\n // Every parser engine is reached by a dynamic `import()` INSIDE the\n // adapter's own `load()`, so \"the optional peer is not installed\" lands\n // here, not on `registry.load()` above. Falling through to `parse-failed`\n // would tell the reader their file is damaged and offer a retry that can\n // never succeed.\n const missingPeer =\n isModuleNotFound(error) && manifest\n ? parserMissingError(manifest.id, manifest.requires ?? [], {\n fileName: resolved.name,\n cause: error,\n })\n : undefined;\n setState({\n status: \"error\",\n source: resolved,\n capabilities: manifest?.capabilities ?? {},\n error: missingPeer ?? toViewerError(error, \"parse-failed\", { fileName: resolved.name }),\n });\n }\n })();\n\n return () => {\n cancelled = true;\n controller.abort();\n instance?.dispose?.();\n // Releases any object URL the source minted for this load.\n resolved.revoke();\n };\n }, [resolved, registry, attempt]);\n\n /* ---- Citations: a prop the app may control, or the viewer's own --------- */\n\n const highlightsControlled = highlightsProp !== undefined;\n const [ownHighlights, setOwnHighlights] = useState(defaultHighlights ?? NO_HIGHLIGHTS);\n const highlights = highlightsControlled ? highlightsProp : ownHighlights;\n const setHighlights = useCallback(\n (next: readonly DocumentHighlight[]) => {\n // Mirroring the platform: a controlled value is never written locally, so\n // the component can't drift from the prop that owns it.\n if (!highlightsControlled) setOwnHighlights(next);\n onHighlightsChange?.(next);\n },\n [highlightsControlled, onHighlightsChange],\n );\n\n const activeControlled = activeHighlightIdProp !== undefined;\n const [ownActiveId, setOwnActiveId] = useState<string | null>(defaultActiveHighlightId);\n const activeHighlightId = activeControlled ? activeHighlightIdProp : ownActiveId;\n const setActiveHighlight = useCallback(\n (id: string | null) => {\n if (!activeControlled) setOwnActiveId(id);\n onActiveHighlightChange?.(id);\n },\n [activeControlled, onActiveHighlightChange],\n );\n\n /* ---- View: which page, at what scale, turned which way ------------------ */\n\n const pageCount = state.document?.pageCount ?? 0;\n\n const pageControlled = pageNumberProp !== undefined;\n const [ownPage, setOwnPage] = useState(defaultPageNumber);\n const rawPage = pageControlled ? pageNumberProp : ownPage;\n // Clamped on READ. Writing a clamped value back at a controlled owner would\n // fight it, and a document whose page count shrank (a different file, same\n // provider) must not blank the canvas while the owner catches up.\n const pageNumber =\n pageCount > 0 ? Math.min(Math.max(1, rawPage), pageCount) : Math.max(1, rawPage);\n const goToPage = useCallback(\n (page: number) => {\n const next = Math.max(1, Math.round(page));\n if (!pageControlled) setOwnPage(next);\n onPageNumberChange?.(next);\n },\n [pageControlled, onPageNumberChange],\n );\n\n const zoomControlled = zoomProp !== undefined;\n const [ownZoom, setOwnZoom] = useState<ZoomLevel>(defaultZoom);\n const zoom = zoomControlled ? zoomProp : ownZoom;\n const setZoom = useCallback(\n (next: ZoomLevel) => {\n if (!zoomControlled) setOwnZoom(next);\n onZoomChange?.(next);\n },\n [zoomControlled, onZoomChange],\n );\n\n // What a fit mode actually became. Only the renderer can know it — it is the\n // one measuring its viewport — so this is a report, not a derivation.\n const [reportedZoom, setReportedZoom] = useState(DEFAULT_ZOOM);\n const effectiveZoom = typeof zoom === \"number\" ? zoom : reportedZoom;\n\n const rotationControlled = rotationProp !== undefined;\n const [ownRotation, setOwnRotation] = useState<DocumentRotation>(defaultRotation);\n const rotation = rotationControlled ? rotationProp : ownRotation;\n const setRotation = useCallback(\n (next: DocumentRotation) => {\n if (!rotationControlled) setOwnRotation(next);\n onRotationChange?.(next);\n },\n [rotationControlled, onRotationChange],\n );\n\n // Opening a DIFFERENT file starts at its first page, the right way up. Zoom\n // deliberately survives: a reader who zoomed in is reading at that size, and\n // snapping back to 100% on every file fights them.\n //\n // Compared against the previous source rather than run on mount, so a\n // `defaultPageNumber` (deep link, restored position) is not clobbered by its\n // own first render.\n const previousSource = useRef(resolved);\n useEffect(() => {\n if (previousSource.current === resolved) return;\n previousSource.current = resolved;\n setOwnPage(1);\n setOwnRotation(0);\n }, [resolved]);\n\n /* ---- Find-in-document: entirely the viewer's own ------------------------ */\n\n const [find, setFind] = useState({\n open: false,\n query: \"\",\n caseSensitive: false,\n activeIndex: 0,\n });\n\n // Whether a `FileViewerFind` part is actually composed into this viewer.\n //\n // The frame swallows Ctrl/Cmd+F, and swallowing it without a box to show is\n // strictly worse than not intercepting at all: the reader loses the browser's\n // own find and gets nothing in return. The parts are composable by design, so\n // \"the adapter could paint a match\" is not the same question as \"there is\n // somewhere to type\" — the find part answers the second by registering itself.\n const [findParts, setFindParts] = useState(0);\n const registerFind = useCallback(() => {\n setFindParts((count) => count + 1);\n return () => setFindParts((count) => count - 1);\n }, []);\n\n const text = state.document?.text;\n const capabilities = state.capabilities;\n const support = capabilities.highlight ?? NO_SUPPORT;\n\n // The declared `search` flag is an override in the OFF direction only. Read\n // as the source of truth it would deny the find box to PDF — the format\n // readers expect it on most — purely because that manifest predates the\n // feature. Read as \"can this adapter paint what find produces\" it is right by\n // construction: find emits `range` addresses, so `range` support is the bar.\n const canFind = (capabilities.search ?? true) && text !== undefined && support.includes(\"range\");\n\n // Deferred so a keystroke paints immediately and the (potentially large)\n // match scan lands a frame later, instead of blocking the caret.\n const query = useDeferredValue(find.query);\n const findMatches = useMemo<FindMatches>(() => {\n if (!find.open || !canFind || text === undefined || query.length === 0) return NO_MATCHES;\n // Deliberately NOT run through `normalizeRanges`: it merges ADJACENT\n // ranges, which is right for a fuzzy matcher painting one contiguous mark\n // and wrong here — searching \"l\" in \"hello\" finds two matches, and merging\n // them into one would make the counter disagree with what a reader counts.\n // A single needle's matches never overlap, so there is nothing to merge.\n const ranges = queryToRanges(text, query, find.caseSensitive);\n return {\n highlights: ranges.slice(0, FIND_MATCH_LIMIT).map(([start, end], index) => ({\n id: findMatchId(index),\n address: { kind: \"range\" as const, start, end },\n source: \"search\" as const,\n })),\n total: ranges.length,\n truncated: ranges.length > FIND_MATCH_LIMIT,\n };\n }, [find.open, find.caseSensitive, canFind, text, query]);\n\n /* ---- Locate: the one step that runs outside the adapter ----------------- */\n\n // Folded once per document, not once per keystroke: find re-resolves on every\n // character, and folding a 2 MB projection each time is what turns a search\n // box into a stutter.\n const normalized = useMemo(\n () => (text === undefined ? undefined : normalizeQuoteTextWithOffsets(text)),\n [text],\n );\n\n // Two things can claim to be \"the current one\": a citation the app pointed at,\n // and the match the reader is stepping through. The precedence is stated once,\n // here — while the find box is open and matching, find wins, because it is what\n // the reader's own keystrokes are moving. Leaving both active would paint the\n // reader's match like every other match while a citation kept the active plate.\n const activeFindId =\n find.open && findMatches.highlights.length > 0 ? findMatchId(find.activeIndex) : undefined;\n\n /**\n * The id the RENDERER is pointed at — the effective one, not the citation knob.\n *\n * A renderer that owns a pager or a tab strip navigates on this, and the shared\n * scroll hook fires on it. Handing it `state.activeHighlightId` instead would\n * leave find-in-document unable to scroll to, page to, or switch sheet to its\n * own match whenever no citation happened to be active.\n */\n const currentHighlightId = activeFindId ?? activeHighlightId;\n\n const resolvedHighlights = useMemo<readonly ResolvedHighlight[]>(() => {\n if (highlights.length === 0 && findMatches.highlights.length === 0) return NO_RESOLVED;\n return resolveHighlights([...highlights, ...findMatches.highlights], {\n normalized,\n textLength: text?.length,\n truncated: state.document?.textTruncated,\n supported: support,\n activeId: currentHighlightId,\n });\n }, [\n highlights,\n findMatches.highlights,\n normalized,\n text?.length,\n state.document?.textTruncated,\n support,\n currentHighlightId,\n ]);\n\n /* ---- Stepping ---------------------------------------------------------- */\n\n const stepCitation = useCallback(\n (delta: 1 | -1) => {\n const list = resolvedHighlights.filter(\n (highlight) => highlight.source === \"citation\" && highlight.status === \"resolved\",\n );\n if (list.length === 0) return;\n const at = list.findIndex((highlight) => highlight.id === activeHighlightId);\n // Nothing active yet: \"next\" starts at the top of the document and\n // \"previous\" at the bottom, rather than both landing on the first.\n const to =\n at === -1 ? (delta === 1 ? 0 : list.length - 1) : (at + delta + list.length) % list.length;\n setActiveHighlight((list[to] as ResolvedHighlight).id);\n },\n [resolvedHighlights, activeHighlightId, setActiveHighlight],\n );\n\n const stepFind = useCallback(\n (delta: 1 | -1) => {\n const count = findMatches.highlights.length;\n if (count === 0) return;\n setFind((current) => ({\n ...current,\n activeIndex: (current.activeIndex + delta + count) % count,\n }));\n },\n [findMatches.highlights.length],\n );\n\n const findState = useMemo<FileViewerFindState>(\n () => ({\n open: find.open,\n query: find.query,\n caseSensitive: find.caseSensitive,\n matches: findMatches.total,\n truncated: findMatches.truncated,\n activeIndex: find.activeIndex,\n }),\n [find, findMatches.total, findMatches.truncated],\n );\n\n const actions = useMemo(\n () => ({\n reload: () => setAttempt((n) => n + 1),\n setHighlights,\n setActiveHighlight,\n nextHighlight: () => stepCitation(1),\n previousHighlight: () => stepCitation(-1),\n openFind: () => setFind((current) => ({ ...current, open: true })),\n // The query survives a close, so re-opening resumes where the reader was;\n // only the box goes away.\n closeFind: () => setFind((current) => ({ ...current, open: false })),\n setFindQuery: (next: string) =>\n setFind((current) => ({ ...current, query: next, activeIndex: 0 })),\n setFindCaseSensitive: (next: boolean) =>\n setFind((current) => ({ ...current, caseSensitive: next, activeIndex: 0 })),\n nextFindMatch: () => stepFind(1),\n previousFindMatch: () => stepFind(-1),\n goToPage,\n // Stop at the ends rather than wrap: a document is not a carousel, and a\n // reader who holds \"next\" past the last page expects to stay there.\n nextPage: () => goToPage(Math.min(pageNumber + 1, pageCount || pageNumber + 1)),\n previousPage: () => goToPage(pageNumber - 1),\n setZoom,\n // Stepping from `effectiveZoom`, not from `zoom`: after a fit the ladder\n // has to continue from what is actually on screen.\n zoomIn: () => setZoom(stepZoom(effectiveZoom, 1)),\n zoomOut: () => setZoom(stepZoom(effectiveZoom, -1)),\n setRotation,\n rotate: (quarterTurns: 1 | -1) =>\n setRotation(((((rotation + quarterTurns * 90) % 360) + 360) % 360) as DocumentRotation),\n reportZoom: setReportedZoom,\n registerFind,\n }),\n [\n setHighlights,\n setActiveHighlight,\n stepCitation,\n stepFind,\n registerFind,\n goToPage,\n pageNumber,\n pageCount,\n setZoom,\n effectiveZoom,\n setRotation,\n rotation,\n ],\n );\n\n const value = useMemo<FileViewerContextValue>(\n () => ({\n state: {\n // A parent's `loading` can add the not-ready state but never clear a\n // real error — losing a failure to a stale prop is worse than a late\n // spinner.\n ...(loading && state.status !== \"error\" ? { ...state, status: \"loading\" as const } : state),\n highlights,\n activeHighlightId,\n find: findState,\n pageNumber,\n pageCount,\n zoom,\n effectiveZoom,\n rotation,\n },\n actions,\n registry,\n meta: {\n baseHeadingLevel,\n resolvedHighlights,\n currentHighlightId,\n highlightSupport: support,\n canFind,\n hasFind: findParts > 0,\n },\n }),\n [\n state,\n loading,\n registry,\n baseHeadingLevel,\n highlights,\n activeHighlightId,\n findState,\n actions,\n resolvedHighlights,\n currentHighlightId,\n support,\n canFind,\n findParts,\n pageNumber,\n pageCount,\n zoom,\n effectiveZoom,\n rotation,\n ],\n );\n\n return <FileViewerContext value={value}>{children}</FileViewerContext>;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Frame */\n/* -------------------------------------------------------------------------- */\n\nexport type FileViewerFrameProps = HTMLAttributes<HTMLDivElement>;\n\n/**\n * The bordered surface the toolbar and content sit in, and the scope of the\n * viewer's find shortcut.\n *\n * Ctrl/Cmd+F is handled HERE rather than on `document`: a page may hold several\n * viewers, or a viewer beside an editor that has its own find, and a\n * document-level listener would let whichever mounted last win. Bound to the\n * frame, the shortcut belongs to whichever viewer the reader is actually inside,\n * and the browser's own find is untouched everywhere else on the page.\n */\nexport const FileViewerFrame = forwardRef<HTMLDivElement, FileViewerFrameProps>(\n function FileViewerFrame({ className, children, onKeyDown, ...props }, ref) {\n const { state, actions, meta } = useFileViewer();\n const { t } = useLocale();\n const frame = useRef<HTMLElement | null>(null);\n const wasOpen = useRef(false);\n\n // Closing the box must hand the caret back, or a keyboard reader is left\n // with focus on nothing at the top of the document.\n const findOpen = state.find.open && meta.canFind;\n useEffect(() => {\n if (wasOpen.current && !findOpen) {\n frame.current\n ?.querySelector<HTMLElement>('[data-slot=\"file-viewer-content\"]')\n ?.focus({ preventScroll: true });\n }\n wasOpen.current = findOpen;\n }, [findOpen]);\n\n return (\n <section\n ref={(node) => {\n frame.current = node;\n if (typeof ref === \"function\") ref(node as HTMLDivElement | null);\n else if (ref) ref.current = node as HTMLDivElement | null;\n }}\n data-slot=\"file-viewer\"\n aria-label={t(\"viewer.label\")}\n className={cn(\n \"bg-card text-card-foreground border-border flex h-full min-h-0 flex-col overflow-hidden rounded-lg border shadow-sm\",\n className,\n )}\n onKeyDown={(event) => {\n // The frame renders a `<section>` while its public props type says\n // `HTMLDivElement` — a pre-existing signature. One cast here beats\n // widening the exported ref type and breaking every caller's\n // `useRef<HTMLDivElement>`.\n onKeyDown?.(event as ReactKeyboardEvent<HTMLDivElement>);\n // Only intercept the browser's shortcut when this viewer can actually\n // honour it — otherwise the reader loses their browser find and gets\n // nothing back. Both halves are needed: an adapter that can paint a\n // match (`canFind`) AND a find part composed in to type into\n // (`hasFind`). A hand-composed frame that omits the part keeps the\n // browser's own find, which is the right answer for it.\n if (!event.defaultPrevented && meta.canFind && meta.hasFind && isFindShortcut(event)) {\n event.preventDefault();\n actions.openFind();\n }\n }}\n {...props}\n >\n {children}\n </section>\n );\n },\n);\n\n/* -------------------------------------------------------------------------- */\n/* Toolbar */\n/* -------------------------------------------------------------------------- */\n\nexport interface FileViewerToolbarProps extends HTMLAttributes<HTMLDivElement> {\n /** Extra controls, placed after the built-in actions. */\n actions?: ReactNode;\n}\n\n/**\n * The identity row: glyph, name, actions.\n *\n * No `role=\"toolbar\"` — that role promises roving-tabindex arrow-key navigation,\n * which this row does not implement. The same decision `ViewToolbar` made, and\n * the reason the P1 `Toolbar` primitive exists (ADR 0024 §5a).\n *\n * With no file it renders NOTHING: a row holding a generic glyph and a blank\n * name reads as a broken render, not as chrome. A screen that needs a permanent\n * header composes its own row around `FileViewerFrame` — that is what the parts\n * are for.\n */\nexport const FileViewerToolbar = forwardRef<HTMLDivElement, FileViewerToolbarProps>(\n function FileViewerToolbar({ className, actions, children, ...props }, ref) {\n const { state } = useFileViewer();\n const { t } = useLocale();\n const source = state.source;\n const Glyph = fileIconFor(source?.name ?? \"\", source?.mediaType);\n\n if (!source) return null;\n\n const download = () => {\n void source.bytes().then((bytes) => {\n downloadBlob(new Blob([bytes], { type: source.mediaType }), source.name);\n });\n };\n\n return (\n <div\n ref={ref}\n data-slot=\"file-viewer-toolbar\"\n className={cn(\n // The divider is the ONLY cue between toolbar and content (same fill,\n // no elevation change) — WCAG 1.4.11, so the strong rung.\n \"border-border-strong flex shrink-0 items-center gap-2 border-b px-3 py-2\",\n className,\n )}\n {...props}\n >\n <Glyph aria-hidden=\"true\" className=\"text-muted-foreground size-4 shrink-0\" />\n {/* min-w-0 is what actually lets the name truncate inside a flex row. */}\n <Text className=\"min-w-0 flex-1 truncate\" title={source.name}>\n {source.name}\n </Text>\n {children}\n {actions}\n <Separator orientation=\"vertical\" className=\"h-4\" />\n {/* `label` is IconButton's single source of truth — it becomes both the\n accessible name and the tooltip, so the two cannot drift. */}\n <IconButton\n variant=\"ghost\"\n label={t(\"viewer.download\", { name: source.name })}\n icon={<DownloadIcon aria-hidden=\"true\" />}\n onClick={download}\n />\n </div>\n );\n },\n);\n\n/* -------------------------------------------------------------------------- */\n/* Highlight status */\n/* -------------------------------------------------------------------------- */\n\nexport type FileViewerHighlightStatusProps = HTMLAttributes<HTMLDivElement>;\n\n/**\n * \"We couldn't find that passage.\"\n *\n * A citation that fails to locate must not fail SILENTLY: the reader clicked a\n * source link and got a document that looks untouched, with no way to tell\n * whether the viewer is broken, the passage moved, or they mis-clicked. The\n * request survives resolution precisely so this line has something to say.\n *\n * Three different pieces of news, three sentences:\n * - `not-found` / `absent` — searched the whole projection; it is not in it.\n * - `not-found` / `truncated` — the projection is capped, so it may lie past it.\n * - `unsupported` — a CAPABILITY GAP: this build cannot point at part of that\n * format. Not the reader's fault and not retryable, same call the error panel\n * already makes for `unsupported-format`.\n *\n * Search misses are excluded: the find bar already counts its own matches, and\n * \"No matches\" there says it better than a second line here would.\n */\nexport const FileViewerHighlightStatus = forwardRef<HTMLDivElement, FileViewerHighlightStatusProps>(\n function FileViewerHighlightStatus({ className, ...props }, ref) {\n const { state, meta } = useFileViewer();\n const { t } = useLocale();\n\n const missed = meta.resolvedHighlights.filter(\n (highlight) =>\n highlight.source === \"citation\" &&\n (highlight.status === \"not-found\" || highlight.status === \"unsupported\"),\n );\n if (missed.length === 0) return null;\n\n const first = missed[0] as ResolvedHighlight;\n const message =\n first.status === \"unsupported\"\n ? t(\"viewer.highlight.unsupported\", {\n format: state.source?.extension.toUpperCase() || \"\",\n })\n : first.reason === \"truncated\"\n ? t(\"viewer.highlight.notFoundTruncated\")\n : t(\"viewer.highlight.notFound\");\n\n return (\n <div\n ref={ref}\n data-slot=\"file-viewer-highlight-status\"\n role=\"status\"\n aria-live=\"polite\"\n className={cn(\n // Information, not a failure — a neutral muted row, never the\n // destructive tone. The divider is the sole cue between it and the\n // content below (WCAG 1.4.11, strong rung).\n \"border-border-strong text-muted-foreground flex shrink-0 items-center gap-2 border-b px-3 py-2\",\n className,\n )}\n {...props}\n >\n <SearchXIcon aria-hidden=\"true\" className=\"size-4 shrink-0\" />\n <span className=\"text-meta min-w-0 flex-1 truncate\">{message}</span>\n </div>\n );\n },\n);\n\n/* -------------------------------------------------------------------------- */\n/* States */\n/* -------------------------------------------------------------------------- */\n\n/**\n * A layout-shaped skeleton, not a spinner: it occupies the box the real content\n * will, so nothing shifts when the file arrives (`loading-states.md`).\n * `aria-hidden` because `Skeleton` is decorative — the single live region on the\n * wrapper is what AT hears.\n */\nexport const FileViewerSkeleton = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(\n function FileViewerSkeleton({ className, ...props }, ref) {\n const { state } = useFileViewer();\n const { t } = useLocale();\n return (\n <div\n ref={ref}\n data-slot=\"file-viewer-skeleton\"\n role=\"status\"\n aria-live=\"polite\"\n className={cn(\"flex h-full flex-col gap-3 p-4\", className)}\n {...props}\n >\n <span className=\"sr-only\">{t(\"viewer.loading\", { name: state.source?.name ?? \"\" })}</span>\n <Skeleton aria-hidden=\"true\" className=\"h-4 w-2/5\" />\n <Skeleton aria-hidden=\"true\" className=\"h-4 w-4/5\" />\n <Skeleton aria-hidden=\"true\" className=\"h-4 w-3/5\" />\n <Skeleton aria-hidden=\"true\" className=\"min-h-24 flex-1\" />\n </div>\n );\n },\n);\n\n/** Message keys per failure code — the code is the contract, the prose is not. */\nconst ERROR_MESSAGES: Record<ViewerErrorCode, { title: string; body: string }> = {\n \"unsupported-format\": {\n title: \"viewer.error.unsupportedFormat\",\n body: \"viewer.error.unsupportedFormatBody\",\n },\n \"parser-missing\": {\n title: \"viewer.error.parserMissing\",\n body: \"viewer.error.parserMissingBody\",\n },\n \"read-failed\": { title: \"viewer.error.readFailed\", body: \"viewer.error.readFailedBody\" },\n \"parse-failed\": { title: \"viewer.error.parseFailed\", body: \"viewer.error.parseFailedBody\" },\n // Neither should ever reach the UI: a protocol mismatch throws at registration\n // and an abort is swallowed as a superseded load. Mapped anyway so the panel\n // can never render an empty title.\n \"protocol-mismatch\": { title: \"viewer.error.parseFailed\", body: \"viewer.error.parseFailedBody\" },\n aborted: { title: \"viewer.error.readFailed\", body: \"viewer.error.readFailedBody\" },\n};\n\n/**\n * A gap in what this build can SHOW — the file is fine, we just cannot draw it.\n * Neither is retryable, and neither is the user's fault, so both are presented\n * as information rather than as a failure (see `FileViewerError`).\n */\nconst CAPABILITY_GAPS = new Set<ViewerErrorCode>([\"unsupported-format\", \"parser-missing\"]);\n\nexport const FileViewerError = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(\n function FileViewerError({ className, ...props }, ref) {\n const { state } = useFileViewer();\n const { t } = useLocale();\n const error = state.error;\n if (!error) return null;\n\n const message = ERROR_MESSAGES[error.code];\n const vars = {\n name: state.source?.name ?? \"\",\n format: state.source?.extension.toUpperCase() || t(\"viewer.error.parseFailed\"),\n packages: error.packages?.join(\", \") ?? \"\",\n };\n\n // \"This build can't draw PDFs\" and \"the network dropped\" are different news.\n // A red alarm on the first one blames the reader for a capability we never\n // shipped, so the gap gets a neutral panel and `status`; a real failure keeps\n // the destructive panel and the `alert` StatePanel already sets.\n const isGap = CAPABILITY_GAPS.has(error.code);\n\n return (\n <div\n ref={ref}\n data-slot=\"file-viewer-error\"\n // min-h-full (not h-full) so a tall panel grows and scrolls instead of\n // being clipped at the top by `justify-center`.\n className={cn(\"flex min-h-full flex-col justify-center p-4\", className)}\n {...(isGap ? { role: \"status\" as const, \"aria-live\": \"polite\" as const } : {})}\n {...props}\n >\n <StatePanel\n kind={isGap ? \"empty\" : \"error\"}\n // A dashed edge invites a drop; this panel accepts nothing. Solid.\n className={isGap ? \"border-solid\" : undefined}\n icon={isGap ? <EyeOffIcon aria-hidden=\"true\" /> : undefined}\n title={t(message.title)}\n description={t(message.body, vars)}\n // `unsupported-format` and `parser-missing` are not retryable — the\n // file has not changed and neither has what is installed. Offering\n // \"Try again\" there teaches users the button does nothing.\n actions={\n error.code === \"read-failed\" || error.code === \"parse-failed\" ? (\n <RetryButton />\n ) : undefined\n }\n />\n </div>\n );\n },\n);\n\n/**\n * The real `Button`, not a styled `<button>`.\n *\n * A hand-rolled `text-primary` link measured 4.14:1 against `StatePanel`'s error\n * wash and failed axe — the brand hue is tuned against `--background`/`--card`,\n * not against a tinted panel. `outline` puts the label on ordinary ink over its\n * own surface, which is contrast-safe on any panel and gives the control a\n * visible hit target besides.\n */\nfunction RetryButton() {\n const { actions } = useFileViewer();\n const { t } = useLocale();\n return (\n <Button variant=\"outline\" size=\"sm\" onClick={actions.reload}>\n {t(\"viewer.retry\")}\n </Button>\n );\n}\n\nexport const FileViewerEmpty = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(\n function FileViewerEmpty({ className, ...props }, ref) {\n const { t } = useLocale();\n return (\n <div\n ref={ref}\n data-slot=\"file-viewer-empty\"\n className={cn(\"flex min-h-full flex-col justify-center p-4\", className)}\n {...props}\n >\n <StatePanel kind=\"empty\" title={t(\"viewer.empty\")} description={t(\"viewer.emptyBody\")} />\n </div>\n );\n },\n);\n\n/* -------------------------------------------------------------------------- */\n/* Content */\n/* -------------------------------------------------------------------------- */\n\nexport type FileViewerContentProps = HTMLAttributes<HTMLDivElement>;\n\n/**\n * The state switch: empty · loading · error · the adapter's own renderer.\n *\n * The adapter supplies its `Renderer` alongside its parser, so this component\n * never grows a per-format `switch` — that is what keeps formats additive.\n *\n * **This is THE scroll boundary** for any adapter that does not manage its own\n * viewport. Two nested `overflow-auto` boxes do not compose: the inner one clips\n * while the outer one's padding stays put, so a long document ends flush against\n * a band of whitespace and reads as a failed render rather than as \"scroll for\n * more\". Adapters whose content simply flows (text, code, markdown, Word) let\n * this scroll; the ones with a fixed sub-control and a scrolling body of their\n * own — PDF pages, PowerPoint slides, a sheet under its tab bar — keep theirs\n * and label it the same way.\n *\n * It is also a **focusable, named region**: a pane that scrolls but contains\n * nothing focusable cannot be reached from a keyboard at all (WCAG 2.1.1), and\n * a plain-text file contains nothing focusable by definition.\n */\nexport const FileViewerContent = forwardRef<HTMLDivElement, FileViewerContentProps>(\n function FileViewerContent({ className, ...props }, ref) {\n const { state, actions, meta } = useFileViewer();\n const { t } = useLocale();\n\n return (\n <div\n ref={ref}\n data-slot=\"file-viewer-content\"\n role=\"region\"\n aria-label={t(\"viewer.content\")}\n tabIndex={0}\n className={cn(\"focus-ring min-h-0 flex-1 overflow-auto p-4\", className)}\n {...props}\n >\n {state.status === \"empty\" && <FileViewerEmpty />}\n {state.status === \"loading\" && <FileViewerSkeleton />}\n {state.status === \"error\" && <FileViewerError />}\n {state.status === \"ready\" && state.adapter && state.document && state.source && (\n <state.adapter.Renderer\n document={state.document}\n source={state.source}\n baseHeadingLevel={meta.baseHeadingLevel}\n highlights={meta.resolvedHighlights}\n // `meta.currentHighlightId`, NOT `state.activeHighlightId`: the\n // renderer must follow the find match too, or find can never scroll,\n // page or switch sheet.\n activeHighlightId={meta.currentHighlightId}\n // The view half (ADR 0026). A renderer that ignores these keeps\n // working — they are optional, and a format with no pages or no\n // scaling never reads them.\n pageNumber={state.pageNumber}\n onPageChange={actions.goToPage}\n zoom={state.zoom}\n onZoomResolved={actions.reportZoom}\n rotation={state.rotation}\n />\n )}\n </div>\n );\n },\n);\n\n/* -------------------------------------------------------------------------- */\n/* Batteries-included */\n/* -------------------------------------------------------------------------- */\n\nexport interface FileViewerProps\n extends\n Omit<FileViewerProviderProps, \"children\">,\n Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n /** Replace the default composition. Rendered inside the provider AND the frame. */\n children?: ReactNode;\n}\n\n/**\n * The default composition — provider + frame + toolbar + content.\n *\n * Reach for the parts when you need a different arrangement; this covers the\n * common case in one element.\n */\nexport const FileViewer = forwardRef<HTMLDivElement, FileViewerProps>(function FileViewer(\n {\n // Every provider prop is destructured BY NAME, and the rest goes to the\n // frame. Peeling off only some of them and spreading the remainder was a\n // silent bug: `baseHeadingLevel` type-checked, never reached the provider,\n // and landed on the `<section>` as an unknown DOM attribute. Adding a\n // provider prop above without adding it here re-creates that exactly.\n source,\n registry,\n loading,\n baseHeadingLevel,\n highlights,\n defaultHighlights,\n onHighlightsChange,\n activeHighlightId,\n defaultActiveHighlightId,\n onActiveHighlightChange,\n pageNumber,\n defaultPageNumber,\n onPageNumberChange,\n zoom,\n defaultZoom,\n onZoomChange,\n rotation,\n defaultRotation,\n onRotationChange,\n children,\n ...props\n },\n ref,\n) {\n return (\n <FileViewerProvider\n source={source}\n registry={registry}\n loading={loading}\n baseHeadingLevel={baseHeadingLevel}\n highlights={highlights}\n defaultHighlights={defaultHighlights}\n onHighlightsChange={onHighlightsChange}\n activeHighlightId={activeHighlightId}\n defaultActiveHighlightId={defaultActiveHighlightId}\n onActiveHighlightChange={onActiveHighlightChange}\n pageNumber={pageNumber}\n defaultPageNumber={defaultPageNumber}\n onPageNumberChange={onPageNumberChange}\n zoom={zoom}\n defaultZoom={defaultZoom}\n onZoomChange={onZoomChange}\n rotation={rotation}\n defaultRotation={defaultRotation}\n onRotationChange={onRotationChange}\n >\n <FileViewerFrame ref={ref} {...props}>\n {children ?? (\n <>\n {/* The page and scale controls live in the identity row, not inside\n the canvas: one row of chrome per viewer, and an app that wants\n them somewhere else composes the parts itself. Each renders\n nothing for a format whose manifest does not claim it. */}\n <FileViewerToolbar>\n <FileViewerPager />\n <FileViewerZoom />\n <FileViewerRotate />\n </FileViewerToolbar>\n {/* Renders nothing until Ctrl/Cmd+F, and nothing at all for a\n format whose adapter cannot paint a range. */}\n <FileViewerFind />\n <FileViewerHighlightStatus />\n <FileViewerContent />\n </>\n )}\n </FileViewerFrame>\n </FileViewerProvider>\n );\n});\n","/**\n * The adapter registry — how a file finds its renderer.\n *\n * Architecture adapted from [anyview](https://github.com/harshpreet931/anyview)\n * (MIT). Four properties are what make it work, and all four are deliberate:\n *\n * 1. **Eager manifests, lazy loaders.** Routing and capability questions are\n * answered from plain data; the parser is fetched only when a file of that\n * kind is actually opened.\n * 2. **Priority override.** A consumer replaces a built-in by registering a\n * higher priority — no fork, no patch.\n * 3. **A fresh adapter per document.** The module (and its parser) is cached;\n * the instance is not, so per-document state cannot leak between files.\n * 4. **A protocol guard.** A mismatched adapter fails with a named error at\n * registration, not with an undefined property at render.\n */\n\nimport { type FileKind, resolveFileKind } from \"@elabs-ai/components-ui\";\n\nimport { ViewerError, isModuleNotFound, parserMissingError, toViewerError } from \"./errors\";\nimport {\n type AdapterLoader,\n type AdapterManifest,\n type AdapterModule,\n PROTOCOL_VERSION,\n} from \"./types\";\n\n/**\n * How specifically a manifest claimed a file. Higher is more specific.\n * A plain object rather than a `const enum` — the repo builds with\n * `isolatedModules`, which cannot inline one.\n */\nconst MatchScore = {\n None: 0,\n Category: 1,\n MediaTypePrefix: 2,\n MediaType: 3,\n Extension: 4,\n} as const;\n\ntype MatchScore = (typeof MatchScore)[keyof typeof MatchScore];\n\ninterface Entry {\n manifest: AdapterManifest;\n loader: AdapterLoader;\n}\n\n/** A registry of file adapters. Create with {@link createRegistry}. */\nexport interface ViewerRegistry {\n /**\n * Add an adapter, or replace one with the same `id`.\n *\n * Replacement is by id and unconditional — the caller asked for it. Priority\n * governs which of several DIFFERENT adapters wins a file, not whether a\n * registration takes effect.\n *\n * @throws {ViewerError} `protocol-mismatch` if the manifest targets another protocol.\n */\n register(manifest: AdapterManifest, loader: AdapterLoader): void;\n /**\n * Every registered manifest, highest priority first. Not the routing order —\n * specificity is per-file, so only {@link detect} can rank for a given file.\n */\n manifests(): AdapterManifest[];\n /** The manifest that should open this file, or `undefined` if none claims it. */\n detect(kind: FileKind): AdapterManifest | undefined;\n /** Convenience over {@link detect} for a name + MIME. */\n detectByName(name: string, mediaType?: string): AdapterManifest | undefined;\n /**\n * Fetch an adapter module by id, caching the MODULE (never an instance).\n *\n * @throws {ViewerError} `unsupported-format` when the id is unknown,\n * `parser-missing` when the module's optional peer is not installed,\n * `protocol-mismatch` when the loaded module disagrees with its manifest.\n */\n load(id: string): Promise<AdapterModule>;\n}\n\n/** How well `manifest` claims `kind`. `MatchScore.None` means \"not mine\". */\nexport function scoreManifest(manifest: AdapterManifest, kind: FileKind): MatchScore {\n if (kind.extension && manifest.extensions?.includes(kind.extension)) {\n return MatchScore.Extension;\n }\n for (const declared of manifest.mediaTypes ?? []) {\n if (declared.endsWith(\"/\")) {\n if (kind.mediaType.startsWith(declared)) return MatchScore.MediaTypePrefix;\n } else if (declared === kind.mediaType) {\n return MatchScore.MediaType;\n }\n }\n if (manifest.categories?.includes(kind.category)) return MatchScore.Category;\n return MatchScore.None;\n}\n\n/** Unwrap `export default` vs named exports, so an adapter can use either. */\nfunction unwrap(loaded: AdapterModule | { default: AdapterModule }): AdapterModule {\n return \"default\" in loaded && loaded.default ? loaded.default : (loaded as AdapterModule);\n}\n\nfunction assertProtocol(manifest: AdapterManifest, where: string): void {\n if (manifest.protocol !== PROTOCOL_VERSION) {\n throw new ViewerError(\n \"protocol-mismatch\",\n `Adapter \"${manifest.id}\" targets viewer protocol ${String(manifest.protocol)}, but this build speaks ${String(PROTOCOL_VERSION)} (${where}).`,\n );\n }\n}\n\n/**\n * Create an empty registry.\n *\n * Empty on purpose: the built-ins live in `createDefaultRegistry()`\n * (`src/adapters`), so a consumer who wants only their own adapters — or only\n * images — never pulls in the rest.\n */\nexport function createRegistry(): ViewerRegistry {\n const entries = new Map<string, Entry>();\n const modules = new Map<string, Promise<AdapterModule>>();\n\n function ordered(): Entry[] {\n return [...entries.values()].sort(\n (a, b) => (b.manifest.priority ?? 0) - (a.manifest.priority ?? 0),\n );\n }\n\n function detect(kind: FileKind): AdapterManifest | undefined {\n let best: { manifest: AdapterManifest; score: MatchScore; priority: number } | undefined;\n for (const { manifest } of entries.values()) {\n const score = scoreManifest(manifest, kind);\n if (score === MatchScore.None) continue;\n const priority = manifest.priority ?? 0;\n // Priority is the consumer's explicit override, so it outranks how\n // specifically an adapter happened to claim the file.\n const wins =\n !best || priority > best.priority || (priority === best.priority && score > best.score);\n if (wins) best = { manifest, score, priority };\n }\n return best?.manifest;\n }\n\n return {\n register(manifest, loader) {\n assertProtocol(manifest, \"registration\");\n entries.set(manifest.id, { manifest, loader });\n // A replaced id must not keep serving the old module.\n modules.delete(manifest.id);\n },\n\n manifests() {\n return ordered().map((entry) => entry.manifest);\n },\n\n detect,\n\n detectByName(name, mediaType) {\n return detect(resolveFileKind(name, mediaType));\n },\n\n async load(id) {\n const entry = entries.get(id);\n if (!entry) {\n throw new ViewerError(\"unsupported-format\", `No adapter is registered for \"${id}\".`);\n }\n\n let pending = modules.get(id);\n if (!pending) {\n pending = entry\n .loader()\n .then((loaded) => {\n const adapterModule = unwrap(loaded);\n assertProtocol(adapterModule.manifest, `module \"${id}\"`);\n return adapterModule;\n })\n .catch((error: unknown) => {\n // Not cached: an optional peer installed later, or a transient\n // chunk-load failure, must be retryable without a page reload.\n modules.delete(id);\n if (isModuleNotFound(error)) {\n throw parserMissingError(id, entry.manifest.requires ?? [], { cause: error });\n }\n throw toViewerError(error, \"parse-failed\");\n });\n modules.set(id, pending);\n }\n return pending;\n },\n };\n}\n","/**\n * The built-in adapters, and the registry that has them all.\n *\n * Every entry is `manifest` (eager, plain data) + `() => import(…)` (lazy). The\n * manifests are imported statically ON PURPOSE — they are a few dozen bytes of\n * data each and answer \"can this be opened, and what controls apply\" with no\n * network. The parsers and renderers behind them are not.\n *\n * `pnpm heavy-deps:check` enforces the split: a static import of `papaparse`\n * (or, from P1, `pdfjs-dist`) fails CI, because those are optional peers — a\n * static edge does not merely bloat a chunk, it makes the package unresolvable\n * for every consumer that did not install that parser.\n */\n\nimport { codeManifest } from \"./code/code-manifest\";\nimport { csvManifest } from \"./csv/csv-manifest\";\nimport { docxManifest } from \"./docx/docx-manifest\";\nimport { markdownManifest } from \"./markdown/markdown-manifest\";\nimport { imageManifest } from \"./image/image-manifest\";\nimport { jsonManifest } from \"./json/json-manifest\";\nimport { mediaManifest } from \"./media/media-manifest\";\nimport { pdfManifest } from \"./pdf/pdf-manifest\";\nimport { pptxManifest } from \"./pptx/pptx-manifest\";\nimport { textManifest } from \"./text/text-manifest\";\nimport { xlsxManifest } from \"./xlsx/xlsx-manifest\";\nimport { createRegistry, type ViewerRegistry } from \"../core/registry\";\n\nexport {\n codeManifest,\n csvManifest,\n docxManifest,\n imageManifest,\n jsonManifest,\n markdownManifest,\n mediaManifest,\n pdfManifest,\n pptxManifest,\n textManifest,\n xlsxManifest,\n};\n\n/**\n * A registry with every built-in adapter registered.\n *\n * Call it per app (or per view) rather than sharing one module-level instance,\n * so one screen's `register()` override cannot leak into another's.\n */\nexport function createDefaultRegistry(): ViewerRegistry {\n const registry = createRegistry();\n registry.register(imageManifest, () => import(\"./image/image-adapter\"));\n registry.register(jsonManifest, () => import(\"./json/json-adapter\"));\n registry.register(csvManifest, () => import(\"./csv/csv-adapter\"));\n registry.register(pdfManifest, () => import(\"./pdf/pdf-adapter\"));\n registry.register(mediaManifest, () => import(\"./media/media-adapter\"));\n registry.register(docxManifest, () => import(\"./docx/docx-adapter\"));\n registry.register(xlsxManifest, () => import(\"./xlsx/xlsx-adapter\"));\n registry.register(pptxManifest, () => import(\"./pptx/pptx-adapter\"));\n registry.register(markdownManifest, () => import(\"./markdown/markdown-adapter\"));\n registry.register(codeManifest, () => import(\"./code/code-adapter\"));\n // Registered last and claiming only broad categories, so anything above wins\n // a file it names specifically. This is the \"readable as text\" backstop.\n registry.register(textManifest, () => import(\"./text/text-adapter\"));\n return registry;\n}\n","/**\n * What the viewer is pointed AT — the vocabulary shared by citations and\n * find-in-document.\n *\n * A {@link DocumentHighlight} is what a caller asks for; a\n * {@link ResolvedHighlight} is what the viewer worked out and what an adapter's\n * `Renderer` is handed. Keeping the two apart is what lets \"we could not find\n * that passage\" be a rendered STATE rather than a silent no-op: the request\n * survives even when the location does not, so the chrome still has something\n * to name.\n *\n * The address vocabulary itself lives in `@elabs-ai/components-ui`\n * (`DocumentAddress`) because the producer of a citation and this consumer are\n * sibling packages that may not import each other. Everything here is\n * adapter-protocol detail and stays in this package.\n */\n\nimport type {\n DocumentAddress,\n DocumentAddressKind,\n DocumentRect,\n MatchRange,\n} from \"@elabs-ai/components-ui\";\n\n/**\n * Where a highlight came from. Both paint identically — only which one is\n * ACTIVE differs — but the origin decides who owns the list: citations are a\n * controlled prop the app supplies, search matches are the viewer's own.\n */\nexport type HighlightSource = \"citation\" | \"search\";\n\n/** A request to point the viewer at part of the open document. */\nexport interface DocumentHighlight {\n /**\n * Stable per highlight. It is what `activeHighlightId` names and what React\n * keys on, so a list that renumbers between renders must not renumber ids.\n */\n id: string;\n /** Which part of the document. */\n address: DocumentAddress;\n /**\n * Short human label — the answer's claim, the source's title. Announced when\n * this highlight becomes active, so prefer something a listener can act on\n * over \"Citation 3\".\n */\n label?: string;\n /** Defaults to `\"citation\"`. */\n source?: HighlightSource;\n}\n\n/**\n * How a request turned out.\n *\n * `unsupported` is a CAPABILITY GAP, not a failure — the same distinction the\n * error panel already draws. \"This build can't locate a rect in a Word file\"\n * is news about what we shipped; it is not the reader's mistake, and it is not\n * retryable.\n */\nexport type HighlightStatus = \"pending\" | \"resolved\" | \"not-found\" | \"unsupported\";\n\n/** Why a passage was not found — the two are genuinely different news. */\nexport type HighlightMissReason =\n /** Searched the whole projection; the passage is not in it. */\n | \"absent\"\n /** The projection is capped, and the passage may lie past the cap. */\n | \"truncated\";\n\n/** A request, plus where it landed. What every adapter `Renderer` receives. */\nexport interface ResolvedHighlight {\n id: string;\n label?: string;\n source: HighlightSource;\n status: HighlightStatus;\n /** The original request, so a renderer can honour a kind the shell cannot. */\n address: DocumentAddress;\n /** Whether the viewer is currently pointed at this one. */\n active: boolean;\n /**\n * 1-based position among the highlights of the same `source` that resolved —\n * the \"3\" in \"3 of 12\". Absent when this one did not resolve, so a miss never\n * silently consumes a number the reader is counting through.\n */\n index?: number;\n /** Offsets into `document.text`, once located. */\n range?: MatchRange;\n /**\n * 1-based page, set only for a `rect` address — the one kind with no range to\n * derive a position from.\n *\n * A `quote` or `range` deliberately leaves this empty even when the caller\n * supplied a page hint: where the passage actually landed is knowable from the\n * adapter's own index, and a stale hint used as an instruction would page the\n * reader somewhere the mark is not.\n */\n page?: number;\n /** Geometry, for a `rect` address. */\n rects?: readonly DocumentRect[];\n /** Only on `not-found`. */\n reason?: HighlightMissReason;\n}\n\n/**\n * The most matches find-in-document will paint.\n *\n * A one-letter query against a 2 MB log matches hundreds of thousands of times;\n * every one of those is a DOM element. The cap keeps typing responsive, and the\n * chrome says so rather than quietly showing a wrong total.\n */\nexport const FIND_MATCH_LIMIT = 2000;\n\n/** Prefix reserved for the viewer's own search matches. */\nconst FIND_ID_PREFIX = \"find:\";\n\n/** The id for the nth (0-based) find match. */\nexport function findMatchId(index: number): string {\n return `${FIND_ID_PREFIX}${String(index)}`;\n}\n\n/** Whether an id belongs to find-in-document rather than to a caller. */\nexport function isFindMatchId(id: string): boolean {\n return id.startsWith(FIND_ID_PREFIX);\n}\n\n/** Which address kinds an adapter honours. Absent means none — the safe default. */\nexport type HighlightSupport = readonly DocumentAddressKind[];\n","/**\n * Turning a request into a location — the LOCATE step, and the only step that\n * runs outside the adapter.\n *\n * The funnel is three stages with three homes. **Locate** (here) answers \"where\n * in the text projection is this?\" and produces character offsets. **Map** (the\n * adapter's renderer) turns those offsets into its own model — block 14, page 3,\n * cell B7. **Paint** (also the renderer) draws it and scrolls to it.\n *\n * Locate lives in the shell rather than in each adapter because its OUTCOME is\n * chrome state, not pixels: \"3 of 12\", \"we couldn't find that passage\", \"this\n * build can't locate a box in a Word file\". Every adapter would otherwise write\n * that logic again, slightly differently, and the shell would have no way to\n * count what it is showing. It is also pure — no DOM, no engine — so it is\n * testable without jsdom.\n */\n\nimport {\n normalizeQuoteText,\n type MatchRange,\n type NormalizedText,\n type QuoteAddress,\n} from \"@elabs-ai/components-ui\";\n\nimport type { DocumentHighlight, HighlightSupport, ResolvedHighlight } from \"./highlight\";\n\n/**\n * Find a quoted passage in a normalized projection and map it back to raw\n * offsets.\n *\n * Both sides are folded first (whitespace, quote glyphs, case), because a\n * citation is re-typed by a model or extracted by a different tool and will\n * essentially never be byte-identical to what our parser produced.\n *\n * Ambiguity is resolved by the caller's own hints and never guessed at: an\n * explicit `occurrence` wins, then proximity to `near.offset`, then the first\n * match. Returning the first match silently would put a citation on the wrong\n * paragraph of a document that repeats a heading.\n */\nexport function locateQuote(\n normalized: NormalizedText,\n address: QuoteAddress,\n): MatchRange | undefined {\n const needle = normalizeQuoteText(address.text);\n if (needle.length === 0) return undefined;\n\n const starts: number[] = [];\n for (let at = normalized.text.indexOf(needle); at !== -1; ) {\n starts.push(at);\n // Step by one, not by the needle's length: overlapping occurrences of a\n // repeated phrase (\"na na na\") are still distinct places in the document.\n at = normalized.text.indexOf(needle, at + 1);\n }\n if (starts.length === 0) return undefined;\n\n const toRange = (start: number): MatchRange => [\n normalized.offsets[start] as number,\n normalized.offsets[start + needle.length] as number,\n ];\n\n if (address.occurrence !== undefined) {\n const chosen = starts[address.occurrence - 1];\n // An out-of-range occurrence is a miss, not a fallback to the first: the\n // caller asked for the fourth of three, and quietly marking the first would\n // be a confidently wrong citation.\n return chosen === undefined ? undefined : toRange(chosen);\n }\n\n const near = address.near?.offset;\n if (near !== undefined) {\n let best = starts[0] as number;\n let bestDistance = Number.POSITIVE_INFINITY;\n for (const start of starts) {\n const distance = Math.abs((normalized.offsets[start] as number) - near);\n if (distance < bestDistance) {\n bestDistance = distance;\n best = start;\n }\n }\n return toRange(best);\n }\n\n return toRange(starts[0] as number);\n}\n\nexport interface HighlightResolveContext {\n /**\n * The document's text projection, folded once with its offset map. Absent\n * when the format has no text projection at all.\n *\n * Pre-folded rather than raw because find-in-document re-resolves on every\n * keystroke, and folding a 2 MB projection per keystroke is what turns a\n * search box into a stutter.\n */\n normalized?: NormalizedText;\n /** Length of the RAW projection, for clamping `range` addresses. */\n textLength?: number;\n /** Whether that projection is capped, which changes what a miss MEANS. */\n truncated?: boolean;\n /** Which address kinds this document's adapter honours. */\n supported: HighlightSupport;\n /** The highlight the viewer is currently pointed at. */\n activeId?: string | null;\n}\n\n/** Sort key: where in the document this landed. Unresolved sorts last. */\nfunction positionOf(highlight: ResolvedHighlight): number {\n if (highlight.range) return highlight.range[0];\n if (highlight.page !== undefined) return highlight.page;\n return Number.POSITIVE_INFINITY;\n}\n\nfunction resolveOne(\n highlight: DocumentHighlight,\n context: HighlightResolveContext,\n): ResolvedHighlight {\n const source = highlight.source ?? \"citation\";\n const address = highlight.address;\n const base = { id: highlight.id, label: highlight.label, source, address, active: false };\n\n // The load-bearing contract: match only the kinds this adapter DECLARED, and\n // report the rest. No exhaustive switch, no `never` fallthrough — that is\n // what lets a fourth address kind be added later without breaking every\n // consumer that predates it.\n if (!context.supported.includes(address.kind)) {\n return { ...base, status: \"unsupported\" };\n }\n\n if (address.kind === \"rect\") {\n // Geometry needs no text and cannot fail to \"locate\" — whether the page\n // exists is the renderer's business, since only it knows the page count.\n return { ...base, status: \"resolved\", page: address.page, rects: address.rects };\n }\n\n const miss = (): ResolvedHighlight => ({\n ...base,\n status: \"not-found\",\n reason: context.truncated ? \"truncated\" : \"absent\",\n });\n\n if (address.kind === \"range\") {\n const length = context.textLength;\n if (length === undefined) return miss();\n const start = Math.max(0, Math.min(address.start, length));\n const end = Math.max(start, Math.min(address.end, length));\n // A range clamped to nothing is a miss, not a zero-width mark: the offsets\n // were computed against a longer projection than the one we have.\n if (end === start) return miss();\n // No `page`. A producer's page number is a hint, and a wrong one used as an\n // instruction turns the pager to a blank page while the mark sits elsewhere.\n // Where the range actually landed is knowable from the adapter's own index,\n // so that is what navigation uses; `page` stays authoritative only for\n // `rect`, which has no range to derive it from.\n return { ...base, status: \"resolved\", range: [start, end] };\n }\n\n if (!context.normalized) return miss();\n const range = locateQuote(context.normalized, address);\n if (!range) return miss();\n return { ...base, status: \"resolved\", range };\n}\n\n/**\n * Resolve every request, in document order, numbered per source.\n *\n * Document order rather than the caller's, because \"next match\" has to mean the\n * next one down the page — an app listing citations in relevance order would\n * otherwise send the reader jumping backwards. Numbering is per `source` so the\n * find box's \"3 of 12\" counts search matches only, and never the citations\n * painted beside them.\n */\nexport function resolveHighlights(\n highlights: readonly DocumentHighlight[],\n context: HighlightResolveContext,\n): ResolvedHighlight[] {\n const resolved = highlights\n .map((highlight) => resolveOne(highlight, context))\n .sort((a, b) => positionOf(a) - positionOf(b));\n\n const counters = new Map<string, number>();\n return resolved.map((highlight) => {\n const next =\n highlight.status === \"resolved\" ? (counters.get(highlight.source) ?? 0) + 1 : undefined;\n if (next !== undefined) counters.set(highlight.source, next);\n return {\n ...highlight,\n index: next,\n active: context.activeId != null && context.activeId === highlight.id,\n };\n });\n}\n","\"use client\";\n\n/**\n * Find-in-document — the viewer's own Ctrl/Cmd+F.\n *\n * It paints through the SAME highlight layer citations use (`source: \"search\"`),\n * so a document never grows two mark systems that disagree about what a mark\n * looks like. Only which one is CURRENT differs, and that lives on its own knob\n * (`find.activeIndex`) so an app controlling citations does not have to honour\n * every keystroke of a search it never asked for.\n *\n * **Not a `role=\"toolbar\"`.** A toolbar is one tab stop with roving arrow keys,\n * and an `<input>` inside one has its Left/Right stolen from the caret. This is\n * `role=\"search\"` — the same call `FileViewerToolbar` already made.\n */\n\nimport {\n cn,\n IconButton,\n InputGroup,\n InputGroupAddon,\n InputGroupButton,\n InputGroupInput,\n Separator,\n useLocale,\n} from \"@elabs-ai/components-ui\";\nimport { CaseSensitiveIcon, ChevronDownIcon, ChevronUpIcon, SearchIcon, XIcon } from \"lucide-react\";\nimport {\n forwardRef,\n useEffect,\n useRef,\n type HTMLAttributes,\n type KeyboardEvent as ReactKeyboardEvent,\n} from \"react\";\n\nimport { FIND_MATCH_LIMIT } from \"../core/highlight\";\nimport { useFileViewer } from \"./file-viewer-context\";\n\n/** Whether a keyboard event is the platform's find shortcut. */\nexport function isFindShortcut(event: {\n key: string;\n metaKey: boolean;\n ctrlKey: boolean;\n}): boolean {\n // Either modifier, not `navigator.platform`: a Mac driven by an external PC\n // keyboard sends Ctrl, and sniffing the platform gets that reader wrong.\n return event.key.toLowerCase() === \"f\" && (event.metaKey || event.ctrlKey);\n}\n\nexport type FileViewerFindProps = HTMLAttributes<HTMLDivElement>;\n\n/**\n * The search row. Renders nothing until `actions.openFind()` (or Ctrl/Cmd+F on\n * the frame) opens it, and nothing at all for a document whose adapter cannot\n * paint a range — an affordance that could never highlight anything is worse\n * than no affordance at all.\n */\nexport const FileViewerFind = forwardRef<HTMLDivElement, FileViewerFindProps>(\n function FileViewerFind({ className, ...props }, ref) {\n const { state, actions, meta } = useFileViewer();\n const { t, formatNumber } = useLocale();\n const input = useRef<HTMLInputElement>(null);\n const find = state.find;\n const open = find.open && meta.canFind;\n const { registerFind } = actions;\n\n // Announce that there IS somewhere to type. The frame only takes Ctrl/Cmd+F\n // away from the browser once this has run — a shortcut swallowed with no box\n // to show leaves a keyboard reader with no find at all. Registered on mount\n // rather than when open, because the box is closed at exactly the moment the\n // shortcut has to be decided.\n useEffect(() => registerFind(), [registerFind]);\n\n // A find box that does not take the caret sends the next keystroke to the\n // document behind it — the one thing every reader expects not to happen.\n useEffect(() => {\n if (open) input.current?.focus();\n }, [open]);\n\n if (!open) return null;\n\n const hasQuery = find.query.length > 0;\n const hasMatches = find.matches > 0;\n\n const onKeyDown = (event: ReactKeyboardEvent<HTMLInputElement>) => {\n if (event.key === \"Escape\") {\n event.preventDefault();\n // Stopped here: an Escape meant for the find box must not also close a\n // Dialog the viewer happens to be sitting in.\n event.stopPropagation();\n actions.closeFind();\n return;\n }\n if (event.key === \"Enter\") {\n event.preventDefault();\n if (event.shiftKey) actions.previousFindMatch();\n else actions.nextFindMatch();\n }\n };\n\n return (\n <div\n ref={ref}\n data-slot=\"file-viewer-find\"\n role=\"search\"\n aria-label={t(\"viewer.find.label\")}\n className={cn(\n // The divider is the only cue between this row and the content below\n // it — same fill, no elevation change (WCAG 1.4.11, strong rung).\n \"border-border-strong flex shrink-0 items-center gap-2 border-b px-3 py-2\",\n className,\n )}\n {...props}\n >\n <InputGroup className=\"h-8 max-w-72\">\n <InputGroupAddon>\n <SearchIcon aria-hidden=\"true\" />\n </InputGroupAddon>\n <InputGroupInput\n ref={input}\n // Not `type=\"search\"`: the browser's own clear affordance is\n // unlabelled and unthemeable, and this row already carries a close\n // control that does something more useful.\n type=\"text\"\n value={find.query}\n aria-label={t(\"viewer.find.label\")}\n placeholder={t(\"viewer.find.placeholder\")}\n autoComplete=\"off\"\n spellCheck={false}\n onChange={(event) => actions.setFindQuery(event.target.value)}\n onKeyDown={onKeyDown}\n />\n <InputGroupAddon align=\"inline-end\">\n {/* A real toggle button, so the state is announced rather than\n inferred from a colour change. */}\n <InputGroupButton\n size=\"icon-xs\"\n aria-pressed={find.caseSensitive}\n aria-label={t(\"viewer.find.caseSensitive\")}\n title={t(\"viewer.find.caseSensitive\")}\n onClick={() => actions.setFindCaseSensitive(!find.caseSensitive)}\n >\n <CaseSensitiveIcon aria-hidden=\"true\" />\n </InputGroupButton>\n </InputGroupAddon>\n </InputGroup>\n\n {/* ONE live region for the whole result state: \"3 of 12\" and \"No\n matches\" replace each other rather than sitting side by side, so a\n fruitless search never reads as \"0 of 0\". */}\n <span\n role=\"status\"\n aria-live=\"polite\"\n className=\"text-meta text-muted-foreground min-w-0 flex-1 truncate tabular-nums\"\n >\n {!hasQuery\n ? null\n : hasMatches\n ? t(\"viewer.find.count\", {\n index: formatNumber(find.activeIndex + 1),\n total: formatNumber(Math.min(find.matches, FIND_MATCH_LIMIT)),\n })\n : t(\"viewer.find.none\")}\n {find.truncated\n ? ` ${t(\"viewer.find.capped\", {\n limit: formatNumber(FIND_MATCH_LIMIT),\n total: formatNumber(find.matches),\n })}`\n : null}\n </span>\n\n {/* `aria-disabled`, never the native attribute: a focused control that\n becomes `disabled` is dropped from the focus order, so a reader who\n tabbed to Next and then cleared the query would have focus silently\n fall to <body>. The actions are the real guard — both no-op with no\n matches (see interaction-guidelines.md). */}\n <IconButton\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-disabled={!hasMatches}\n label={t(\"viewer.find.previous\")}\n icon={<ChevronUpIcon aria-hidden=\"true\" />}\n onClick={actions.previousFindMatch}\n />\n <IconButton\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-disabled={!hasMatches}\n label={t(\"viewer.find.next\")}\n icon={<ChevronDownIcon aria-hidden=\"true\" />}\n onClick={actions.nextFindMatch}\n />\n <Separator orientation=\"vertical\" className=\"h-4\" />\n <IconButton\n variant=\"ghost\"\n size=\"icon-sm\"\n label={t(\"viewer.find.close\")}\n icon={<XIcon aria-hidden=\"true\" />}\n onClick={actions.closeFind}\n />\n </div>\n );\n },\n);\n","\"use client\";\n\n/**\n * The context contract behind every `FileViewer` part.\n *\n * Per `component-api.md` (\"Lift state into the Provider; expose a\n * `state` / `actions` interface\"), `FileViewerProvider` is the only thing that\n * knows HOW a file is loaded. Parts read the interface below, so a sibling\n * control placed outside the visual frame but inside the provider — a download\n * button in a page header, a format badge in a breadcrumb — reads and drives the\n * same state with no prop-drilling.\n *\n * This module is the CONTRACT only; the provider itself lives beside the parts\n * it feeds, in `file-viewer.tsx`.\n */\n\nimport type { ProseHeadingLevel, ResolvedFileSource } from \"@elabs-ai/components-ui\";\nimport { createContext, use } from \"react\";\n\nimport type { ViewerError } from \"../core/errors\";\nimport type { DocumentHighlight, HighlightSupport, ResolvedHighlight } from \"../core/highlight\";\nimport type { ViewerRegistry } from \"../core/registry\";\nimport type {\n AdapterCapabilities,\n AdapterDocument,\n AdapterModule,\n DocumentRotation,\n ZoomLevel,\n} from \"../core/types\";\n\n/**\n * Where a file is in its journey to the screen.\n *\n * `loading` means \"no renderable content yet\" — the canonical signal from\n * `.claude/rules/loading-states.md`, rendered as a layout-shaped skeleton.\n * There is no separate `isStreaming`: a file arrives settled or not at all.\n */\nexport type FileViewerStatus = \"empty\" | \"loading\" | \"ready\" | \"error\";\n\n/**\n * The LOAD half of the state — everything the fetch-and-parse effect owns.\n *\n * Split from the highlight half because the two have different lifetimes: this\n * one is replaced wholesale each time a file is opened, while the citations\n * pointing into it are a prop the app controls and outlive any single parse.\n * Folding them together would mean every `setState` in the load effect had to\n * remember to carry the highlights forward.\n */\nexport interface FileViewerLoadState {\n status: FileViewerStatus;\n /** The resolved source, available as soon as there IS one — before any read. */\n source?: ResolvedFileSource;\n /** The adapter's parsed output. Only in `ready`. */\n document?: AdapterDocument;\n /** The adapter module that produced it, for its `Renderer`. Only in `ready`. */\n adapter?: AdapterModule;\n /** What the chrome may offer. Known from the manifest BEFORE the parser loads. */\n capabilities: AdapterCapabilities;\n /** Only in `error`. Always a `ViewerError`, so `code` can drive the message. */\n error?: ViewerError;\n}\n\n/**\n * How the open document is being LOOKED at — which page, at what scale, turned\n * which way.\n *\n * Separate from the load state because it survives nothing and owns nothing: it\n * is pure view, reset (page, rotation) or carried (zoom) when a new file opens.\n * It lives in the provider rather than inside each adapter's `Renderer` so a\n * control can sit anywhere — the shell toolbar, an app's own page header, a\n * deep link — instead of only inside the canvas (ADR 0026).\n */\nexport interface FileViewerViewState {\n /** 1-based. `1` for a format that does not paginate. */\n pageNumber: number;\n /** `0` until a paginated document is ready, and for formats with no pages. */\n pageCount: number;\n /** What was ASKED for: a fixed scale, or a fit mode the renderer resolves. */\n zoom: ZoomLevel;\n /**\n * What that resolved to, as a number — what the zoom control shows and what\n * `zoomIn`/`zoomOut` step from. Equal to `zoom` whenever `zoom` is a number.\n */\n effectiveZoom: number;\n /** Quarter-turns clockwise. Reset to `0` when a different file is opened. */\n rotation: DocumentRotation;\n}\n\nexport interface FileViewerState extends FileViewerLoadState, FileViewerViewState {\n /**\n * The parts of the document to point at, as REQUESTED. What was actually\n * located is `meta.resolvedHighlights` — the request survives a miss so the\n * chrome can say \"we couldn't find that passage\" instead of showing nothing.\n */\n highlights: readonly DocumentHighlight[];\n /** Which one the viewer is pointed at. `null` is \"none\", explicitly. */\n activeHighlightId: string | null;\n find: FileViewerFindState;\n}\n\n/** What find-in-document is doing right now. */\nexport interface FileViewerFindState {\n /** Whether the search box is showing. */\n open: boolean;\n query: string;\n caseSensitive: boolean;\n /** How many matches the current query has. */\n matches: number;\n /** Whether that count hit `FIND_MATCH_LIMIT` and is therefore a floor. */\n truncated: boolean;\n /**\n * Which match is current, 0-based — the \"3\" in \"3 of 12\", minus one.\n *\n * Deliberately NOT the same knob as `activeHighlightId`. Find is the viewer's\n * own, and an app that controls `activeHighlightId` to drive citations would\n * otherwise have to also honour every keystroke of a search it never asked\n * for, or silently break next/previous.\n */\n activeIndex: number;\n}\n\nexport interface FileViewerActions {\n /** Re-run the load. The retry action on the error state. */\n reload: () => void;\n /**\n * Replace the citations. While `highlights` is controlled this writes no local\n * state — but it still calls `onHighlightsChange`, so the owner can accept the\n * request. Mirroring the platform: a controlled input reports, it does not\n * self-update.\n */\n setHighlights: (highlights: readonly DocumentHighlight[]) => void;\n /** Point the viewer at one highlight, or at none. */\n setActiveHighlight: (id: string | null) => void;\n /** Move to the next/previous CITATION in document order, wrapping around. */\n nextHighlight: () => void;\n previousHighlight: () => void;\n openFind: () => void;\n closeFind: () => void;\n setFindQuery: (query: string) => void;\n setFindCaseSensitive: (caseSensitive: boolean) => void;\n /** Move to the next/previous SEARCH match, wrapping around. */\n nextFindMatch: () => void;\n previousFindMatch: () => void;\n /**\n * Turn to a page, 1-based. Clamped to the document — an out-of-range page is\n * a caller's arithmetic slip, not a reason to blank the canvas.\n */\n goToPage: (page: number) => void;\n /** Turn one page. Both stop at the ends rather than wrapping: a document is not a carousel. */\n nextPage: () => void;\n previousPage: () => void;\n /** Draw at a fixed scale, or hand the renderer a fit mode to resolve. */\n setZoom: (zoom: ZoomLevel) => void;\n /**\n * Step to the next stop above/below what is currently ON SCREEN — so zooming\n * in from a fitted page continues from the fitted scale, not from wherever the\n * fixed ladder was last parked.\n */\n zoomIn: () => void;\n zoomOut: () => void;\n setRotation: (rotation: DocumentRotation) => void;\n /** Turn the document a quarter-turn: `1` clockwise, `-1` counter-clockwise. */\n rotate: (quarterTurns: 1 | -1) => void;\n /**\n * The renderer's report channel for {@link FileViewerViewState.effectiveZoom}\n * — not for app code. `FileViewerContent` wires it to the adapter's\n * `onZoomResolved`, the same way `registerFind` is wired to a part rather than\n * called by a consumer.\n */\n reportZoom: (scale: number) => void;\n /**\n * Tell the viewer a find part is mounted; call the returned function on\n * unmount. `FileViewerFind` does this for you — it exists so the frame knows\n * whether taking Ctrl/Cmd+F off the browser leads anywhere.\n */\n registerFind: () => () => void;\n}\n\nexport interface FileViewerContextValue {\n state: FileViewerState;\n actions: FileViewerActions;\n registry: ViewerRegistry;\n meta: {\n /**\n * The rung a viewed document's own top-level heading renders at. Passed to\n * every adapter `Renderer`; see `AdapterRendererProps.baseHeadingLevel`.\n */\n baseHeadingLevel: ProseHeadingLevel;\n /**\n * Citations and find matches, LOCATED, in document order, numbered — what\n * an adapter `Renderer` is handed and what the chrome counts.\n */\n resolvedHighlights: readonly ResolvedHighlight[];\n /**\n * Which highlight the viewer is EFFECTIVELY pointed at, and what an adapter\n * `Renderer` receives as `activeHighlightId`.\n *\n * Not the same knob as `state.activeHighlightId`: that one is the citation\n * the app controls, while the reader stepping through find matches is also\n * \"current\". Find outranks the citation while its box is open and matching,\n * so navigation and scrolling follow whichever the reader is actually moving.\n */\n currentHighlightId: string | null;\n /** Which address kinds this document's adapter declared it can paint. */\n highlightSupport: HighlightSupport;\n /**\n * Whether find-in-document applies to the open document. Derived, not read\n * straight off the manifest — see `AdapterCapabilities.search`.\n */\n canFind: boolean;\n /**\n * Whether a `FileViewerFind` part is composed into this viewer.\n *\n * Separate from {@link canFind}, which only says the ADAPTER could paint a\n * match. The frame needs both before it takes Ctrl/Cmd+F away from the\n * browser: intercepting the shortcut with nowhere to type leaves a keyboard\n * reader with no find at all.\n */\n hasFind: boolean;\n };\n}\n\n/**\n * Exported for `FileViewerProvider` (which lives with the parts it feeds, in\n * `file-viewer.tsx`) — NOT part of the package's public surface.\n */\nexport const FileViewerContext = createContext<FileViewerContextValue | null>(null);\n\n/**\n * Read the viewer state. Throws outside a provider — a part that silently\n * rendered nothing would be far harder to diagnose than a named error.\n */\nexport function useFileViewer(): FileViewerContextValue {\n const value = use(FileViewerContext);\n if (!value) {\n throw new Error(\"useFileViewer must be used inside a <FileViewerProvider> (or <FileViewer>).\");\n }\n return value;\n}\n","\"use client\";\n\n/**\n * `FileViewerPager` — previous / a page you can type into / next.\n *\n * A PART, not adapter chrome. The page lives in the provider (ADR 0026), so this\n * row can sit in the viewer's own toolbar, in an app's page header, or beside a\n * thumbnail rail, and every copy of it stays in step. While the page was\n * `useState` inside `PdfRenderer` there could only ever be one pager, inside the\n * canvas.\n *\n * ## Why this is not a `role=\"toolbar\"`\n *\n * The role promises roving-tabindex arrow-key navigation, and the page field is\n * a TEXT INPUT — ArrowLeft/ArrowRight there move the caret. A toolbar that\n * swallowed them would make the number unusable to a keyboard reader, so this is\n * a plain named group of ordinary tab stops: the same call `FileViewerToolbar`\n * and `ViewToolbar` make, for the same reason.\n */\n\nimport { cn, IconButton, Input, useLocale } from \"@elabs-ai/components-ui\";\nimport { ChevronLeftIcon, ChevronRightIcon } from \"lucide-react\";\nimport { forwardRef, useState, type HTMLAttributes } from \"react\";\n\nimport { useFileViewer } from \"./file-viewer-context\";\n\nexport type FileViewerPagerProps = HTMLAttributes<HTMLDivElement>;\n\nexport const FileViewerPager = forwardRef<HTMLDivElement, FileViewerPagerProps>(\n function FileViewerPager({ className, ...props }, ref) {\n const { state, actions } = useFileViewer();\n const { t, formatNumber } = useLocale();\n\n // The half-typed value, while the reader is typing it. `null` means \"not\n // editing\", so the field follows the document the rest of the time — a\n // citation that turns the page updates the number under the caret too.\n const [draft, setDraft] = useState<string | null>(null);\n\n const { pageNumber, pageCount } = state;\n\n // No pages, or none known yet: render nothing rather than an inert \"1 of 0\".\n // Chrome with nothing in it reads as a broken render (viewer-components.md).\n if (!state.capabilities.pages || pageCount === 0) return null;\n\n const commit = () => {\n if (draft === null) return;\n const parsed = Number.parseInt(draft, 10);\n setDraft(null);\n // A blank or nonsense entry is a reader changing their mind, not a\n // navigation — snap back to where they are instead of jumping to page 1.\n if (Number.isNaN(parsed)) return;\n actions.goToPage(parsed);\n };\n\n return (\n <div\n ref={ref}\n data-slot=\"file-viewer-pager\"\n role=\"group\"\n aria-label={t(\"viewer.pager.controls\")}\n className={cn(\"flex shrink-0 items-center gap-1\", className)}\n {...props}\n >\n <IconButton\n variant=\"ghost\"\n size=\"icon-sm\"\n label={t(\"viewer.pager.previous\")}\n icon={<ChevronLeftIcon aria-hidden=\"true\" />}\n disabled={pageNumber <= 1}\n onClick={actions.previousPage}\n />\n <Input\n // Explicit: Radix's toolbar slot and several wrappers default an\n // unset `type` to `\"button\"`, which would silently turn the field\n // into a button the day this moves inside one.\n type=\"text\"\n inputMode=\"numeric\"\n autoComplete=\"off\"\n spellCheck={false}\n aria-label={t(\"viewer.pager.pageNumber\")}\n value={draft ?? String(pageNumber)}\n onChange={(event) => setDraft(event.target.value)}\n onBlur={commit}\n onKeyDown={(event) => {\n if (event.key === \"Enter\") {\n event.preventDefault();\n commit();\n } else if (event.key === \"Escape\") {\n // Abandon the edit without navigating; focus stays put, so the\n // reader can try again without re-reaching the field.\n setDraft(null);\n }\n }}\n className=\"h-7 w-12 px-1 text-center tabular-nums\"\n />\n <span className=\"text-meta text-muted-foreground whitespace-nowrap tabular-nums\">\n {t(\"viewer.pager.of\", { total: formatNumber(pageCount) })}\n </span>\n <IconButton\n variant=\"ghost\"\n size=\"icon-sm\"\n label={t(\"viewer.pager.next\")}\n icon={<ChevronRightIcon aria-hidden=\"true\" />}\n disabled={pageNumber >= pageCount}\n onClick={actions.nextPage}\n />\n {/* Paging repaints a canvas, which announces nothing on its own, and the\n field's own value change is not announced either. ONE live region for\n the group carries the whole sentence — `loading-states.md`'s rule that\n a region announces once, not per element. */}\n <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {t(\"viewer.pager.status\", {\n page: formatNumber(pageNumber),\n total: formatNumber(pageCount),\n })}\n </span>\n </div>\n );\n },\n);\n","\"use client\";\n\n/**\n * `FileViewerZoom` and `FileViewerRotate` — how big, and which way up.\n *\n * Parts over the provider's view state (ADR 0026), so an app can put the scale\n * control in its own header without reaching inside the canvas.\n *\n * Each renders NOTHING for a format whose manifest does not claim the capability\n * — an inert zoom control over a CSV is chrome with nothing in it, which reads\n * as a broken render (`viewer-components.md`). That absence is also the fix for\n * the `image` manifest's old lie: it declared `zoom` and `rotate` while its\n * renderer implemented neither, so the claim was invisible either way.\n */\n\nimport {\n cn,\n IconButton,\n Select,\n SelectContent,\n SelectItem,\n SelectSeparator,\n SelectTrigger,\n SelectValue,\n useLocale,\n} from \"@elabs-ai/components-ui\";\nimport { RotateCwIcon, ZoomInIcon, ZoomOutIcon } from \"lucide-react\";\nimport { forwardRef, type HTMLAttributes } from \"react\";\n\nimport type { ZoomLevel } from \"../core/types\";\nimport { canStepZoom, isZoomFit, VIEWER_ZOOM_STEPS } from \"../core/zoom\";\nimport { useFileViewer } from \"./file-viewer-context\";\n\nexport type FileViewerZoomProps = HTMLAttributes<HTMLDivElement>;\n\n/**\n * Zoom out · the current level · zoom in.\n *\n * The middle control is a `Select` rather than a read-out because the stops are\n * the API: a reader who wants 200% should not have to press \"+\" four times, and\n * the two fit modes have no number to press towards at all.\n *\n * Not a `role=\"toolbar\"` — see `FileViewerPager`. It sits in the same row, and\n * one row that is half roving-tabindex and half ordinary tab stops is worse for\n * a keyboard reader than one that is consistently ordinary.\n */\nexport const FileViewerZoom = forwardRef<HTMLDivElement, FileViewerZoomProps>(\n function FileViewerZoom({ className, ...props }, ref) {\n const { state, actions } = useFileViewer();\n const { t, formatNumber } = useLocale();\n\n if (!state.capabilities.zoom) return null;\n\n const { zoom, effectiveZoom } = state;\n const percent = (scale: number) =>\n formatNumber(scale, { style: \"percent\", maximumFractionDigits: 0 });\n\n return (\n <div\n ref={ref}\n data-slot=\"file-viewer-zoom\"\n role=\"group\"\n aria-label={t(\"viewer.zoom.controls\")}\n className={cn(\"flex shrink-0 items-center gap-1\", className)}\n {...props}\n >\n <IconButton\n variant=\"ghost\"\n size=\"icon-sm\"\n label={t(\"viewer.zoom.out\")}\n icon={<ZoomOutIcon aria-hidden=\"true\" />}\n // Stepping is measured against what is ON SCREEN, so a page fitted to\n // 137% still has somewhere to go in both directions.\n disabled={!canStepZoom(effectiveZoom, -1)}\n onClick={actions.zoomOut}\n />\n <Select\n value={zoomToValue(zoom)}\n onValueChange={(value) => actions.setZoom(valueToZoom(value))}\n >\n <SelectTrigger\n size=\"sm\"\n aria-label={t(\"viewer.zoom.level\")}\n className=\"h-7 w-24 gap-1 px-2\"\n >\n {/* Children override the selected item's own text: a fit mode has to\n read as \"Fit width\", but a fixed stop reads better as the number\n than as a row label, and both have to fit one narrow trigger. */}\n <SelectValue>\n {isZoomFit(zoom)\n ? t(zoom === \"fit-width\" ? \"viewer.zoom.fitWidth\" : \"viewer.zoom.fitPage\")\n : percent(zoom)}\n </SelectValue>\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"fit-width\">{t(\"viewer.zoom.fitWidth\")}</SelectItem>\n <SelectItem value=\"fit-page\">{t(\"viewer.zoom.fitPage\")}</SelectItem>\n <SelectSeparator />\n {VIEWER_ZOOM_STEPS.map((step) => (\n <SelectItem key={step} value={String(step)}>\n {percent(step)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <IconButton\n variant=\"ghost\"\n size=\"icon-sm\"\n label={t(\"viewer.zoom.in\")}\n icon={<ZoomInIcon aria-hidden=\"true\" />}\n disabled={!canStepZoom(effectiveZoom, 1)}\n onClick={actions.zoomIn}\n />\n {/* One live region for the group: neither a repainted canvas nor a\n `Select`'s own value change announces the new scale. */}\n <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {t(\"viewer.zoom.status\", { level: percent(effectiveZoom) })}\n </span>\n </div>\n );\n },\n);\n\n/** `Select` speaks strings; the state is a number or a fit mode. */\nfunction zoomToValue(zoom: ZoomLevel): string {\n return isZoomFit(zoom) ? zoom : String(zoom);\n}\n\nfunction valueToZoom(value: string): ZoomLevel {\n if (value === \"fit-width\" || value === \"fit-page\") return value;\n const parsed = Number.parseFloat(value);\n return Number.isNaN(parsed) ? 1 : parsed;\n}\n\nexport type FileViewerRotateProps = HTMLAttributes<HTMLButtonElement>;\n\n/**\n * One button, one quarter-turn clockwise.\n *\n * Document-level and clockwise-only on purpose: a counter-clockwise button is a\n * second control for something three presses of this one already do, and\n * per-page rotation would change the file, which is authoring rather than\n * viewing. `actions.rotate(-1)` is there for an app that wants the other one.\n */\nexport const FileViewerRotate = forwardRef<HTMLButtonElement, FileViewerRotateProps>(\n function FileViewerRotate({ className, ...props }, ref) {\n const { state, actions } = useFileViewer();\n const { t } = useLocale();\n\n if (!state.capabilities.rotate) return null;\n\n return (\n <IconButton\n ref={ref}\n data-slot=\"file-viewer-rotate\"\n variant=\"ghost\"\n size=\"icon-sm\"\n label={t(\"viewer.rotate\")}\n icon={<RotateCwIcon aria-hidden=\"true\" />}\n className={cn(\"shrink-0\", className)}\n onClick={() => actions.rotate(1)}\n {...props}\n />\n );\n },\n);\n"],"mappings":"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","names":["cn","IconButton","resolveFileKind","Separator","useLocale","forwardRef","useEffect","useRef","useState","FileViewerFind","cn","IconButton","useLocale","forwardRef","jsx","jsxs","forwardRef","FileViewerPager","useLocale","cn","IconButton","cn","IconButton","useLocale","forwardRef","jsx","jsxs","forwardRef","FileViewerZoom","useLocale","cn","IconButton","FileViewerRotate","jsx","jsxs","useState","useEffect","resolveFileKind","useRef","forwardRef","FileViewerFrame","useLocale","cn","FileViewerToolbar","Separator","IconButton","FileViewerHighlightStatus","FileViewerSkeleton","FileViewerError","FileViewerEmpty","FileViewerContent","FileViewer"]}
@@ -312,7 +312,7 @@ function PptxSlideView({
312
312
  "aria-label": t("viewer.pptx.slide", {
313
313
  slide: formatNumber(slideNumber2)
314
314
  }),
315
- className: "border-border bg-card focus-visible:ring-ring mx-auto flex aspect-video w-full max-w-3xl flex-col gap-3 overflow-auto rounded-md border p-6 shadow-sm focus-visible:outline-none focus-visible:ring-2",
315
+ className: "border-border bg-card focus-ring mx-auto flex aspect-video w-full max-w-3xl flex-col gap-3 overflow-auto rounded-md border p-6 shadow-sm",
316
316
  children: [
317
317
  /* @__PURE__ */ jsx(ProseHeading, { level: baseHeadingLevel, children: slide.title === void 0 ? t("viewer.pptx.untitled") : /* @__PURE__ */ jsx(MarkedText, { text: slide.title, marks, start: starts?.get(PPTX_TITLE_LINE) }) }),
318
318
  isEmpty ? /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-body", children: t("viewer.pptx.empty") }) : /* @__PURE__ */ jsx("ul", { className: "text-body space-y-1.5", children: slide.lines.map((line, lineIndex) => /* @__PURE__ */ jsx(
@@ -348,4 +348,4 @@ var pptx_adapter_default = adapterModule;
348
348
  export {
349
349
  pptx_adapter_default as default
350
350
  };
351
- //# sourceMappingURL=pptx-adapter-6GEQLS2Z.js.map
351
+ //# sourceMappingURL=pptx-adapter-BONWFHYT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/adapters/pptx/pptx-adapter.tsx","../src/adapters/pptx/pptx-model.ts"],"sourcesContent":["\"use client\";\n\n/**\n * PowerPoint adapter — a deck becomes a slide outline you can page through.\n *\n * A `.pptx` is a zip of XML, so there is no PowerPoint library here: jszip opens\n * the container and the platform's own `DOMParser` reads the parts. What is\n * extracted, and why it is an outline rather than a reproduction of the slide\n * canvas, is argued in `pptx-model.ts`.\n *\n * The deck is one continuous column of slides, virtualized over the shell's\n * viewport — the same treatment the PDF pages get, for the same reason: reading\n * a deck end to end is scrolling, not clicking \"next\" 60 times. The slide number\n * follows the scroll, and the pager scrolls.\n */\n\nimport type { ProseHeadingLevel, ResolvedFileSource } from \"@elabs-ai/components-ui\";\nimport { cn, ProseHeading, useLocale } from \"@elabs-ai/components-ui\";\nimport { useCallback, useEffect, useMemo, useRef } from \"react\";\n\nimport { MarkedText } from \"../../components/marked-text\";\nimport { ViewerError, toViewerError } from \"../../core/errors\";\nimport { toMarkRanges, type MarkRanges } from \"../../core/highlight-marks\";\nimport { spanAt, type TextIndex } from \"../../core/text-index\";\nimport { useScrollActiveHighlightIntoView } from \"../../core/use-highlight-scroll\";\nimport { usePageControl } from \"../../core/use-page-control\";\nimport { usePagedScroll } from \"../../core/use-paged-scroll\";\nimport { useViewportSize } from \"../../core/use-viewport-size\";\nimport type {\n AdapterDocument,\n AdapterLoadContext,\n AdapterModule,\n AdapterRendererProps,\n FileAdapter,\n} from \"../../core/types\";\nimport { pptxManifest } from \"./pptx-manifest\";\nimport {\n notesTarget,\n orderSlidePaths,\n parseNotes,\n parseSlide,\n slidesToTextWithMap,\n PPTX_NOTES_LINE,\n PPTX_TITLE_LINE,\n type PptxRef,\n type PptxSlide,\n} from \"./pptx-model\";\n\nexport interface PptxDocument extends AdapterDocument {\n kind: \"pptx\";\n slides: PptxSlide[];\n pageCount: number;\n /** Which slide and line each stretch of `text` came from. */\n textIndex?: TextIndex<PptxRef>;\n}\n\n/** Indent per outline level. Four rungs is as deep as a readable slide ever goes. */\nconst LEVEL_INDENT = [\"ps-0\", \"ps-4\", \"ps-8\", \"ps-12\"] as const;\n\n/** The slide column's own cap, matching the `max-w-3xl` on the frame. */\nconst SLIDE_MAX_WIDTH = 768;\n\n/** Space between two slides in the column, in CSS pixels. */\nconst SLIDE_GAP = 16;\n\n/** Room reserved for a slide's speaker notes before they are measured. */\nconst NOTES_ESTIMATE = 72;\n\nclass PptxAdapter implements FileAdapter {\n async load(source: ResolvedFileSource, context: AdapterLoadContext): Promise<PptxDocument> {\n let buffer: ArrayBuffer;\n try {\n buffer = await source.bytes(context.signal);\n } catch (error) {\n throw toViewerError(error, \"read-failed\", { fileName: source.name });\n }\n\n // Dynamic: the ONLY edge to the optional peer (heavy-deps:check).\n const JSZip = (await import(\"jszip\")).default;\n\n try {\n const zip = await JSZip.loadAsync(buffer);\n const parser = new DOMParser();\n const parse = (xml: string) => parser.parseFromString(xml, \"application/xml\");\n\n const slidePaths = orderSlidePaths(Object.keys(zip.files));\n if (slidePaths.length === 0) {\n throw new ViewerError(\"parse-failed\", \"The presentation contains no slides.\", {\n fileName: source.name,\n });\n }\n\n const slides: PptxSlide[] = [];\n for (const [position, path] of slidePaths.entries()) {\n if (context.signal?.aborted) break;\n const xml = await zip.file(path)?.async(\"string\");\n if (!xml) continue;\n const slide = parseSlide(parse(xml), position + 1);\n\n // Notes are best-effort: a deck with no notes part, or one whose\n // relationships do not resolve, is a complete deck — not a failure.\n const relsXml = await zip\n .file(`${dirOf(path)}/_rels/${baseOf(path)}.rels`)\n ?.async(\"string\");\n const notesPath = relsXml ? notesTarget(parse(relsXml), path) : undefined;\n const notesXml = notesPath ? await zip.file(notesPath)?.async(\"string\") : undefined;\n if (notesXml) slide.notes = parseNotes(parse(notesXml));\n\n slides.push(slide);\n }\n\n const textIndex = slidesToTextWithMap(slides);\n return {\n kind: \"pptx\",\n slides,\n pageCount: slides.length,\n text: textIndex.text,\n textIndex,\n };\n } catch (error) {\n throw toViewerError(error, \"parse-failed\", { fileName: source.name });\n }\n }\n}\n\nfunction dirOf(path: string): string {\n return path.slice(0, path.lastIndexOf(\"/\"));\n}\n\nfunction baseOf(path: string): string {\n return path.slice(path.lastIndexOf(\"/\") + 1);\n}\n\n/* -------------------------------------------------------------------------- */\n/* Renderer */\n/* -------------------------------------------------------------------------- */\n\nfunction PptxRenderer({\n document: doc,\n className,\n baseHeadingLevel = 2,\n highlights,\n activeHighlightId,\n pageNumber,\n onPageChange,\n}: AdapterRendererProps) {\n const deck = doc as PptxDocument;\n // One pager for every paginated format, so a deck's slides and a PDF's pages\n // are driven by the same shell control (ADR 0026). 1-based on the wire; this\n // renderer's array is 0-based, and the conversion stays local.\n const [slideNumber, goToSlide] = usePageControl(pageNumber, onPageChange, deck.slides.length);\n const listRef = useRef<HTMLDivElement>(null);\n const viewport = useViewportSize(listRef);\n\n const marks = useMemo(\n () => toMarkRanges(highlights, deck.text?.length ?? 0),\n [highlights, deck.text],\n );\n\n // A slide frame is 16:9 inside a capped column, so its height follows from the\n // pane's width — no measurement of the slide itself is needed for the first\n // frame. `measureElement` corrects a slide whose outline overflows the ratio,\n // and whose notes are longer than the estimate.\n const frameWidth = Math.min(SLIDE_MAX_WIDTH, (viewport?.width ?? SLIDE_MAX_WIDTH) - SLIDE_GAP);\n const estimateSize = useCallback(\n (index: number) =>\n Math.round((frameWidth * 9) / 16) +\n SLIDE_GAP +\n (deck.slides[index]?.notes ? NOTES_ESTIMATE : 0),\n [frameWidth, deck.slides],\n );\n\n const virtualizer = usePagedScroll({\n count: deck.slides.length,\n listRef,\n pageNumber: slideNumber,\n goToPage: goToSlide,\n estimateSize,\n sizeKey: frameWidth,\n });\n\n // Where each line of the deck begins in the projection, keyed by slide. Built\n // once for the whole deck rather than per slide: several slides are on screen\n // at a time now, and one pass over the spans is cheaper than one pass per\n // visible slide on every scroll.\n const starts = useMemo(() => {\n const map = new Map<number, Map<number, number>>();\n for (const span of deck.textIndex?.spans ?? []) {\n const slide = map.get(span.ref.slide) ?? new Map<number, number>();\n slide.set(span.ref.line, span.start);\n map.set(span.ref.slide, slide);\n }\n return map;\n }, [deck.textIndex]);\n\n // Scrolling to the cited slide is this format's half of \"take me there\". Keyed\n // on the slide NUMBER, so a reader who scrolls away while the same citation is\n // still active is not dragged back (the same trade the PDF column makes).\n const activeSlide = useMemo(() => {\n const active = highlights?.find((highlight) => highlight.id === activeHighlightId);\n if (!active || active.status !== \"resolved\" || !active.range || !deck.textIndex) {\n return undefined;\n }\n return spanAt(deck.textIndex, active.range[0])?.ref.slide;\n }, [highlights, activeHighlightId, deck.textIndex]);\n useEffect(() => {\n if (activeSlide === undefined) return;\n const position = deck.slides.findIndex((candidate) => candidate.index === activeSlide);\n if (position >= 0) goToSlide(position + 1);\n }, [activeSlide, deck.slides, goToSlide]);\n\n // The mark only exists once the cited slide is mounted, so the scroll waits\n // for the slide as well as for the id.\n useScrollActiveHighlightIntoView(listRef, activeHighlightId, slideNumber);\n\n return (\n // No viewport of its own: the slide chrome moved to the shell (ADR 0026), so\n // there is no fixed control above a scrolling body and `FileViewerContent`\n // is the one scroll boundary (`viewer-components.md`).\n <div\n ref={listRef}\n data-slot=\"pptx-slides\"\n className={cn(\"relative w-full\", className)}\n style={{ height: virtualizer.getTotalSize() }}\n >\n {virtualizer.getVirtualItems().map((item) => {\n const slide = deck.slides[item.index];\n if (!slide) return null;\n return (\n <div\n key={item.key}\n data-index={item.index}\n ref={virtualizer.measureElement}\n className=\"absolute inset-x-0 top-0\"\n style={{\n paddingBottom: SLIDE_GAP,\n transform: `translateY(${item.start - virtualizer.options.scrollMargin}px)`,\n }}\n >\n <PptxSlideView\n slide={slide}\n slideNumber={item.index + 1}\n baseHeadingLevel={baseHeadingLevel}\n marks={marks}\n starts={starts.get(slide.index)}\n />\n </div>\n );\n })}\n </div>\n );\n}\n\ninterface PptxSlideViewProps {\n slide: PptxSlide;\n /** 1-based position in the deck, which is what the reader is shown. */\n slideNumber: number;\n baseHeadingLevel: ProseHeadingLevel;\n marks: MarkRanges;\n /** Where each of this slide's lines begins in the projection. */\n starts?: Map<number, number>;\n}\n\n/** One slide: the 16:9 frame, its outline, and any speaker notes beneath it. */\nfunction PptxSlideView({\n slide,\n slideNumber,\n baseHeadingLevel,\n marks,\n starts,\n}: PptxSlideViewProps) {\n const { t, formatNumber } = useLocale();\n const isEmpty = !slide.title && slide.lines.length === 0;\n\n return (\n <>\n {/* The slide surface: `bg-card` above the pane's ground, `aspect-video`\n so a deck still reads as a deck — this is the shape of the thing,\n which is the one part of the layout an outline can honestly keep. */}\n {/* A slide that overflows its 16:9 frame scrolls, and a scrollable\n region with no focusable content cannot be reached by keyboard\n (WCAG 2.1.1). `tabIndex={0}` makes the slide itself the stop that\n arrow keys and Page Up/Down drive. This is the one nested scroller the\n rule allows: a fixed-ratio frame whose content may not fit it. */}\n <section\n tabIndex={0}\n data-slot=\"pptx-slide\"\n data-page={slideNumber}\n aria-label={t(\"viewer.pptx.slide\", {\n slide: formatNumber(slideNumber),\n })}\n className=\"border-border bg-card focus-ring mx-auto flex aspect-video w-full max-w-3xl flex-col gap-3 overflow-auto rounded-md border p-6 shadow-sm\"\n >\n {/* A slide title is the deck's top rung, so it sits at the host's base.\n The stand-in for an untitled slide is OUR text, not the deck's, so\n it is not in the projection and cannot be cited. */}\n <ProseHeading level={baseHeadingLevel}>\n {slide.title === undefined ? (\n t(\"viewer.pptx.untitled\")\n ) : (\n <MarkedText text={slide.title} marks={marks} start={starts?.get(PPTX_TITLE_LINE)} />\n )}\n </ProseHeading>\n {isEmpty ? (\n <p className=\"text-muted-foreground text-body\">{t(\"viewer.pptx.empty\")}</p>\n ) : (\n <ul className=\"text-body space-y-1.5\">\n {slide.lines.map((line, lineIndex) => (\n <li\n key={lineIndex}\n className={cn(\"whitespace-pre-wrap\", LEVEL_INDENT[Math.min(line.level, 3)])}\n >\n <MarkedText text={line.text} marks={marks} start={starts?.get(lineIndex)} />\n </li>\n ))}\n </ul>\n )}\n </section>\n\n {slide.notes && (\n <section\n aria-label={t(\"viewer.pptx.notes\")}\n className=\"mx-auto mt-3 w-full max-w-3xl space-y-1\"\n >\n <h3 className=\"text-meta text-muted-foreground\">{t(\"viewer.pptx.notes\")}</h3>\n <p className=\"text-body text-muted-foreground whitespace-pre-wrap\">\n <MarkedText text={slide.notes} marks={marks} start={starts?.get(PPTX_NOTES_LINE)} />\n </p>\n </section>\n )}\n </>\n );\n}\n\nconst adapterModule: AdapterModule = {\n manifest: pptxManifest,\n create: () => new PptxAdapter(),\n Renderer: PptxRenderer,\n};\n\nexport default adapterModule;\n","/**\n * PowerPoint XML → a slide outline.\n *\n * ## What a deck preview honestly is\n *\n * A `.pptx` slide is absolutely-positioned drawing: shapes at EMU coordinates,\n * theme-driven fills, transitions, embedded media. Reproducing that faithfully\n * is a rendering engine, not a preview — and a half-faithful reproduction is\n * worse than none, because it looks like the deck while quietly lying about it.\n *\n * So this reads the deck as an OUTLINE: per slide, the title, the text in\n * document order (with its indent level), and the speaker notes. That is the\n * content a reader is looking for when they open a deck in a file browser, it\n * renders in this system's typography, and it is honest about what it is.\n * Anything positional — layout, images, charts, transitions — is deliberately\n * absent; the toolbar's download is the answer for the real thing.\n *\n * Parsing goes through namespace URIs rather than `a:t` / `p:sp` tag names,\n * because the prefix is only a convention: a generator is free to bind the same\n * namespace to a different prefix, and tag-name matching would silently return\n * an empty deck.\n */\n\nimport { createTextIndexBuilder, type TextIndex } from \"../../core/text-index\";\n\n/** The DrawingML namespace — text, paragraphs, tables. */\nexport const DRAWING_NS = \"http://schemas.openxmlformats.org/drawingml/2006/main\";\n/** The PresentationML namespace — shapes, placeholders, the shape tree. */\nexport const PRESENTATION_NS = \"http://schemas.openxmlformats.org/presentationml/2006/main\";\n\n/** One line of slide text, with the indent level PowerPoint gave it. */\nexport interface PptxLine {\n text: string;\n /** Outline depth, `0` for a top-level bullet. */\n level: number;\n}\n\nexport interface PptxSlide {\n /** 1-based position in the deck. */\n index: number;\n /** The title placeholder's text, when the slide has one. */\n title?: string;\n /** Every other line, in document order. */\n lines: PptxLine[];\n /** The slide's speaker notes, when it has any. */\n notes?: string;\n}\n\n/**\n * Order slide parts the way the deck does.\n *\n * A zip lists its entries in whatever order they were written, and\n * `slide10.xml` sorts before `slide2.xml` as a string — so the number is pulled\n * out and compared as a number.\n */\nexport function orderSlidePaths(paths: string[]): string[] {\n return paths\n .filter((path) => /^ppt\\/slides\\/slide\\d+\\.xml$/.test(path))\n .sort((left, right) => slideNumber(left) - slideNumber(right));\n}\n\n/** The `N` in `…/slideN.xml`, or `0` when there is none. */\nexport function slideNumber(path: string): number {\n const match = /(\\d+)\\.xml$/.exec(path);\n return match?.[1] ? Number(match[1]) : 0;\n}\n\n/** All text under a node, in document order, as one string. */\nfunction textOf(node: Element): string {\n const runs = node.getElementsByTagNameNS(DRAWING_NS, \"t\");\n let text = \"\";\n for (const run of Array.from(runs)) text += run.textContent ?? \"\";\n return text.trim();\n}\n\n/** The paragraphs directly under a shape, as lines. Empty paragraphs are dropped. */\nfunction linesOf(shape: Element): PptxLine[] {\n const lines: PptxLine[] = [];\n for (const paragraph of Array.from(shape.getElementsByTagNameNS(DRAWING_NS, \"p\"))) {\n const text = textOf(paragraph);\n if (!text) continue;\n // `lvl` is absent for a top-level bullet, which is the common case.\n const properties = paragraph.getElementsByTagNameNS(DRAWING_NS, \"pPr\")[0];\n const level = Number(properties?.getAttribute(\"lvl\") ?? 0);\n lines.push({ text, level: Number.isFinite(level) ? level : 0 });\n }\n return lines;\n}\n\n/** The placeholder type a shape declares (`title`, `ctrTitle`, `body`, …), if any. */\nfunction placeholderType(shape: Element): string | undefined {\n const placeholder = shape.getElementsByTagNameNS(PRESENTATION_NS, \"ph\")[0];\n return placeholder?.getAttribute(\"type\") ?? undefined;\n}\n\n/** Table text, row by row, tab-separated — a table on a slide is usually the point of it. */\nfunction tableLines(frame: Element): PptxLine[] {\n const lines: PptxLine[] = [];\n for (const row of Array.from(frame.getElementsByTagNameNS(DRAWING_NS, \"tr\"))) {\n const cells = Array.from(row.getElementsByTagNameNS(DRAWING_NS, \"tc\")).map(textOf);\n const text = cells.join(\"\\t\").trim();\n if (text) lines.push({ text, level: 0 });\n }\n return lines;\n}\n\n/**\n * Parse one `ppt/slides/slideN.xml` document into a slide.\n *\n * The shape tree is walked in document order, so the outline reads in the order\n * the shapes were authored rather than in the order the file happens to store\n * them. A shape kind this does not name contributes nothing — the same\n * allowlist-by-parse the Word adapter uses.\n */\nexport function parseSlide(document: Document, index: number): PptxSlide {\n const tree = document.getElementsByTagNameNS(PRESENTATION_NS, \"spTree\")[0];\n const slide: PptxSlide = { index, lines: [] };\n if (!tree) return slide;\n\n for (const node of Array.from(tree.children)) {\n if (node.namespaceURI !== PRESENTATION_NS) continue;\n if (node.localName === \"sp\") {\n const type = placeholderType(node);\n const lines = linesOf(node);\n if ((type === \"title\" || type === \"ctrTitle\") && !slide.title) {\n // A title placeholder can hold several paragraphs; they are one heading.\n const title = lines.map((line) => line.text).join(\" \");\n if (title) slide.title = title;\n continue;\n }\n slide.lines.push(...lines);\n continue;\n }\n if (node.localName === \"graphicFrame\") slide.lines.push(...tableLines(node));\n }\n return slide;\n}\n\n/** Parse a `ppt/notesSlides/notesSlideN.xml` document into its notes text. */\nexport function parseNotes(document: Document): string | undefined {\n const tree = document.getElementsByTagNameNS(PRESENTATION_NS, \"spTree\")[0];\n if (!tree) return undefined;\n\n const lines: string[] = [];\n for (const shape of Array.from(tree.getElementsByTagNameNS(PRESENTATION_NS, \"sp\"))) {\n // The notes part repeats the slide's own body as a non-editable copy; only\n // the notes placeholder holds what the presenter actually wrote.\n if (placeholderType(shape) !== \"body\") continue;\n for (const line of linesOf(shape)) lines.push(line.text);\n }\n const notes = lines.join(\"\\n\").trim();\n return notes || undefined;\n}\n\n/** The OPC relationships namespace — how a part points at another part. */\nexport const RELATIONSHIP_NS = \"http://schemas.openxmlformats.org/package/2006/relationships\";\n\n/**\n * Resolve a relationship target (`../notesSlides/notesSlide1.xml`) against the\n * part that declared it (`ppt/slides/slide1.xml`).\n *\n * Zip entry names are plain strings, not URLs, so `..` has to be walked by hand.\n */\nexport function resolveRelative(fromPart: string, target: string): string {\n if (target.startsWith(\"/\")) return target.slice(1);\n const segments = fromPart.split(\"/\").slice(0, -1);\n for (const segment of target.split(\"/\")) {\n if (segment === \".\" || segment === \"\") continue;\n if (segment === \"..\") segments.pop();\n else segments.push(segment);\n }\n return segments.join(\"/\");\n}\n\n/**\n * The notes part a slide points at, if any.\n *\n * Read from the slide's own `_rels`, not by matching `slideN` to `notesSlideN`:\n * the numbers agree in decks PowerPoint wrote from scratch and drift in decks\n * that have had slides deleted, which would attach the wrong presenter notes to\n * the wrong slide — a quiet, plausible-looking error.\n */\nexport function notesTarget(rels: Document, slidePath: string): string | undefined {\n for (const relationship of Array.from(\n rels.getElementsByTagNameNS(RELATIONSHIP_NS, \"Relationship\"),\n )) {\n if (!relationship.getAttribute(\"Type\")?.endsWith(\"/notesSlide\")) continue;\n const target = relationship.getAttribute(\"Target\");\n if (target) return resolveRelative(slidePath, target);\n }\n return undefined;\n}\n\n/** Between two lines of the same slide. */\nexport const PPTX_LINE_SEPARATOR = \"\\n\";\n/** Between two slides. */\nexport const PPTX_SLIDE_SEPARATOR = \"\\n\\n\";\n\n/** The `line` of a slide's title. */\nexport const PPTX_TITLE_LINE = -1;\n/** The `line` of a slide's speaker notes. */\nexport const PPTX_NOTES_LINE = -2;\n\n/** Where a stretch of the projection came from in the outline. */\nexport interface PptxRef {\n /** 1-based slide position, the same number {@link PptxSlide.index} carries. */\n slide: number;\n /** Body-line index, or {@link PPTX_TITLE_LINE} / {@link PPTX_NOTES_LINE}. */\n line: number;\n}\n\n/**\n * Plain-text projection of a deck, plus the map back into the outline.\n *\n * A slide's title, its lines and its notes are each their own chunk, so a\n * citation resolves to the exact line the renderer draws rather than to \"slide\n * 4\". The blank line between slides is written before whichever chunk turns out\n * to be the next slide's first — a slide with no text at all contributes\n * nothing, instead of a run of empty lines that would shift every offset after\n * it.\n */\nexport function slidesToTextWithMap(slides: PptxSlide[]): TextIndex<PptxRef> {\n const builder = createTextIndexBuilder<PptxRef>({ separator: PPTX_LINE_SEPARATOR });\n slides.forEach((slide, index) => {\n let separator = index === 0 ? undefined : PPTX_SLIDE_SEPARATOR;\n const push = (chunk: string | undefined, line: number) => {\n if (!chunk) return;\n builder.push(chunk, { slide: slide.index, line }, separator);\n separator = undefined;\n };\n\n push(slide.title, PPTX_TITLE_LINE);\n slide.lines.forEach((line, lineIndex) => {\n push(line.text, lineIndex);\n });\n push(slide.notes, PPTX_NOTES_LINE);\n });\n return builder.build();\n}\n\n/**\n * Plain-text projection of a deck — powers search, copy and the raw view. A thin\n * wrapper so the projection has exactly one definition: it is the index's own\n * text, never a second assembly that could drift from it.\n */\nexport function slidesToText(slides: PptxSlide[]): string {\n return slidesToTextWithMap(slides).text;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAiBA,SAAS,IAAI,cAAc,iBAAiB;AAC5C,SAAS,aAAa,WAAW,SAAS,cAAc;;;ACQjD,IAAM,aAAa;AAEnB,IAAM,kBAAkB;AA2BxB,SAAS,gBAAgB,OAA2B;AACzD,SAAO,MACJ,OAAO,CAAC,SAAS,+BAA+B,KAAK,IAAI,CAAC,EAC1D,KAAK,CAAC,MAAM,UAAU,YAAY,IAAI,IAAI,YAAY,KAAK,CAAC;AACjE;AAGO,SAAS,YAAY,MAAsB;AAChD,QAAM,QAAQ,cAAc,KAAK,IAAI;AACrC,SAAO,QAAQ,CAAC,IAAI,OAAO,MAAM,CAAC,CAAC,IAAI;AACzC;AAGA,SAAS,OAAO,MAAuB;AACrC,QAAM,OAAO,KAAK,uBAAuB,YAAY,GAAG;AACxD,MAAI,OAAO;AACX,aAAW,OAAO,MAAM,KAAK,IAAI,EAAG,SAAQ,IAAI,eAAe;AAC/D,SAAO,KAAK,KAAK;AACnB;AAGA,SAAS,QAAQ,OAA4B;AAC3C,QAAM,QAAoB,CAAC;AAC3B,aAAW,aAAa,MAAM,KAAK,MAAM,uBAAuB,YAAY,GAAG,CAAC,GAAG;AACjF,UAAM,OAAO,OAAO,SAAS;AAC7B,QAAI,CAAC,KAAM;AAEX,UAAM,aAAa,UAAU,uBAAuB,YAAY,KAAK,EAAE,CAAC;AACxE,UAAM,QAAQ,OAAO,YAAY,aAAa,KAAK,KAAK,CAAC;AACzD,UAAM,KAAK,EAAE,MAAM,OAAO,OAAO,SAAS,KAAK,IAAI,QAAQ,EAAE,CAAC;AAAA,EAChE;AACA,SAAO;AACT;AAGA,SAAS,gBAAgB,OAAoC;AAC3D,QAAM,cAAc,MAAM,uBAAuB,iBAAiB,IAAI,EAAE,CAAC;AACzE,SAAO,aAAa,aAAa,MAAM,KAAK;AAC9C;AAGA,SAAS,WAAW,OAA4B;AAC9C,QAAM,QAAoB,CAAC;AAC3B,aAAW,OAAO,MAAM,KAAK,MAAM,uBAAuB,YAAY,IAAI,CAAC,GAAG;AAC5E,UAAM,QAAQ,MAAM,KAAK,IAAI,uBAAuB,YAAY,IAAI,CAAC,EAAE,IAAI,MAAM;AACjF,UAAM,OAAO,MAAM,KAAK,GAAI,EAAE,KAAK;AACnC,QAAI,KAAM,OAAM,KAAK,EAAE,MAAM,OAAO,EAAE,CAAC;AAAA,EACzC;AACA,SAAO;AACT;AAUO,SAAS,WAAW,UAAoB,OAA0B;AACvE,QAAM,OAAO,SAAS,uBAAuB,iBAAiB,QAAQ,EAAE,CAAC;AACzE,QAAM,QAAmB,EAAE,OAAO,OAAO,CAAC,EAAE;AAC5C,MAAI,CAAC,KAAM,QAAO;AAElB,aAAW,QAAQ,MAAM,KAAK,KAAK,QAAQ,GAAG;AAC5C,QAAI,KAAK,iBAAiB,gBAAiB;AAC3C,QAAI,KAAK,cAAc,MAAM;AAC3B,YAAM,OAAO,gBAAgB,IAAI;AACjC,YAAM,QAAQ,QAAQ,IAAI;AAC1B,WAAK,SAAS,WAAW,SAAS,eAAe,CAAC,MAAM,OAAO;AAE7D,cAAM,QAAQ,MAAM,IAAI,CAAC,SAAS,KAAK,IAAI,EAAE,KAAK,GAAG;AACrD,YAAI,MAAO,OAAM,QAAQ;AACzB;AAAA,MACF;AACA,YAAM,MAAM,KAAK,GAAG,KAAK;AACzB;AAAA,IACF;AACA,QAAI,KAAK,cAAc,eAAgB,OAAM,MAAM,KAAK,GAAG,WAAW,IAAI,CAAC;AAAA,EAC7E;AACA,SAAO;AACT;AAGO,SAAS,WAAW,UAAwC;AACjE,QAAM,OAAO,SAAS,uBAAuB,iBAAiB,QAAQ,EAAE,CAAC;AACzE,MAAI,CAAC,KAAM,QAAO;AAElB,QAAM,QAAkB,CAAC;AACzB,aAAW,SAAS,MAAM,KAAK,KAAK,uBAAuB,iBAAiB,IAAI,CAAC,GAAG;AAGlF,QAAI,gBAAgB,KAAK,MAAM,OAAQ;AACvC,eAAW,QAAQ,QAAQ,KAAK,EAAG,OAAM,KAAK,KAAK,IAAI;AAAA,EACzD;AACA,QAAM,QAAQ,MAAM,KAAK,IAAI,EAAE,KAAK;AACpC,SAAO,SAAS;AAClB;AAGO,IAAM,kBAAkB;AAQxB,SAAS,gBAAgB,UAAkB,QAAwB;AACxE,MAAI,OAAO,WAAW,GAAG,EAAG,QAAO,OAAO,MAAM,CAAC;AACjD,QAAM,WAAW,SAAS,MAAM,GAAG,EAAE,MAAM,GAAG,EAAE;AAChD,aAAW,WAAW,OAAO,MAAM,GAAG,GAAG;AACvC,QAAI,YAAY,OAAO,YAAY,GAAI;AACvC,QAAI,YAAY,KAAM,UAAS,IAAI;AAAA,QAC9B,UAAS,KAAK,OAAO;AAAA,EAC5B;AACA,SAAO,SAAS,KAAK,GAAG;AAC1B;AAUO,SAAS,YAAY,MAAgB,WAAuC;AACjF,aAAW,gBAAgB,MAAM;AAAA,IAC/B,KAAK,uBAAuB,iBAAiB,cAAc;AAAA,EAC7D,GAAG;AACD,QAAI,CAAC,aAAa,aAAa,MAAM,GAAG,SAAS,aAAa,EAAG;AACjE,UAAM,SAAS,aAAa,aAAa,QAAQ;AACjD,QAAI,OAAQ,QAAO,gBAAgB,WAAW,MAAM;AAAA,EACtD;AACA,SAAO;AACT;AAGO,IAAM,sBAAsB;AAE5B,IAAM,uBAAuB;AAG7B,IAAM,kBAAkB;AAExB,IAAM,kBAAkB;AAoBxB,SAAS,oBAAoB,QAAyC;AAC3E,QAAM,UAAU,uBAAgC,EAAE,WAAW,oBAAoB,CAAC;AAClF,SAAO,QAAQ,CAAC,OAAO,UAAU;AAC/B,QAAI,YAAY,UAAU,IAAI,SAAY;AAC1C,UAAM,OAAO,CAAC,OAA2B,SAAiB;AACxD,UAAI,CAAC,MAAO;AACZ,cAAQ,KAAK,OAAO,EAAE,OAAO,MAAM,OAAO,KAAK,GAAG,SAAS;AAC3D,kBAAY;AAAA,IACd;AAEA,SAAK,MAAM,OAAO,eAAe;AACjC,UAAM,MAAM,QAAQ,CAAC,MAAM,cAAc;AACvC,WAAK,KAAK,MAAM,SAAS;AAAA,IAC3B,CAAC;AACD,SAAK,MAAM,OAAO,eAAe;AAAA,EACnC,CAAC;AACD,SAAO,QAAQ,MAAM;AACvB;;;ADCY,SAoCR,UApCQ,KA6CN,YA7CM;AAtLZ,IAAM,eAAe,CAAC,QAAQ,QAAQ,QAAQ,OAAO;AAGrD,IAAM,kBAAkB;AAGxB,IAAM,YAAY;AAGlB,IAAM,iBAAiB;AAEvB,IAAM,cAAN,MAAyC;AAAA,EACvC,MAAM,KAAK,QAA4B,SAAoD;AACzF,QAAI;AACJ,QAAI;AACF,eAAS,MAAM,OAAO,MAAM,QAAQ,MAAM;AAAA,IAC5C,SAAS,OAAO;AACd,YAAM,cAAc,OAAO,eAAe,EAAE,UAAU,OAAO,KAAK,CAAC;AAAA,IACrE;AAGA,UAAM,SAAS,MAAM,OAAO,OAAO,GAAG;AAEtC,QAAI;AACF,YAAM,MAAM,MAAM,MAAM,UAAU,MAAM;AACxC,YAAM,SAAS,IAAI,UAAU;AAC7B,YAAM,QAAQ,CAAC,QAAgB,OAAO,gBAAgB,KAAK,iBAAiB;AAE5E,YAAM,aAAa,gBAAgB,OAAO,KAAK,IAAI,KAAK,CAAC;AACzD,UAAI,WAAW,WAAW,GAAG;AAC3B,cAAM,IAAI,YAAY,gBAAgB,wCAAwC;AAAA,UAC5E,UAAU,OAAO;AAAA,QACnB,CAAC;AAAA,MACH;AAEA,YAAM,SAAsB,CAAC;AAC7B,iBAAW,CAAC,UAAU,IAAI,KAAK,WAAW,QAAQ,GAAG;AACnD,YAAI,QAAQ,QAAQ,QAAS;AAC7B,cAAM,MAAM,MAAM,IAAI,KAAK,IAAI,GAAG,MAAM,QAAQ;AAChD,YAAI,CAAC,IAAK;AACV,cAAM,QAAQ,WAAW,MAAM,GAAG,GAAG,WAAW,CAAC;AAIjD,cAAM,UAAU,MAAM,IACnB,KAAK,GAAG,MAAM,IAAI,CAAC,UAAU,OAAO,IAAI,CAAC,OAAO,GAC/C,MAAM,QAAQ;AAClB,cAAM,YAAY,UAAU,YAAY,MAAM,OAAO,GAAG,IAAI,IAAI;AAChE,cAAM,WAAW,YAAY,MAAM,IAAI,KAAK,SAAS,GAAG,MAAM,QAAQ,IAAI;AAC1E,YAAI,SAAU,OAAM,QAAQ,WAAW,MAAM,QAAQ,CAAC;AAEtD,eAAO,KAAK,KAAK;AAAA,MACnB;AAEA,YAAM,YAAY,oBAAoB,MAAM;AAC5C,aAAO;AAAA,QACL,MAAM;AAAA,QACN;AAAA,QACA,WAAW,OAAO;AAAA,QAClB,MAAM,UAAU;AAAA,QAChB;AAAA,MACF;AAAA,IACF,SAAS,OAAO;AACd,YAAM,cAAc,OAAO,gBAAgB,EAAE,UAAU,OAAO,KAAK,CAAC;AAAA,IACtE;AAAA,EACF;AACF;AAEA,SAAS,MAAM,MAAsB;AACnC,SAAO,KAAK,MAAM,GAAG,KAAK,YAAY,GAAG,CAAC;AAC5C;AAEA,SAAS,OAAO,MAAsB;AACpC,SAAO,KAAK,MAAM,KAAK,YAAY,GAAG,IAAI,CAAC;AAC7C;AAMA,SAAS,aAAa;AAAA,EACpB,UAAU;AAAA,EACV;AAAA,EACA,mBAAmB;AAAA,EACnB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAyB;AACvB,QAAM,OAAO;AAIb,QAAM,CAACA,cAAa,SAAS,IAAI,eAAe,YAAY,cAAc,KAAK,OAAO,MAAM;AAC5F,QAAM,UAAU,OAAuB,IAAI;AAC3C,QAAM,WAAW,gBAAgB,OAAO;AAExC,QAAM,QAAQ;AAAA,IACZ,MAAM,aAAa,YAAY,KAAK,MAAM,UAAU,CAAC;AAAA,IACrD,CAAC,YAAY,KAAK,IAAI;AAAA,EACxB;AAMA,QAAM,aAAa,KAAK,IAAI,kBAAkB,UAAU,SAAS,mBAAmB,SAAS;AAC7F,QAAM,eAAe;AAAA,IACnB,CAAC,UACC,KAAK,MAAO,aAAa,IAAK,EAAE,IAChC,aACC,KAAK,OAAO,KAAK,GAAG,QAAQ,iBAAiB;AAAA,IAChD,CAAC,YAAY,KAAK,MAAM;AAAA,EAC1B;AAEA,QAAM,cAAc,eAAe;AAAA,IACjC,OAAO,KAAK,OAAO;AAAA,IACnB;AAAA,IACA,YAAYA;AAAA,IACZ,UAAU;AAAA,IACV;AAAA,IACA,SAAS;AAAA,EACX,CAAC;AAMD,QAAM,SAAS,QAAQ,MAAM;AAC3B,UAAM,MAAM,oBAAI,IAAiC;AACjD,eAAW,QAAQ,KAAK,WAAW,SAAS,CAAC,GAAG;AAC9C,YAAM,QAAQ,IAAI,IAAI,KAAK,IAAI,KAAK,KAAK,oBAAI,IAAoB;AACjE,YAAM,IAAI,KAAK,IAAI,MAAM,KAAK,KAAK;AACnC,UAAI,IAAI,KAAK,IAAI,OAAO,KAAK;AAAA,IAC/B;AACA,WAAO;AAAA,EACT,GAAG,CAAC,KAAK,SAAS,CAAC;AAKnB,QAAM,cAAc,QAAQ,MAAM;AAChC,UAAM,SAAS,YAAY,KAAK,CAAC,cAAc,UAAU,OAAO,iBAAiB;AACjF,QAAI,CAAC,UAAU,OAAO,WAAW,cAAc,CAAC,OAAO,SAAS,CAAC,KAAK,WAAW;AAC/E,aAAO;AAAA,IACT;AACA,WAAO,OAAO,KAAK,WAAW,OAAO,MAAM,CAAC,CAAC,GAAG,IAAI;AAAA,EACtD,GAAG,CAAC,YAAY,mBAAmB,KAAK,SAAS,CAAC;AAClD,YAAU,MAAM;AACd,QAAI,gBAAgB,OAAW;AAC/B,UAAM,WAAW,KAAK,OAAO,UAAU,CAAC,cAAc,UAAU,UAAU,WAAW;AACrF,QAAI,YAAY,EAAG,WAAU,WAAW,CAAC;AAAA,EAC3C,GAAG,CAAC,aAAa,KAAK,QAAQ,SAAS,CAAC;AAIxC,mCAAiC,SAAS,mBAAmBA,YAAW;AAExE;AAAA;AAAA;AAAA;AAAA,IAIE;AAAA,MAAC;AAAA;AAAA,QACC,KAAK;AAAA,QACL,aAAU;AAAA,QACV,WAAW,GAAG,mBAAmB,SAAS;AAAA,QAC1C,OAAO,EAAE,QAAQ,YAAY,aAAa,EAAE;AAAA,QAE3C,sBAAY,gBAAgB,EAAE,IAAI,CAAC,SAAS;AAC3C,gBAAM,QAAQ,KAAK,OAAO,KAAK,KAAK;AACpC,cAAI,CAAC,MAAO,QAAO;AACnB,iBACE;AAAA,YAAC;AAAA;AAAA,cAEC,cAAY,KAAK;AAAA,cACjB,KAAK,YAAY;AAAA,cACjB,WAAU;AAAA,cACV,OAAO;AAAA,gBACL,eAAe;AAAA,gBACf,WAAW,cAAc,KAAK,QAAQ,YAAY,QAAQ,YAAY;AAAA,cACxE;AAAA,cAEA;AAAA,gBAAC;AAAA;AAAA,kBACC;AAAA,kBACA,aAAa,KAAK,QAAQ;AAAA,kBAC1B;AAAA,kBACA;AAAA,kBACA,QAAQ,OAAO,IAAI,MAAM,KAAK;AAAA;AAAA,cAChC;AAAA;AAAA,YAfK,KAAK;AAAA,UAgBZ;AAAA,QAEJ,CAAC;AAAA;AAAA,IACH;AAAA;AAEJ;AAaA,SAAS,cAAc;AAAA,EACrB;AAAA,EACA,aAAAA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAuB;AACrB,QAAM,EAAE,GAAG,aAAa,IAAI,UAAU;AACtC,QAAM,UAAU,CAAC,MAAM,SAAS,MAAM,MAAM,WAAW;AAEvD,SACE,iCASE;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,UAAU;AAAA,QACV,aAAU;AAAA,QACV,aAAWA;AAAA,QACX,cAAY,EAAE,qBAAqB;AAAA,UACjC,OAAO,aAAaA,YAAW;AAAA,QACjC,CAAC;AAAA,QACD,WAAU;AAAA,QAKV;AAAA,8BAAC,gBAAa,OAAO,kBAClB,gBAAM,UAAU,SACf,EAAE,sBAAsB,IAExB,oBAAC,cAAW,MAAM,MAAM,OAAO,OAAc,OAAO,QAAQ,IAAI,eAAe,GAAG,GAEtF;AAAA,UACC,UACC,oBAAC,OAAE,WAAU,mCAAmC,YAAE,mBAAmB,GAAE,IAEvE,oBAAC,QAAG,WAAU,yBACX,gBAAM,MAAM,IAAI,CAAC,MAAM,cACtB;AAAA,YAAC;AAAA;AAAA,cAEC,WAAW,GAAG,uBAAuB,aAAa,KAAK,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC;AAAA,cAE1E,8BAAC,cAAW,MAAM,KAAK,MAAM,OAAc,OAAO,QAAQ,IAAI,SAAS,GAAG;AAAA;AAAA,YAHrE;AAAA,UAIP,CACD,GACH;AAAA;AAAA;AAAA,IAEJ;AAAA,IAEC,MAAM,SACL;AAAA,MAAC;AAAA;AAAA,QACC,cAAY,EAAE,mBAAmB;AAAA,QACjC,WAAU;AAAA,QAEV;AAAA,8BAAC,QAAG,WAAU,mCAAmC,YAAE,mBAAmB,GAAE;AAAA,UACxE,oBAAC,OAAE,WAAU,uDACX,8BAAC,cAAW,MAAM,MAAM,OAAO,OAAc,OAAO,QAAQ,IAAI,eAAe,GAAG,GACpF;AAAA;AAAA;AAAA,IACF;AAAA,KAEJ;AAEJ;AAEA,IAAM,gBAA+B;AAAA,EACnC,UAAU;AAAA,EACV,QAAQ,MAAM,IAAI,YAAY;AAAA,EAC9B,UAAU;AACZ;AAEA,IAAO,uBAAQ;","names":["slideNumber"]}
@@ -6,7 +6,7 @@ import {
6
6
  import {
7
7
  SheetTable,
8
8
  gridToText
9
- } from "./chunk-NMA57QZ7.js";
9
+ } from "./chunk-XBOR2YXS.js";
10
10
  import "./chunk-2NQ4RSJ3.js";
11
11
  import {
12
12
  spanAt
@@ -164,4 +164,4 @@ export {
164
164
  xlsx_adapter_default as default,
165
165
  looksLikeWorkbook
166
166
  };
167
- //# sourceMappingURL=xlsx-adapter-CM2Y6AKQ.js.map
167
+ //# sourceMappingURL=xlsx-adapter-O3XIVLFC.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elabs-ai/components-viewer",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -39,8 +39,8 @@
39
39
  "jszip": "^3.10.1",
40
40
  "shiki": "^3.22.0",
41
41
  "streamdown": "^2.4.0",
42
- "@elabs-ai/components-tokens": "4.0.0",
43
- "@elabs-ai/components-ui": "4.0.0"
42
+ "@elabs-ai/components-tokens": "4.1.0",
43
+ "@elabs-ai/components-ui": "4.1.0"
44
44
  },
45
45
  "peerDependenciesMeta": {
46
46
  "papaparse": {
@@ -88,9 +88,9 @@
88
88
  "typescript": "^5.7.3",
89
89
  "vitest": "^3.0.2",
90
90
  "@elabs-ai/components-eslint-config": "0.1.0",
91
- "@elabs-ai/components-typescript-config": "0.1.0",
92
- "@elabs-ai/components-tokens": "4.0.0",
93
- "@elabs-ai/components-ui": "4.0.0"
91
+ "@elabs-ai/components-tokens": "4.1.0",
92
+ "@elabs-ai/components-ui": "4.1.0",
93
+ "@elabs-ai/components-typescript-config": "0.1.0"
94
94
  },
95
95
  "scripts": {
96
96
  "build": "tsup && node ../../scripts/link-dist-css.mjs",
@@ -289,7 +289,7 @@ function PptxSlideView({
289
289
  aria-label={t("viewer.pptx.slide", {
290
290
  slide: formatNumber(slideNumber),
291
291
  })}
292
- className="border-border bg-card focus-visible:ring-ring mx-auto flex aspect-video w-full max-w-3xl flex-col gap-3 overflow-auto rounded-md border p-6 shadow-sm focus-visible:outline-none focus-visible:ring-2"
292
+ className="border-border bg-card focus-ring mx-auto flex aspect-video w-full max-w-3xl flex-col gap-3 overflow-auto rounded-md border p-6 shadow-sm"
293
293
  >
294
294
  {/* A slide title is the deck's top rung, so it sits at the host's base.
295
295
  The stand-in for an untitled slide is OUR text, not the deck's, so
@@ -88,7 +88,7 @@ export function SheetTable({
88
88
  tabIndex={0}
89
89
  role="group"
90
90
  aria-label={t("viewer.content")}
91
- className="focus-visible:ring-ring min-h-0 flex-1 overflow-auto focus-visible:outline-none focus-visible:ring-2"
91
+ className="focus-ring min-h-0 flex-1 overflow-auto"
92
92
  >
93
93
  <Table>
94
94
  <TableCaption className="sr-only">
@@ -199,6 +199,20 @@ export const PlainText: Story = {
199
199
  * is a deliberate trade: fenced code renders as an unhighlighted block rather
200
200
  * than costing a consumer four more packages to open one file. A source file
201
201
  * opened directly still gets Shiki — see `Source code` below.
202
+ *
203
+ * Pick a markdown renderer by where the markdown is going to be READ: a file the
204
+ * app did not write, opened here → this adapter; a read-only document in a chat
205
+ * or a side rail → `AI/MarkdownView`; the preview pane of the markdown editor →
206
+ * `Editor/MarkdownPreview`; streaming into a message as the model writes it →
207
+ * `MessageResponse` on `AI/Message`. See
208
+ * [Choosing between similar components](?path=/docs/docs-choosing-between-similar-components--docs).
209
+ *
210
+ * The element map here is deliberately a near-copy of `MarkdownView`'s, not a
211
+ * shared module: `@elabs-ai/components-ai`, `@elabs-ai/components-editor` and
212
+ * `@elabs-ai/components-viewer` are leaves that may not import one another, and
213
+ * the half that could move down (the `Prose*` primitives, the Streamdown locale
214
+ * bridge) already has. What differs is the job — a file arrives settled or not at
215
+ * all, so this adapter takes no plugins and does not stream.
202
216
  */
203
217
  export const Markdown: Story = {
204
218
  args: { source: { kind: "text", text: MARKDOWN, name: "README.md" } },
@@ -1013,10 +1013,7 @@ export const FileViewerContent = forwardRef<HTMLDivElement, FileViewerContentPro
1013
1013
  role="region"
1014
1014
  aria-label={t("viewer.content")}
1015
1015
  tabIndex={0}
1016
- className={cn(
1017
- "focus-visible:ring-ring min-h-0 flex-1 overflow-auto p-4 focus-visible:outline-none focus-visible:ring-2",
1018
- className,
1019
- )}
1016
+ className={cn("focus-ring min-h-0 flex-1 overflow-auto p-4", className)}
1020
1017
  {...props}
1021
1018
  >
1022
1019
  {state.status === "empty" && <FileViewerEmpty />}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/adapters/pptx/pptx-adapter.tsx","../src/adapters/pptx/pptx-model.ts"],"sourcesContent":["\"use client\";\n\n/**\n * PowerPoint adapter — a deck becomes a slide outline you can page through.\n *\n * A `.pptx` is a zip of XML, so there is no PowerPoint library here: jszip opens\n * the container and the platform's own `DOMParser` reads the parts. What is\n * extracted, and why it is an outline rather than a reproduction of the slide\n * canvas, is argued in `pptx-model.ts`.\n *\n * The deck is one continuous column of slides, virtualized over the shell's\n * viewport — the same treatment the PDF pages get, for the same reason: reading\n * a deck end to end is scrolling, not clicking \"next\" 60 times. The slide number\n * follows the scroll, and the pager scrolls.\n */\n\nimport type { ProseHeadingLevel, ResolvedFileSource } from \"@elabs-ai/components-ui\";\nimport { cn, ProseHeading, useLocale } from \"@elabs-ai/components-ui\";\nimport { useCallback, useEffect, useMemo, useRef } from \"react\";\n\nimport { MarkedText } from \"../../components/marked-text\";\nimport { ViewerError, toViewerError } from \"../../core/errors\";\nimport { toMarkRanges, type MarkRanges } from \"../../core/highlight-marks\";\nimport { spanAt, type TextIndex } from \"../../core/text-index\";\nimport { useScrollActiveHighlightIntoView } from \"../../core/use-highlight-scroll\";\nimport { usePageControl } from \"../../core/use-page-control\";\nimport { usePagedScroll } from \"../../core/use-paged-scroll\";\nimport { useViewportSize } from \"../../core/use-viewport-size\";\nimport type {\n AdapterDocument,\n AdapterLoadContext,\n AdapterModule,\n AdapterRendererProps,\n FileAdapter,\n} from \"../../core/types\";\nimport { pptxManifest } from \"./pptx-manifest\";\nimport {\n notesTarget,\n orderSlidePaths,\n parseNotes,\n parseSlide,\n slidesToTextWithMap,\n PPTX_NOTES_LINE,\n PPTX_TITLE_LINE,\n type PptxRef,\n type PptxSlide,\n} from \"./pptx-model\";\n\nexport interface PptxDocument extends AdapterDocument {\n kind: \"pptx\";\n slides: PptxSlide[];\n pageCount: number;\n /** Which slide and line each stretch of `text` came from. */\n textIndex?: TextIndex<PptxRef>;\n}\n\n/** Indent per outline level. Four rungs is as deep as a readable slide ever goes. */\nconst LEVEL_INDENT = [\"ps-0\", \"ps-4\", \"ps-8\", \"ps-12\"] as const;\n\n/** The slide column's own cap, matching the `max-w-3xl` on the frame. */\nconst SLIDE_MAX_WIDTH = 768;\n\n/** Space between two slides in the column, in CSS pixels. */\nconst SLIDE_GAP = 16;\n\n/** Room reserved for a slide's speaker notes before they are measured. */\nconst NOTES_ESTIMATE = 72;\n\nclass PptxAdapter implements FileAdapter {\n async load(source: ResolvedFileSource, context: AdapterLoadContext): Promise<PptxDocument> {\n let buffer: ArrayBuffer;\n try {\n buffer = await source.bytes(context.signal);\n } catch (error) {\n throw toViewerError(error, \"read-failed\", { fileName: source.name });\n }\n\n // Dynamic: the ONLY edge to the optional peer (heavy-deps:check).\n const JSZip = (await import(\"jszip\")).default;\n\n try {\n const zip = await JSZip.loadAsync(buffer);\n const parser = new DOMParser();\n const parse = (xml: string) => parser.parseFromString(xml, \"application/xml\");\n\n const slidePaths = orderSlidePaths(Object.keys(zip.files));\n if (slidePaths.length === 0) {\n throw new ViewerError(\"parse-failed\", \"The presentation contains no slides.\", {\n fileName: source.name,\n });\n }\n\n const slides: PptxSlide[] = [];\n for (const [position, path] of slidePaths.entries()) {\n if (context.signal?.aborted) break;\n const xml = await zip.file(path)?.async(\"string\");\n if (!xml) continue;\n const slide = parseSlide(parse(xml), position + 1);\n\n // Notes are best-effort: a deck with no notes part, or one whose\n // relationships do not resolve, is a complete deck — not a failure.\n const relsXml = await zip\n .file(`${dirOf(path)}/_rels/${baseOf(path)}.rels`)\n ?.async(\"string\");\n const notesPath = relsXml ? notesTarget(parse(relsXml), path) : undefined;\n const notesXml = notesPath ? await zip.file(notesPath)?.async(\"string\") : undefined;\n if (notesXml) slide.notes = parseNotes(parse(notesXml));\n\n slides.push(slide);\n }\n\n const textIndex = slidesToTextWithMap(slides);\n return {\n kind: \"pptx\",\n slides,\n pageCount: slides.length,\n text: textIndex.text,\n textIndex,\n };\n } catch (error) {\n throw toViewerError(error, \"parse-failed\", { fileName: source.name });\n }\n }\n}\n\nfunction dirOf(path: string): string {\n return path.slice(0, path.lastIndexOf(\"/\"));\n}\n\nfunction baseOf(path: string): string {\n return path.slice(path.lastIndexOf(\"/\") + 1);\n}\n\n/* -------------------------------------------------------------------------- */\n/* Renderer */\n/* -------------------------------------------------------------------------- */\n\nfunction PptxRenderer({\n document: doc,\n className,\n baseHeadingLevel = 2,\n highlights,\n activeHighlightId,\n pageNumber,\n onPageChange,\n}: AdapterRendererProps) {\n const deck = doc as PptxDocument;\n // One pager for every paginated format, so a deck's slides and a PDF's pages\n // are driven by the same shell control (ADR 0026). 1-based on the wire; this\n // renderer's array is 0-based, and the conversion stays local.\n const [slideNumber, goToSlide] = usePageControl(pageNumber, onPageChange, deck.slides.length);\n const listRef = useRef<HTMLDivElement>(null);\n const viewport = useViewportSize(listRef);\n\n const marks = useMemo(\n () => toMarkRanges(highlights, deck.text?.length ?? 0),\n [highlights, deck.text],\n );\n\n // A slide frame is 16:9 inside a capped column, so its height follows from the\n // pane's width — no measurement of the slide itself is needed for the first\n // frame. `measureElement` corrects a slide whose outline overflows the ratio,\n // and whose notes are longer than the estimate.\n const frameWidth = Math.min(SLIDE_MAX_WIDTH, (viewport?.width ?? SLIDE_MAX_WIDTH) - SLIDE_GAP);\n const estimateSize = useCallback(\n (index: number) =>\n Math.round((frameWidth * 9) / 16) +\n SLIDE_GAP +\n (deck.slides[index]?.notes ? NOTES_ESTIMATE : 0),\n [frameWidth, deck.slides],\n );\n\n const virtualizer = usePagedScroll({\n count: deck.slides.length,\n listRef,\n pageNumber: slideNumber,\n goToPage: goToSlide,\n estimateSize,\n sizeKey: frameWidth,\n });\n\n // Where each line of the deck begins in the projection, keyed by slide. Built\n // once for the whole deck rather than per slide: several slides are on screen\n // at a time now, and one pass over the spans is cheaper than one pass per\n // visible slide on every scroll.\n const starts = useMemo(() => {\n const map = new Map<number, Map<number, number>>();\n for (const span of deck.textIndex?.spans ?? []) {\n const slide = map.get(span.ref.slide) ?? new Map<number, number>();\n slide.set(span.ref.line, span.start);\n map.set(span.ref.slide, slide);\n }\n return map;\n }, [deck.textIndex]);\n\n // Scrolling to the cited slide is this format's half of \"take me there\". Keyed\n // on the slide NUMBER, so a reader who scrolls away while the same citation is\n // still active is not dragged back (the same trade the PDF column makes).\n const activeSlide = useMemo(() => {\n const active = highlights?.find((highlight) => highlight.id === activeHighlightId);\n if (!active || active.status !== \"resolved\" || !active.range || !deck.textIndex) {\n return undefined;\n }\n return spanAt(deck.textIndex, active.range[0])?.ref.slide;\n }, [highlights, activeHighlightId, deck.textIndex]);\n useEffect(() => {\n if (activeSlide === undefined) return;\n const position = deck.slides.findIndex((candidate) => candidate.index === activeSlide);\n if (position >= 0) goToSlide(position + 1);\n }, [activeSlide, deck.slides, goToSlide]);\n\n // The mark only exists once the cited slide is mounted, so the scroll waits\n // for the slide as well as for the id.\n useScrollActiveHighlightIntoView(listRef, activeHighlightId, slideNumber);\n\n return (\n // No viewport of its own: the slide chrome moved to the shell (ADR 0026), so\n // there is no fixed control above a scrolling body and `FileViewerContent`\n // is the one scroll boundary (`viewer-components.md`).\n <div\n ref={listRef}\n data-slot=\"pptx-slides\"\n className={cn(\"relative w-full\", className)}\n style={{ height: virtualizer.getTotalSize() }}\n >\n {virtualizer.getVirtualItems().map((item) => {\n const slide = deck.slides[item.index];\n if (!slide) return null;\n return (\n <div\n key={item.key}\n data-index={item.index}\n ref={virtualizer.measureElement}\n className=\"absolute inset-x-0 top-0\"\n style={{\n paddingBottom: SLIDE_GAP,\n transform: `translateY(${item.start - virtualizer.options.scrollMargin}px)`,\n }}\n >\n <PptxSlideView\n slide={slide}\n slideNumber={item.index + 1}\n baseHeadingLevel={baseHeadingLevel}\n marks={marks}\n starts={starts.get(slide.index)}\n />\n </div>\n );\n })}\n </div>\n );\n}\n\ninterface PptxSlideViewProps {\n slide: PptxSlide;\n /** 1-based position in the deck, which is what the reader is shown. */\n slideNumber: number;\n baseHeadingLevel: ProseHeadingLevel;\n marks: MarkRanges;\n /** Where each of this slide's lines begins in the projection. */\n starts?: Map<number, number>;\n}\n\n/** One slide: the 16:9 frame, its outline, and any speaker notes beneath it. */\nfunction PptxSlideView({\n slide,\n slideNumber,\n baseHeadingLevel,\n marks,\n starts,\n}: PptxSlideViewProps) {\n const { t, formatNumber } = useLocale();\n const isEmpty = !slide.title && slide.lines.length === 0;\n\n return (\n <>\n {/* The slide surface: `bg-card` above the pane's ground, `aspect-video`\n so a deck still reads as a deck — this is the shape of the thing,\n which is the one part of the layout an outline can honestly keep. */}\n {/* A slide that overflows its 16:9 frame scrolls, and a scrollable\n region with no focusable content cannot be reached by keyboard\n (WCAG 2.1.1). `tabIndex={0}` makes the slide itself the stop that\n arrow keys and Page Up/Down drive. This is the one nested scroller the\n rule allows: a fixed-ratio frame whose content may not fit it. */}\n <section\n tabIndex={0}\n data-slot=\"pptx-slide\"\n data-page={slideNumber}\n aria-label={t(\"viewer.pptx.slide\", {\n slide: formatNumber(slideNumber),\n })}\n className=\"border-border bg-card focus-visible:ring-ring mx-auto flex aspect-video w-full max-w-3xl flex-col gap-3 overflow-auto rounded-md border p-6 shadow-sm focus-visible:outline-none focus-visible:ring-2\"\n >\n {/* A slide title is the deck's top rung, so it sits at the host's base.\n The stand-in for an untitled slide is OUR text, not the deck's, so\n it is not in the projection and cannot be cited. */}\n <ProseHeading level={baseHeadingLevel}>\n {slide.title === undefined ? (\n t(\"viewer.pptx.untitled\")\n ) : (\n <MarkedText text={slide.title} marks={marks} start={starts?.get(PPTX_TITLE_LINE)} />\n )}\n </ProseHeading>\n {isEmpty ? (\n <p className=\"text-muted-foreground text-body\">{t(\"viewer.pptx.empty\")}</p>\n ) : (\n <ul className=\"text-body space-y-1.5\">\n {slide.lines.map((line, lineIndex) => (\n <li\n key={lineIndex}\n className={cn(\"whitespace-pre-wrap\", LEVEL_INDENT[Math.min(line.level, 3)])}\n >\n <MarkedText text={line.text} marks={marks} start={starts?.get(lineIndex)} />\n </li>\n ))}\n </ul>\n )}\n </section>\n\n {slide.notes && (\n <section\n aria-label={t(\"viewer.pptx.notes\")}\n className=\"mx-auto mt-3 w-full max-w-3xl space-y-1\"\n >\n <h3 className=\"text-meta text-muted-foreground\">{t(\"viewer.pptx.notes\")}</h3>\n <p className=\"text-body text-muted-foreground whitespace-pre-wrap\">\n <MarkedText text={slide.notes} marks={marks} start={starts?.get(PPTX_NOTES_LINE)} />\n </p>\n </section>\n )}\n </>\n );\n}\n\nconst adapterModule: AdapterModule = {\n manifest: pptxManifest,\n create: () => new PptxAdapter(),\n Renderer: PptxRenderer,\n};\n\nexport default adapterModule;\n","/**\n * PowerPoint XML → a slide outline.\n *\n * ## What a deck preview honestly is\n *\n * A `.pptx` slide is absolutely-positioned drawing: shapes at EMU coordinates,\n * theme-driven fills, transitions, embedded media. Reproducing that faithfully\n * is a rendering engine, not a preview — and a half-faithful reproduction is\n * worse than none, because it looks like the deck while quietly lying about it.\n *\n * So this reads the deck as an OUTLINE: per slide, the title, the text in\n * document order (with its indent level), and the speaker notes. That is the\n * content a reader is looking for when they open a deck in a file browser, it\n * renders in this system's typography, and it is honest about what it is.\n * Anything positional — layout, images, charts, transitions — is deliberately\n * absent; the toolbar's download is the answer for the real thing.\n *\n * Parsing goes through namespace URIs rather than `a:t` / `p:sp` tag names,\n * because the prefix is only a convention: a generator is free to bind the same\n * namespace to a different prefix, and tag-name matching would silently return\n * an empty deck.\n */\n\nimport { createTextIndexBuilder, type TextIndex } from \"../../core/text-index\";\n\n/** The DrawingML namespace — text, paragraphs, tables. */\nexport const DRAWING_NS = \"http://schemas.openxmlformats.org/drawingml/2006/main\";\n/** The PresentationML namespace — shapes, placeholders, the shape tree. */\nexport const PRESENTATION_NS = \"http://schemas.openxmlformats.org/presentationml/2006/main\";\n\n/** One line of slide text, with the indent level PowerPoint gave it. */\nexport interface PptxLine {\n text: string;\n /** Outline depth, `0` for a top-level bullet. */\n level: number;\n}\n\nexport interface PptxSlide {\n /** 1-based position in the deck. */\n index: number;\n /** The title placeholder's text, when the slide has one. */\n title?: string;\n /** Every other line, in document order. */\n lines: PptxLine[];\n /** The slide's speaker notes, when it has any. */\n notes?: string;\n}\n\n/**\n * Order slide parts the way the deck does.\n *\n * A zip lists its entries in whatever order they were written, and\n * `slide10.xml` sorts before `slide2.xml` as a string — so the number is pulled\n * out and compared as a number.\n */\nexport function orderSlidePaths(paths: string[]): string[] {\n return paths\n .filter((path) => /^ppt\\/slides\\/slide\\d+\\.xml$/.test(path))\n .sort((left, right) => slideNumber(left) - slideNumber(right));\n}\n\n/** The `N` in `…/slideN.xml`, or `0` when there is none. */\nexport function slideNumber(path: string): number {\n const match = /(\\d+)\\.xml$/.exec(path);\n return match?.[1] ? Number(match[1]) : 0;\n}\n\n/** All text under a node, in document order, as one string. */\nfunction textOf(node: Element): string {\n const runs = node.getElementsByTagNameNS(DRAWING_NS, \"t\");\n let text = \"\";\n for (const run of Array.from(runs)) text += run.textContent ?? \"\";\n return text.trim();\n}\n\n/** The paragraphs directly under a shape, as lines. Empty paragraphs are dropped. */\nfunction linesOf(shape: Element): PptxLine[] {\n const lines: PptxLine[] = [];\n for (const paragraph of Array.from(shape.getElementsByTagNameNS(DRAWING_NS, \"p\"))) {\n const text = textOf(paragraph);\n if (!text) continue;\n // `lvl` is absent for a top-level bullet, which is the common case.\n const properties = paragraph.getElementsByTagNameNS(DRAWING_NS, \"pPr\")[0];\n const level = Number(properties?.getAttribute(\"lvl\") ?? 0);\n lines.push({ text, level: Number.isFinite(level) ? level : 0 });\n }\n return lines;\n}\n\n/** The placeholder type a shape declares (`title`, `ctrTitle`, `body`, …), if any. */\nfunction placeholderType(shape: Element): string | undefined {\n const placeholder = shape.getElementsByTagNameNS(PRESENTATION_NS, \"ph\")[0];\n return placeholder?.getAttribute(\"type\") ?? undefined;\n}\n\n/** Table text, row by row, tab-separated — a table on a slide is usually the point of it. */\nfunction tableLines(frame: Element): PptxLine[] {\n const lines: PptxLine[] = [];\n for (const row of Array.from(frame.getElementsByTagNameNS(DRAWING_NS, \"tr\"))) {\n const cells = Array.from(row.getElementsByTagNameNS(DRAWING_NS, \"tc\")).map(textOf);\n const text = cells.join(\"\\t\").trim();\n if (text) lines.push({ text, level: 0 });\n }\n return lines;\n}\n\n/**\n * Parse one `ppt/slides/slideN.xml` document into a slide.\n *\n * The shape tree is walked in document order, so the outline reads in the order\n * the shapes were authored rather than in the order the file happens to store\n * them. A shape kind this does not name contributes nothing — the same\n * allowlist-by-parse the Word adapter uses.\n */\nexport function parseSlide(document: Document, index: number): PptxSlide {\n const tree = document.getElementsByTagNameNS(PRESENTATION_NS, \"spTree\")[0];\n const slide: PptxSlide = { index, lines: [] };\n if (!tree) return slide;\n\n for (const node of Array.from(tree.children)) {\n if (node.namespaceURI !== PRESENTATION_NS) continue;\n if (node.localName === \"sp\") {\n const type = placeholderType(node);\n const lines = linesOf(node);\n if ((type === \"title\" || type === \"ctrTitle\") && !slide.title) {\n // A title placeholder can hold several paragraphs; they are one heading.\n const title = lines.map((line) => line.text).join(\" \");\n if (title) slide.title = title;\n continue;\n }\n slide.lines.push(...lines);\n continue;\n }\n if (node.localName === \"graphicFrame\") slide.lines.push(...tableLines(node));\n }\n return slide;\n}\n\n/** Parse a `ppt/notesSlides/notesSlideN.xml` document into its notes text. */\nexport function parseNotes(document: Document): string | undefined {\n const tree = document.getElementsByTagNameNS(PRESENTATION_NS, \"spTree\")[0];\n if (!tree) return undefined;\n\n const lines: string[] = [];\n for (const shape of Array.from(tree.getElementsByTagNameNS(PRESENTATION_NS, \"sp\"))) {\n // The notes part repeats the slide's own body as a non-editable copy; only\n // the notes placeholder holds what the presenter actually wrote.\n if (placeholderType(shape) !== \"body\") continue;\n for (const line of linesOf(shape)) lines.push(line.text);\n }\n const notes = lines.join(\"\\n\").trim();\n return notes || undefined;\n}\n\n/** The OPC relationships namespace — how a part points at another part. */\nexport const RELATIONSHIP_NS = \"http://schemas.openxmlformats.org/package/2006/relationships\";\n\n/**\n * Resolve a relationship target (`../notesSlides/notesSlide1.xml`) against the\n * part that declared it (`ppt/slides/slide1.xml`).\n *\n * Zip entry names are plain strings, not URLs, so `..` has to be walked by hand.\n */\nexport function resolveRelative(fromPart: string, target: string): string {\n if (target.startsWith(\"/\")) return target.slice(1);\n const segments = fromPart.split(\"/\").slice(0, -1);\n for (const segment of target.split(\"/\")) {\n if (segment === \".\" || segment === \"\") continue;\n if (segment === \"..\") segments.pop();\n else segments.push(segment);\n }\n return segments.join(\"/\");\n}\n\n/**\n * The notes part a slide points at, if any.\n *\n * Read from the slide's own `_rels`, not by matching `slideN` to `notesSlideN`:\n * the numbers agree in decks PowerPoint wrote from scratch and drift in decks\n * that have had slides deleted, which would attach the wrong presenter notes to\n * the wrong slide — a quiet, plausible-looking error.\n */\nexport function notesTarget(rels: Document, slidePath: string): string | undefined {\n for (const relationship of Array.from(\n rels.getElementsByTagNameNS(RELATIONSHIP_NS, \"Relationship\"),\n )) {\n if (!relationship.getAttribute(\"Type\")?.endsWith(\"/notesSlide\")) continue;\n const target = relationship.getAttribute(\"Target\");\n if (target) return resolveRelative(slidePath, target);\n }\n return undefined;\n}\n\n/** Between two lines of the same slide. */\nexport const PPTX_LINE_SEPARATOR = \"\\n\";\n/** Between two slides. */\nexport const PPTX_SLIDE_SEPARATOR = \"\\n\\n\";\n\n/** The `line` of a slide's title. */\nexport const PPTX_TITLE_LINE = -1;\n/** The `line` of a slide's speaker notes. */\nexport const PPTX_NOTES_LINE = -2;\n\n/** Where a stretch of the projection came from in the outline. */\nexport interface PptxRef {\n /** 1-based slide position, the same number {@link PptxSlide.index} carries. */\n slide: number;\n /** Body-line index, or {@link PPTX_TITLE_LINE} / {@link PPTX_NOTES_LINE}. */\n line: number;\n}\n\n/**\n * Plain-text projection of a deck, plus the map back into the outline.\n *\n * A slide's title, its lines and its notes are each their own chunk, so a\n * citation resolves to the exact line the renderer draws rather than to \"slide\n * 4\". The blank line between slides is written before whichever chunk turns out\n * to be the next slide's first — a slide with no text at all contributes\n * nothing, instead of a run of empty lines that would shift every offset after\n * it.\n */\nexport function slidesToTextWithMap(slides: PptxSlide[]): TextIndex<PptxRef> {\n const builder = createTextIndexBuilder<PptxRef>({ separator: PPTX_LINE_SEPARATOR });\n slides.forEach((slide, index) => {\n let separator = index === 0 ? undefined : PPTX_SLIDE_SEPARATOR;\n const push = (chunk: string | undefined, line: number) => {\n if (!chunk) return;\n builder.push(chunk, { slide: slide.index, line }, separator);\n separator = undefined;\n };\n\n push(slide.title, PPTX_TITLE_LINE);\n slide.lines.forEach((line, lineIndex) => {\n push(line.text, lineIndex);\n });\n push(slide.notes, PPTX_NOTES_LINE);\n });\n return builder.build();\n}\n\n/**\n * Plain-text projection of a deck — powers search, copy and the raw view. A thin\n * wrapper so the projection has exactly one definition: it is the index's own\n * text, never a second assembly that could drift from it.\n */\nexport function slidesToText(slides: PptxSlide[]): string {\n return slidesToTextWithMap(slides).text;\n}\n"],"mappings":"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","names":["slideNumber"]}