@gmickel/gno 1.29.6 → 1.30.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -5
- package/browser-extension/artifacts/gno-browser-clipper-v1.30.4.zip +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.30.4.zip.sha256 +1 -0
- package/browser-extension/dist/chunk-627emwpj.js +75 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/browser-extension/dist/preview.html +1 -1
- package/browser-extension/dist/service-worker.js +47 -22
- package/package.json +42 -39
- package/src/converters/adapters/officeparser/adapter.ts +7 -3
- package/src/core/network-boundary-inventory.ts +51 -0
- package/src/core/project-profile.ts +2 -2
- package/src/publish/encrypted-export.ts +1 -1
- package/src/serve/AGENTS.md +5 -1
- package/src/serve/CLAUDE.md +5 -1
- package/src/serve/fn112-routes.ts +232 -0
- package/src/serve/pdfjs-assets.ts +391 -0
- package/src/serve/public/components/ai-elements/code-block.tsx +28 -10
- package/src/serve/public/components/pdf/PdfPageView.tsx +428 -0
- package/src/serve/public/components/pdf/PdfToolbar.tsx +384 -0
- package/src/serve/public/components/pdf/PdfViewer.tsx +539 -0
- package/src/serve/public/components/pdf/pdf-viewer-deps.tsx +94 -0
- package/src/serve/public/globals.built.css +2 -2
- package/src/serve/public/globals.css +113 -0
- package/src/serve/public/hooks/use-pdf-document.ts +199 -0
- package/src/serve/public/hooks/use-pdf-pages.ts +1197 -0
- package/src/serve/public/lib/doc-asset-url.ts +57 -0
- package/src/serve/public/lib/math-sum-precise.ts +34 -0
- package/src/serve/public/lib/pdf.ts +772 -0
- package/src/serve/public/pages/DocView.tsx +295 -39
- package/src/serve/public/pages/doc-pdf-viewer.tsx +7 -0
- package/src/serve/routes/api.ts +154 -14
- package/src/serve/server.ts +190 -37
- package/src/serve/spa-bundle-source.ts +99 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.29.6.zip +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.29.6.zip.sha256 +0 -1
- package/browser-extension/dist/chunk-b2zm0jjd.js +0 -50
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChevronLeft,
|
|
3
|
+
ChevronRight,
|
|
4
|
+
Download,
|
|
5
|
+
Maximize2,
|
|
6
|
+
Minus,
|
|
7
|
+
Plus,
|
|
8
|
+
StretchHorizontal,
|
|
9
|
+
} from "lucide-react";
|
|
10
|
+
import { useEffect, useId, useState, type ReactNode } from "react";
|
|
11
|
+
|
|
12
|
+
import type { FitMode } from "../../hooks/use-pdf-pages";
|
|
13
|
+
|
|
14
|
+
import { MAX_ZOOM, MIN_ZOOM } from "../../lib/pdf";
|
|
15
|
+
import { Button } from "../ui/button";
|
|
16
|
+
import { Input } from "../ui/input";
|
|
17
|
+
import {
|
|
18
|
+
Select,
|
|
19
|
+
SelectContent,
|
|
20
|
+
SelectItem,
|
|
21
|
+
SelectTrigger,
|
|
22
|
+
SelectValue,
|
|
23
|
+
} from "../ui/select";
|
|
24
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
|
25
|
+
|
|
26
|
+
export type PdfToolbarProps = {
|
|
27
|
+
page: number;
|
|
28
|
+
numPages: number;
|
|
29
|
+
/** Logical zoom (1 = 100%). */
|
|
30
|
+
zoom: number;
|
|
31
|
+
fitMode: FitMode;
|
|
32
|
+
/** When true, all interactive controls are disabled (empty / zero-page). */
|
|
33
|
+
disabled?: boolean;
|
|
34
|
+
downloadUrl: string;
|
|
35
|
+
onPageChange: (page: number) => void;
|
|
36
|
+
onZoomIn: () => void;
|
|
37
|
+
onZoomOut: () => void;
|
|
38
|
+
/** Commit an exact zoom level (1 = 100%) from the zoom-level combobox. */
|
|
39
|
+
onZoomTo: (level: number) => void;
|
|
40
|
+
onFitMode: (mode: "width" | "page") => void;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Fixed zoom stops, all inside the viewer's existing MIN_ZOOM (0.25) /
|
|
45
|
+
* MAX_ZOOM (4) bounds — no new zoom math and no new bounds. 100% is the
|
|
46
|
+
* default level and is marked as such in its accessible name.
|
|
47
|
+
*/
|
|
48
|
+
export const ZOOM_STOPS = [0.5, 0.75, 1, 1.25, 1.5, 2, 3, 4] as const;
|
|
49
|
+
|
|
50
|
+
function zoomStopLabel(level: number): string {
|
|
51
|
+
return `${Math.round(level * 100)}%`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function clampPage(raw: number, numPages: number): number {
|
|
55
|
+
if (!Number.isFinite(raw) || numPages < 1) {
|
|
56
|
+
return 1;
|
|
57
|
+
}
|
|
58
|
+
return Math.min(numPages, Math.max(1, Math.trunc(raw)));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Digits-only finite values (including 0) are numeric and clamp to 1..N.
|
|
63
|
+
* Empty / non-numeric → null (caller reverts, no callback).
|
|
64
|
+
*/
|
|
65
|
+
function parsePageDraft(draft: string, numPages: number): number | null {
|
|
66
|
+
const trimmed = draft.trim();
|
|
67
|
+
if (!trimmed || !/^\d+$/u.test(trimmed)) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
const n = Number(trimmed);
|
|
71
|
+
if (!Number.isFinite(n)) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
return clampPage(n, numPages);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function IconButton({
|
|
78
|
+
label,
|
|
79
|
+
disabled,
|
|
80
|
+
onClick,
|
|
81
|
+
children,
|
|
82
|
+
testId,
|
|
83
|
+
pressed,
|
|
84
|
+
}: {
|
|
85
|
+
label: string;
|
|
86
|
+
disabled?: boolean;
|
|
87
|
+
onClick?: () => void;
|
|
88
|
+
children: ReactNode;
|
|
89
|
+
testId?: string;
|
|
90
|
+
pressed?: boolean;
|
|
91
|
+
}) {
|
|
92
|
+
return (
|
|
93
|
+
<Tooltip>
|
|
94
|
+
<TooltipTrigger asChild>
|
|
95
|
+
<Button
|
|
96
|
+
aria-label={label}
|
|
97
|
+
aria-pressed={pressed}
|
|
98
|
+
className="cursor-pointer focus-visible:ring-primary/50"
|
|
99
|
+
data-testid={testId}
|
|
100
|
+
disabled={disabled}
|
|
101
|
+
onClick={onClick}
|
|
102
|
+
size="icon-sm"
|
|
103
|
+
type="button"
|
|
104
|
+
variant="ghost"
|
|
105
|
+
>
|
|
106
|
+
{children}
|
|
107
|
+
</Button>
|
|
108
|
+
</TooltipTrigger>
|
|
109
|
+
<TooltipContent side="bottom">{label}</TooltipContent>
|
|
110
|
+
</Tooltip>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Scholarly Dusk instrument-rail toolbar. Pure/controlled — no pdfjs imports.
|
|
116
|
+
* DocView owns the Pages/Text view toggle; this rail never renders it.
|
|
117
|
+
*/
|
|
118
|
+
export function PdfToolbar({
|
|
119
|
+
page,
|
|
120
|
+
numPages,
|
|
121
|
+
zoom,
|
|
122
|
+
fitMode,
|
|
123
|
+
disabled = false,
|
|
124
|
+
downloadUrl,
|
|
125
|
+
onPageChange,
|
|
126
|
+
onZoomIn,
|
|
127
|
+
onZoomOut,
|
|
128
|
+
onZoomTo,
|
|
129
|
+
onFitMode,
|
|
130
|
+
}: PdfToolbarProps) {
|
|
131
|
+
const liveId = useId();
|
|
132
|
+
const [draft, setDraft] = useState(String(page));
|
|
133
|
+
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
setDraft(String(page));
|
|
136
|
+
}, [page]);
|
|
137
|
+
|
|
138
|
+
const atStart = page <= 1 || numPages < 1;
|
|
139
|
+
const atEnd = page >= numPages || numPages < 1;
|
|
140
|
+
const controlsDisabled = disabled || numPages < 1;
|
|
141
|
+
const zoomPercent = Math.round(zoom * 100);
|
|
142
|
+
const atMinZoom = zoom <= MIN_ZOOM;
|
|
143
|
+
const atMaxZoom = zoom >= MAX_ZOOM;
|
|
144
|
+
|
|
145
|
+
const commitDraft = () => {
|
|
146
|
+
if (controlsDisabled) {
|
|
147
|
+
setDraft(String(page));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const parsed = parsePageDraft(draft, numPages);
|
|
151
|
+
if (parsed === null) {
|
|
152
|
+
setDraft(String(page));
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
setDraft(String(parsed));
|
|
156
|
+
if (parsed !== page) {
|
|
157
|
+
onPageChange(parsed);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<div
|
|
163
|
+
className="gno-pdf-toolbar sticky top-0 z-10 flex flex-wrap items-center gap-x-2 gap-y-1.5 rounded-t-lg border-border/40 border-b bg-background/85 px-3 py-2 backdrop-blur-[20px]"
|
|
164
|
+
data-testid="pdf-toolbar"
|
|
165
|
+
role="toolbar"
|
|
166
|
+
aria-label="PDF toolbar"
|
|
167
|
+
>
|
|
168
|
+
{/* Group A — page navigation */}
|
|
169
|
+
<div
|
|
170
|
+
className="flex items-center gap-1"
|
|
171
|
+
data-testid="pdf-toolbar-group-nav"
|
|
172
|
+
>
|
|
173
|
+
<IconButton
|
|
174
|
+
disabled={controlsDisabled || atStart}
|
|
175
|
+
label="Previous page"
|
|
176
|
+
onClick={() => onPageChange(clampPage(page - 1, numPages))}
|
|
177
|
+
testId="pdf-toolbar-prev"
|
|
178
|
+
>
|
|
179
|
+
<ChevronLeft />
|
|
180
|
+
</IconButton>
|
|
181
|
+
|
|
182
|
+
<Input
|
|
183
|
+
aria-label="Page number"
|
|
184
|
+
className="hidden h-8 w-12 px-1 text-center font-mono text-xs tabular-nums lg:inline-flex"
|
|
185
|
+
data-testid="pdf-toolbar-page-input"
|
|
186
|
+
disabled={controlsDisabled}
|
|
187
|
+
inputMode="numeric"
|
|
188
|
+
onBlur={commitDraft}
|
|
189
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
190
|
+
onKeyDown={(e) => {
|
|
191
|
+
if (e.key === "Enter") {
|
|
192
|
+
e.preventDefault();
|
|
193
|
+
commitDraft();
|
|
194
|
+
} else if (e.key === "Escape") {
|
|
195
|
+
e.preventDefault();
|
|
196
|
+
setDraft(String(page));
|
|
197
|
+
(e.target as HTMLInputElement).blur();
|
|
198
|
+
}
|
|
199
|
+
}}
|
|
200
|
+
value={draft}
|
|
201
|
+
/>
|
|
202
|
+
|
|
203
|
+
<span
|
|
204
|
+
aria-atomic="true"
|
|
205
|
+
aria-live="polite"
|
|
206
|
+
className="min-w-[3.5rem] px-1 text-center font-mono text-muted-foreground text-xs tabular-nums"
|
|
207
|
+
data-testid="pdf-toolbar-page-indicator"
|
|
208
|
+
id={liveId}
|
|
209
|
+
>
|
|
210
|
+
<span className="text-primary">{Math.max(0, page)}</span>
|
|
211
|
+
<span className="text-muted-foreground/70">
|
|
212
|
+
{" "}
|
|
213
|
+
/ {Math.max(0, numPages)}
|
|
214
|
+
</span>
|
|
215
|
+
</span>
|
|
216
|
+
|
|
217
|
+
<IconButton
|
|
218
|
+
disabled={controlsDisabled || atEnd}
|
|
219
|
+
label="Next page"
|
|
220
|
+
onClick={() => onPageChange(clampPage(page + 1, numPages))}
|
|
221
|
+
testId="pdf-toolbar-next"
|
|
222
|
+
>
|
|
223
|
+
<ChevronRight />
|
|
224
|
+
</IconButton>
|
|
225
|
+
</div>
|
|
226
|
+
|
|
227
|
+
{/* Group B — zoom */}
|
|
228
|
+
<div
|
|
229
|
+
className="flex items-center gap-1"
|
|
230
|
+
data-testid="pdf-toolbar-group-zoom"
|
|
231
|
+
>
|
|
232
|
+
<IconButton
|
|
233
|
+
disabled={controlsDisabled || atMinZoom}
|
|
234
|
+
label="Zoom out"
|
|
235
|
+
onClick={onZoomOut}
|
|
236
|
+
testId="pdf-toolbar-zoom-out"
|
|
237
|
+
>
|
|
238
|
+
<Minus />
|
|
239
|
+
</IconButton>
|
|
240
|
+
|
|
241
|
+
{/*
|
|
242
|
+
Zoom-level combobox. Deliberately inherits the previous readout's
|
|
243
|
+
exact type treatment (font-mono text-xs tabular-nums) and h-8 sizing,
|
|
244
|
+
with the primitive's border/shadow suppressed, so the toolbar's
|
|
245
|
+
resting rhythm is unchanged next to the ghost stepper buttons — the
|
|
246
|
+
control reads as the same numeral it always was, now operable.
|
|
247
|
+
*/}
|
|
248
|
+
<Select
|
|
249
|
+
disabled={controlsDisabled}
|
|
250
|
+
onValueChange={(v) => onZoomTo(Number(v))}
|
|
251
|
+
// Always controlled: a zoom that is not a fixed stop simply has no
|
|
252
|
+
// matching item, and the trigger keeps showing the live percentage.
|
|
253
|
+
value={String(zoom)}
|
|
254
|
+
>
|
|
255
|
+
<SelectTrigger
|
|
256
|
+
aria-label={`Zoom level, ${zoomPercent} percent`}
|
|
257
|
+
className="h-8 min-w-[4.25rem] cursor-pointer gap-1 border-transparent bg-transparent px-2 font-mono text-xs tabular-nums shadow-none hover:bg-accent hover:text-accent-foreground focus-visible:ring-primary/50 dark:bg-transparent dark:hover:bg-accent"
|
|
258
|
+
data-testid="pdf-toolbar-zoom-level"
|
|
259
|
+
size="sm"
|
|
260
|
+
>
|
|
261
|
+
<SelectValue placeholder={`${zoomPercent}%`}>
|
|
262
|
+
{zoomPercent}%
|
|
263
|
+
</SelectValue>
|
|
264
|
+
</SelectTrigger>
|
|
265
|
+
<SelectContent
|
|
266
|
+
className="min-w-[6rem]"
|
|
267
|
+
data-testid="pdf-toolbar-zoom-level-list"
|
|
268
|
+
>
|
|
269
|
+
{ZOOM_STOPS.map((level) => (
|
|
270
|
+
<SelectItem
|
|
271
|
+
aria-label={
|
|
272
|
+
level === 1
|
|
273
|
+
? "100 percent, default level"
|
|
274
|
+
: `${Math.round(level * 100)} percent`
|
|
275
|
+
}
|
|
276
|
+
className="cursor-pointer font-mono text-xs tabular-nums"
|
|
277
|
+
data-testid={`pdf-toolbar-zoom-option-${Math.round(level * 100)}`}
|
|
278
|
+
key={level}
|
|
279
|
+
value={String(level)}
|
|
280
|
+
>
|
|
281
|
+
{zoomStopLabel(level)}
|
|
282
|
+
</SelectItem>
|
|
283
|
+
))}
|
|
284
|
+
</SelectContent>
|
|
285
|
+
</Select>
|
|
286
|
+
|
|
287
|
+
<IconButton
|
|
288
|
+
disabled={controlsDisabled || atMaxZoom}
|
|
289
|
+
label="Zoom in"
|
|
290
|
+
onClick={onZoomIn}
|
|
291
|
+
testId="pdf-toolbar-zoom-in"
|
|
292
|
+
>
|
|
293
|
+
<Plus />
|
|
294
|
+
</IconButton>
|
|
295
|
+
</div>
|
|
296
|
+
|
|
297
|
+
{/*
|
|
298
|
+
Structural <lg two-row split: A+B on row 1, C+D on row 2.
|
|
299
|
+
Desktop (lg+) hides the break so all groups share one row.
|
|
300
|
+
Source order A→B→break→C→D preserved.
|
|
301
|
+
*/}
|
|
302
|
+
<span
|
|
303
|
+
aria-hidden
|
|
304
|
+
className="h-0 w-full basis-full lg:hidden"
|
|
305
|
+
data-testid="pdf-toolbar-mobile-break"
|
|
306
|
+
/>
|
|
307
|
+
|
|
308
|
+
{/* Group C — fit modes */}
|
|
309
|
+
<div
|
|
310
|
+
className="flex items-center gap-0.5 rounded-md border border-border/40 p-0.5"
|
|
311
|
+
data-testid="pdf-toolbar-fit-group"
|
|
312
|
+
role="group"
|
|
313
|
+
aria-label="Fit mode"
|
|
314
|
+
>
|
|
315
|
+
<Button
|
|
316
|
+
aria-label="Fit width"
|
|
317
|
+
aria-pressed={fitMode === "width"}
|
|
318
|
+
className="h-7 cursor-pointer gap-1 px-2 text-xs focus-visible:ring-primary/50 data-[pressed=true]:bg-primary/15 data-[pressed=true]:text-primary"
|
|
319
|
+
data-pressed={fitMode === "width" ? "true" : "false"}
|
|
320
|
+
data-testid="pdf-toolbar-fit-width"
|
|
321
|
+
disabled={controlsDisabled}
|
|
322
|
+
onClick={() => onFitMode("width")}
|
|
323
|
+
size="sm"
|
|
324
|
+
type="button"
|
|
325
|
+
variant="ghost"
|
|
326
|
+
>
|
|
327
|
+
<StretchHorizontal className="size-3.5" />
|
|
328
|
+
<span
|
|
329
|
+
className="hidden lg:inline"
|
|
330
|
+
data-testid="pdf-toolbar-fit-width-label"
|
|
331
|
+
>
|
|
332
|
+
Fit width
|
|
333
|
+
</span>
|
|
334
|
+
</Button>
|
|
335
|
+
<Button
|
|
336
|
+
aria-label="Fit page"
|
|
337
|
+
aria-pressed={fitMode === "page"}
|
|
338
|
+
className="h-7 cursor-pointer gap-1 px-2 text-xs focus-visible:ring-primary/50 data-[pressed=true]:bg-primary/15 data-[pressed=true]:text-primary"
|
|
339
|
+
data-pressed={fitMode === "page" ? "true" : "false"}
|
|
340
|
+
data-testid="pdf-toolbar-fit-page"
|
|
341
|
+
disabled={controlsDisabled}
|
|
342
|
+
onClick={() => onFitMode("page")}
|
|
343
|
+
size="sm"
|
|
344
|
+
type="button"
|
|
345
|
+
variant="ghost"
|
|
346
|
+
>
|
|
347
|
+
<Maximize2 className="size-3.5" />
|
|
348
|
+
<span
|
|
349
|
+
className="hidden lg:inline"
|
|
350
|
+
data-testid="pdf-toolbar-fit-page-label"
|
|
351
|
+
>
|
|
352
|
+
Fit page
|
|
353
|
+
</span>
|
|
354
|
+
</Button>
|
|
355
|
+
</div>
|
|
356
|
+
|
|
357
|
+
<span className="flex-1" data-testid="pdf-toolbar-spacer" />
|
|
358
|
+
|
|
359
|
+
{/* Group D — download */}
|
|
360
|
+
<div data-testid="pdf-toolbar-group-download">
|
|
361
|
+
<Tooltip>
|
|
362
|
+
<TooltipTrigger asChild>
|
|
363
|
+
<Button
|
|
364
|
+
asChild
|
|
365
|
+
className="cursor-pointer focus-visible:ring-primary/50"
|
|
366
|
+
data-testid="pdf-toolbar-download"
|
|
367
|
+
size="icon-sm"
|
|
368
|
+
variant="ghost"
|
|
369
|
+
>
|
|
370
|
+
<a
|
|
371
|
+
aria-label="Download original"
|
|
372
|
+
download
|
|
373
|
+
href={downloadUrl || undefined}
|
|
374
|
+
>
|
|
375
|
+
<Download />
|
|
376
|
+
</a>
|
|
377
|
+
</Button>
|
|
378
|
+
</TooltipTrigger>
|
|
379
|
+
<TooltipContent side="bottom">Download original</TooltipContent>
|
|
380
|
+
</Tooltip>
|
|
381
|
+
</div>
|
|
382
|
+
</div>
|
|
383
|
+
);
|
|
384
|
+
}
|