@drawnagency/primitives 0.1.99 → 0.1.101
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-HKHEAH5F.js → chunk-6SLGKRLW.js} +8 -60
- package/dist/chunk-BS5AQHZG.js +45 -0
- package/dist/{chunk-GYCBWGVW.js → chunk-LBHQWIVL.js} +79 -24
- package/dist/{chunk-KP4DWTPR.js → chunk-P3NNI3QI.js} +328 -4
- 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 +38 -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/primitives/DocumentDropZone.d.ts +18 -0
- package/dist/components/primitives/DocumentDropZone.d.ts.map +1 -0
- package/dist/components/sections/Document/index.d.ts +24 -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 +24 -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 +13 -0
- package/dist/lib/dexie.d.ts.map +1 -1
- package/dist/lib/dexie.js +39 -2
- 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 +59 -3
- 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 +133 -0
- package/src/components/docfile/doc-file-state.ts +208 -0
- package/src/components/editor/SettingsForm.tsx +10 -0
- package/src/components/primitives/DocumentDropZone.tsx +79 -0
- package/src/components/sections/Document/index.tsx +194 -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 +132 -8
- 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 +24 -1
- package/src/components/shell/MediaLibraryModal.tsx +223 -45
- package/src/hooks/useDocumentUploads.ts +336 -0
- package/src/hooks/useEditorPublish.ts +180 -8
- package/src/hooks/useMediaPipeline.ts +141 -5
- package/src/hooks/useResolvedDocument.ts +25 -0
- package/src/lib/dexie.ts +66 -1
- 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 +58 -4
- /package/dist/{chunk-FOY5TF7D.js → chunk-ZDQ3TT47.js} +0 -0
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
|
@@ -111,9 +111,18 @@ export declare const ASSET_CACHE_CONTROL = "max-age=31536000, immutable";
|
|
|
111
111
|
* can slot in. All paths are SITE-RELATIVE ("media/{hash}/original.mp4") —
|
|
112
112
|
* implementations prepend the tenant prefix ("sites/{siteId}/") themselves so
|
|
113
113
|
* a caller can never address another tenant's objects.
|
|
114
|
+
*
|
|
115
|
+
* This bucket is PUBLIC: an object's URL is statically derivable from the
|
|
116
|
+
* bucket's public base, so `createUploadUrl` returns it once at upload time
|
|
117
|
+
* (built locally in standalone mode, brokered from the admin app's env in
|
|
118
|
+
* platform mode) and the media manifest stores that absolute URL. Rendering
|
|
119
|
+
* reads the manifest — nothing on the read path re-derives a URL.
|
|
114
120
|
*/
|
|
115
121
|
export interface AssetStore {
|
|
116
|
-
/**
|
|
122
|
+
/**
|
|
123
|
+
* Mint a signed direct-upload URL for the browser (PUT), along with the
|
|
124
|
+
* object's permanent public URL for the caller to persist.
|
|
125
|
+
*/
|
|
117
126
|
createUploadUrl(path: string, opts: {
|
|
118
127
|
contentType: string;
|
|
119
128
|
}): Promise<{
|
|
@@ -121,8 +130,6 @@ export interface AssetStore {
|
|
|
121
130
|
path: string;
|
|
122
131
|
publicUrl: string;
|
|
123
132
|
}>;
|
|
124
|
-
/** Deterministic public CDN URL for an object (no network call). */
|
|
125
|
-
getPublicUrl(path: string): string;
|
|
126
133
|
delete(paths: string[]): Promise<void>;
|
|
127
134
|
/** Recursive listing under a site-relative prefix. */
|
|
128
135
|
list(prefix: string): Promise<{
|
|
@@ -131,4 +138,53 @@ export interface AssetStore {
|
|
|
131
138
|
updatedAt: string;
|
|
132
139
|
}[]>;
|
|
133
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* `Cache-Control` every DOCUMENT upload must set. Deliberately NOT
|
|
143
|
+
* ASSET_CACHE_CONTROL: documents live in a PRIVATE bucket reachable only
|
|
144
|
+
* through short-lived presigned GETs, and a shared cache keyed on the full
|
|
145
|
+
* signed URL would otherwise retain the bytes for a year after the signature
|
|
146
|
+
* expired. `private` keeps the copy in the requesting browser only.
|
|
147
|
+
*/
|
|
148
|
+
export declare const DOCUMENT_CACHE_CONTROL = "private, max-age=31536000, immutable";
|
|
149
|
+
/**
|
|
150
|
+
* Optional PRIVATE document store — a second bucket with no public domain.
|
|
151
|
+
*
|
|
152
|
+
* Deliberately not an AssetStore: the assets bucket is public by configuration
|
|
153
|
+
* at R2_PUBLIC_BASE (videos depend on it), so a signed GET there would protect
|
|
154
|
+
* nothing and hosted HTML would render unsandboxed on a sibling origin. Hence
|
|
155
|
+
* the asymmetry with AssetStore — an asset has a permanent public URL handed
|
|
156
|
+
* back by `createUploadUrl`, while a document has no durable URL at all: every
|
|
157
|
+
* read is a short-lived signed GET from `createDownloadUrl`, minted only after
|
|
158
|
+
* the gated route has authorized the requester.
|
|
159
|
+
*
|
|
160
|
+
* Paths are SITE-RELATIVE ("documents/{hash}/original.pdf"); implementations
|
|
161
|
+
* prepend the tenant prefix themselves so a caller can never address another
|
|
162
|
+
* tenant's objects.
|
|
163
|
+
*/
|
|
164
|
+
export interface DocumentStore {
|
|
165
|
+
/** Mint a signed direct-upload URL for the browser (PUT). */
|
|
166
|
+
createUploadUrl(path: string, opts: {
|
|
167
|
+
contentType: string;
|
|
168
|
+
}): Promise<{
|
|
169
|
+
uploadUrl: string;
|
|
170
|
+
path: string;
|
|
171
|
+
}>;
|
|
172
|
+
/**
|
|
173
|
+
* Mint a short-lived signed GET. `downloadFilename`, when given, signs
|
|
174
|
+
* `response-content-disposition: attachment` into the URL — R2 sends no
|
|
175
|
+
* Content-Disposition of its own, so this is the only way ?download=1 can
|
|
176
|
+
* force a download on the bucket tier.
|
|
177
|
+
*/
|
|
178
|
+
createDownloadUrl(path: string, opts: {
|
|
179
|
+
expiresInSeconds: number;
|
|
180
|
+
downloadFilename?: string;
|
|
181
|
+
}): Promise<string>;
|
|
182
|
+
delete(paths: string[]): Promise<void>;
|
|
183
|
+
/** Recursive listing under a site-relative prefix. */
|
|
184
|
+
list(prefix: string): Promise<Array<{
|
|
185
|
+
path: string;
|
|
186
|
+
size: number;
|
|
187
|
+
updatedAt: string;
|
|
188
|
+
}>>;
|
|
189
|
+
}
|
|
134
190
|
//# 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
|
|
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.101",
|
|
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,133 @@
|
|
|
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
|
+
// PDF open-parameters hide the native viewer's toolbar/side panel in
|
|
116
|
+
// Chromium so the pane's own top bar is the only chrome; Firefox and
|
|
117
|
+
// Safari ignore them harmlessly. A client-side fragment: it survives
|
|
118
|
+
// the gated route's 302 and works on pre-save blob: URLs too.
|
|
119
|
+
src={doc.kind === "pdf" ? `${doc.url}#toolbar=0&navpanes=0` : doc.url}
|
|
120
|
+
title={doc.title}
|
|
121
|
+
// Opaque origin for authored HTML: no portal cookies, storage or
|
|
122
|
+
// parent DOM. The ATTRIBUTE applies regardless of origin, so
|
|
123
|
+
// containment survives the bucket tier's 302 to a presigned R2 GET,
|
|
124
|
+
// where no CSP header comes back. PDFs get the browser's native
|
|
125
|
+
// viewer, which a sandbox would break.
|
|
126
|
+
sandbox={doc.kind === "html" ? "allow-scripts" : undefined}
|
|
127
|
+
onLoad={() => setState("ready")}
|
|
128
|
+
className="min-h-0 w-full flex-1 border-0 bg-base"
|
|
129
|
+
/>
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import type { SiteDocument } from "../../lib/documents";
|
|
2
|
+
import { createEvent } from "../../lib/events";
|
|
3
|
+
|
|
4
|
+
/** Toggled on <html> while a document is open (the chat's class mechanism). */
|
|
5
|
+
export const DOC_OPEN_CLASS = "bp-doc-open";
|
|
6
|
+
|
|
7
|
+
/** Held on <html> for a beat after close. bp-doc-open's `transition: left`
|
|
8
|
+
* rule leaves WITH the class, so without this the nav sidebar snaps home
|
|
9
|
+
* instead of gliding back the way it glided out — this class carries the
|
|
10
|
+
* same transition while the sidebar makes the return trip. */
|
|
11
|
+
export const DOC_CLOSING_CLASS = "bp-doc-closing";
|
|
12
|
+
|
|
13
|
+
/** Slightly longer than the 300ms CSS ease so the class outlives the glide. */
|
|
14
|
+
const DOC_CLOSING_MS = 350;
|
|
15
|
+
|
|
16
|
+
/** The one breakpoint the doc feature keys off: at or above it the overlay
|
|
17
|
+
* gets the desktop stage; below it a PDF goes to the OS viewer instead (iOS
|
|
18
|
+
* Safari cannot scroll an iframed PDF, Android Chrome forces a download).
|
|
19
|
+
* 1024px — the SAME breakpoint as U5's desktop CSS block and the width where
|
|
20
|
+
* the nav sidebar exists at all. Deliberately NOT the chat's 768px dock query:
|
|
21
|
+
* keying JS at 768 while the pane's desktop rules start at 1024 would give
|
|
22
|
+
* 768–1023px viewers framed PDFs inside the mobile pane geometry, under a
|
|
23
|
+
* docked chat. */
|
|
24
|
+
export const DOC_DESKTOP_QUERY = "(min-width: 1024px)";
|
|
25
|
+
|
|
26
|
+
/** Cross-island open request. Islands that would rather not import this module
|
|
27
|
+
* can dispatch it instead; applying an already-current value is a no-op, so
|
|
28
|
+
* the dispatch-listen pair cannot loop. */
|
|
29
|
+
export const docFileOpenEvent = createEvent<{ sectionId: string | null }>("bp-doc-file-open");
|
|
30
|
+
|
|
31
|
+
interface DocFileStore {
|
|
32
|
+
open: string | null;
|
|
33
|
+
/** null = not seeded yet. Distinct from [] (seeded, no documents) — a row
|
|
34
|
+
* that hydrates before the overlay must not flash "file missing". */
|
|
35
|
+
docs: SiteDocument[] | null;
|
|
36
|
+
openListeners: Set<(id: string | null) => void>;
|
|
37
|
+
docsListeners: Set<(docs: SiteDocument[] | null) => void>;
|
|
38
|
+
teardown: (() => void) | null;
|
|
39
|
+
closingTimer: ReturnType<typeof setTimeout> | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Symbol-keyed on globalThis for the same reason the section registry is: the
|
|
43
|
+
// bundler may hand two islands two copies of this module, and a split store
|
|
44
|
+
// would let the nav open a document the overlay never hears about.
|
|
45
|
+
const STORE_KEY = Symbol.for("@drawnagency/primitives/doc-file-state");
|
|
46
|
+
const g = globalThis as unknown as Record<symbol, DocFileStore | undefined>;
|
|
47
|
+
|
|
48
|
+
function store(): DocFileStore {
|
|
49
|
+
let s = g[STORE_KEY];
|
|
50
|
+
if (!s) {
|
|
51
|
+
s = { open: null, docs: null, openListeners: new Set(), docsListeners: new Set(), teardown: null, closingTimer: null };
|
|
52
|
+
g[STORE_KEY] = s;
|
|
53
|
+
}
|
|
54
|
+
return s;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function isDocDesktop(): boolean {
|
|
58
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return true;
|
|
59
|
+
return window.matchMedia(DOC_DESKTOP_QUERY).matches;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function readHash(): string | null {
|
|
63
|
+
if (typeof window === "undefined") return null;
|
|
64
|
+
const match = /^#doc=(.+)$/.exec(window.location.hash);
|
|
65
|
+
if (!match) return null;
|
|
66
|
+
try {
|
|
67
|
+
return decodeURIComponent(match[1]);
|
|
68
|
+
} catch {
|
|
69
|
+
return match[1];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function apply(sectionId: string | null): void {
|
|
74
|
+
const s = store();
|
|
75
|
+
if (s.open === sectionId) return;
|
|
76
|
+
const wasOpen = s.open !== null;
|
|
77
|
+
s.open = sectionId;
|
|
78
|
+
if (typeof document !== "undefined") {
|
|
79
|
+
const root = document.documentElement;
|
|
80
|
+
root.classList.toggle(DOC_OPEN_CLASS, sectionId !== null);
|
|
81
|
+
// A reopen during the return glide cancels the closing state outright.
|
|
82
|
+
if (s.closingTimer !== null) {
|
|
83
|
+
clearTimeout(s.closingTimer);
|
|
84
|
+
s.closingTimer = null;
|
|
85
|
+
root.classList.remove(DOC_CLOSING_CLASS);
|
|
86
|
+
}
|
|
87
|
+
if (sectionId === null && wasOpen) {
|
|
88
|
+
root.classList.add(DOC_CLOSING_CLASS);
|
|
89
|
+
s.closingTimer = setTimeout(() => {
|
|
90
|
+
root.classList.remove(DOC_CLOSING_CLASS);
|
|
91
|
+
s.closingTimer = null;
|
|
92
|
+
}, DOC_CLOSING_MS);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
for (const listener of [...s.openListeners]) listener(sectionId);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function syncFromHash(): void {
|
|
99
|
+
const next = readHash();
|
|
100
|
+
// "Ignores hash writes that still contain its own value" — a same-value
|
|
101
|
+
// write (e.g. a repeated nav click on the already-open document) must not
|
|
102
|
+
// re-notify. NOTE the one hash writer that CAN silently stomp #doc is a nav
|
|
103
|
+
// HEADING click (useActiveHeadings' setActiveSection does a replaceState —
|
|
104
|
+
// click-driven, not scroll-driven, so the scroll lock does not prevent it);
|
|
105
|
+
// U3 closes the overlay in handleHeadingNav before that write happens.
|
|
106
|
+
if (next === store().open) return;
|
|
107
|
+
apply(next);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function openDocFile(sectionId: string): void {
|
|
111
|
+
if (typeof window !== "undefined") {
|
|
112
|
+
const hash = `#doc=${encodeURIComponent(sectionId)}`;
|
|
113
|
+
if (window.location.hash !== hash) {
|
|
114
|
+
// pushState so Back closes the overlay. It fires no hashchange, which is
|
|
115
|
+
// why the atom — not the hash — is the store.
|
|
116
|
+
window.history.pushState(null, "", hash);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
apply(sectionId);
|
|
120
|
+
docFileOpenEvent.dispatch({ sectionId });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function closeDocFile(): void {
|
|
124
|
+
if (typeof window !== "undefined" && readHash() !== null) {
|
|
125
|
+
// replaceState, never history.back(): back() races the editor's own
|
|
126
|
+
// pushState in goToPage and is unobservable in jsdom. The cost is that Back
|
|
127
|
+
// after a manual close returns to an identical URL — invisible to the user.
|
|
128
|
+
const { pathname, search } = window.location;
|
|
129
|
+
window.history.replaceState(null, "", `${pathname}${search}`);
|
|
130
|
+
}
|
|
131
|
+
apply(null);
|
|
132
|
+
docFileOpenEvent.dispatch({ sectionId: null });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function getOpenDocFile(): string | null {
|
|
136
|
+
return store().open;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function subscribeDocFile(cb: (sectionId: string | null) => void): () => void {
|
|
140
|
+
const s = store();
|
|
141
|
+
s.openListeners.add(cb);
|
|
142
|
+
if (!s.teardown && typeof window !== "undefined") {
|
|
143
|
+
const onHash = () => syncFromHash();
|
|
144
|
+
const onPop = () => syncFromHash();
|
|
145
|
+
const onRequest = (detail: { sectionId: string | null }) => {
|
|
146
|
+
// The already-current check must come FIRST and cover null too: this
|
|
147
|
+
// listener also hears the event openDocFile/closeDocFile dispatch
|
|
148
|
+
// themselves, and an unguarded close would re-close → re-dispatch →
|
|
149
|
+
// recurse until the stack blew (jsdom swallows listener throws, so the
|
|
150
|
+
// suite only surfaced it as an unhandled RangeError).
|
|
151
|
+
if (detail.sectionId === s.open) return;
|
|
152
|
+
if (detail.sectionId === null) closeDocFile();
|
|
153
|
+
else openDocFile(detail.sectionId);
|
|
154
|
+
};
|
|
155
|
+
window.addEventListener("hashchange", onHash);
|
|
156
|
+
window.addEventListener("popstate", onPop);
|
|
157
|
+
const unlistenRequest = docFileOpenEvent.listen(onRequest);
|
|
158
|
+
s.teardown = () => {
|
|
159
|
+
window.removeEventListener("hashchange", onHash);
|
|
160
|
+
window.removeEventListener("popstate", onPop);
|
|
161
|
+
unlistenRequest();
|
|
162
|
+
};
|
|
163
|
+
// Deep link: the hash is an INPUT, read once when the first listener wires up.
|
|
164
|
+
syncFromHash();
|
|
165
|
+
}
|
|
166
|
+
return () => {
|
|
167
|
+
s.openListeners.delete(cb);
|
|
168
|
+
if (s.openListeners.size === 0 && s.teardown) {
|
|
169
|
+
s.teardown();
|
|
170
|
+
s.teardown = null;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Authoritative publish — the editor's live draft list. */
|
|
176
|
+
export function setSiteDocuments(docs: SiteDocument[]): void {
|
|
177
|
+
const s = store();
|
|
178
|
+
s.docs = docs;
|
|
179
|
+
for (const listener of [...s.docsListeners]) listener(docs);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** First-writer-wins publish for the SSR-derived list, so an island that mounts
|
|
183
|
+
* after the editor has already published cannot clobber the live list. */
|
|
184
|
+
export function seedSiteDocuments(docs: SiteDocument[]): void {
|
|
185
|
+
if (store().docs !== null) return;
|
|
186
|
+
setSiteDocuments(docs);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function getSiteDocuments(): SiteDocument[] | null {
|
|
190
|
+
return store().docs;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function subscribeSiteDocuments(cb: (docs: SiteDocument[] | null) => void): () => void {
|
|
194
|
+
const s = store();
|
|
195
|
+
s.docsListeners.add(cb);
|
|
196
|
+
return () => { s.docsListeners.delete(cb); };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Test-only: drop the store and any window wiring (mirrors clearRegistry). */
|
|
200
|
+
export function resetDocFileState(): void {
|
|
201
|
+
const s = g[STORE_KEY];
|
|
202
|
+
if (s?.teardown) s.teardown();
|
|
203
|
+
if (s?.closingTimer) clearTimeout(s.closingTimer);
|
|
204
|
+
if (typeof document !== "undefined") {
|
|
205
|
+
document.documentElement.classList.remove(DOC_OPEN_CLASS, DOC_CLOSING_CLASS);
|
|
206
|
+
}
|
|
207
|
+
g[STORE_KEY] = undefined;
|
|
208
|
+
}
|
|
@@ -6,6 +6,7 @@ import { Checkbox } from "../shared/Checkbox";
|
|
|
6
6
|
import { FormLabel } from "../shared/FormLabel";
|
|
7
7
|
import { LinkField } from "../shared/LinkField";
|
|
8
8
|
import { VideoSourceField } from "../shared/VideoSourceField";
|
|
9
|
+
import { DocumentSourceField, type DocumentRef } from "../shared/DocumentSourceField";
|
|
9
10
|
import { Tabs } from "../shared/Tabs";
|
|
10
11
|
import type { LinkValue } from "../../schemas/link";
|
|
11
12
|
import type { VideoSource } from "../sections/Video/VideoBlock";
|
|
@@ -174,6 +175,15 @@ function renderField(
|
|
|
174
175
|
/>
|
|
175
176
|
);
|
|
176
177
|
|
|
178
|
+
case "document-source":
|
|
179
|
+
return (
|
|
180
|
+
<DocumentSourceField
|
|
181
|
+
key={key}
|
|
182
|
+
value={value as DocumentRef | undefined}
|
|
183
|
+
onChange={(v) => handleFieldChange(key, v)}
|
|
184
|
+
/>
|
|
185
|
+
);
|
|
186
|
+
|
|
177
187
|
default:
|
|
178
188
|
return null;
|
|
179
189
|
}
|