@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.
- package/LICENSE +21 -0
- package/README.md +143 -0
- package/dist/chunk-2NQ4RSJ3.js +18 -0
- package/dist/chunk-2NQ4RSJ3.js.map +1 -0
- package/dist/chunk-3U3MESBU.js +21 -0
- package/dist/chunk-3U3MESBU.js.map +1 -0
- package/dist/chunk-4OFQYALG.js +147 -0
- package/dist/chunk-4OFQYALG.js.map +1 -0
- package/dist/chunk-5OMONA2G.js +87 -0
- package/dist/chunk-5OMONA2G.js.map +1 -0
- package/dist/chunk-AORNMH77.js +58 -0
- package/dist/chunk-AORNMH77.js.map +1 -0
- package/dist/chunk-BCF3RWRL.js +18 -0
- package/dist/chunk-BCF3RWRL.js.map +1 -0
- package/dist/chunk-FELIAGKI.js +22 -0
- package/dist/chunk-FELIAGKI.js.map +1 -0
- package/dist/chunk-GGR7BNW4.js +18 -0
- package/dist/chunk-GGR7BNW4.js.map +1 -0
- package/dist/chunk-H3OAON3D.js +22 -0
- package/dist/chunk-H3OAON3D.js.map +1 -0
- package/dist/chunk-HMG2ERXH.js +17 -0
- package/dist/chunk-HMG2ERXH.js.map +1 -0
- package/dist/chunk-KOMDRWOU.js +21 -0
- package/dist/chunk-KOMDRWOU.js.map +1 -0
- package/dist/chunk-NMA57QZ7.js +117 -0
- package/dist/chunk-NMA57QZ7.js.map +1 -0
- package/dist/chunk-RGWDIQJR.js +26 -0
- package/dist/chunk-RGWDIQJR.js.map +1 -0
- package/dist/chunk-SLXRUYT3.js +91 -0
- package/dist/chunk-SLXRUYT3.js.map +1 -0
- package/dist/chunk-UJIJECEF.js +21 -0
- package/dist/chunk-UJIJECEF.js.map +1 -0
- package/dist/chunk-UL43NGUG.js +68 -0
- package/dist/chunk-UL43NGUG.js.map +1 -0
- package/dist/chunk-VO2273Z2.js +55 -0
- package/dist/chunk-VO2273Z2.js.map +1 -0
- package/dist/code-adapter-ADZ4UOGN.js +226 -0
- package/dist/code-adapter-ADZ4UOGN.js.map +1 -0
- package/dist/csv-adapter-6VU3FFVU.js +100 -0
- package/dist/csv-adapter-6VU3FFVU.js.map +1 -0
- package/dist/docx-adapter-5CQDHWTD.js +383 -0
- package/dist/docx-adapter-5CQDHWTD.js.map +1 -0
- package/dist/image-adapter-WOHZR24J.js +132 -0
- package/dist/image-adapter-WOHZR24J.js.map +1 -0
- package/dist/index.d.ts +1353 -0
- package/dist/index.js +1364 -0
- package/dist/index.js.map +1 -0
- package/dist/json-adapter-ZUW5GQHE.js +90 -0
- package/dist/json-adapter-ZUW5GQHE.js.map +1 -0
- package/dist/markdown-adapter-YC6WTBS4.js +214 -0
- package/dist/markdown-adapter-YC6WTBS4.js.map +1 -0
- package/dist/media-adapter-MCTB4GBH.js +78 -0
- package/dist/media-adapter-MCTB4GBH.js.map +1 -0
- package/dist/pdf-adapter-5PMKEXUD.js +424 -0
- package/dist/pdf-adapter-5PMKEXUD.js.map +1 -0
- package/dist/pptx-adapter-6GEQLS2Z.js +351 -0
- package/dist/pptx-adapter-6GEQLS2Z.js.map +1 -0
- package/dist/text-adapter-NFNWB5W3.js +81 -0
- package/dist/text-adapter-NFNWB5W3.js.map +1 -0
- package/dist/xlsx-adapter-CM2Y6AKQ.js +167 -0
- package/dist/xlsx-adapter-CM2Y6AKQ.js.map +1 -0
- package/package.json +103 -0
- package/src/adapters/code/code-adapter.test.tsx +221 -0
- package/src/adapters/code/code-adapter.tsx +272 -0
- package/src/adapters/code/code-language.test.ts +45 -0
- package/src/adapters/code/code-language.ts +100 -0
- package/src/adapters/code/code-manifest.ts +19 -0
- package/src/adapters/code/code-theme.test.ts +46 -0
- package/src/adapters/code/code-theme.ts +99 -0
- package/src/adapters/csv/csv-adapter.test.tsx +169 -0
- package/src/adapters/csv/csv-adapter.tsx +146 -0
- package/src/adapters/csv/csv-manifest.ts +20 -0
- package/src/adapters/docx/docx-adapter.test.tsx +192 -0
- package/src/adapters/docx/docx-adapter.tsx +328 -0
- package/src/adapters/docx/docx-manifest.ts +18 -0
- package/src/adapters/docx/docx-model.test.ts +181 -0
- package/src/adapters/docx/docx-model.ts +294 -0
- package/src/adapters/image/image-adapter.test.tsx +141 -0
- package/src/adapters/image/image-adapter.tsx +191 -0
- package/src/adapters/image/image-manifest.ts +15 -0
- package/src/adapters/index.ts +64 -0
- package/src/adapters/json/json-adapter.test.tsx +77 -0
- package/src/adapters/json/json-adapter.tsx +141 -0
- package/src/adapters/json/json-manifest.ts +11 -0
- package/src/adapters/markdown/markdown-adapter.test.tsx +228 -0
- package/src/adapters/markdown/markdown-adapter.tsx +353 -0
- package/src/adapters/markdown/markdown-manifest.ts +21 -0
- package/src/adapters/markdown/markdown-marks.test.ts +85 -0
- package/src/adapters/markdown/markdown-marks.ts +119 -0
- package/src/adapters/media/media-adapter.test.tsx +79 -0
- package/src/adapters/media/media-adapter.tsx +123 -0
- package/src/adapters/media/media-manifest.ts +17 -0
- package/src/adapters/office-fixture.ts +239 -0
- package/src/adapters/pdf/pdf-adapter.test.tsx +489 -0
- package/src/adapters/pdf/pdf-adapter.tsx +599 -0
- package/src/adapters/pdf/pdf-engine.ts +171 -0
- package/src/adapters/pdf/pdf-fixture.ts +27 -0
- package/src/adapters/pdf/pdf-manifest.ts +30 -0
- package/src/adapters/pdf/pdf-text.test.ts +171 -0
- package/src/adapters/pdf/pdf-text.ts +202 -0
- package/src/adapters/pptx/pptx-adapter.test.tsx +241 -0
- package/src/adapters/pptx/pptx-adapter.tsx +341 -0
- package/src/adapters/pptx/pptx-manifest.ts +19 -0
- package/src/adapters/pptx/pptx-model.test.ts +219 -0
- package/src/adapters/pptx/pptx-model.ts +248 -0
- package/src/adapters/text/text-adapter.test.tsx +125 -0
- package/src/adapters/text/text-adapter.tsx +117 -0
- package/src/adapters/text/text-manifest.ts +19 -0
- package/src/adapters/xlsx/xlsx-adapter.test.tsx +159 -0
- package/src/adapters/xlsx/xlsx-adapter.tsx +250 -0
- package/src/adapters/xlsx/xlsx-manifest.ts +26 -0
- package/src/components/grid-text.test.ts +68 -0
- package/src/components/grid-text.ts +77 -0
- package/src/components/marked-text.tsx +36 -0
- package/src/components/sheet-table.tsx +142 -0
- package/src/core/errors.ts +120 -0
- package/src/core/highlight-marks.test.ts +95 -0
- package/src/core/highlight-marks.ts +95 -0
- package/src/core/highlight-resolve.test.ts +208 -0
- package/src/core/highlight-resolve.ts +191 -0
- package/src/core/highlight.ts +125 -0
- package/src/core/registry.test.ts +172 -0
- package/src/core/registry.ts +188 -0
- package/src/core/scroll-host.ts +42 -0
- package/src/core/text-index.test.ts +95 -0
- package/src/core/text-index.ts +159 -0
- package/src/core/types.ts +285 -0
- package/src/core/use-highlight-scroll.ts +59 -0
- package/src/core/use-page-control.ts +47 -0
- package/src/core/use-paged-scroll.ts +194 -0
- package/src/core/use-viewport-size.ts +54 -0
- package/src/core/zoom.ts +56 -0
- package/src/file-viewer/file-viewer-context.tsx +239 -0
- package/src/file-viewer/file-viewer-find.tsx +204 -0
- package/src/file-viewer/file-viewer-pager.tsx +120 -0
- package/src/file-viewer/file-viewer-zoom.tsx +166 -0
- package/src/file-viewer/file-viewer.stories.tsx +947 -0
- package/src/file-viewer/file-viewer.test.tsx +947 -0
- package/src/file-viewer/file-viewer.tsx +1143 -0
- 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
|
+
);
|