@elabs-ai/components-viewer 4.0.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.
Files changed (140) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +143 -0
  3. package/dist/chunk-2NQ4RSJ3.js +18 -0
  4. package/dist/chunk-2NQ4RSJ3.js.map +1 -0
  5. package/dist/chunk-3U3MESBU.js +21 -0
  6. package/dist/chunk-3U3MESBU.js.map +1 -0
  7. package/dist/chunk-4OFQYALG.js +147 -0
  8. package/dist/chunk-4OFQYALG.js.map +1 -0
  9. package/dist/chunk-5OMONA2G.js +87 -0
  10. package/dist/chunk-5OMONA2G.js.map +1 -0
  11. package/dist/chunk-AORNMH77.js +58 -0
  12. package/dist/chunk-AORNMH77.js.map +1 -0
  13. package/dist/chunk-BCF3RWRL.js +18 -0
  14. package/dist/chunk-BCF3RWRL.js.map +1 -0
  15. package/dist/chunk-FELIAGKI.js +22 -0
  16. package/dist/chunk-FELIAGKI.js.map +1 -0
  17. package/dist/chunk-GGR7BNW4.js +18 -0
  18. package/dist/chunk-GGR7BNW4.js.map +1 -0
  19. package/dist/chunk-H3OAON3D.js +22 -0
  20. package/dist/chunk-H3OAON3D.js.map +1 -0
  21. package/dist/chunk-HMG2ERXH.js +17 -0
  22. package/dist/chunk-HMG2ERXH.js.map +1 -0
  23. package/dist/chunk-KOMDRWOU.js +21 -0
  24. package/dist/chunk-KOMDRWOU.js.map +1 -0
  25. package/dist/chunk-NMA57QZ7.js +117 -0
  26. package/dist/chunk-NMA57QZ7.js.map +1 -0
  27. package/dist/chunk-RGWDIQJR.js +26 -0
  28. package/dist/chunk-RGWDIQJR.js.map +1 -0
  29. package/dist/chunk-SLXRUYT3.js +91 -0
  30. package/dist/chunk-SLXRUYT3.js.map +1 -0
  31. package/dist/chunk-UJIJECEF.js +21 -0
  32. package/dist/chunk-UJIJECEF.js.map +1 -0
  33. package/dist/chunk-UL43NGUG.js +68 -0
  34. package/dist/chunk-UL43NGUG.js.map +1 -0
  35. package/dist/chunk-VO2273Z2.js +55 -0
  36. package/dist/chunk-VO2273Z2.js.map +1 -0
  37. package/dist/code-adapter-ADZ4UOGN.js +226 -0
  38. package/dist/code-adapter-ADZ4UOGN.js.map +1 -0
  39. package/dist/csv-adapter-6VU3FFVU.js +100 -0
  40. package/dist/csv-adapter-6VU3FFVU.js.map +1 -0
  41. package/dist/docx-adapter-5CQDHWTD.js +383 -0
  42. package/dist/docx-adapter-5CQDHWTD.js.map +1 -0
  43. package/dist/image-adapter-WOHZR24J.js +132 -0
  44. package/dist/image-adapter-WOHZR24J.js.map +1 -0
  45. package/dist/index.d.ts +1353 -0
  46. package/dist/index.js +1364 -0
  47. package/dist/index.js.map +1 -0
  48. package/dist/json-adapter-ZUW5GQHE.js +90 -0
  49. package/dist/json-adapter-ZUW5GQHE.js.map +1 -0
  50. package/dist/markdown-adapter-YC6WTBS4.js +214 -0
  51. package/dist/markdown-adapter-YC6WTBS4.js.map +1 -0
  52. package/dist/media-adapter-MCTB4GBH.js +78 -0
  53. package/dist/media-adapter-MCTB4GBH.js.map +1 -0
  54. package/dist/pdf-adapter-5PMKEXUD.js +424 -0
  55. package/dist/pdf-adapter-5PMKEXUD.js.map +1 -0
  56. package/dist/pptx-adapter-6GEQLS2Z.js +351 -0
  57. package/dist/pptx-adapter-6GEQLS2Z.js.map +1 -0
  58. package/dist/text-adapter-NFNWB5W3.js +81 -0
  59. package/dist/text-adapter-NFNWB5W3.js.map +1 -0
  60. package/dist/xlsx-adapter-CM2Y6AKQ.js +167 -0
  61. package/dist/xlsx-adapter-CM2Y6AKQ.js.map +1 -0
  62. package/package.json +103 -0
  63. package/src/adapters/code/code-adapter.test.tsx +221 -0
  64. package/src/adapters/code/code-adapter.tsx +272 -0
  65. package/src/adapters/code/code-language.test.ts +45 -0
  66. package/src/adapters/code/code-language.ts +100 -0
  67. package/src/adapters/code/code-manifest.ts +19 -0
  68. package/src/adapters/code/code-theme.test.ts +46 -0
  69. package/src/adapters/code/code-theme.ts +99 -0
  70. package/src/adapters/csv/csv-adapter.test.tsx +169 -0
  71. package/src/adapters/csv/csv-adapter.tsx +146 -0
  72. package/src/adapters/csv/csv-manifest.ts +20 -0
  73. package/src/adapters/docx/docx-adapter.test.tsx +192 -0
  74. package/src/adapters/docx/docx-adapter.tsx +328 -0
  75. package/src/adapters/docx/docx-manifest.ts +18 -0
  76. package/src/adapters/docx/docx-model.test.ts +181 -0
  77. package/src/adapters/docx/docx-model.ts +294 -0
  78. package/src/adapters/image/image-adapter.test.tsx +141 -0
  79. package/src/adapters/image/image-adapter.tsx +191 -0
  80. package/src/adapters/image/image-manifest.ts +15 -0
  81. package/src/adapters/index.ts +64 -0
  82. package/src/adapters/json/json-adapter.test.tsx +77 -0
  83. package/src/adapters/json/json-adapter.tsx +141 -0
  84. package/src/adapters/json/json-manifest.ts +11 -0
  85. package/src/adapters/markdown/markdown-adapter.test.tsx +228 -0
  86. package/src/adapters/markdown/markdown-adapter.tsx +353 -0
  87. package/src/adapters/markdown/markdown-manifest.ts +21 -0
  88. package/src/adapters/markdown/markdown-marks.test.ts +85 -0
  89. package/src/adapters/markdown/markdown-marks.ts +119 -0
  90. package/src/adapters/media/media-adapter.test.tsx +79 -0
  91. package/src/adapters/media/media-adapter.tsx +123 -0
  92. package/src/adapters/media/media-manifest.ts +17 -0
  93. package/src/adapters/office-fixture.ts +239 -0
  94. package/src/adapters/pdf/pdf-adapter.test.tsx +489 -0
  95. package/src/adapters/pdf/pdf-adapter.tsx +599 -0
  96. package/src/adapters/pdf/pdf-engine.ts +171 -0
  97. package/src/adapters/pdf/pdf-fixture.ts +27 -0
  98. package/src/adapters/pdf/pdf-manifest.ts +30 -0
  99. package/src/adapters/pdf/pdf-text.test.ts +171 -0
  100. package/src/adapters/pdf/pdf-text.ts +202 -0
  101. package/src/adapters/pptx/pptx-adapter.test.tsx +241 -0
  102. package/src/adapters/pptx/pptx-adapter.tsx +341 -0
  103. package/src/adapters/pptx/pptx-manifest.ts +19 -0
  104. package/src/adapters/pptx/pptx-model.test.ts +219 -0
  105. package/src/adapters/pptx/pptx-model.ts +248 -0
  106. package/src/adapters/text/text-adapter.test.tsx +125 -0
  107. package/src/adapters/text/text-adapter.tsx +117 -0
  108. package/src/adapters/text/text-manifest.ts +19 -0
  109. package/src/adapters/xlsx/xlsx-adapter.test.tsx +159 -0
  110. package/src/adapters/xlsx/xlsx-adapter.tsx +250 -0
  111. package/src/adapters/xlsx/xlsx-manifest.ts +26 -0
  112. package/src/components/grid-text.test.ts +68 -0
  113. package/src/components/grid-text.ts +77 -0
  114. package/src/components/marked-text.tsx +36 -0
  115. package/src/components/sheet-table.tsx +142 -0
  116. package/src/core/errors.ts +120 -0
  117. package/src/core/highlight-marks.test.ts +95 -0
  118. package/src/core/highlight-marks.ts +95 -0
  119. package/src/core/highlight-resolve.test.ts +208 -0
  120. package/src/core/highlight-resolve.ts +191 -0
  121. package/src/core/highlight.ts +125 -0
  122. package/src/core/registry.test.ts +172 -0
  123. package/src/core/registry.ts +188 -0
  124. package/src/core/scroll-host.ts +42 -0
  125. package/src/core/text-index.test.ts +95 -0
  126. package/src/core/text-index.ts +159 -0
  127. package/src/core/types.ts +285 -0
  128. package/src/core/use-highlight-scroll.ts +59 -0
  129. package/src/core/use-page-control.ts +47 -0
  130. package/src/core/use-paged-scroll.ts +194 -0
  131. package/src/core/use-viewport-size.ts +54 -0
  132. package/src/core/zoom.ts +56 -0
  133. package/src/file-viewer/file-viewer-context.tsx +239 -0
  134. package/src/file-viewer/file-viewer-find.tsx +204 -0
  135. package/src/file-viewer/file-viewer-pager.tsx +120 -0
  136. package/src/file-viewer/file-viewer-zoom.tsx +166 -0
  137. package/src/file-viewer/file-viewer.stories.tsx +947 -0
  138. package/src/file-viewer/file-viewer.test.tsx +947 -0
  139. package/src/file-viewer/file-viewer.tsx +1143 -0
  140. package/src/index.ts +157 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Manuel Reimitz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,143 @@
1
+ <!-- brand-ui:gen:readme:start -->
2
+ <!-- Generated by scripts/gen-package-readmes.mjs — do not edit inside these markers. -->
3
+
4
+ # `@elabs-ai/components-viewer`
5
+
6
+ > FileViewer — any file (image, text, JSON, CSV) via a pluggable adapter registry.
7
+
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`.
11
+
12
+ ## Install
13
+
14
+ Inside this monorepo the packages resolve as workspace dependencies:
15
+
16
+ ```json
17
+ "@elabs-ai/components-viewer": "workspace:*"
18
+ ```
19
+
20
+ ## Set up styling (do not skip)
21
+
22
+ Components are Tailwind v4 classes backed by semantic tokens. Two lines in
23
+ your CSS entry, or **everything renders unstyled** — the single most common
24
+ mistake:
25
+
26
+ ```css
27
+ @import "@elabs-ai/components-tokens/styles.css";
28
+ @source "../node_modules/@elabs-ai/components-viewer/dist";
29
+ ```
30
+
31
+ The `@source` line is required because Tailwind ignores `node_modules`. Add
32
+ one per brand-ui package you render. Then wrap your app once:
33
+
34
+ ```tsx
35
+ import { ThemeProvider } from "@elabs-ai/components-tokens";
36
+
37
+ <ThemeProvider defaultTheme="light">{children}</ThemeProvider>;
38
+ ```
39
+
40
+ ## What's in it
41
+
42
+ 19 exported components — including `ACTIVE_HIGHLIGHT_SELECTOR`, `DEFAULT_ZOOM`, `FileViewer`, `FileViewerContent`, `FileViewerEmpty`.
43
+
44
+ Don't guess the API — ask the CLI:
45
+
46
+ ```bash
47
+ pnpm add -D @elabs-ai/components-cli
48
+ pnpm exec brand-ui search <query> # find a component
49
+ pnpm exec brand-ui docs <Name> # its real props, from source
50
+ ```
51
+
52
+ ## Using an AI coding agent?
53
+
54
+ For Claude Code, install the plugin from this repo's checkout:
55
+
56
+ ```
57
+ /plugin marketplace add .
58
+ /plugin install brand-ui
59
+ ```
60
+
61
+ `brand-ui docs <Name>` returns intent, composition, state→token mappings and
62
+ anti-patterns — tell your agent to run it instead of guessing a prop. The CLI
63
+ also runs as an MCP server (`brand-ui mcp`).
64
+
65
+ ## Full guide
66
+
67
+ Tailwind and Next.js wiring, per-package extras, agent enablement and a
68
+ prompt for migrating an existing project: `docs/CONSUMING.md`.
69
+
70
+ ## License
71
+
72
+ UNLICENSED — private.
73
+
74
+ <!-- brand-ui:gen:readme:end -->
75
+
76
+ ---
77
+
78
+ ## Package detail
79
+
80
+ Renders a file — whatever it is, wherever it came from — themed and accessible.
81
+
82
+ ```tsx
83
+ import { FileViewer } from "@elabs-ai/components-viewer";
84
+
85
+ <div className="h-[600px]">
86
+ <FileViewer source={{ kind: "file", file }} />
87
+ </div>;
88
+ ```
89
+
90
+ ### The input is always a `FileSource`
91
+
92
+ One union from `@elabs-ai/components-ui` covers every way an app can
93
+ be holding a file — a user's `File`, an in-memory `Blob` or `ArrayBuffer`, a
94
+ string of content, or a remote URL. The `url` variant carries `init`, so a file
95
+ behind auth is reachable:
96
+
97
+ ```tsx
98
+ <FileViewer
99
+ source={{
100
+ kind: "url",
101
+ url: "https://files.internal/report.csv",
102
+ init: { headers: { Authorization: `Bearer ${token}` } },
103
+ }}
104
+ />
105
+ ```
106
+
107
+ ### Formats come from adapters, and adapters are pluggable
108
+
109
+ Every format is an adapter in a registry: a small eager manifest (what it can
110
+ open, what the toolbar should offer) plus a lazy loader. Nothing heavy is in the
111
+ entry chunk, and an app that never opens a spreadsheet never downloads a
112
+ spreadsheet parser.
113
+
114
+ Register your own — or override a built-in by registering a higher priority for
115
+ the same id:
116
+
117
+ ```tsx
118
+ import { createRegistry, FileViewer } from "@elabs-ai/components-viewer";
119
+
120
+ const registry = createRegistry();
121
+ registry.register(myManifest, () => import("./my-adapter"));
122
+
123
+ <FileViewer source={source} registry={registry} />;
124
+ ```
125
+
126
+ ### Parsers are optional peers
127
+
128
+ Only install what you open. A format whose parser is absent renders a clear
129
+ "can't open this here" state with a download action — never a module-resolution
130
+ crash.
131
+
132
+ | Format | Peer to install |
133
+ | --------------- | --------------- |
134
+ | `.csv` / `.tsv` | `papaparse` |
135
+
136
+ Images, plain text and JSON need nothing — they use platform APIs.
137
+
138
+ ### Adapters emit data, not HTML
139
+
140
+ An adapter returns a model (rows, a tree, a page) that brand-ui components
141
+ render, so the viewer's chrome is fully token-driven and re-themes with the rest
142
+ of your app. Document _content_ — a photo, a rendered page — renders as
143
+ authored, because that is the file.
@@ -0,0 +1,18 @@
1
+ "use client";
2
+ import {
3
+ localizeRanges
4
+ } from "./chunk-VO2273Z2.js";
5
+
6
+ // src/components/marked-text.tsx
7
+ import { MatchHighlight } from "@elabs-ai/components-ui";
8
+ import { Fragment, jsx } from "react/jsx-runtime";
9
+ function MarkedText({ text, marks, start }) {
10
+ const local = !marks || start === void 0 ? void 0 : localizeRanges(marks, start, start + text.length);
11
+ if (!local || local.ranges.length === 0) return /* @__PURE__ */ jsx(Fragment, { children: text });
12
+ return /* @__PURE__ */ jsx(MatchHighlight, { text, ranges: local.ranges, activeIndex: local.activeIndex });
13
+ }
14
+
15
+ export {
16
+ MarkedText
17
+ };
18
+ //# sourceMappingURL=chunk-2NQ4RSJ3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/marked-text.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * One slice of text, with whatever marks land in it.\n *\n * The last step of the highlight funnel, and the same three lines in every\n * adapter that draws its text in pieces — a Word run, a spreadsheet cell, a\n * slide's bullet. Each of them holds document offsets for its own slice and\n * needs the ranges rebased before a `<mark>` layer can paint them, so the\n * rebase-then-paint pair is named once here rather than copied a fourth time\n * (`.claude/rules/design-first.md` — patterns over instances).\n *\n * `start === undefined` means \"this slice is not in the projection\" (an adapter\n * with no index, a cell past the truncation bound), which renders as plain text\n * rather than as an error: a document that cannot be addressed is still a\n * document that can be read.\n */\n\nimport { MatchHighlight } from \"@elabs-ai/components-ui\";\n\nimport { localizeRanges, type MarkRanges } from \"../core/highlight-marks\";\n\nexport interface MarkedTextProps {\n text: string;\n /** Marks in PROJECTION offsets — rebased here, not by the caller. */\n marks: MarkRanges | undefined;\n /** Where this slice begins in the projection. */\n start: number | undefined;\n}\n\nexport function MarkedText({ text, marks, start }: MarkedTextProps) {\n const local =\n !marks || start === undefined ? undefined : localizeRanges(marks, start, start + text.length);\n if (!local || local.ranges.length === 0) return <>{text}</>;\n return <MatchHighlight text={text} ranges={local.ranges} activeIndex={local.activeIndex} />;\n}\n"],"mappings":";;;;;;AAkBA,SAAS,sBAAsB;AAemB;AAH3C,SAAS,WAAW,EAAE,MAAM,OAAO,MAAM,GAAoB;AAClE,QAAM,QACJ,CAAC,SAAS,UAAU,SAAY,SAAY,eAAe,OAAO,OAAO,QAAQ,KAAK,MAAM;AAC9F,MAAI,CAAC,SAAS,MAAM,OAAO,WAAW,EAAG,QAAO,gCAAG,gBAAK;AACxD,SAAO,oBAAC,kBAAe,MAAY,QAAQ,MAAM,QAAQ,aAAa,MAAM,aAAa;AAC3F;","names":[]}
@@ -0,0 +1,21 @@
1
+ "use client";
2
+ import {
3
+ PROTOCOL_VERSION
4
+ } from "./chunk-AORNMH77.js";
5
+
6
+ // src/adapters/csv/csv-manifest.ts
7
+ var csvManifest = {
8
+ id: "csv",
9
+ protocol: PROTOCOL_VERSION,
10
+ extensions: ["csv", "tsv"],
11
+ mediaTypes: ["text/csv", "text/tab-separated-values"],
12
+ // Addressed against the PARSED grid (`gridToText`), not the raw bytes — see
13
+ // the adapter. No `rect`: a table cell has no page geometry to point at.
14
+ capabilities: { text: true, search: true, highlight: ["quote", "range"] },
15
+ requires: ["papaparse"]
16
+ };
17
+
18
+ export {
19
+ csvManifest
20
+ };
21
+ //# sourceMappingURL=chunk-3U3MESBU.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/adapters/csv/csv-manifest.ts"],"sourcesContent":["import type { AdapterManifest } from \"../../core/types\";\nimport { PROTOCOL_VERSION } from \"../../core/types\";\n\n/**\n * Eager, data-only (see `image-manifest.ts`).\n *\n * `requires` names the OPTIONAL peer this adapter dynamically imports. It is\n * what the \"install this to open that\" message names when the import cannot\n * resolve — the registry never installs anything itself.\n */\nexport const csvManifest: AdapterManifest = {\n id: \"csv\",\n protocol: PROTOCOL_VERSION,\n extensions: [\"csv\", \"tsv\"],\n mediaTypes: [\"text/csv\", \"text/tab-separated-values\"],\n // Addressed against the PARSED grid (`gridToText`), not the raw bytes — see\n // the adapter. No `rect`: a table cell has no page geometry to point at.\n capabilities: { text: true, search: true, highlight: [\"quote\", \"range\"] },\n requires: [\"papaparse\"],\n};\n"],"mappings":";;;;;;AAUO,IAAM,cAA+B;AAAA,EAC1C,IAAI;AAAA,EACJ,UAAU;AAAA,EACV,YAAY,CAAC,OAAO,KAAK;AAAA,EACzB,YAAY,CAAC,YAAY,2BAA2B;AAAA;AAAA;AAAA,EAGpD,cAAc,EAAE,MAAM,MAAM,QAAQ,MAAM,WAAW,CAAC,SAAS,OAAO,EAAE;AAAA,EACxE,UAAU,CAAC,WAAW;AACxB;","names":[]}
@@ -0,0 +1,147 @@
1
+ "use client";
2
+
3
+ // src/core/use-page-control.ts
4
+ import { useCallback, useState } from "react";
5
+ function usePageControl(pageNumber, onPageChange, pageCount) {
6
+ const controlled = pageNumber !== void 0;
7
+ const [own, setOwn] = useState(1);
8
+ const raw = controlled ? pageNumber : own;
9
+ const current = pageCount > 0 ? Math.min(Math.max(1, raw), pageCount) : Math.max(1, raw);
10
+ const goToPage = useCallback(
11
+ (page) => {
12
+ const next = pageCount > 0 ? Math.min(Math.max(1, page), pageCount) : Math.max(1, page);
13
+ if (!controlled) setOwn(next);
14
+ onPageChange?.(next);
15
+ },
16
+ [controlled, onPageChange, pageCount]
17
+ );
18
+ return [current, goToPage];
19
+ }
20
+
21
+ // src/core/use-paged-scroll.ts
22
+ import { useVirtualizer } from "@tanstack/react-virtual";
23
+ import { useEffect, useLayoutEffect, useRef, useState as useState2 } from "react";
24
+
25
+ // src/core/scroll-host.ts
26
+ var SCROLL_HOST_SELECTOR = '[data-slot="file-viewer-content"]';
27
+ function findScrollHost(node) {
28
+ if (!node) return null;
29
+ const shell = node.closest(SCROLL_HOST_SELECTOR);
30
+ if (shell) return shell;
31
+ const view = node.ownerDocument?.defaultView;
32
+ if (!view) return null;
33
+ let current = node.parentElement;
34
+ while (current) {
35
+ const { overflowY, overflowX } = view.getComputedStyle(current);
36
+ if (/(auto|scroll|overlay)/.test(overflowY) || /(auto|scroll|overlay)/.test(overflowX)) {
37
+ return current;
38
+ }
39
+ current = current.parentElement;
40
+ }
41
+ return null;
42
+ }
43
+
44
+ // src/core/use-paged-scroll.ts
45
+ var EDGE_SLACK = 8;
46
+ function usePagedScroll({
47
+ count,
48
+ listRef,
49
+ pageNumber,
50
+ goToPage,
51
+ estimateSize,
52
+ sizeKey
53
+ }) {
54
+ const [host, setHost] = useState2(null);
55
+ const [scrollMargin, setScrollMargin] = useState2(0);
56
+ useEffect(() => {
57
+ const list = listRef.current;
58
+ const found = findScrollHost(list) ?? list?.parentElement ?? null;
59
+ setHost(found);
60
+ if (!list || !found) return;
61
+ setScrollMargin(
62
+ list.getBoundingClientRect().top - found.getBoundingClientRect().top + found.scrollTop
63
+ );
64
+ }, [listRef]);
65
+ const virtualizer = useVirtualizer({
66
+ count,
67
+ getScrollElement: () => host,
68
+ estimateSize,
69
+ overscan: 1,
70
+ scrollMargin,
71
+ measureElement: (element, entry, instance) => {
72
+ const measured = entry?.borderBoxSize?.[0]?.blockSize ?? element.getBoundingClientRect().height;
73
+ if (measured > 0) return measured;
74
+ const index = Number(element.getAttribute("data-index"));
75
+ return Number.isNaN(index) ? 0 : instance.options.estimateSize(index);
76
+ }
77
+ });
78
+ const items = virtualizer.getVirtualItems();
79
+ const scrollOffset = virtualizer.scrollOffset ?? 0;
80
+ const top = items.find((item) => item.start + item.size > scrollOffset + EDGE_SLACK);
81
+ const active = top ? top.index + 1 : 1;
82
+ const anchor = useRef({ index: 0, fraction: 0 });
83
+ useEffect(() => {
84
+ if (!top || top.size <= 0) return;
85
+ const fraction = (scrollOffset - top.start) / top.size;
86
+ anchor.current = { index: top.index, fraction: Math.min(fraction, 1) };
87
+ }, [top, scrollOffset]);
88
+ const reported = useRef(0);
89
+ const requested = useRef(0);
90
+ const lastActive = useRef(active);
91
+ useEffect(() => {
92
+ if (count === 0 || active === lastActive.current) return;
93
+ lastActive.current = active;
94
+ if (active === pageNumber) return;
95
+ reported.current = active;
96
+ goToPage(active);
97
+ }, [active, pageNumber, count, goToPage]);
98
+ useEffect(() => {
99
+ if (!host || count === 0 || requested.current === pageNumber) return;
100
+ requested.current = pageNumber;
101
+ if (pageNumber === reported.current || pageNumber === active) return;
102
+ virtualizer.scrollToIndex(pageNumber - 1, { align: "start" });
103
+ }, [host, pageNumber, active, count, virtualizer]);
104
+ const lastSizeKey = useRef(sizeKey);
105
+ useLayoutEffect(() => {
106
+ if (lastSizeKey.current === sizeKey) return;
107
+ lastSizeKey.current = sizeKey;
108
+ if (count === 0) return;
109
+ virtualizer.measure();
110
+ const { index, fraction } = anchor.current;
111
+ if (index === 0 && fraction <= 0) return;
112
+ const start = virtualizer.getOffsetForIndex(index, "start")?.[0];
113
+ if (start === void 0) return;
114
+ const size = virtualizer.measurementsCache[index]?.size ?? 0;
115
+ virtualizer.scrollToOffset(start + fraction * size, { align: "start" });
116
+ }, [sizeKey, count, virtualizer]);
117
+ return virtualizer;
118
+ }
119
+
120
+ // src/core/use-viewport-size.ts
121
+ import { useEffect as useEffect2, useState as useState3 } from "react";
122
+ function useViewportSize(ref) {
123
+ const [size, setSize] = useState3();
124
+ useEffect2(() => {
125
+ const host = findScrollHost(ref.current) ?? ref.current?.parentElement ?? ref.current;
126
+ if (!host || typeof ResizeObserver === "undefined") return;
127
+ const measure = () => {
128
+ const { clientWidth: width, clientHeight: height } = host;
129
+ if (width === 0 || height === 0) return;
130
+ setSize(
131
+ (current) => current && Math.abs(current.width - width) < 1 && Math.abs(current.height - height) < 1 ? current : { width, height }
132
+ );
133
+ };
134
+ measure();
135
+ const observer = new ResizeObserver(measure);
136
+ observer.observe(host);
137
+ return () => observer.disconnect();
138
+ }, [ref]);
139
+ return size;
140
+ }
141
+
142
+ export {
143
+ usePageControl,
144
+ usePagedScroll,
145
+ useViewportSize
146
+ };
147
+ //# sourceMappingURL=chunk-4OFQYALG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/use-page-control.ts","../src/core/use-paged-scroll.ts","../src/core/scroll-host.ts","../src/core/use-viewport-size.ts"],"sourcesContent":["\"use client\";\n\n/**\n * The page half of the renderer contract, as one hook.\n *\n * `AdapterRendererProps.pageNumber` is optional on purpose: inside a\n * `FileViewerProvider` the shell owns the page so a control can live anywhere,\n * but a `Renderer` mounted on its own — a test, an embed that wants nothing but\n * the canvas — must still be able to turn a page. That is the ordinary\n * controlled/uncontrolled trio (`component-api.md`), and both paginated adapters\n * need exactly the same six lines of it.\n */\n\nimport { useCallback, useState } from \"react\";\n\n/**\n * @param pageNumber The controlled page, or `undefined` for uncontrolled.\n * @param onPageChange Reported on every navigation, controlled or not.\n * @param pageCount Upper bound. `0` while the count is not known yet.\n * @returns The page to render, and the function that navigates to another.\n */\nexport function usePageControl(\n pageNumber: number | undefined,\n onPageChange: ((page: number) => void) | undefined,\n pageCount: number,\n): readonly [number, (page: number) => void] {\n const controlled = pageNumber !== undefined;\n const [own, setOwn] = useState(1);\n\n // Clamped on READ, not on write: a controlled owner that has not yet heard\n // about a shorter document would otherwise render page 40 of 12 and blank the\n // canvas, and clamping its prop back at it would fight the owner.\n const raw = controlled ? pageNumber : own;\n const current = pageCount > 0 ? Math.min(Math.max(1, raw), pageCount) : Math.max(1, raw);\n\n const goToPage = useCallback(\n (page: number) => {\n const next = pageCount > 0 ? Math.min(Math.max(1, page), pageCount) : Math.max(1, page);\n // Mirroring the platform: a controlled value is never written locally.\n if (!controlled) setOwn(next);\n onPageChange?.(next);\n },\n [controlled, onPageChange, pageCount],\n );\n\n return [current, goToPage] as const;\n}\n","\"use client\";\n\n/**\n * Continuous scrolling for a paginated document, over the shell's own viewport.\n *\n * ## Why the pages are virtualized\n *\n * A 900-page PDF is 900 canvases, 900 text layers and 900 rasterization jobs. A\n * plain stack would allocate all of them before the first page appeared. Only the\n * pages near the viewport are mounted, so the cost is a function of the WINDOW,\n * not of the document.\n *\n * ## Why the virtualizer scrolls someone else's element\n *\n * `FileViewerContent` is the one scroll boundary (`viewer-components.md`), so the\n * list is a CHILD of the scrolling box rather than being one. That is what\n * `scrollMargin` is for: every measurement is offset by where the list starts\n * inside the pane, which is the pane's own padding.\n *\n * ## The page number is a two-way binding, and that is the whole difficulty\n *\n * The shell owns `pageNumber` (ADR 0026), so it flows two ways: the reader\n * scrolls and the pager must follow; the reader types \"7\" and the document must\n * scroll. Naively wiring both makes them fight — a scroll reports page 3, the\n * prop becomes 3, and the \"the prop changed, scroll to it\" effect snaps the\n * reader back to the top of page 3 mid-gesture. Each direction therefore\n * remembers what it last said, and ignores an echo of its own words.\n */\n\nimport { useVirtualizer, type Virtualizer } from \"@tanstack/react-virtual\";\nimport { useEffect, useLayoutEffect, useRef, useState, type RefObject } from \"react\";\n\nimport { findScrollHost } from \"./scroll-host\";\n\n/**\n * Slack under the viewport's top edge, in CSS pixels.\n *\n * Without it the page whose last pixel row is still visible counts as the page\n * being read, so the pager flickers back a page at every boundary.\n */\nconst EDGE_SLACK = 8;\n\nexport interface PagedScrollOptions {\n /** Pages in the document. `0` disables the virtualizer entirely. */\n count: number;\n /**\n * The element the pages are laid out in. Must be a descendant of the scroll\n * host — its `offsetTop` is what `scrollMargin` is derived from.\n */\n listRef: RefObject<HTMLElement | null>;\n /** The page the shell wants shown, 1-based. */\n pageNumber: number;\n /** Report the page the reader scrolled to, 1-based. */\n goToPage: (page: number) => void;\n /** Height of page `index` at the current scale, in CSS pixels. */\n estimateSize: (index: number) => number;\n /**\n * A value that changes whenever EVERY page's size changes — the scale, in\n * practice. Every cached measurement is thrown away and the reader is put back\n * where they were reading, at the new size.\n */\n sizeKey?: unknown;\n}\n\n/**\n * @returns The virtualizer. Render `getVirtualItems()` inside `listRef`'s\n * element, sized to `getTotalSize()`, translated by `start - scrollMargin`.\n */\nexport function usePagedScroll({\n count,\n listRef,\n pageNumber,\n goToPage,\n estimateSize,\n sizeKey,\n}: PagedScrollOptions): Virtualizer<HTMLElement, Element> {\n // Resolved after mount rather than passed in: a renderer may be composed into\n // the shell, into a consumer's own frame, or mounted bare in a test, and only\n // the DOM knows which. State (not a ref) so the virtualizer re-reads it.\n const [host, setHost] = useState<HTMLElement | null>(null);\n const [scrollMargin, setScrollMargin] = useState(0);\n useEffect(() => {\n const list = listRef.current;\n const found = findScrollHost(list) ?? list?.parentElement ?? null;\n setHost(found);\n if (!list || !found) return;\n // Deliberately geometric rather than `offsetTop`: `offsetTop` is measured\n // from the nearest POSITIONED ancestor, and the scrolling pane is not one —\n // so it would report the distance to some outer frame and put every page at\n // the wrong offset. This is the list's true start inside the scrolled\n // content, which is the pane's own padding.\n setScrollMargin(\n list.getBoundingClientRect().top - found.getBoundingClientRect().top + found.scrollTop,\n );\n }, [listRef]);\n\n const virtualizer = useVirtualizer({\n count,\n getScrollElement: () => host,\n estimateSize,\n overscan: 1,\n scrollMargin,\n measureElement: (element, entry, instance) => {\n const measured =\n entry?.borderBoxSize?.[0]?.blockSize ?? element.getBoundingClientRect().height;\n // A hidden pane measures zero, and so does a layout-less environment.\n // Recording that would collapse the page to nothing and — because the\n // measurement is cached — leave it collapsed after the pane is shown\n // again. Keeping the estimate is the same guard `useViewportSize` carries,\n // for the same reason.\n if (measured > 0) return measured;\n const index = Number(element.getAttribute(\"data-index\"));\n return Number.isNaN(index) ? 0 : instance.options.estimateSize(index);\n },\n });\n\n const items = virtualizer.getVirtualItems();\n const scrollOffset = virtualizer.scrollOffset ?? 0;\n\n // The page under the viewport's top edge — the one a reader would name if you\n // asked what page they are on.\n const top = items.find((item) => item.start + item.size > scrollOffset + EDGE_SLACK);\n const active = top ? top.index + 1 : 1;\n\n // Where the reader is WITHIN that page, so a scale change can put them back.\n // Recorded on every scroll rather than captured at the moment of the change:\n // by then the measurements have already been reset.\n const anchor = useRef({ index: 0, fraction: 0 });\n useEffect(() => {\n if (!top || top.size <= 0) return;\n // Deliberately NOT clamped at the low end. At the very top of a document the\n // reader is ABOVE page 1's start — the pane's own padding sits between them —\n // and flooring that to 0 would scroll the padding away the first time the\n // scale resolves, so simply opening a file nudges the view.\n const fraction = (scrollOffset - top.start) / top.size;\n anchor.current = { index: top.index, fraction: Math.min(fraction, 1) };\n }, [top, scrollOffset]);\n\n // ── The page number, in both directions ──────────────────────────────────\n // Both start at 0 — a page number that cannot occur — so the FIRST value of\n // `pageNumber` still counts as something the shell asked for. A shell that\n // mounts already pointing at page 140 (a deep link, a citation restored from a\n // URL) has made a request that nothing has honoured yet.\n const reported = useRef(0);\n const requested = useRef(0);\n\n const lastActive = useRef(active);\n useEffect(() => {\n // Only a CHANGE in the page under the top edge is news. Reporting the\n // initial value would answer the deep link above with \"you are on page 1\"\n // before the scroll that honours it has even been attempted.\n if (count === 0 || active === lastActive.current) return;\n lastActive.current = active;\n if (active === pageNumber) return;\n reported.current = active;\n goToPage(active);\n }, [active, pageNumber, count, goToPage]);\n\n useEffect(() => {\n // `host` is resolved in an effect, so the first pass through here has\n // nothing to scroll — and virtual-core silently drops a scroll with no\n // element. Waiting for it is what makes the mount-time request land.\n if (!host || count === 0 || requested.current === pageNumber) return;\n requested.current = pageNumber;\n // Our own report coming back around. Scrolling here would fight the gesture\n // that produced it.\n if (pageNumber === reported.current || pageNumber === active) return;\n virtualizer.scrollToIndex(pageNumber - 1, { align: \"start\" });\n }, [host, pageNumber, active, count, virtualizer]);\n\n // ── Re-anchor after a scale change ───────────────────────────────────────\n const lastSizeKey = useRef(sizeKey);\n useLayoutEffect(() => {\n if (lastSizeKey.current === sizeKey) return;\n lastSizeKey.current = sizeKey;\n if (count === 0) return;\n\n // Measured heights describe the OLD scale, so keeping them would place every\n // page at the wrong offset until it happened to be re-measured.\n virtualizer.measure();\n\n const { index, fraction } = anchor.current;\n // At the very top there is no reading position to preserve, and scrolling to\n // page 1's own start would scroll the pane's top padding away — so simply\n // opening a file and letting its fit resolve would nudge the view.\n if (index === 0 && fraction <= 0) return;\n const start = virtualizer.getOffsetForIndex(index, \"start\")?.[0];\n if (start === undefined) return;\n const size = virtualizer.measurementsCache[index]?.size ?? 0;\n virtualizer.scrollToOffset(start + fraction * size, { align: \"start\" });\n }, [sizeKey, count, virtualizer]);\n\n return virtualizer;\n}\n","/**\n * Finding the element a renderer actually scrolls inside.\n *\n * `FileViewerContent` is THE scroll boundary (`viewer-components.md`), and now\n * that page and zoom chrome lives in the shell (ADR 0026) even the PDF adapter\n * no longer has fixed controls of its own to justify a second viewport. But a\n * renderer still has to MEASURE that box — a fit-to-width scale is a function of\n * the viewport's width, and a virtualized page list is a function of its height.\n *\n * Resolved from the DOM rather than passed as a prop because a renderer may be\n * mounted anywhere: inside the shell, inside a consumer's own frame, or bare in\n * a test. Same shape as extend-hq/ui's `resolveScrollAreaViewport`.\n */\n\n/** The `data-slot` the shell's scrolling pane carries. */\nexport const SCROLL_HOST_SELECTOR = '[data-slot=\"file-viewer-content\"]';\n\n/**\n * The nearest scrolling ancestor of `node`.\n *\n * Prefers the shell's pane by its stable `data-slot`; falls back to the first\n * ancestor whose computed overflow actually scrolls, so a renderer composed into\n * someone else's frame still measures the right box. `null` when nothing\n * scrolls — the caller should then fall back to its own element.\n */\nexport function findScrollHost(node: Element | null | undefined): HTMLElement | null {\n if (!node) return null;\n const shell = node.closest<HTMLElement>(SCROLL_HOST_SELECTOR);\n if (shell) return shell;\n\n const view = node.ownerDocument?.defaultView;\n if (!view) return null;\n let current = node.parentElement;\n while (current) {\n const { overflowY, overflowX } = view.getComputedStyle(current);\n if (/(auto|scroll|overlay)/.test(overflowY) || /(auto|scroll|overlay)/.test(overflowX)) {\n return current;\n }\n current = current.parentElement;\n }\n return null;\n}\n","\"use client\";\n\n/**\n * The size of the box a renderer scrolls inside, kept current.\n *\n * Needed because a fit-to-width scale is a function of the VIEWPORT, not of the\n * document: only the renderer can compute it, and only by measuring. Measures\n * the container rather than the window (`window.innerWidth` would be wrong the\n * moment a viewer sits in a resizable pane, a dialog or a split workspace).\n */\n\nimport { useEffect, useState, type RefObject } from \"react\";\n\nimport { findScrollHost } from \"./scroll-host\";\n\nexport interface ViewportSize {\n width: number;\n height: number;\n}\n\n/**\n * @param ref An element inside the scrolling box — typically the renderer's root.\n * @returns The scroll host's content size, or `undefined` until it is measured.\n */\nexport function useViewportSize(ref: RefObject<HTMLElement | null>): ViewportSize | undefined {\n const [size, setSize] = useState<ViewportSize>();\n\n useEffect(() => {\n const host = findScrollHost(ref.current) ?? ref.current?.parentElement ?? ref.current;\n if (!host || typeof ResizeObserver === \"undefined\") return;\n\n const measure = () => {\n const { clientWidth: width, clientHeight: height } = host;\n // A hidden pane measures zero. Re-fitting to a zero-width viewport would\n // rasterize a zero-width page and clear the canvas — and nothing would\n // measure again when the pane is shown, because its size did not change\n // from the observer's point of view. Ignoring the reading keeps the last\n // good one. (The guard extend-hq/ui's `useElementWidth` carries too.)\n if (width === 0 || height === 0) return;\n setSize((current) =>\n current && Math.abs(current.width - width) < 1 && Math.abs(current.height - height) < 1\n ? current\n : { width, height },\n );\n };\n\n measure();\n const observer = new ResizeObserver(measure);\n observer.observe(host);\n return () => observer.disconnect();\n }, [ref]);\n\n return size;\n}\n"],"mappings":"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","names":["useState","useState","useEffect","useState","useState","useEffect"]}
@@ -0,0 +1,87 @@
1
+ "use client";
2
+ import {
3
+ PROTOCOL_VERSION
4
+ } from "./chunk-AORNMH77.js";
5
+
6
+ // src/adapters/code/code-language.ts
7
+ var LANGUAGE_BY_EXTENSION = {
8
+ bash: "shellscript",
9
+ c: "c",
10
+ cc: "cpp",
11
+ cjs: "javascript",
12
+ cpp: "cpp",
13
+ cs: "csharp",
14
+ css: "css",
15
+ cts: "typescript",
16
+ dart: "dart",
17
+ diff: "diff",
18
+ dockerfile: "docker",
19
+ go: "go",
20
+ gql: "graphql",
21
+ graphql: "graphql",
22
+ groovy: "groovy",
23
+ h: "c",
24
+ hcl: "hcl",
25
+ hpp: "cpp",
26
+ htm: "html",
27
+ html: "html",
28
+ ini: "ini",
29
+ java: "java",
30
+ js: "javascript",
31
+ jsx: "jsx",
32
+ kt: "kotlin",
33
+ kts: "kotlin",
34
+ less: "less",
35
+ lua: "lua",
36
+ makefile: "make",
37
+ mjs: "javascript",
38
+ mts: "typescript",
39
+ patch: "diff",
40
+ php: "php",
41
+ pl: "perl",
42
+ proto: "proto",
43
+ ps1: "powershell",
44
+ py: "python",
45
+ r: "r",
46
+ rb: "ruby",
47
+ rs: "rust",
48
+ sass: "sass",
49
+ scala: "scala",
50
+ scss: "scss",
51
+ sh: "shellscript",
52
+ sql: "sql",
53
+ svelte: "svelte",
54
+ swift: "swift",
55
+ tf: "terraform",
56
+ toml: "toml",
57
+ ts: "typescript",
58
+ tsx: "tsx",
59
+ vue: "vue",
60
+ xml: "xml",
61
+ yaml: "yaml",
62
+ yml: "yaml",
63
+ zsh: "shellscript"
64
+ };
65
+ var CODE_EXTENSIONS = Object.keys(LANGUAGE_BY_EXTENSION);
66
+ function languageFor(fileName) {
67
+ const name = fileName.toLowerCase();
68
+ const dot = name.lastIndexOf(".");
69
+ const extension = dot > 0 ? name.slice(dot + 1) : name;
70
+ const map = LANGUAGE_BY_EXTENSION;
71
+ return map[extension] ?? map[name];
72
+ }
73
+
74
+ // src/adapters/code/code-manifest.ts
75
+ var codeManifest = {
76
+ id: "code",
77
+ protocol: PROTOCOL_VERSION,
78
+ extensions: CODE_EXTENSIONS,
79
+ capabilities: { text: true, search: true, highlight: ["quote", "range"] },
80
+ requires: ["shiki"]
81
+ };
82
+
83
+ export {
84
+ languageFor,
85
+ codeManifest
86
+ };
87
+ //# sourceMappingURL=chunk-5OMONA2G.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/adapters/code/code-language.ts","../src/adapters/code/code-manifest.ts"],"sourcesContent":["/**\n * Extension → Shiki language id.\n *\n * A pure data module on purpose: `code-manifest.ts` derives the extensions it\n * claims from this map, so the list the registry matches on and the list the\n * highlighter can actually tokenize can never drift apart. Adding a language is\n * one line here.\n *\n * What is deliberately ABSENT is as load-bearing as what is present. `json`,\n * `csv`, `md` and `svg` are claimed by their own adapters — a JSON file gets a\n * collapsible tree, not a coloured wall of text — and an extension listed in two\n * manifests would be decided by registration order rather than by design.\n */\n\nimport type { BundledLanguage } from \"shiki\";\n\n/**\n * Shiki's own language ids.\n *\n * `satisfies Record<string, BundledLanguage>` is the point of the annotation: a\n * typo'd grammar name fails typecheck HERE, rather than at runtime as a file\n * that silently refuses to highlight. Type-only import, so this stays a pure\n * data module with no edge to the engine.\n */\nconst LANGUAGE_BY_EXTENSION = {\n bash: \"shellscript\",\n c: \"c\",\n cc: \"cpp\",\n cjs: \"javascript\",\n cpp: \"cpp\",\n cs: \"csharp\",\n css: \"css\",\n cts: \"typescript\",\n dart: \"dart\",\n diff: \"diff\",\n dockerfile: \"docker\",\n go: \"go\",\n gql: \"graphql\",\n graphql: \"graphql\",\n groovy: \"groovy\",\n h: \"c\",\n hcl: \"hcl\",\n hpp: \"cpp\",\n htm: \"html\",\n html: \"html\",\n ini: \"ini\",\n java: \"java\",\n js: \"javascript\",\n jsx: \"jsx\",\n kt: \"kotlin\",\n kts: \"kotlin\",\n less: \"less\",\n lua: \"lua\",\n makefile: \"make\",\n mjs: \"javascript\",\n mts: \"typescript\",\n patch: \"diff\",\n php: \"php\",\n pl: \"perl\",\n proto: \"proto\",\n ps1: \"powershell\",\n py: \"python\",\n r: \"r\",\n rb: \"ruby\",\n rs: \"rust\",\n sass: \"sass\",\n scala: \"scala\",\n scss: \"scss\",\n sh: \"shellscript\",\n sql: \"sql\",\n svelte: \"svelte\",\n swift: \"swift\",\n tf: \"terraform\",\n toml: \"toml\",\n ts: \"typescript\",\n tsx: \"tsx\",\n vue: \"vue\",\n xml: \"xml\",\n yaml: \"yaml\",\n yml: \"yaml\",\n zsh: \"shellscript\",\n} as const satisfies Record<string, BundledLanguage>;\n\n/** Every extension the code adapter claims, lowercase and without the dot. */\nexport const CODE_EXTENSIONS = Object.keys(LANGUAGE_BY_EXTENSION);\n\n/**\n * The Shiki language for a file name, or `undefined` when nothing matches.\n *\n * Matches the extension first, then the whole (lowercased) file name — that is\n * what catches `Dockerfile` and `Makefile`, which carry their language in the\n * name and have no extension at all.\n */\nexport function languageFor(fileName: string): BundledLanguage | undefined {\n const name = fileName.toLowerCase();\n const dot = name.lastIndexOf(\".\");\n const extension = dot > 0 ? name.slice(dot + 1) : name;\n const map: Record<string, BundledLanguage> = LANGUAGE_BY_EXTENSION;\n return map[extension] ?? map[name];\n}\n","import type { AdapterManifest } from \"../../core/types\";\nimport { PROTOCOL_VERSION } from \"../../core/types\";\nimport { CODE_EXTENSIONS } from \"./code-language\";\n\n/**\n * Eager, data-only (see `image-manifest.ts`).\n *\n * Claims EXTENSIONS only — never the `code` category. That is what keeps the\n * plain-text adapter as the backstop: a `.log`, a `.env` or an unknown\n * extension still opens as text instead of failing to find a grammar, and a\n * consumer who never installs Shiki loses highlighting rather than the file.\n */\nexport const codeManifest: AdapterManifest = {\n id: \"code\",\n protocol: PROTOCOL_VERSION,\n extensions: CODE_EXTENSIONS,\n capabilities: { text: true, search: true, highlight: [\"quote\", \"range\"] },\n requires: [\"shiki\"],\n};\n"],"mappings":";;;;;;AAwBA,IAAM,wBAAwB;AAAA,EAC5B,MAAM;AAAA,EACN,GAAG;AAAA,EACH,IAAI;AAAA,EACJ,KAAK;AAAA,EACL,KAAK;AAAA,EACL,IAAI;AAAA,EACJ,KAAK;AAAA,EACL,KAAK;AAAA,EACL,MAAM;AAAA,EACN,MAAM;AAAA,EACN,YAAY;AAAA,EACZ,IAAI;AAAA,EACJ,KAAK;AAAA,EACL,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,GAAG;AAAA,EACH,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,MAAM;AAAA,EACN,KAAK;AAAA,EACL,MAAM;AAAA,EACN,IAAI;AAAA,EACJ,KAAK;AAAA,EACL,IAAI;AAAA,EACJ,KAAK;AAAA,EACL,MAAM;AAAA,EACN,KAAK;AAAA,EACL,UAAU;AAAA,EACV,KAAK;AAAA,EACL,KAAK;AAAA,EACL,OAAO;AAAA,EACP,KAAK;AAAA,EACL,IAAI;AAAA,EACJ,OAAO;AAAA,EACP,KAAK;AAAA,EACL,IAAI;AAAA,EACJ,GAAG;AAAA,EACH,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,OAAO;AAAA,EACP,MAAM;AAAA,EACN,IAAI;AAAA,EACJ,KAAK;AAAA,EACL,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,IAAI;AAAA,EACJ,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,MAAM;AAAA,EACN,KAAK;AAAA,EACL,KAAK;AACP;AAGO,IAAM,kBAAkB,OAAO,KAAK,qBAAqB;AASzD,SAAS,YAAY,UAA+C;AACzE,QAAM,OAAO,SAAS,YAAY;AAClC,QAAM,MAAM,KAAK,YAAY,GAAG;AAChC,QAAM,YAAY,MAAM,IAAI,KAAK,MAAM,MAAM,CAAC,IAAI;AAClD,QAAM,MAAuC;AAC7C,SAAO,IAAI,SAAS,KAAK,IAAI,IAAI;AACnC;;;ACvFO,IAAM,eAAgC;AAAA,EAC3C,IAAI;AAAA,EACJ,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,cAAc,EAAE,MAAM,MAAM,QAAQ,MAAM,WAAW,CAAC,SAAS,OAAO,EAAE;AAAA,EACxE,UAAU,CAAC,OAAO;AACpB;","names":[]}
@@ -0,0 +1,58 @@
1
+ "use client";
2
+
3
+ // src/core/types.ts
4
+ var PROTOCOL_VERSION = 1;
5
+
6
+ // src/core/errors.ts
7
+ var ViewerError = class extends Error {
8
+ code;
9
+ fileName;
10
+ packages;
11
+ constructor(code, message, options = {}) {
12
+ super(message, { cause: options.cause });
13
+ this.name = "ViewerError";
14
+ this.code = code;
15
+ this.fileName = options.fileName;
16
+ this.packages = options.packages;
17
+ }
18
+ };
19
+ function isViewerError(value) {
20
+ return value instanceof ViewerError || value instanceof Error && value.name === "ViewerError";
21
+ }
22
+ function isAbort(value) {
23
+ if (isViewerError(value)) return value.code === "aborted";
24
+ return value instanceof Error && (value.name === "AbortError" || value.name === "TimeoutError");
25
+ }
26
+ function toViewerError(value, fallbackCode, options = {}) {
27
+ if (isViewerError(value)) return value;
28
+ if (isAbort(value)) {
29
+ return new ViewerError("aborted", "The file load was cancelled.", { ...options, cause: value });
30
+ }
31
+ const message = value instanceof Error ? value.message : String(value);
32
+ return new ViewerError(fallbackCode, message, { ...options, cause: value });
33
+ }
34
+ function isModuleNotFound(value) {
35
+ if (!(value instanceof Error)) return false;
36
+ if ("code" in value && value.code === "ERR_MODULE_NOT_FOUND") return true;
37
+ return /cannot find module|failed to resolve|dynamically imported module|module not found/i.test(
38
+ value.message
39
+ );
40
+ }
41
+ function parserMissingError(adapterId, packages, options = {}) {
42
+ return new ViewerError(
43
+ "parser-missing",
44
+ packages.length > 0 ? `The "${adapterId}" adapter needs ${packages.join(", ")}, which is not installed.` : `The "${adapterId}" adapter could not be loaded.`,
45
+ { ...options, packages }
46
+ );
47
+ }
48
+
49
+ export {
50
+ PROTOCOL_VERSION,
51
+ ViewerError,
52
+ isViewerError,
53
+ isAbort,
54
+ toViewerError,
55
+ isModuleNotFound,
56
+ parserMissingError
57
+ };
58
+ //# sourceMappingURL=chunk-AORNMH77.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/types.ts","../src/core/errors.ts"],"sourcesContent":["/**\n * The adapter protocol — what a format plugin declares, what it produces, and\n * what renders it.\n *\n * ## The two halves, and why they are split\n *\n * A registry entry is an eager {@link AdapterManifest} plus a lazy loader. The\n * manifest is plain data available the moment the registry is created, so the\n * chrome can decide which controls to show and whether a file is openable at all\n * **without downloading a parser**. The loader is a `() => import(…)` that pulls\n * the parser AND its renderer together, only once a file of that kind is opened.\n *\n * ## Adapters emit DATA, not HTML (ADR 0024 §3)\n *\n * An adapter's `load()` returns an {@link AdapterDocument} — a model — and ships\n * a `Renderer` that draws it with brand-ui components. No adapter returns an\n * HTML string, so no adapter can smuggle a colour, a font or a border past the\n * token layer. This is the single decision that keeps the viewer themeable.\n */\n\nimport type {\n DocumentAddressKind,\n FileCategory,\n ProseHeadingLevel,\n ResolvedFileSource,\n} from \"@elabs-ai/components-ui\";\nimport type { ComponentType } from \"react\";\n\nimport type { ResolvedHighlight } from \"./highlight\";\nimport type { TextIndex } from \"./text-index\";\n\n/**\n * The adapter protocol version. Bumped only when the shapes below change in a\n * way an existing adapter would not survive; an adapter declaring a different\n * version is rejected with a named error rather than crashing at render.\n */\nexport const PROTOCOL_VERSION = 1;\n\n/**\n * A quarter-turn clockwise rotation applied to the whole document.\n *\n * Document-level, not per-page: turning one page of a scan and leaving the rest\n * is an AUTHORING act (it changes the file), and this package reads files.\n */\nexport type DocumentRotation = 0 | 90 | 180 | 270;\n\n/** Scale the content to a dimension of the viewport instead of to a fixed number. */\nexport type ZoomFit = \"fit-width\" | \"fit-page\";\n\n/** One page's size at scale 1, in CSS pixels. */\nexport interface PageSize {\n width: number;\n height: number;\n}\n\n/**\n * What the chrome asks a renderer to draw at.\n *\n * A fit mode is a REQUEST, not a scale: only the renderer knows how wide its own\n * viewport is and how big the page is, so it resolves the request and reports the\n * number back through {@link AdapterRendererProps.onZoomResolved}. Resolving it\n * in the provider would mean the shell measuring a box it does not own.\n */\nexport type ZoomLevel = number | ZoomFit;\n\n/** What the chrome may offer for a format, known before any parser is fetched. */\nexport interface AdapterCapabilities {\n /** The document is paginated — show the pager. */\n pages?: boolean;\n /** The rendered content can be scaled — show the zoom control. */\n zoom?: boolean;\n /** The rendered content can be rotated. */\n rotate?: boolean;\n /**\n * The document exposes text that can be searched.\n *\n * An OVERRIDE, not the source of truth: the shell offers find-in-document\n * whenever a format has both a text projection and a text-based\n * {@link AdapterCapabilities.highlight} kind, and reads this only when an\n * adapter says otherwise. Left as the source of truth it would deny the find\n * box to PDF — the format readers most expect it on — purely because that\n * manifest predates the feature and never declared the flag.\n *\n * It overrides in the OFF direction only, so a declared `true` is inert: it\n * cannot grant find to a format with no text or no range-painting renderer,\n * because the box would then have nothing to show for a match.\n */\n search?: boolean;\n /**\n * Which {@link DocumentAddress} kinds this adapter's `Renderer` can actually\n * paint. Absent means none.\n *\n * On the EAGER manifest on purpose: an app can decide whether to show a\n * \"jump to the cited passage\" affordance before it downloads pdf.js.\n *\n * It is a promise the renderer has to keep — `pnpm viewer-highlight:check`\n * fails a manifest that declares a kind its adapter never exercises.\n */\n highlight?: readonly DocumentAddressKind[];\n /** The document has an outline/table of contents for the sidebar. */\n outline?: boolean;\n /** The document can produce page thumbnails for the sidebar. */\n thumbnails?: boolean;\n /** A plain-text projection exists — enables the raw/source toggle and copy. */\n text?: boolean;\n}\n\n/**\n * What an adapter can open. At least one of `extensions` / `mediaTypes` /\n * `categories` must be present, or nothing will ever route to it.\n */\nexport interface AdapterManifest {\n /**\n * Stable identity and the OVERRIDE key: registering a different loader under\n * an existing id replaces it, which is how a consumer swaps a built-in.\n */\n id: string;\n /** Must equal {@link PROTOCOL_VERSION}. */\n protocol: number;\n /**\n * Tie-break when several adapters claim the same file. Built-ins are `0`;\n * register above it to win, below it to act only as a fallback.\n */\n priority?: number;\n /** Extensions claimed, lowercase and without the dot. The most specific match. */\n extensions?: string[];\n /** MIME types claimed. A trailing slash makes it a prefix (`\"image/\"`). */\n mediaTypes?: string[];\n /** Coarse categories claimed. The broadest match — a catch-all fallback. */\n categories?: FileCategory[];\n /** Which chrome controls apply. Absent keys are `false`. */\n capabilities?: AdapterCapabilities;\n /**\n * Optional peer packages this adapter dynamically imports. Purely\n * informational to the registry — it is what the \"install this to open that\"\n * message names when the import fails to resolve.\n */\n requires?: string[];\n}\n\n/**\n * An adapter's parsed output. Adapters EXTEND this with their own payload; the\n * fields here are the parts the shell itself understands, so it can drive the\n * pager, the search box and the raw toggle without knowing the format.\n */\nexport interface AdapterDocument {\n /** Adapter-defined discriminant, matched by that adapter's own `Renderer`. */\n readonly kind: string;\n /** Page count for a paginated document. Omit for single-surface formats. */\n readonly pageCount?: number;\n /**\n * Each page's size at scale 1, in document order.\n *\n * What lets a continuously-scrolling renderer reserve the right height for a\n * page it has not drawn yet, so the scrollbar means something from the first\n * frame instead of growing as pages arrive. May be SHORTER than\n * {@link AdapterDocument.pageCount} — measuring 900 pages up front costs 900\n * round-trips to a worker, so an adapter is free to describe the pages it\n * already touched and let the renderer assume the rest match.\n */\n readonly pageSizes?: readonly PageSize[];\n /** Plain-text projection, when the format has one. Powers search, copy and the raw view. */\n readonly text?: string;\n /**\n * Whether {@link AdapterDocument.text} is CAPPED rather than complete.\n *\n * Changes what \"we couldn't find that passage\" means: absent from the whole\n * document, or simply past the part we previewed. Those deserve different\n * sentences, and only the adapter knows which one is true.\n */\n readonly textTruncated?: boolean;\n /**\n * The map from {@link AdapterDocument.text} back into this adapter's own\n * model, so a character range can become a block, a page or a cell.\n *\n * The `ref` type is the adapter's business — its own `Renderer` is the only\n * thing that reads it, which is why it is `unknown` here.\n */\n readonly textIndex?: TextIndex<unknown>;\n}\n\n/** Everything an adapter is given while loading a document. */\nexport interface AdapterLoadContext {\n /** Cancels the load — always honour it; a stale parse must not win a race. */\n signal?: AbortSignal;\n /** Report determinate progress in `0..1` where the format allows it. */\n onProgress?: (fraction: number) => void;\n}\n\n/** A parser instance. One per document — adapters may hold per-document state. */\nexport interface FileAdapter {\n load(source: ResolvedFileSource, context: AdapterLoadContext): Promise<AdapterDocument>;\n /** Release anything the document holds (workers, object URLs, decoded buffers). */\n dispose?(): void;\n}\n\n/** Props every adapter `Renderer` receives. */\nexport interface AdapterRendererProps {\n /** The value this adapter's own `load()` returned — narrow it by `kind`. */\n document: AdapterDocument;\n /** The source, for identity, `alt` text and download actions. */\n source: ResolvedFileSource;\n className?: string;\n /**\n * The heading rung a document's own top-level heading renders at.\n *\n * A viewed file carries its OWN heading tree, and that tree has to be\n * relative to the page hosting it: a README's `#` inside an app that already\n * has an `<h1>` would otherwise put two `h1`s in a screen reader's flat\n * heading list. `FileViewerContent` passes the provider's value; an adapter\n * that renders headings offsets by it (`clampHeadingLevel(own + base - 1)`),\n * and one that renders none ignores it. Same seam as\n * `@elabs-ai/components-ai`'s `MarkdownView baseHeadingLevel`.\n *\n * Optional, so adding it is not a `PROTOCOL_VERSION` change.\n */\n baseHeadingLevel?: ProseHeadingLevel;\n /**\n * The parts of the document to mark, already located — citations from the\n * app and the viewer's own find-in-document matches, in document order.\n *\n * The shell has done LOCATE (a quote is now a character range); the renderer\n * does MAP and PAINT, because only it knows that offsets 812–847 are the\n * second half of block 14. An adapter renders only the entries whose `status`\n * is `\"resolved\"` and whose `address.kind` it declared, and ignores the rest\n * — the shell is what tells the reader about a miss.\n *\n * Optional, so adding it is not a `PROTOCOL_VERSION` change.\n */\n highlights?: readonly ResolvedHighlight[];\n /**\n * Which highlight the viewer is pointed at. The renderer scrolls it into\n * view, pages to it if the format paginates, and draws it distinctly.\n *\n * Also present as `active` on the entry itself; this is the shortcut for a\n * renderer that only needs to know whether anything changed.\n */\n activeHighlightId?: string | null;\n /**\n * Which page the reader is on, 1-based — the provider's, not the renderer's.\n *\n * Page, zoom and rotation are PROVIDER state (ADR 0026) so a control can live\n * anywhere: the shell's own toolbar, an app's page header, a deep link. While\n * these were `useState` inside each renderer nothing outside the canvas could\n * read or drive them.\n *\n * Uncontrolled when absent — a renderer used outside a `FileViewerProvider`\n * keeps its own page, so it still works standalone. Optional, so adding it is\n * not a `PROTOCOL_VERSION` change.\n */\n pageNumber?: number;\n /**\n * Report a page the RENDERER navigated to on its own — turning to a cited\n * page, or (once pages scroll continuously) the page scrolled into view. The\n * provider is what keeps the pager's number honest.\n */\n onPageChange?: (page: number) => void;\n /** The scale, or a fit mode to resolve. Absent means the renderer's own default. */\n zoom?: ZoomLevel;\n /**\n * Report the scale a {@link ZoomFit} actually resolved to.\n *\n * Only a renderer knows this — it is the one measuring its viewport. Without\n * it the chrome cannot show a percentage for a fitted page, and the reader's\n * next \"zoom in\" would step from the last fixed stop rather than from what is\n * on screen. A renderer that never resolves a fit mode never calls it.\n */\n onZoomResolved?: (scale: number) => void;\n /** Quarter-turns clockwise. Ignored by renderers whose manifest omits `rotate`. */\n rotation?: DocumentRotation;\n}\n\n/**\n * What an adapter module exports. `manifest` is re-declared here so the registry\n * can verify the loaded module is the one the eager manifest promised.\n */\nexport interface AdapterModule {\n manifest: AdapterManifest;\n /** Fresh instance per document — never a shared singleton. */\n create(): FileAdapter;\n Renderer: ComponentType<AdapterRendererProps>;\n}\n\n/** A lazy adapter loader. Always `() => import(\"…\")` so nothing heavy is eager. */\nexport type AdapterLoader = () => Promise<AdapterModule | { default: AdapterModule }>;\n","/**\n * Typed failures, so the UI can decide what to SHOW without string-matching a\n * message.\n *\n * The `message` on these errors is developer-facing — it goes to the console and\n * to bug reports. Everything the user reads is looked up from {@link ViewerErrorCode}\n * through `t()` (ADR 0017), which is why the code, not the prose, is the contract.\n */\n\n/** Why a file could not be shown. */\nexport type ViewerErrorCode =\n /** No registered adapter claims this file. */\n | \"unsupported-format\"\n /** An adapter was built against a different registry protocol. */\n | \"protocol-mismatch\"\n /** The adapter exists but its optional peer dependency is not installed. */\n | \"parser-missing\"\n /** The bytes could not be obtained (network, permissions, a revoked blob). */\n | \"read-failed\"\n /** The bytes arrived but the parser rejected them. */\n | \"parse-failed\"\n /** The load was cancelled — a new source, or an unmounted view. */\n | \"aborted\";\n\nexport interface ViewerErrorOptions extends ErrorOptions {\n /** The file this is about, for the developer-facing message. */\n fileName?: string;\n /** For `parser-missing`: the packages a consumer has to install. */\n packages?: string[];\n}\n\n/** A failure a `FileViewer` can render, carrying a machine-readable {@link ViewerErrorCode}. */\nexport class ViewerError extends Error {\n readonly code: ViewerErrorCode;\n readonly fileName?: string;\n readonly packages?: string[];\n\n constructor(code: ViewerErrorCode, message: string, options: ViewerErrorOptions = {}) {\n super(message, { cause: options.cause });\n this.name = \"ViewerError\";\n this.code = code;\n this.fileName = options.fileName;\n this.packages = options.packages;\n }\n}\n\n/** True for a {@link ViewerError}, without relying on `instanceof` across bundles. */\nexport function isViewerError(value: unknown): value is ViewerError {\n return value instanceof ViewerError || (value instanceof Error && value.name === \"ViewerError\");\n}\n\n/** True when the failure is a cancellation rather than something to report. */\nexport function isAbort(value: unknown): boolean {\n if (isViewerError(value)) return value.code === \"aborted\";\n return value instanceof Error && (value.name === \"AbortError\" || value.name === \"TimeoutError\");\n}\n\n/**\n * Wrap an unknown throw as a {@link ViewerError}, preserving an existing one and\n * mapping an abort to the `aborted` code so callers do not report cancellations\n * as failures.\n */\nexport function toViewerError(\n value: unknown,\n fallbackCode: ViewerErrorCode,\n options: ViewerErrorOptions = {},\n): ViewerError {\n if (isViewerError(value)) return value;\n if (isAbort(value)) {\n return new ViewerError(\"aborted\", \"The file load was cancelled.\", { ...options, cause: value });\n }\n const message = value instanceof Error ? value.message : String(value);\n return new ViewerError(fallbackCode, message, { ...options, cause: value });\n}\n\n/**\n * A dynamic `import()` of an OPTIONAL peer that is not installed.\n *\n * Bundlers and Node report this differently (`ERR_MODULE_NOT_FOUND`,\n * `Cannot find module`, `Failed to resolve module specifier`, Vite's\n * `Failed to fetch dynamically imported module`), so match on the shapes rather\n * than one runtime's wording. A false positive here is cheap — the user is told\n * to install a package instead of seeing a raw parse error — and a false\n * negative is the anyview failure mode this exists to avoid: an unhelpful\n * module-resolution stack trace where a \"install papaparse to open CSV files\"\n * message belongs.\n */\nexport function isModuleNotFound(value: unknown): boolean {\n if (!(value instanceof Error)) return false;\n if (\"code\" in value && value.code === \"ERR_MODULE_NOT_FOUND\") return true;\n return /cannot find module|failed to resolve|dynamically imported module|module not found/i.test(\n value.message,\n );\n}\n\n/**\n * The `parser-missing` error, worded the same wherever the peer turns out to be\n * absent.\n *\n * There are TWO such places, which is why this is a function and not a literal\n * at one call site. The registry catches it when the adapter MODULE cannot be\n * fetched; `FileViewerProvider` catches it when the module loaded fine and the\n * adapter's own `await import(\"mammoth\")` — inside `load()`, where every parser\n * engine is actually reached — rejects. Without the second, a consumer who\n * skipped an optional peer was told their file was damaged and offered a retry\n * that could never work.\n */\nexport function parserMissingError(\n adapterId: string,\n packages: string[],\n options: ViewerErrorOptions = {},\n): ViewerError {\n return new ViewerError(\n \"parser-missing\",\n packages.length > 0\n ? `The \"${adapterId}\" adapter needs ${packages.join(\", \")}, which is not installed.`\n : `The \"${adapterId}\" adapter could not be loaded.`,\n { ...options, packages },\n );\n}\n"],"mappings":";;;AAoCO,IAAM,mBAAmB;;;ACJzB,IAAM,cAAN,cAA0B,MAAM;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EAET,YAAY,MAAuB,SAAiB,UAA8B,CAAC,GAAG;AACpF,UAAM,SAAS,EAAE,OAAO,QAAQ,MAAM,CAAC;AACvC,SAAK,OAAO;AACZ,SAAK,OAAO;AACZ,SAAK,WAAW,QAAQ;AACxB,SAAK,WAAW,QAAQ;AAAA,EAC1B;AACF;AAGO,SAAS,cAAc,OAAsC;AAClE,SAAO,iBAAiB,eAAgB,iBAAiB,SAAS,MAAM,SAAS;AACnF;AAGO,SAAS,QAAQ,OAAyB;AAC/C,MAAI,cAAc,KAAK,EAAG,QAAO,MAAM,SAAS;AAChD,SAAO,iBAAiB,UAAU,MAAM,SAAS,gBAAgB,MAAM,SAAS;AAClF;AAOO,SAAS,cACd,OACA,cACA,UAA8B,CAAC,GAClB;AACb,MAAI,cAAc,KAAK,EAAG,QAAO;AACjC,MAAI,QAAQ,KAAK,GAAG;AAClB,WAAO,IAAI,YAAY,WAAW,gCAAgC,EAAE,GAAG,SAAS,OAAO,MAAM,CAAC;AAAA,EAChG;AACA,QAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,SAAO,IAAI,YAAY,cAAc,SAAS,EAAE,GAAG,SAAS,OAAO,MAAM,CAAC;AAC5E;AAcO,SAAS,iBAAiB,OAAyB;AACxD,MAAI,EAAE,iBAAiB,OAAQ,QAAO;AACtC,MAAI,UAAU,SAAS,MAAM,SAAS,uBAAwB,QAAO;AACrE,SAAO,qFAAqF;AAAA,IAC1F,MAAM;AAAA,EACR;AACF;AAcO,SAAS,mBACd,WACA,UACA,UAA8B,CAAC,GAClB;AACb,SAAO,IAAI;AAAA,IACT;AAAA,IACA,SAAS,SAAS,IACd,QAAQ,SAAS,mBAAmB,SAAS,KAAK,IAAI,CAAC,8BACvD,QAAQ,SAAS;AAAA,IACrB,EAAE,GAAG,SAAS,SAAS;AAAA,EACzB;AACF;","names":[]}
@@ -0,0 +1,18 @@
1
+ "use client";
2
+ import {
3
+ PROTOCOL_VERSION
4
+ } from "./chunk-AORNMH77.js";
5
+
6
+ // src/adapters/json/json-manifest.ts
7
+ var jsonManifest = {
8
+ id: "json",
9
+ protocol: PROTOCOL_VERSION,
10
+ extensions: ["json"],
11
+ mediaTypes: ["application/json"],
12
+ capabilities: { text: true, search: true }
13
+ };
14
+
15
+ export {
16
+ jsonManifest
17
+ };
18
+ //# sourceMappingURL=chunk-BCF3RWRL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/adapters/json/json-manifest.ts"],"sourcesContent":["import type { AdapterManifest } from \"../../core/types\";\nimport { PROTOCOL_VERSION } from \"../../core/types\";\n\n/** Eager, data-only (see `image-manifest.ts`). */\nexport const jsonManifest: AdapterManifest = {\n id: \"json\",\n protocol: PROTOCOL_VERSION,\n extensions: [\"json\"],\n mediaTypes: [\"application/json\"],\n capabilities: { text: true, search: true },\n};\n"],"mappings":";;;;;;AAIO,IAAM,eAAgC;AAAA,EAC3C,IAAI;AAAA,EACJ,UAAU;AAAA,EACV,YAAY,CAAC,MAAM;AAAA,EACnB,YAAY,CAAC,kBAAkB;AAAA,EAC/B,cAAc,EAAE,MAAM,MAAM,QAAQ,KAAK;AAC3C;","names":[]}