@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.
Files changed (36) hide show
  1. package/README.md +15 -5
  2. package/browser-extension/artifacts/gno-browser-clipper-v1.30.4.zip +0 -0
  3. package/browser-extension/artifacts/gno-browser-clipper-v1.30.4.zip.sha256 +1 -0
  4. package/browser-extension/dist/chunk-627emwpj.js +75 -0
  5. package/browser-extension/dist/manifest.json +1 -1
  6. package/browser-extension/dist/preview.html +1 -1
  7. package/browser-extension/dist/service-worker.js +47 -22
  8. package/package.json +42 -39
  9. package/src/converters/adapters/officeparser/adapter.ts +7 -3
  10. package/src/core/network-boundary-inventory.ts +51 -0
  11. package/src/core/project-profile.ts +2 -2
  12. package/src/publish/encrypted-export.ts +1 -1
  13. package/src/serve/AGENTS.md +5 -1
  14. package/src/serve/CLAUDE.md +5 -1
  15. package/src/serve/fn112-routes.ts +232 -0
  16. package/src/serve/pdfjs-assets.ts +391 -0
  17. package/src/serve/public/components/ai-elements/code-block.tsx +28 -10
  18. package/src/serve/public/components/pdf/PdfPageView.tsx +428 -0
  19. package/src/serve/public/components/pdf/PdfToolbar.tsx +384 -0
  20. package/src/serve/public/components/pdf/PdfViewer.tsx +539 -0
  21. package/src/serve/public/components/pdf/pdf-viewer-deps.tsx +94 -0
  22. package/src/serve/public/globals.built.css +2 -2
  23. package/src/serve/public/globals.css +113 -0
  24. package/src/serve/public/hooks/use-pdf-document.ts +199 -0
  25. package/src/serve/public/hooks/use-pdf-pages.ts +1197 -0
  26. package/src/serve/public/lib/doc-asset-url.ts +57 -0
  27. package/src/serve/public/lib/math-sum-precise.ts +34 -0
  28. package/src/serve/public/lib/pdf.ts +772 -0
  29. package/src/serve/public/pages/DocView.tsx +295 -39
  30. package/src/serve/public/pages/doc-pdf-viewer.tsx +7 -0
  31. package/src/serve/routes/api.ts +154 -14
  32. package/src/serve/server.ts +190 -37
  33. package/src/serve/spa-bundle-source.ts +99 -0
  34. package/browser-extension/artifacts/gno-browser-clipper-v1.29.6.zip +0 -0
  35. package/browser-extension/artifacts/gno-browser-clipper-v1.29.6.zip.sha256 +0 -1
  36. 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
+ }