@drawnagency/primitives 0.1.99 → 0.1.100
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/dist/{chunk-FAZFADJK.js → chunk-3TO4RRHY.js} +3 -1
- package/dist/{chunk-KP4DWTPR.js → chunk-72FMQQIA.js} +246 -3
- package/dist/chunk-BS5AQHZG.js +45 -0
- package/dist/{chunk-HKHEAH5F.js → chunk-GOCFTXR3.js} +8 -60
- package/dist/{chunk-GYCBWGVW.js → chunk-LBHQWIVL.js} +79 -24
- package/dist/components/chat/ChatPanel.d.ts.map +1 -1
- package/dist/components/docfile/DocFileOverlay.d.ts +12 -0
- package/dist/components/docfile/DocFileOverlay.d.ts.map +1 -0
- package/dist/components/docfile/DocFilePane.d.ts +17 -0
- package/dist/components/docfile/DocFilePane.d.ts.map +1 -0
- package/dist/components/docfile/doc-file-state.d.ts +33 -0
- package/dist/components/docfile/doc-file-state.d.ts.map +1 -0
- package/dist/components/editor/SettingsForm.d.ts.map +1 -1
- package/dist/components/editor/index.js +1 -1
- package/dist/components/sections/Document/index.d.ts +18 -0
- package/dist/components/sections/Document/index.d.ts.map +1 -0
- package/dist/components/sections/all-sections.d.ts +10 -0
- package/dist/components/sections/all-sections.d.ts.map +1 -1
- package/dist/components/sections/brand-guide.d.ts +10 -0
- package/dist/components/sections/brand-guide.d.ts.map +1 -1
- package/dist/components/sections/brand-guide.js +3 -1
- package/dist/components/sections/register-schemas.js +5 -3
- package/dist/components/sections/register.js +5 -3
- package/dist/components/shared/DocumentSourceField.d.ts +13 -0
- package/dist/components/shared/DocumentSourceField.d.ts.map +1 -0
- package/dist/components/shared/Navigation.d.ts.map +1 -1
- package/dist/components/shell/EditorModal.d.ts.map +1 -1
- package/dist/components/shell/EditorShell.d.ts.map +1 -1
- package/dist/components/shell/MediaBreadcrumb.d.ts.map +1 -1
- package/dist/components/shell/MediaFolderTile.d.ts +3 -1
- package/dist/components/shell/MediaFolderTile.d.ts.map +1 -1
- package/dist/components/shell/MediaFolderTree.d.ts +3 -0
- package/dist/components/shell/MediaFolderTree.d.ts.map +1 -1
- package/dist/components/shell/MediaLibraryContext.d.ts +18 -1
- package/dist/components/shell/MediaLibraryContext.d.ts.map +1 -1
- package/dist/components/shell/MediaLibraryModal.d.ts +21 -1
- package/dist/components/shell/MediaLibraryModal.d.ts.map +1 -1
- package/dist/hooks/useDocumentUploads.d.ts +32 -0
- package/dist/hooks/useDocumentUploads.d.ts.map +1 -0
- package/dist/hooks/useEditorPublish.d.ts +18 -1
- package/dist/hooks/useEditorPublish.d.ts.map +1 -1
- package/dist/hooks/useMediaPipeline.d.ts +14 -1
- package/dist/hooks/useMediaPipeline.d.ts.map +1 -1
- package/dist/hooks/useResolvedDocument.d.ts +12 -0
- package/dist/hooks/useResolvedDocument.d.ts.map +1 -0
- package/dist/index.js +56 -32
- package/dist/lib/dexie.d.ts +11 -0
- package/dist/lib/dexie.d.ts.map +1 -1
- package/dist/lib/dexie.js +32 -0
- package/dist/lib/document-refs.d.ts +22 -0
- package/dist/lib/document-refs.d.ts.map +1 -0
- package/dist/lib/document-uploads.d.ts +38 -0
- package/dist/lib/document-uploads.d.ts.map +1 -0
- package/dist/lib/documents.d.ts +65 -0
- package/dist/lib/documents.d.ts.map +1 -0
- package/dist/lib/index.js +4 -4
- package/dist/lib/media-folders.d.ts +8 -0
- package/dist/lib/media-folders.d.ts.map +1 -1
- package/dist/lib/media-folders.js +2 -0
- package/dist/lib/nav.d.ts +29 -1
- package/dist/lib/nav.d.ts.map +1 -1
- package/dist/lib/platform-broker.d.ts +12 -0
- package/dist/lib/platform-broker.d.ts.map +1 -1
- package/dist/lib/platform-broker.js +12 -3
- package/dist/lib/registry.d.ts +10 -0
- package/dist/lib/registry.d.ts.map +1 -1
- package/dist/lib/save-chunks.d.ts +15 -0
- package/dist/lib/save-chunks.d.ts.map +1 -1
- package/dist/lib/scroll-lock.d.ts +2 -0
- package/dist/lib/scroll-lock.d.ts.map +1 -0
- package/dist/schemas/documents.d.ts +80 -0
- package/dist/schemas/documents.d.ts.map +1 -0
- package/dist/schemas/documents.js +25 -0
- package/dist/schemas/index.d.ts +1 -0
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +23 -1
- package/dist/storage/index.d.ts +2 -2
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +3 -1
- package/dist/storage/types.d.ts +47 -0
- package/dist/storage/types.d.ts.map +1 -1
- package/package.json +5 -1
- package/src/components/chat/ChatPanel.tsx +5 -4
- package/src/components/docfile/DocFileOverlay.tsx +54 -0
- package/src/components/docfile/DocFilePane.tsx +129 -0
- package/src/components/docfile/doc-file-state.ts +180 -0
- package/src/components/editor/SettingsForm.tsx +10 -0
- package/src/components/sections/Document/index.tsx +163 -0
- package/src/components/sections/brand-guide.ts +2 -1
- package/src/components/shared/DocumentSourceField.tsx +51 -0
- package/src/components/shared/Navigation.tsx +53 -10
- package/src/components/shell/EditorModal.tsx +2 -5
- package/src/components/shell/EditorShell.tsx +111 -7
- package/src/components/shell/MediaBreadcrumb.tsx +14 -1
- package/src/components/shell/MediaFolderTile.tsx +4 -2
- package/src/components/shell/MediaFolderTree.tsx +20 -3
- package/src/components/shell/MediaLibraryContext.tsx +18 -1
- package/src/components/shell/MediaLibraryModal.tsx +223 -45
- package/src/hooks/useDocumentUploads.ts +330 -0
- package/src/hooks/useEditorPublish.ts +180 -8
- package/src/hooks/useMediaPipeline.ts +136 -5
- package/src/hooks/useResolvedDocument.ts +25 -0
- package/src/lib/dexie.ts +54 -0
- package/src/lib/document-refs.ts +83 -0
- package/src/lib/document-uploads.ts +66 -0
- package/src/lib/documents.ts +138 -0
- package/src/lib/media-folders.ts +9 -0
- package/src/lib/nav.ts +46 -2
- package/src/lib/platform-broker.ts +18 -3
- package/src/lib/registry.ts +9 -0
- package/src/lib/save-chunks.ts +42 -0
- package/src/lib/scroll-lock.ts +27 -0
- package/src/schemas/documents.ts +82 -0
- package/src/schemas/index.ts +1 -0
- package/src/storage/index.ts +2 -2
- package/src/storage/types.ts +46 -0
- /package/dist/{chunk-FOY5TF7D.js → chunk-ZDQ3TT47.js} +0 -0
|
@@ -25,4 +25,19 @@ export interface PackedUploads {
|
|
|
25
25
|
* for the caller to reject with a per-file error. Items are never split
|
|
26
26
|
* across chunks (the manifest may only reference fully-committed items). */
|
|
27
27
|
export declare function packUploads(uploads: MediaUploadPayload[], budget?: number): PackedUploads;
|
|
28
|
+
export interface DocumentUploadPayload {
|
|
29
|
+
id: string;
|
|
30
|
+
path: string;
|
|
31
|
+
base64: string;
|
|
32
|
+
}
|
|
33
|
+
/** Approximate serialized size of one document upload — base64 dominates; the
|
|
34
|
+
* allowance covers JSON keys, quoting, and the manifest entry riding along. */
|
|
35
|
+
export declare function documentUploadSizeBytes(upload: DocumentUploadPayload): number;
|
|
36
|
+
/** Greedy in-order packing of document uploads. A 2.5MiB document base64s to
|
|
37
|
+
* ~3.4MB, so two never share a request — hence chunking rather than one blob
|
|
38
|
+
* request. Documents are never packed with sections/index. */
|
|
39
|
+
export declare function packDocumentUploads(uploads: DocumentUploadPayload[], budget?: number): {
|
|
40
|
+
chunks: DocumentUploadPayload[][];
|
|
41
|
+
oversized: DocumentUploadPayload[];
|
|
42
|
+
};
|
|
28
43
|
//# sourceMappingURL=save-chunks.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"save-chunks.d.ts","sourceRoot":"","sources":["../../src/lib/save-chunks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC3C;AAED;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,QAAkB,CAAC;AAEvD;6EAC6E;AAC7E,wBAAgB,eAAe,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,CAElE;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,kBAAkB,EAAE,EAAE,CAAC;IAC/B,SAAS,EAAE,kBAAkB,EAAE,CAAC;CACjC;AAED;;;6EAG6E;AAC7E,wBAAgB,WAAW,CAAC,OAAO,EAAE,kBAAkB,EAAE,EAAE,MAAM,SAA0B,GAAG,aAAa,CAsB1G"}
|
|
1
|
+
{"version":3,"file":"save-chunks.d.ts","sourceRoot":"","sources":["../../src/lib/save-chunks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC3C;AAED;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,QAAkB,CAAC;AAEvD;6EAC6E;AAC7E,wBAAgB,eAAe,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,CAElE;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,kBAAkB,EAAE,EAAE,CAAC;IAC/B,SAAS,EAAE,kBAAkB,EAAE,CAAC;CACjC;AAED;;;6EAG6E;AAC7E,wBAAgB,WAAW,CAAC,OAAO,EAAE,kBAAkB,EAAE,EAAE,MAAM,SAA0B,GAAG,aAAa,CAsB1G;AAED,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;gFACgF;AAChF,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,qBAAqB,GAAG,MAAM,CAE7E;AAED;;+DAE+D;AAC/D,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,qBAAqB,EAAE,EAChC,MAAM,SAA0B,GAC/B;IAAE,MAAM,EAAE,qBAAqB,EAAE,EAAE,CAAC;IAAC,SAAS,EAAE,qBAAqB,EAAE,CAAA;CAAE,CAsB3E"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scroll-lock.d.ts","sourceRoot":"","sources":["../../src/lib/scroll-lock.ts"],"names":[],"mappings":"AAYA,wBAAgB,cAAc,IAAI,MAAM,IAAI,CAc3C"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Document metadata lives in its OWN file, not in image-manifest.json.
|
|
4
|
+
*
|
|
5
|
+
* The image manifest is hard-`.parse`d on every page load across five routes,
|
|
6
|
+
* its remote-merge guard fails open on parse failure, and the authoring CLI
|
|
7
|
+
* parse-catch-WRITES it — so one entry a stale package version cannot
|
|
8
|
+
* represent means whole-site 500s or a wiped manifest. A sidecar is simply
|
|
9
|
+
* absent from an older writer's file list and survives untouched. Same
|
|
10
|
+
* structural answer as nav.json; do not "simplify" this back into either
|
|
11
|
+
* index.json or image-manifest.json.
|
|
12
|
+
*/
|
|
13
|
+
export declare const DOCUMENT_MANIFEST_PATH = "src/content/document-manifest.json";
|
|
14
|
+
/**
|
|
15
|
+
* Git-tier cap. 2.5 MiB base64-encodes to ~3.4 MB, inside the 4 MB post-base64
|
|
16
|
+
* chunk budget with envelope headroom.
|
|
17
|
+
*/
|
|
18
|
+
export declare const DOCUMENT_GIT_MAX_BYTES = 2621440;
|
|
19
|
+
/** Bucket-tier cap. Its lower bound is DOCUMENT_GIT_MAX_BYTES — no dead zone. */
|
|
20
|
+
export declare const DOCUMENT_BUCKET_MAX_BYTES = 209715200;
|
|
21
|
+
/**
|
|
22
|
+
* The only two accepted upload types, and the ONLY source of a document's file
|
|
23
|
+
* extension — never the media pipeline's `?? "bin"` fallback.
|
|
24
|
+
*/
|
|
25
|
+
export declare const DOCUMENT_MIME_TO_EXT: {
|
|
26
|
+
readonly "application/pdf": "pdf";
|
|
27
|
+
readonly "text/html": "html";
|
|
28
|
+
};
|
|
29
|
+
/** Reverse of DOCUMENT_MIME_TO_EXT, for setting Content-Type when serving. */
|
|
30
|
+
export declare const DOCUMENT_KIND_TO_MIME: {
|
|
31
|
+
readonly pdf: "application/pdf";
|
|
32
|
+
readonly html: "text/html";
|
|
33
|
+
};
|
|
34
|
+
export declare const DocumentManifestEntrySchema: z.ZodObject<{
|
|
35
|
+
filename: z.ZodString;
|
|
36
|
+
size: z.ZodNumber;
|
|
37
|
+
kind: z.ZodEnum<{
|
|
38
|
+
pdf: "pdf";
|
|
39
|
+
html: "html";
|
|
40
|
+
}>;
|
|
41
|
+
storage: z.ZodEnum<{
|
|
42
|
+
git: "git";
|
|
43
|
+
bucket: "bucket";
|
|
44
|
+
}>;
|
|
45
|
+
path: z.ZodString;
|
|
46
|
+
}, z.core.$strip>;
|
|
47
|
+
export declare const DocumentManifestSchema: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
48
|
+
filename: z.ZodString;
|
|
49
|
+
size: z.ZodNumber;
|
|
50
|
+
kind: z.ZodEnum<{
|
|
51
|
+
pdf: "pdf";
|
|
52
|
+
html: "html";
|
|
53
|
+
}>;
|
|
54
|
+
storage: z.ZodEnum<{
|
|
55
|
+
git: "git";
|
|
56
|
+
bucket: "bucket";
|
|
57
|
+
}>;
|
|
58
|
+
path: z.ZodString;
|
|
59
|
+
}, z.core.$strip>>;
|
|
60
|
+
export type DocumentManifest = z.infer<typeof DocumentManifestSchema>;
|
|
61
|
+
export type DocumentManifestEntry = z.infer<typeof DocumentManifestEntrySchema>;
|
|
62
|
+
/**
|
|
63
|
+
* The gated URL for a document, at BOTH storage tiers. Single-sourced here so
|
|
64
|
+
* the section row, the overlay, the nav group and the tests cannot drift — a
|
|
65
|
+
* bucket-tier document is still fetched through this route, which authorizes
|
|
66
|
+
* the requester and only then 302s to a short-lived presigned GET.
|
|
67
|
+
*/
|
|
68
|
+
export declare function documentFileUrl(documentId: string, filename: string, opts?: {
|
|
69
|
+
download?: boolean;
|
|
70
|
+
}): string;
|
|
71
|
+
/** Git-tier repo path. `slugName` comes from sanitizeMediaName(file.name). */
|
|
72
|
+
export declare function documentGitPath(slugName: string, hash: string, kind: "pdf" | "html"): string;
|
|
73
|
+
/**
|
|
74
|
+
* Bucket-tier object key, SITE-RELATIVE — the store prepends the tenant prefix,
|
|
75
|
+
* so this is also the form stored in the manifest, matched by the delete
|
|
76
|
+
* broker's regex, and compared by the publish-time GC. The id is the 32-hex
|
|
77
|
+
* `documentId` minted by upload-intent, never the client's content hash.
|
|
78
|
+
*/
|
|
79
|
+
export declare function documentBucketPath(documentId: string, kind: "pdf" | "html"): string;
|
|
80
|
+
//# sourceMappingURL=documents.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"documents.d.ts","sourceRoot":"","sources":["../../src/schemas/documents.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;;;;;GAUG;AACH,eAAO,MAAM,sBAAsB,uCAAuC,CAAC;AAE3E;;;GAGG;AACH,eAAO,MAAM,sBAAsB,UAAY,CAAC;AAEhD,iFAAiF;AACjF,eAAO,MAAM,yBAAyB,YAAc,CAAC;AAErD;;;GAGG;AACH,eAAO,MAAM,oBAAoB;;;CAGvB,CAAC;AAEX,8EAA8E;AAC9E,eAAO,MAAM,qBAAqB;;;CAGxB,CAAC;AAEX,eAAO,MAAM,2BAA2B;;;;;;;;;;;;iBAOtC,CAAC;AAEH,eAAO,MAAM,sBAAsB;;;;;;;;;;;;kBAAoD,CAAC;AAExF,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AACtE,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,2BAA2B,CAAC,CAAC;AAEhF;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAO,GAChC,MAAM,CAGR;AAED,8EAA8E;AAC9E,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,GAAG,MAAM,GAAG,MAAM,CAE5F;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,GAAG,MAAM,GAAG,MAAM,CAEnF"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DOCUMENT_BUCKET_MAX_BYTES,
|
|
3
|
+
DOCUMENT_GIT_MAX_BYTES,
|
|
4
|
+
DOCUMENT_KIND_TO_MIME,
|
|
5
|
+
DOCUMENT_MANIFEST_PATH,
|
|
6
|
+
DOCUMENT_MIME_TO_EXT,
|
|
7
|
+
DocumentManifestEntrySchema,
|
|
8
|
+
DocumentManifestSchema,
|
|
9
|
+
documentBucketPath,
|
|
10
|
+
documentFileUrl,
|
|
11
|
+
documentGitPath
|
|
12
|
+
} from "../chunk-BS5AQHZG.js";
|
|
13
|
+
import "../chunk-JSBRDJBE.js";
|
|
14
|
+
export {
|
|
15
|
+
DOCUMENT_BUCKET_MAX_BYTES,
|
|
16
|
+
DOCUMENT_GIT_MAX_BYTES,
|
|
17
|
+
DOCUMENT_KIND_TO_MIME,
|
|
18
|
+
DOCUMENT_MANIFEST_PATH,
|
|
19
|
+
DOCUMENT_MIME_TO_EXT,
|
|
20
|
+
DocumentManifestEntrySchema,
|
|
21
|
+
DocumentManifestSchema,
|
|
22
|
+
documentBucketPath,
|
|
23
|
+
documentFileUrl,
|
|
24
|
+
documentGitPath
|
|
25
|
+
};
|
package/dist/schemas/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/schemas/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,OAAO,CAAC;AACtB,cAAc,SAAS,CAAC;AACxB,cAAc,YAAY,CAAC;AAC3B,cAAc,sBAAsB,CAAC;AACrC,cAAc,QAAQ,CAAC;AACvB,cAAc,QAAQ,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/schemas/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,OAAO,CAAC;AACtB,cAAc,SAAS,CAAC;AACxB,cAAc,YAAY,CAAC;AAC3B,cAAc,sBAAsB,CAAC;AACrC,cAAc,QAAQ,CAAC;AACvB,cAAc,QAAQ,CAAC;AACvB,cAAc,aAAa,CAAC"}
|
package/dist/schemas/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
AudienceNameSchema,
|
|
4
4
|
MediaGridOptionsSchema,
|
|
5
5
|
slugifyAudienceName
|
|
6
|
-
} from "../chunk-
|
|
6
|
+
} from "../chunk-ZDQ3TT47.js";
|
|
7
7
|
import {
|
|
8
8
|
AudienceSchema,
|
|
9
9
|
RoleSchema,
|
|
@@ -16,6 +16,18 @@ import {
|
|
|
16
16
|
LinkValueSchema,
|
|
17
17
|
isSafeHref
|
|
18
18
|
} from "../chunk-PZJL2S3Z.js";
|
|
19
|
+
import {
|
|
20
|
+
DOCUMENT_BUCKET_MAX_BYTES,
|
|
21
|
+
DOCUMENT_GIT_MAX_BYTES,
|
|
22
|
+
DOCUMENT_KIND_TO_MIME,
|
|
23
|
+
DOCUMENT_MANIFEST_PATH,
|
|
24
|
+
DOCUMENT_MIME_TO_EXT,
|
|
25
|
+
DocumentManifestEntrySchema,
|
|
26
|
+
DocumentManifestSchema,
|
|
27
|
+
documentBucketPath,
|
|
28
|
+
documentFileUrl,
|
|
29
|
+
documentGitPath
|
|
30
|
+
} from "../chunk-BS5AQHZG.js";
|
|
19
31
|
import {
|
|
20
32
|
NavEntrySchema,
|
|
21
33
|
NavFileSchema
|
|
@@ -58,6 +70,13 @@ export {
|
|
|
58
70
|
ChatbotConfigSchema,
|
|
59
71
|
ChatbotTaskSchema,
|
|
60
72
|
DEFAULT_LINK,
|
|
73
|
+
DOCUMENT_BUCKET_MAX_BYTES,
|
|
74
|
+
DOCUMENT_GIT_MAX_BYTES,
|
|
75
|
+
DOCUMENT_KIND_TO_MIME,
|
|
76
|
+
DOCUMENT_MANIFEST_PATH,
|
|
77
|
+
DOCUMENT_MIME_TO_EXT,
|
|
78
|
+
DocumentManifestEntrySchema,
|
|
79
|
+
DocumentManifestSchema,
|
|
61
80
|
FontNameSchema,
|
|
62
81
|
HexColorSchema,
|
|
63
82
|
ImageManifestSchema,
|
|
@@ -84,6 +103,9 @@ export {
|
|
|
84
103
|
StatusSchema,
|
|
85
104
|
TextLineSchema,
|
|
86
105
|
VariantSchema,
|
|
106
|
+
documentBucketPath,
|
|
107
|
+
documentFileUrl,
|
|
108
|
+
documentGitPath,
|
|
87
109
|
getSectionContentSchema,
|
|
88
110
|
getSectionSchema,
|
|
89
111
|
isSafeHref,
|
package/dist/storage/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export type { StorageProvider, FileWrite, AssetStore } from "./types";
|
|
2
|
-
export { StorageConflictError, IdempotencyLookupUnavailableError, ASSET_CACHE_CONTROL, IDEMPOTENCY_TRAILER, IDEMPOTENCY_KEY_RE, withIdempotencyTrailer } from "./types";
|
|
1
|
+
export type { StorageProvider, FileWrite, AssetStore, DocumentStore } from "./types";
|
|
2
|
+
export { StorageConflictError, IdempotencyLookupUnavailableError, ASSET_CACHE_CONTROL, DOCUMENT_CACHE_CONTROL, IDEMPOTENCY_TRAILER, IDEMPOTENCY_KEY_RE, withIdempotencyTrailer } from "./types";
|
|
3
3
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/storage/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,eAAe,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/storage/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,eAAe,EAAE,SAAS,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACrF,OAAO,EAAE,oBAAoB,EAAE,iCAAiC,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAC"}
|
package/dist/storage/index.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ASSET_CACHE_CONTROL,
|
|
3
|
+
DOCUMENT_CACHE_CONTROL,
|
|
3
4
|
IDEMPOTENCY_KEY_RE,
|
|
4
5
|
IDEMPOTENCY_TRAILER,
|
|
5
6
|
IdempotencyLookupUnavailableError,
|
|
6
7
|
StorageConflictError,
|
|
7
8
|
withIdempotencyTrailer
|
|
8
|
-
} from "../chunk-
|
|
9
|
+
} from "../chunk-3TO4RRHY.js";
|
|
9
10
|
import "../chunk-JSBRDJBE.js";
|
|
10
11
|
export {
|
|
11
12
|
ASSET_CACHE_CONTROL,
|
|
13
|
+
DOCUMENT_CACHE_CONTROL,
|
|
12
14
|
IDEMPOTENCY_KEY_RE,
|
|
13
15
|
IDEMPOTENCY_TRAILER,
|
|
14
16
|
IdempotencyLookupUnavailableError,
|
package/dist/storage/types.d.ts
CHANGED
|
@@ -131,4 +131,51 @@ export interface AssetStore {
|
|
|
131
131
|
updatedAt: string;
|
|
132
132
|
}[]>;
|
|
133
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* `Cache-Control` every DOCUMENT upload must set. Deliberately NOT
|
|
136
|
+
* ASSET_CACHE_CONTROL: documents live in a PRIVATE bucket reachable only
|
|
137
|
+
* through short-lived presigned GETs, and a shared cache keyed on the full
|
|
138
|
+
* signed URL would otherwise retain the bytes for a year after the signature
|
|
139
|
+
* expired. `private` keeps the copy in the requesting browser only.
|
|
140
|
+
*/
|
|
141
|
+
export declare const DOCUMENT_CACHE_CONTROL = "private, max-age=31536000, immutable";
|
|
142
|
+
/**
|
|
143
|
+
* Optional PRIVATE document store — a second bucket with no public domain.
|
|
144
|
+
*
|
|
145
|
+
* Deliberately not an AssetStore: the assets bucket is public by configuration
|
|
146
|
+
* at R2_PUBLIC_BASE (videos depend on it), so a signed GET there would protect
|
|
147
|
+
* nothing and hosted HTML would render unsandboxed on a sibling origin. Hence
|
|
148
|
+
* no `getPublicUrl` here at all — every read is a short-lived signed GET,
|
|
149
|
+
* minted only after the gated route has authorized the requester.
|
|
150
|
+
*
|
|
151
|
+
* Paths are SITE-RELATIVE ("documents/{hash}/original.pdf"); implementations
|
|
152
|
+
* prepend the tenant prefix themselves so a caller can never address another
|
|
153
|
+
* tenant's objects.
|
|
154
|
+
*/
|
|
155
|
+
export interface DocumentStore {
|
|
156
|
+
/** Mint a signed direct-upload URL for the browser (PUT). */
|
|
157
|
+
createUploadUrl(path: string, opts: {
|
|
158
|
+
contentType: string;
|
|
159
|
+
}): Promise<{
|
|
160
|
+
uploadUrl: string;
|
|
161
|
+
path: string;
|
|
162
|
+
}>;
|
|
163
|
+
/**
|
|
164
|
+
* Mint a short-lived signed GET. `downloadFilename`, when given, signs
|
|
165
|
+
* `response-content-disposition: attachment` into the URL — R2 sends no
|
|
166
|
+
* Content-Disposition of its own, so this is the only way ?download=1 can
|
|
167
|
+
* force a download on the bucket tier.
|
|
168
|
+
*/
|
|
169
|
+
createDownloadUrl(path: string, opts: {
|
|
170
|
+
expiresInSeconds: number;
|
|
171
|
+
downloadFilename?: string;
|
|
172
|
+
}): Promise<string>;
|
|
173
|
+
delete(paths: string[]): Promise<void>;
|
|
174
|
+
/** Recursive listing under a site-relative prefix. */
|
|
175
|
+
list(prefix: string): Promise<Array<{
|
|
176
|
+
path: string;
|
|
177
|
+
size: number;
|
|
178
|
+
updatedAt: string;
|
|
179
|
+
}>>;
|
|
180
|
+
}
|
|
134
181
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/storage/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,qBAAa,oBAAqB,SAAQ,KAAK;aACjB,cAAc,EAAE,MAAM,GAAG,IAAI;gBAA7B,cAAc,EAAE,MAAM,GAAG,IAAI;CAI1D;AAED,MAAM,WAAW,eAAe;IAC9B,UAAU,CACR,KAAK,EAAE,SAAS,EAAE,EAClB,OAAO,EAAE,MAAM,EAIf,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GACxC,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAEhC,QAAQ,CACN,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,GAC5B,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IAE9B,YAAY,IAAI,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAE7C,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAE9B,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAE9E;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;IACH,0BAA0B,CAAC,CACzB,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAC3B,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC3B;AAED;;;;;GAKG;AACH,qBAAa,iCAAkC,SAAQ,KAAK;aAC5B,KAAK,CAAC,EAAE,OAAO;gBAAf,KAAK,CAAC,EAAE,OAAO,YAAA;CAI9C;AAED;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,oBAAoB,CAAC;AAErD;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,QAA6B,CAAC;AAE7D;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAM5E;AAED,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAAC;IACpC,QAAQ,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;CAC/B,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,mBAAmB,gCAAgC,CAAC;AAEjE;;;;;;GAMG;AACH,MAAM,WAAW,UAAU;IACzB,6DAA6D;IAC7D,eAAe,CACb,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE;QAAE,WAAW,EAAE,MAAM,CAAA;KAAE,GAC5B,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAEnE,oEAAoE;IACpE,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAEnC,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEvC,sDAAsD;IACtD,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC,CAAC;CACpF"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/storage/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drawnagency/primitives",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.100",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./package.json": "./package.json",
|
|
@@ -24,6 +24,10 @@
|
|
|
24
24
|
"types": "./dist/schemas/link.d.ts",
|
|
25
25
|
"default": "./dist/schemas/link.js"
|
|
26
26
|
},
|
|
27
|
+
"./schemas/documents": {
|
|
28
|
+
"types": "./dist/schemas/documents.d.ts",
|
|
29
|
+
"default": "./dist/schemas/documents.js"
|
|
30
|
+
},
|
|
27
31
|
"./lib": {
|
|
28
32
|
"types": "./dist/lib/index.d.ts",
|
|
29
33
|
"default": "./dist/lib/index.js"
|
|
@@ -4,6 +4,7 @@ import { ArrowDown, ChevronDown, ChevronRight, History, Pin, PinOff, SquarePen,
|
|
|
4
4
|
import { cn } from "../../lib/cn";
|
|
5
5
|
import { ensureSanitizer, sanitizeHtml } from "../../lib/sanitize";
|
|
6
6
|
import { resolveThinkingPhrases } from "../../lib/chatbot";
|
|
7
|
+
import { lockBodyScroll } from "../../lib/scroll-lock";
|
|
7
8
|
import { loadHistory, upsertConversation, setConversationPinned, removeConversation, type StoredConversation } from "./chat-history";
|
|
8
9
|
import { Select } from "../shared/Select";
|
|
9
10
|
import { IconButton } from "../shared/IconButton";
|
|
@@ -299,9 +300,7 @@ export default function ChatPanel({ open, closing, animateEnter, onClose, siteNa
|
|
|
299
300
|
// The open sheet is modal: the page behind it must not scroll.
|
|
300
301
|
useEffect(() => {
|
|
301
302
|
if (mode !== "sheet" || !open) return;
|
|
302
|
-
|
|
303
|
-
document.body.style.overflow = "hidden";
|
|
304
|
-
return () => { document.body.style.overflow = prev; };
|
|
303
|
+
return lockBodyScroll();
|
|
305
304
|
}, [mode, open]);
|
|
306
305
|
|
|
307
306
|
// Explicit-open focus: composer on desktop, the panel itself on the sheet
|
|
@@ -487,7 +486,9 @@ export default function ChatPanel({ open, closing, animateEnter, onClose, siteNa
|
|
|
487
486
|
// width, so the panel always tucks below it — the toolbar never
|
|
488
487
|
// moves during the open/close slide.
|
|
489
488
|
mode === "docked" && cn(
|
|
490
|
-
|
|
489
|
+
// bp-chat-panel is a STABLE hook, not styling: base.css pins the
|
|
490
|
+
// docked panel to the 120rem band edge under html.bp-doc-open.
|
|
491
|
+
"bp-chat-panel fixed right-0 bottom-0 z-40 w-[var(--chat-w)] border-l border-base-200",
|
|
491
492
|
variant === "editor" ? "top-16" : "top-16 xl:top-0",
|
|
492
493
|
"motion-safe:transition-transform motion-safe:duration-300",
|
|
493
494
|
open ? "translate-x-0" : "translate-x-full",
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { lazy, Suspense, useEffect, useState } from "react";
|
|
2
|
+
import type { SiteDocument } from "../../lib/documents";
|
|
3
|
+
import {
|
|
4
|
+
subscribeDocFile,
|
|
5
|
+
subscribeSiteDocuments,
|
|
6
|
+
getSiteDocuments,
|
|
7
|
+
seedSiteDocuments,
|
|
8
|
+
closeDocFile,
|
|
9
|
+
isDocDesktop,
|
|
10
|
+
} from "./doc-file-state";
|
|
11
|
+
|
|
12
|
+
const DocFilePane = lazy(() => import("./DocFilePane"));
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* One mount per page, rendered into the Layout `chrome` slot (OUTSIDE <main>:
|
|
16
|
+
* the chat dock's band-shift transform would re-anchor a position:fixed
|
|
17
|
+
* descendant). `documents` is the requester-filtered SSR list — on /edit it is
|
|
18
|
+
* initial state only; EditorShell republishes the live draft list.
|
|
19
|
+
*/
|
|
20
|
+
export default function DocFileOverlay({
|
|
21
|
+
documents,
|
|
22
|
+
editor = false,
|
|
23
|
+
}: { documents: SiteDocument[]; editor?: boolean }) {
|
|
24
|
+
const [docs, setDocs] = useState<SiteDocument[] | null>(null);
|
|
25
|
+
const [openId, setOpenId] = useState<string | null>(null);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
seedSiteDocuments(documents);
|
|
29
|
+
setDocs(getSiteDocuments());
|
|
30
|
+
return subscribeSiteDocuments(setDocs);
|
|
31
|
+
}, [documents]);
|
|
32
|
+
|
|
33
|
+
useEffect(() => subscribeDocFile(setOpenId), []);
|
|
34
|
+
|
|
35
|
+
const doc = openId && docs ? (docs.find((d) => d.sectionId === openId) ?? null) : null;
|
|
36
|
+
|
|
37
|
+
// Below the desktop breakpoint a PDF goes to the OS viewer. The row already
|
|
38
|
+
// short-circuits, but a Documents-nav entry lands here, so the guard belongs
|
|
39
|
+
// at the pane too.
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (!doc || doc.kind !== "pdf" || isDocDesktop()) return;
|
|
42
|
+
window.open(doc.url, "_blank", "noopener,noreferrer");
|
|
43
|
+
closeDocFile();
|
|
44
|
+
}, [doc]);
|
|
45
|
+
|
|
46
|
+
if (!doc) return null;
|
|
47
|
+
if (doc.kind === "pdf" && !isDocDesktop()) return null;
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<Suspense fallback={null}>
|
|
51
|
+
<DocFilePane doc={doc} onClose={closeDocFile} editor={editor} />
|
|
52
|
+
</Suspense>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { Download, X } from "lucide-react";
|
|
3
|
+
import type { SiteDocument } from "../../lib/documents";
|
|
4
|
+
import { documentFileUrl } from "../../schemas/documents";
|
|
5
|
+
import { lockBodyScroll } from "../../lib/scroll-lock";
|
|
6
|
+
|
|
7
|
+
/** How long a frame may sit on "loading" before we offer a retry. */
|
|
8
|
+
const LOAD_TIMEOUT_MS = 15_000;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The solid stage: an opaque, flat, bordered pane between the nav edge and the
|
|
12
|
+
* chat edge. Geometry lives in base.css (.doc-file-pane) because it is calc()
|
|
13
|
+
* against doc mode's own 120rem stage band — the band the nav and chat are
|
|
14
|
+
* pinned to under html.bp-doc-open (NOT the page's 80rem --layout-max-width
|
|
15
|
+
* band, which doc mode never touches). z-30 — above unpositioned page content,
|
|
16
|
+
* BELOW the nav and docked chat (z-40) and the mobile nav header / chat sheet
|
|
17
|
+
* (z-50), so all chrome stays reachable. `editor` adds top clearance for the
|
|
18
|
+
* edit route's always-fixed h-16 toolbar.
|
|
19
|
+
*/
|
|
20
|
+
export default function DocFilePane({
|
|
21
|
+
doc,
|
|
22
|
+
onClose,
|
|
23
|
+
editor = false,
|
|
24
|
+
}: { doc: SiteDocument; onClose: () => void; editor?: boolean }) {
|
|
25
|
+
const [attempt, setAttempt] = useState(0);
|
|
26
|
+
const [state, setState] = useState<"loading" | "ready" | "error">("loading");
|
|
27
|
+
const frameRef = useRef<HTMLIFrameElement | null>(null);
|
|
28
|
+
|
|
29
|
+
useEffect(() => lockBodyScroll(), []);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
const onKey = (e: KeyboardEvent) => {
|
|
33
|
+
if (e.key !== "Escape") return;
|
|
34
|
+
e.preventDefault();
|
|
35
|
+
onClose();
|
|
36
|
+
};
|
|
37
|
+
window.addEventListener("keydown", onKey);
|
|
38
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
39
|
+
}, [onClose]);
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
setState("loading");
|
|
43
|
+
const timer = setTimeout(() => setState((s) => (s === "loading" ? "error" : s)), LOAD_TIMEOUT_MS);
|
|
44
|
+
return () => clearTimeout(timer);
|
|
45
|
+
}, [attempt, doc.url]);
|
|
46
|
+
|
|
47
|
+
// Natively, not via an onError prop: React registers a non-delegated `error`
|
|
48
|
+
// listener for <img>/<image> but for <iframe> only `load` (react-dom
|
|
49
|
+
// setInitialProperties), and `error` does not bubble — so an onError prop on
|
|
50
|
+
// an iframe is never called. Browsers rarely fire it either, which is what
|
|
51
|
+
// LOAD_TIMEOUT_MS is for; this covers the cases that do.
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
const frame = frameRef.current;
|
|
54
|
+
if (!frame) return;
|
|
55
|
+
const onError = () => setState("error");
|
|
56
|
+
frame.addEventListener("error", onError);
|
|
57
|
+
return () => frame.removeEventListener("error", onError);
|
|
58
|
+
}, [attempt, doc.url]);
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div
|
|
62
|
+
role="dialog"
|
|
63
|
+
aria-label={doc.title}
|
|
64
|
+
data-testid="doc-file-pane"
|
|
65
|
+
className={`doc-file-pane${editor ? " doc-file-pane--editor" : ""} fixed z-30 flex flex-col overflow-hidden rounded-outer border border-base-200 bg-base`}
|
|
66
|
+
>
|
|
67
|
+
<div className="flex shrink-0 items-center justify-between gap-3 border-b border-base-200 px-4 py-2">
|
|
68
|
+
<div className="flex min-w-0 items-baseline gap-2">
|
|
69
|
+
<span className="truncate text-sm font-medium text-base-contrast">{doc.title}</span>
|
|
70
|
+
<span className="truncate font-mono text-xs text-base-contrast/70">{doc.filename}</span>
|
|
71
|
+
</div>
|
|
72
|
+
<div className="flex shrink-0 items-center gap-1">
|
|
73
|
+
<a
|
|
74
|
+
// Built by documentFileUrl's `download` option, never by appending
|
|
75
|
+
// a query string: `doc.url` is a blob: URL for a document staged
|
|
76
|
+
// this session, and "blob:…?download=1" resolves to nothing. A blob
|
|
77
|
+
// URL is already directly downloadable, so it passes through.
|
|
78
|
+
href={
|
|
79
|
+
doc.url.startsWith("blob:")
|
|
80
|
+
? doc.url
|
|
81
|
+
: documentFileUrl(doc.documentId, doc.filename, { download: true })
|
|
82
|
+
}
|
|
83
|
+
download={doc.filename}
|
|
84
|
+
aria-label={`Download ${doc.filename}`}
|
|
85
|
+
className="cursor-pointer rounded-inner p-1.5 text-base-contrast/60 transition-colors hover:text-primary"
|
|
86
|
+
>
|
|
87
|
+
<Download size={16} aria-hidden="true" />
|
|
88
|
+
</a>
|
|
89
|
+
<button
|
|
90
|
+
type="button"
|
|
91
|
+
onClick={onClose}
|
|
92
|
+
aria-label="Close document"
|
|
93
|
+
className="cursor-pointer rounded-inner p-1.5 text-base-contrast/60 transition-colors hover:text-primary"
|
|
94
|
+
>
|
|
95
|
+
<X size={16} aria-hidden="true" />
|
|
96
|
+
</button>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
{state === "error" ? (
|
|
101
|
+
<div className="flex min-h-0 flex-1 flex-col items-center justify-center gap-3 p-6 text-center">
|
|
102
|
+
<p className="text-sm text-base-contrast">Couldn't load document.</p>
|
|
103
|
+
<button
|
|
104
|
+
type="button"
|
|
105
|
+
onClick={() => setAttempt((n) => n + 1)}
|
|
106
|
+
className="cursor-pointer rounded-inner border border-base-200 px-3 py-1.5 text-sm text-base-contrast transition-colors hover:text-primary"
|
|
107
|
+
>
|
|
108
|
+
Retry
|
|
109
|
+
</button>
|
|
110
|
+
</div>
|
|
111
|
+
) : (
|
|
112
|
+
<iframe
|
|
113
|
+
key={attempt}
|
|
114
|
+
ref={frameRef}
|
|
115
|
+
src={doc.url}
|
|
116
|
+
title={doc.title}
|
|
117
|
+
// Opaque origin for authored HTML: no portal cookies, storage or
|
|
118
|
+
// parent DOM. The ATTRIBUTE applies regardless of origin, so
|
|
119
|
+
// containment survives the bucket tier's 302 to a presigned R2 GET,
|
|
120
|
+
// where no CSP header comes back. PDFs get the browser's native
|
|
121
|
+
// viewer, which a sandbox would break.
|
|
122
|
+
sandbox={doc.kind === "html" ? "allow-scripts" : undefined}
|
|
123
|
+
onLoad={() => setState("ready")}
|
|
124
|
+
className="min-h-0 w-full flex-1 border-0 bg-base"
|
|
125
|
+
/>
|
|
126
|
+
)}
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
}
|