@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
@@ -0,0 +1,166 @@
1
+ "use client";
2
+
3
+ /**
4
+ * `FileViewerZoom` and `FileViewerRotate` — how big, and which way up.
5
+ *
6
+ * Parts over the provider's view state (ADR 0026), so an app can put the scale
7
+ * control in its own header without reaching inside the canvas.
8
+ *
9
+ * Each renders NOTHING for a format whose manifest does not claim the capability
10
+ * — an inert zoom control over a CSV is chrome with nothing in it, which reads
11
+ * as a broken render (`viewer-components.md`). That absence is also the fix for
12
+ * the `image` manifest's old lie: it declared `zoom` and `rotate` while its
13
+ * renderer implemented neither, so the claim was invisible either way.
14
+ */
15
+
16
+ import {
17
+ cn,
18
+ IconButton,
19
+ Select,
20
+ SelectContent,
21
+ SelectItem,
22
+ SelectSeparator,
23
+ SelectTrigger,
24
+ SelectValue,
25
+ useLocale,
26
+ } from "@elabs-ai/components-ui";
27
+ import { RotateCwIcon, ZoomInIcon, ZoomOutIcon } from "lucide-react";
28
+ import { forwardRef, type HTMLAttributes } from "react";
29
+
30
+ import type { ZoomLevel } from "../core/types";
31
+ import { canStepZoom, isZoomFit, VIEWER_ZOOM_STEPS } from "../core/zoom";
32
+ import { useFileViewer } from "./file-viewer-context";
33
+
34
+ export type FileViewerZoomProps = HTMLAttributes<HTMLDivElement>;
35
+
36
+ /**
37
+ * Zoom out · the current level · zoom in.
38
+ *
39
+ * The middle control is a `Select` rather than a read-out because the stops are
40
+ * the API: a reader who wants 200% should not have to press "+" four times, and
41
+ * the two fit modes have no number to press towards at all.
42
+ *
43
+ * Not a `role="toolbar"` — see `FileViewerPager`. It sits in the same row, and
44
+ * one row that is half roving-tabindex and half ordinary tab stops is worse for
45
+ * a keyboard reader than one that is consistently ordinary.
46
+ */
47
+ export const FileViewerZoom = forwardRef<HTMLDivElement, FileViewerZoomProps>(
48
+ function FileViewerZoom({ className, ...props }, ref) {
49
+ const { state, actions } = useFileViewer();
50
+ const { t, formatNumber } = useLocale();
51
+
52
+ if (!state.capabilities.zoom) return null;
53
+
54
+ const { zoom, effectiveZoom } = state;
55
+ const percent = (scale: number) =>
56
+ formatNumber(scale, { style: "percent", maximumFractionDigits: 0 });
57
+
58
+ return (
59
+ <div
60
+ ref={ref}
61
+ data-slot="file-viewer-zoom"
62
+ role="group"
63
+ aria-label={t("viewer.zoom.controls")}
64
+ className={cn("flex shrink-0 items-center gap-1", className)}
65
+ {...props}
66
+ >
67
+ <IconButton
68
+ variant="ghost"
69
+ size="icon-sm"
70
+ label={t("viewer.zoom.out")}
71
+ icon={<ZoomOutIcon aria-hidden="true" />}
72
+ // Stepping is measured against what is ON SCREEN, so a page fitted to
73
+ // 137% still has somewhere to go in both directions.
74
+ disabled={!canStepZoom(effectiveZoom, -1)}
75
+ onClick={actions.zoomOut}
76
+ />
77
+ <Select
78
+ value={zoomToValue(zoom)}
79
+ onValueChange={(value) => actions.setZoom(valueToZoom(value))}
80
+ >
81
+ <SelectTrigger
82
+ size="sm"
83
+ aria-label={t("viewer.zoom.level")}
84
+ className="h-7 w-24 gap-1 px-2"
85
+ >
86
+ {/* Children override the selected item's own text: a fit mode has to
87
+ read as "Fit width", but a fixed stop reads better as the number
88
+ than as a row label, and both have to fit one narrow trigger. */}
89
+ <SelectValue>
90
+ {isZoomFit(zoom)
91
+ ? t(zoom === "fit-width" ? "viewer.zoom.fitWidth" : "viewer.zoom.fitPage")
92
+ : percent(zoom)}
93
+ </SelectValue>
94
+ </SelectTrigger>
95
+ <SelectContent>
96
+ <SelectItem value="fit-width">{t("viewer.zoom.fitWidth")}</SelectItem>
97
+ <SelectItem value="fit-page">{t("viewer.zoom.fitPage")}</SelectItem>
98
+ <SelectSeparator />
99
+ {VIEWER_ZOOM_STEPS.map((step) => (
100
+ <SelectItem key={step} value={String(step)}>
101
+ {percent(step)}
102
+ </SelectItem>
103
+ ))}
104
+ </SelectContent>
105
+ </Select>
106
+ <IconButton
107
+ variant="ghost"
108
+ size="icon-sm"
109
+ label={t("viewer.zoom.in")}
110
+ icon={<ZoomInIcon aria-hidden="true" />}
111
+ disabled={!canStepZoom(effectiveZoom, 1)}
112
+ onClick={actions.zoomIn}
113
+ />
114
+ {/* One live region for the group: neither a repainted canvas nor a
115
+ `Select`'s own value change announces the new scale. */}
116
+ <span role="status" aria-live="polite" className="sr-only">
117
+ {t("viewer.zoom.status", { level: percent(effectiveZoom) })}
118
+ </span>
119
+ </div>
120
+ );
121
+ },
122
+ );
123
+
124
+ /** `Select` speaks strings; the state is a number or a fit mode. */
125
+ function zoomToValue(zoom: ZoomLevel): string {
126
+ return isZoomFit(zoom) ? zoom : String(zoom);
127
+ }
128
+
129
+ function valueToZoom(value: string): ZoomLevel {
130
+ if (value === "fit-width" || value === "fit-page") return value;
131
+ const parsed = Number.parseFloat(value);
132
+ return Number.isNaN(parsed) ? 1 : parsed;
133
+ }
134
+
135
+ export type FileViewerRotateProps = HTMLAttributes<HTMLButtonElement>;
136
+
137
+ /**
138
+ * One button, one quarter-turn clockwise.
139
+ *
140
+ * Document-level and clockwise-only on purpose: a counter-clockwise button is a
141
+ * second control for something three presses of this one already do, and
142
+ * per-page rotation would change the file, which is authoring rather than
143
+ * viewing. `actions.rotate(-1)` is there for an app that wants the other one.
144
+ */
145
+ export const FileViewerRotate = forwardRef<HTMLButtonElement, FileViewerRotateProps>(
146
+ function FileViewerRotate({ className, ...props }, ref) {
147
+ const { state, actions } = useFileViewer();
148
+ const { t } = useLocale();
149
+
150
+ if (!state.capabilities.rotate) return null;
151
+
152
+ return (
153
+ <IconButton
154
+ ref={ref}
155
+ data-slot="file-viewer-rotate"
156
+ variant="ghost"
157
+ size="icon-sm"
158
+ label={t("viewer.rotate")}
159
+ icon={<RotateCwIcon aria-hidden="true" />}
160
+ className={cn("shrink-0", className)}
161
+ onClick={() => actions.rotate(1)}
162
+ {...props}
163
+ />
164
+ );
165
+ },
166
+ );