@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
|
@@ -451,3 +451,116 @@ mark {
|
|
|
451
451
|
scroll-behavior: auto !important;
|
|
452
452
|
}
|
|
453
453
|
}
|
|
454
|
+
|
|
455
|
+
/* ── Native PDF viewer (fn-112) ───────────────────────────────────────────── */
|
|
456
|
+
|
|
457
|
+
.gno-pdf-viewer {
|
|
458
|
+
isolation: isolate;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
.gno-pdf-page-column {
|
|
462
|
+
scrollbar-gutter: stable;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
.gno-pdf-page {
|
|
466
|
+
position: relative;
|
|
467
|
+
margin: 0 auto 1rem;
|
|
468
|
+
max-width: 100%;
|
|
469
|
+
background: hsl(var(--muted) / 0.2);
|
|
470
|
+
border: 1px solid hsl(var(--border) / 0.4);
|
|
471
|
+
border-radius: 0.25rem;
|
|
472
|
+
box-shadow:
|
|
473
|
+
0 1px 2px hsl(var(--background) / 0.4),
|
|
474
|
+
0 8px 24px -12px hsl(var(--foreground) / 0.18);
|
|
475
|
+
/* pdfjs v5 TextLayer CSS contract on the page wrapper (inherits to layers) */
|
|
476
|
+
--scale-factor: 1;
|
|
477
|
+
--user-unit: 1;
|
|
478
|
+
--total-scale-factor: calc(var(--scale-factor) * var(--user-unit, 1));
|
|
479
|
+
--scale-round-x: 1px;
|
|
480
|
+
--scale-round-y: 1px;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.gno-pdf-page-inner {
|
|
484
|
+
position: relative;
|
|
485
|
+
/* Existing Scholarly Dusk semantic surface token — no invented paper color */
|
|
486
|
+
background: hsl(var(--card));
|
|
487
|
+
overflow: hidden;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
.gno-pdf-canvas {
|
|
491
|
+
display: block;
|
|
492
|
+
width: 100%;
|
|
493
|
+
height: auto;
|
|
494
|
+
vertical-align: top;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/* Text layer: transparent text over canvas, Scholarly Dusk selection */
|
|
498
|
+
.gno-pdf-text-layer,
|
|
499
|
+
.textLayer {
|
|
500
|
+
position: absolute;
|
|
501
|
+
inset: 0;
|
|
502
|
+
overflow: clip;
|
|
503
|
+
opacity: 1;
|
|
504
|
+
line-height: 1;
|
|
505
|
+
text-size-adjust: none;
|
|
506
|
+
forced-color-adjust: none;
|
|
507
|
+
transform-origin: 0 0;
|
|
508
|
+
z-index: 2;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
.gno-pdf-text-layer :is(span, br),
|
|
512
|
+
.textLayer :is(span, br) {
|
|
513
|
+
color: transparent;
|
|
514
|
+
position: absolute;
|
|
515
|
+
white-space: pre;
|
|
516
|
+
cursor: text;
|
|
517
|
+
transform-origin: 0% 0%;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
.gno-pdf-text-layer ::selection,
|
|
521
|
+
.textLayer ::selection {
|
|
522
|
+
background: hsl(var(--primary) / 0.35);
|
|
523
|
+
color: transparent;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
.gno-pdf-annotation-layer {
|
|
527
|
+
position: absolute;
|
|
528
|
+
inset: 0;
|
|
529
|
+
z-index: 3;
|
|
530
|
+
pointer-events: none;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
.gno-pdf-annotation-link,
|
|
534
|
+
.gno-pdf-annotation-inert {
|
|
535
|
+
position: absolute;
|
|
536
|
+
pointer-events: auto;
|
|
537
|
+
background: transparent;
|
|
538
|
+
border: none;
|
|
539
|
+
padding: 0;
|
|
540
|
+
cursor: pointer;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
.gno-pdf-annotation-inert {
|
|
544
|
+
cursor: default;
|
|
545
|
+
pointer-events: none;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
.gno-pdf-page[data-rendered="false"] .gno-pdf-page-inner {
|
|
549
|
+
animation: gno-pdf-pulse 1.6s ease-in-out infinite;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
@keyframes gno-pdf-pulse {
|
|
553
|
+
0%,
|
|
554
|
+
100% {
|
|
555
|
+
opacity: 1;
|
|
556
|
+
}
|
|
557
|
+
50% {
|
|
558
|
+
opacity: 0.85;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
@media (prefers-reduced-motion: reduce) {
|
|
563
|
+
.gno-pdf-page[data-rendered="false"] .gno-pdf-page-inner {
|
|
564
|
+
animation: none;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
classifyPdfError as defaultClassifyPdfError,
|
|
5
|
+
getDocument as defaultGetDocument,
|
|
6
|
+
getPdfMetrics as defaultGetPdfMetrics,
|
|
7
|
+
type GnoDocumentLoadingTask,
|
|
8
|
+
type PdfFallbackReason,
|
|
9
|
+
type PDFDocumentProxy,
|
|
10
|
+
} from "../lib/pdf";
|
|
11
|
+
|
|
12
|
+
export type PdfDocumentStatus = "loading" | "ready" | "error";
|
|
13
|
+
|
|
14
|
+
export type UsePdfDocumentResult = {
|
|
15
|
+
status: PdfDocumentStatus;
|
|
16
|
+
doc: PDFDocumentProxy | null;
|
|
17
|
+
numPages: number;
|
|
18
|
+
firstPageReady: boolean;
|
|
19
|
+
error: PdfFallbackReason | null;
|
|
20
|
+
errorMessage: string | null;
|
|
21
|
+
docId: string | null;
|
|
22
|
+
retry: () => void;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Optional test doubles only. Production call sites pass nothing and use the
|
|
27
|
+
* facade defaults. Not a public product API surface.
|
|
28
|
+
*/
|
|
29
|
+
export type UsePdfDocumentDeps = {
|
|
30
|
+
getDocument?: typeof defaultGetDocument;
|
|
31
|
+
classifyPdfError?: typeof defaultClassifyPdfError;
|
|
32
|
+
getPdfMetrics?: typeof defaultGetPdfMetrics;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type LoadOwnership = {
|
|
36
|
+
/** Minted opaque id for this load attempt. */
|
|
37
|
+
docId: string;
|
|
38
|
+
task: GnoDocumentLoadingTask;
|
|
39
|
+
/** Set only after promise resolves into viewer ownership. */
|
|
40
|
+
viewerDoc: PDFDocumentProxy | null;
|
|
41
|
+
/** True once teardown for this load has run (idempotent). */
|
|
42
|
+
tornDown: boolean;
|
|
43
|
+
/** True once documentDestroy was emitted (success path only). */
|
|
44
|
+
destroyMetricEmitted: boolean;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Load a PDF document from a same-origin asset URL.
|
|
49
|
+
*
|
|
50
|
+
* Teardown ownership (I3-04): loadingTask.destroy() owns the transport for the
|
|
51
|
+
* entire load lifecycle. documentDestroy is emitted exactly once per
|
|
52
|
+
* successfully loaded viewer instance, never for rejected/never-loaded
|
|
53
|
+
* attempts.
|
|
54
|
+
*/
|
|
55
|
+
export function usePdfDocument(
|
|
56
|
+
url: string | null,
|
|
57
|
+
deps: UsePdfDocumentDeps = {}
|
|
58
|
+
): UsePdfDocumentResult {
|
|
59
|
+
const getDocument = deps.getDocument ?? defaultGetDocument;
|
|
60
|
+
const classifyPdfError = deps.classifyPdfError ?? defaultClassifyPdfError;
|
|
61
|
+
const getPdfMetrics = deps.getPdfMetrics ?? defaultGetPdfMetrics;
|
|
62
|
+
|
|
63
|
+
const [status, setStatus] = useState<PdfDocumentStatus>("loading");
|
|
64
|
+
const [doc, setDoc] = useState<PDFDocumentProxy | null>(null);
|
|
65
|
+
const [numPages, setNumPages] = useState(0);
|
|
66
|
+
const [firstPageReady, setFirstPageReady] = useState(false);
|
|
67
|
+
const [error, setError] = useState<PdfFallbackReason | null>(null);
|
|
68
|
+
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
69
|
+
const [docId, setDocId] = useState<string | null>(null);
|
|
70
|
+
const [retryToken, setRetryToken] = useState(0);
|
|
71
|
+
|
|
72
|
+
const generationRef = useRef(0);
|
|
73
|
+
const ownershipRef = useRef<LoadOwnership | null>(null);
|
|
74
|
+
|
|
75
|
+
const retry = useCallback(() => {
|
|
76
|
+
setRetryToken((t) => t + 1);
|
|
77
|
+
}, []);
|
|
78
|
+
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (!url) {
|
|
81
|
+
setStatus("error");
|
|
82
|
+
setError("network");
|
|
83
|
+
setErrorMessage("No document URL");
|
|
84
|
+
setDoc(null);
|
|
85
|
+
setNumPages(0);
|
|
86
|
+
setFirstPageReady(false);
|
|
87
|
+
setDocId(null);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
generationRef.current += 1;
|
|
92
|
+
const generation = generationRef.current;
|
|
93
|
+
const metrics = getPdfMetrics();
|
|
94
|
+
|
|
95
|
+
setStatus("loading");
|
|
96
|
+
setDoc(null);
|
|
97
|
+
setNumPages(0);
|
|
98
|
+
setFirstPageReady(false);
|
|
99
|
+
setError(null);
|
|
100
|
+
setErrorMessage(null);
|
|
101
|
+
|
|
102
|
+
const loadingTask = getDocument({ url });
|
|
103
|
+
const instanceDocId = loadingTask.gnoDocId;
|
|
104
|
+
setDocId(instanceDocId);
|
|
105
|
+
|
|
106
|
+
const ownership: LoadOwnership = {
|
|
107
|
+
docId: instanceDocId,
|
|
108
|
+
task: loadingTask,
|
|
109
|
+
viewerDoc: null,
|
|
110
|
+
tornDown: false,
|
|
111
|
+
destroyMetricEmitted: false,
|
|
112
|
+
};
|
|
113
|
+
ownershipRef.current = ownership;
|
|
114
|
+
|
|
115
|
+
const isStale = (): boolean => {
|
|
116
|
+
if (generation !== generationRef.current) {
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
if (ownership.tornDown) {
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
return Boolean((loadingTask as { destroyed?: boolean }).destroyed);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Idempotent teardown for this load.
|
|
127
|
+
* - Always destroy the loading task exactly once.
|
|
128
|
+
* - Emit documentDestroy only for a viewer-owned success.
|
|
129
|
+
* - A stale late resolution needs no separate proxy cleanup: the loading
|
|
130
|
+
* task already owns and destroys its transport.
|
|
131
|
+
*/
|
|
132
|
+
const teardown = (): void => {
|
|
133
|
+
if (ownership.tornDown) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
ownership.tornDown = true;
|
|
137
|
+
|
|
138
|
+
const viewerDoc = ownership.viewerDoc;
|
|
139
|
+
ownership.viewerDoc = null;
|
|
140
|
+
|
|
141
|
+
if (viewerDoc) {
|
|
142
|
+
if (!ownership.destroyMetricEmitted) {
|
|
143
|
+
ownership.destroyMetricEmitted = true;
|
|
144
|
+
metrics.recordDocumentDestroy({ docId: ownership.docId });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
void ownership.task.destroy().catch(() => undefined);
|
|
150
|
+
} catch {
|
|
151
|
+
// ignore
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
loadingTask.promise
|
|
156
|
+
.then(async (pdf) => {
|
|
157
|
+
if (isStale()) {
|
|
158
|
+
teardown();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
ownership.viewerDoc = pdf;
|
|
162
|
+
setDoc(pdf);
|
|
163
|
+
setNumPages(pdf.numPages);
|
|
164
|
+
setStatus("ready");
|
|
165
|
+
setFirstPageReady(pdf.numPages > 0);
|
|
166
|
+
})
|
|
167
|
+
.catch((err: unknown) => {
|
|
168
|
+
if (isStale()) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const reason = classifyPdfError(err);
|
|
172
|
+
setStatus("error");
|
|
173
|
+
setError(reason);
|
|
174
|
+
setErrorMessage(err instanceof Error ? err.message : String(err));
|
|
175
|
+
setDoc(null);
|
|
176
|
+
setNumPages(0);
|
|
177
|
+
setFirstPageReady(false);
|
|
178
|
+
teardown();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
return () => {
|
|
182
|
+
if (ownershipRef.current === ownership) {
|
|
183
|
+
ownershipRef.current = null;
|
|
184
|
+
}
|
|
185
|
+
teardown();
|
|
186
|
+
};
|
|
187
|
+
}, [url, retryToken, getDocument, classifyPdfError, getPdfMetrics]);
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
status,
|
|
191
|
+
doc,
|
|
192
|
+
numPages,
|
|
193
|
+
firstPageReady,
|
|
194
|
+
error,
|
|
195
|
+
errorMessage,
|
|
196
|
+
docId,
|
|
197
|
+
retry,
|
|
198
|
+
};
|
|
199
|
+
}
|