@drawnagency/primitives 0.1.109 → 0.1.111
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/auth/cookies.d.ts +8 -0
- package/dist/auth/cookies.d.ts.map +1 -1
- package/dist/auth/index.d.ts +1 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +3 -1
- package/dist/auth/types.d.ts +11 -4
- package/dist/auth/types.d.ts.map +1 -1
- package/dist/{chunk-WXBHP4JY.js → chunk-3GAG6ZXB.js} +55 -46
- package/dist/{chunk-FM4M4Q2W.js → chunk-4TZJZU2T.js} +23 -3
- package/dist/{chunk-5PVRACZY.js → chunk-7MI3HCOF.js} +60 -23
- package/dist/{chunk-WBDPDGFA.js → chunk-7WQJ57XG.js} +6 -31
- package/dist/{chunk-LQHYVPBS.js → chunk-ALMEC3XN.js} +11 -12
- package/dist/{chunk-TZQPOR5A.js → chunk-DMCGN5AS.js} +10 -3
- package/dist/{chunk-ZKVKNUJC.js → chunk-ELQ4C677.js} +1 -1
- package/dist/{chunk-5BM2Y3NJ.js → chunk-FRVO75DK.js} +14 -2
- package/dist/{chunk-VKAGMEKE.js → chunk-FTPETWXE.js} +1 -0
- package/dist/{chunk-3TO4RRHY.js → chunk-GPPVYLEI.js} +7 -0
- package/dist/{chunk-NLQ5F3RQ.js → chunk-HBULBDGP.js} +1 -1
- package/dist/{chunk-67ZOQCUD.js → chunk-NXGIHSL5.js} +67 -50
- package/dist/{chunk-CPTFFLK2.js → chunk-OTDA6KWL.js} +10 -3
- package/dist/{chunk-XIDE4TDF.js → chunk-OUYB2GCW.js} +1 -1
- package/dist/{chunk-72CG6AA2.js → chunk-RA5IOFPY.js} +10 -2
- package/dist/{chunk-GEENI3OC.js → chunk-SW6PCQQD.js} +4 -0
- package/dist/{chunk-BOZUEKWL.js → chunk-UVIYI4J6.js} +1 -1
- package/dist/{chunk-AB3XVXI3.js → chunk-VCCNKTFD.js} +37 -29
- package/dist/{chunk-XBYJQ5RS.js → chunk-XQMDWXD6.js} +21 -6
- package/dist/collab/rebase.d.ts +6 -0
- package/dist/collab/rebase.d.ts.map +1 -1
- package/dist/collab/rebase.js +12 -3
- package/dist/collab/recovery.d.ts +0 -20
- package/dist/collab/recovery.d.ts.map +1 -1
- package/dist/components/brandguide/ColorSwatchSettings.d.ts.map +1 -1
- package/dist/components/brandguide/ColorsView.d.ts.map +1 -1
- package/dist/components/brandguide/colors-shared.d.ts +10 -1
- package/dist/components/brandguide/colors-shared.d.ts.map +1 -1
- package/dist/components/chat/ChatPanel.d.ts.map +1 -1
- package/dist/components/docfile/DocFilePane.d.ts.map +1 -1
- package/dist/components/docfile/PdfCanvasViewer.d.ts.map +1 -1
- package/dist/components/docfile/doc-file-state.d.ts.map +1 -1
- package/dist/components/editor/IndicatorPill.d.ts.map +1 -1
- package/dist/components/editor/NavGroupRow.d.ts +7 -2
- package/dist/components/editor/NavGroupRow.d.ts.map +1 -1
- package/dist/components/editor/PagesModal.d.ts.map +1 -1
- package/dist/components/editor/index.js +13 -12
- package/dist/components/primitives/EditableRichText.d.ts.map +1 -1
- package/dist/components/primitives/LinkPopover.d.ts.map +1 -1
- package/dist/components/primitives/index.js +14 -12
- package/dist/components/primitives/useEditableCollection.d.ts.map +1 -1
- package/dist/components/primitives/useEditablePlainText.d.ts.map +1 -1
- package/dist/components/sections/IconList/index.d.ts.map +1 -1
- package/dist/components/sections/Prose/Prose.d.ts.map +1 -1
- package/dist/components/sections/brand-guide.js +8 -8
- package/dist/components/sections/register-schemas.js +14 -14
- package/dist/components/sections/register.js +14 -14
- package/dist/components/shared/FontPicker.d.ts.map +1 -1
- package/dist/components/shared/HistoryChangesModal.d.ts.map +1 -1
- package/dist/components/shared/HistoryPopover.d.ts.map +1 -1
- package/dist/components/shared/Navigation.d.ts.map +1 -1
- package/dist/components/shared/Popover.d.ts.map +1 -1
- package/dist/components/shell/AudienceAddForm.d.ts +1 -5
- package/dist/components/shell/AudienceAddForm.d.ts.map +1 -1
- package/dist/components/shell/AudienceRow.d.ts +2 -3
- package/dist/components/shell/AudienceRow.d.ts.map +1 -1
- package/dist/components/shell/BugReportFAB.d.ts.map +1 -1
- package/dist/components/shell/EditorShell.d.ts.map +1 -1
- package/dist/components/shell/MediaLibraryModal.d.ts.map +1 -1
- package/dist/components/shell/SiteSettingsUsers.d.ts.map +1 -1
- package/dist/components/shell/SiteSettingsViewerAccess.d.ts.map +1 -1
- package/dist/components/shell/view-branch.d.ts.map +1 -1
- package/dist/document/components/DocRail.d.ts.map +1 -1
- package/dist/document/components/DocViewer.d.ts.map +1 -1
- package/dist/document/components/measure.d.ts.map +1 -1
- package/dist/document/components/useDocPages.d.ts.map +1 -1
- package/dist/document/engine/blocks.d.ts.map +1 -1
- package/dist/document/engine/masonry.d.ts.map +1 -1
- package/dist/document/engine/paginate.d.ts.map +1 -1
- package/dist/document/engine/settings.d.ts +3 -0
- package/dist/document/engine/settings.d.ts.map +1 -1
- package/dist/document/index.js +79 -28
- package/dist/hooks/useBuildStatus.d.ts.map +1 -1
- package/dist/hooks/useDocumentUploads.d.ts +1 -0
- package/dist/hooks/useDocumentUploads.d.ts.map +1 -1
- package/dist/hooks/useEditorPersistence.d.ts.map +1 -1
- package/dist/hooks/useEditorPublish.d.ts.map +1 -1
- package/dist/hooks/useFocusTrap.d.ts +6 -0
- package/dist/hooks/useFocusTrap.d.ts.map +1 -1
- package/dist/hooks/useMediaPipeline.d.ts +3 -2
- package/dist/hooks/useMediaPipeline.d.ts.map +1 -1
- package/dist/hooks/useSanitizedHtml.d.ts +12 -0
- package/dist/hooks/useSanitizedHtml.d.ts.map +1 -0
- package/dist/index.js +19 -13
- package/dist/lib/block-move.d.ts.map +1 -1
- package/dist/lib/contrast.d.ts.map +1 -1
- package/dist/lib/dexie.d.ts +2 -8
- package/dist/lib/dexie.d.ts.map +1 -1
- package/dist/lib/dexie.js +21 -49
- package/dist/lib/index-ops.d.ts.map +1 -1
- package/dist/lib/index-ops.js +43 -23
- package/dist/lib/index.js +8 -8
- package/dist/lib/loader.d.ts.map +1 -1
- package/dist/lib/media-folders.d.ts +12 -4
- package/dist/lib/media-folders.d.ts.map +1 -1
- package/dist/lib/media-folders.js +12 -2
- package/dist/lib/migrate-sections-transform.js +1 -1
- package/dist/lib/nav-progress.d.ts +14 -0
- package/dist/lib/nav-progress.d.ts.map +1 -0
- package/dist/lib/nav-tree.d.ts.map +1 -1
- package/dist/lib/nav.d.ts.map +1 -1
- package/dist/lib/pages.d.ts.map +1 -1
- package/dist/lib/registry.d.ts.map +1 -1
- package/dist/lib/registry.js +1 -1
- package/dist/lib/sanitize.d.ts +6 -0
- package/dist/lib/sanitize.d.ts.map +1 -1
- package/dist/lib/timestamp.d.ts.map +1 -1
- package/dist/media/index.js +4 -4
- package/dist/media/queue.d.ts.map +1 -1
- package/dist/media/resolve.d.ts.map +1 -1
- package/dist/media/utils.d.ts.map +1 -1
- package/dist/media/videoPoster.d.ts.map +1 -1
- package/dist/schemas/audience.d.ts +5 -1
- package/dist/schemas/audience.d.ts.map +1 -1
- package/dist/schemas/audience.js +1 -1
- package/dist/schemas/index.js +8 -6
- package/dist/schemas/sections.d.ts +12 -0
- package/dist/schemas/sections.d.ts.map +1 -1
- package/dist/storage/index.d.ts +1 -1
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +3 -1
- package/dist/storage/types.d.ts +9 -0
- package/dist/storage/types.d.ts.map +1 -1
- package/dist/types/database.d.ts +0 -3
- package/dist/types/database.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/auth/cookies.ts +11 -0
- package/src/auth/index.ts +1 -1
- package/src/auth/types.ts +18 -4
- package/src/collab/rebase.ts +26 -1
- package/src/collab/recovery.ts +29 -3
- package/src/components/brandguide/ColorSwatchSettings.tsx +40 -13
- package/src/components/brandguide/ColorsEditable.tsx +7 -9
- package/src/components/brandguide/ColorsView.tsx +12 -11
- package/src/components/brandguide/colors-shared.ts +17 -1
- package/src/components/chat/ChatPanel.tsx +19 -0
- package/src/components/docfile/DocFilePane.tsx +11 -0
- package/src/components/docfile/PdfCanvasViewer.tsx +46 -5
- package/src/components/docfile/doc-file-state.ts +7 -0
- package/src/components/editor/IndicatorPill.tsx +5 -1
- package/src/components/editor/NavGroupRow.tsx +8 -3
- package/src/components/editor/PagesModal.tsx +10 -2
- package/src/components/primitives/EditableList.tsx +10 -2
- package/src/components/primitives/EditableRichText.tsx +4 -3
- package/src/components/primitives/LinkPopover.tsx +23 -1
- package/src/components/primitives/useEditableCollection.ts +61 -41
- package/src/components/primitives/useEditablePlainText.ts +24 -3
- package/src/components/sections/IconList/index.tsx +8 -1
- package/src/components/sections/Prose/Prose.tsx +4 -2
- package/src/components/shared/FontPicker.tsx +16 -4
- package/src/components/shared/HistoryChangesModal.tsx +12 -1
- package/src/components/shared/HistoryPopover.tsx +18 -4
- package/src/components/shared/Navigation.tsx +20 -2
- package/src/components/shared/Popover.tsx +9 -5
- package/src/components/shell/AudienceAddForm.tsx +6 -26
- package/src/components/shell/AudienceRow.tsx +29 -39
- package/src/components/shell/BugReportFAB.tsx +20 -2
- package/src/components/shell/EditorShell.tsx +200 -50
- package/src/components/shell/MediaLibraryModal.tsx +52 -10
- package/src/components/shell/SiteSettingsUsers.tsx +3 -0
- package/src/components/shell/SiteSettingsViewerAccess.tsx +21 -16
- package/src/components/shell/view-branch.ts +4 -1
- package/src/document/components/DocRail.tsx +27 -2
- package/src/document/components/DocViewer.tsx +5 -1
- package/src/document/components/measure.tsx +22 -6
- package/src/document/components/useDocPages.ts +13 -3
- package/src/document/engine/blocks.ts +33 -4
- package/src/document/engine/masonry.ts +8 -2
- package/src/document/engine/paginate.ts +4 -1
- package/src/document/engine/settings.ts +40 -3
- package/src/hooks/useBuildStatus.ts +6 -0
- package/src/hooks/useDocumentUploads.ts +46 -3
- package/src/hooks/useEditorPersistence.ts +33 -19
- package/src/hooks/useEditorPublish.ts +83 -34
- package/src/hooks/useFocusTrap.ts +18 -0
- package/src/hooks/useMediaPipeline.ts +111 -38
- package/src/hooks/useSanitizedHtml.ts +29 -0
- package/src/lib/audience-secret.ts +1 -0
- package/src/lib/block-move.ts +13 -2
- package/src/lib/contrast.ts +14 -14
- package/src/lib/dexie.ts +46 -49
- package/src/lib/index-ops.ts +63 -20
- package/src/lib/loader.ts +41 -12
- package/src/lib/media-folders.ts +31 -5
- package/src/lib/migrate-sections-transform.ts +22 -2
- package/src/lib/nav-progress.ts +108 -0
- package/src/lib/nav-tree.ts +16 -2
- package/src/lib/nav.ts +10 -1
- package/src/lib/pages.ts +24 -6
- package/src/lib/registry.ts +3 -0
- package/src/lib/sanitize.ts +9 -0
- package/src/lib/timestamp.ts +5 -0
- package/src/media/queue.ts +4 -0
- package/src/media/resolve.ts +14 -1
- package/src/media/utils.ts +3 -1
- package/src/media/videoPoster.ts +14 -3
- package/src/schemas/audience.ts +10 -2
- package/src/schemas/sections.ts +23 -1
- package/src/storage/index.ts +1 -1
- package/src/storage/types.ts +13 -0
- package/src/types/database.ts +0 -3
|
@@ -6,6 +6,8 @@ import Fab from "../shared/Fab";
|
|
|
6
6
|
|
|
7
7
|
type Category = "Visual" | "Unexpected Behavior" | "Media / Media Library" | "Save / Publish" | "Feature Request" | "Other";
|
|
8
8
|
|
|
9
|
+
const MAX_IMAGES = 5;
|
|
10
|
+
|
|
9
11
|
const CATEGORIES: Category[] = [
|
|
10
12
|
"Visual",
|
|
11
13
|
"Unexpected Behavior",
|
|
@@ -38,8 +40,11 @@ export function BugReportFAB() {
|
|
|
38
40
|
const [isDragOver, setIsDragOver] = useState(false);
|
|
39
41
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
40
42
|
const [submitError, setSubmitError] = useState<string | null>(null);
|
|
43
|
+
const [imageNotice, setImageNotice] = useState<string | null>(null);
|
|
41
44
|
const [descriptionError, setDescriptionError] = useState<string | null>(null);
|
|
42
45
|
const [toast, setToast] = useState<string | null>(null);
|
|
46
|
+
const toastTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
47
|
+
useEffect(() => () => { if (toastTimerRef.current) clearTimeout(toastTimerRef.current); }, []);
|
|
43
48
|
const capturedContextRef = useRef<CapturedContext | null>(null);
|
|
44
49
|
const dropZoneRef = useRef<HTMLDivElement>(null);
|
|
45
50
|
|
|
@@ -87,11 +92,22 @@ export function BugReportFAB() {
|
|
|
87
92
|
name: file.name,
|
|
88
93
|
})),
|
|
89
94
|
);
|
|
90
|
-
setImages((prev) =>
|
|
95
|
+
setImages((prev) => {
|
|
96
|
+
const next = [...prev, ...newPreviews];
|
|
97
|
+
// The cap keeps the FIRST five, so a sixth drop silently threw away the
|
|
98
|
+
// shot the reporter had just taken. Say what happened instead.
|
|
99
|
+
if (next.length > MAX_IMAGES) {
|
|
100
|
+
setImageNotice(`Only ${MAX_IMAGES} screenshots can be attached — ${next.length - MAX_IMAGES} were not added.`);
|
|
101
|
+
return next.slice(0, MAX_IMAGES);
|
|
102
|
+
}
|
|
103
|
+
setImageNotice(null);
|
|
104
|
+
return next;
|
|
105
|
+
});
|
|
91
106
|
}, [fileToDataUri]);
|
|
92
107
|
|
|
93
108
|
const handleRemoveImage = useCallback((index: number) => {
|
|
94
109
|
setImages((prev) => prev.filter((_, i) => i !== index));
|
|
110
|
+
setImageNotice(null);
|
|
95
111
|
}, []);
|
|
96
112
|
|
|
97
113
|
const handleDragOver = useCallback((e: React.DragEvent) => {
|
|
@@ -171,7 +187,8 @@ export function BugReportFAB() {
|
|
|
171
187
|
|
|
172
188
|
handleClose();
|
|
173
189
|
setToast("Bug report submitted");
|
|
174
|
-
|
|
190
|
+
if (toastTimerRef.current) clearTimeout(toastTimerRef.current);
|
|
191
|
+
toastTimerRef.current = setTimeout(() => setToast(null), 3000);
|
|
175
192
|
} catch (err) {
|
|
176
193
|
setSubmitError(err instanceof Error ? err.message : "Submission failed");
|
|
177
194
|
} finally {
|
|
@@ -287,6 +304,7 @@ export function BugReportFAB() {
|
|
|
287
304
|
Drop images here or paste from clipboard
|
|
288
305
|
</p>
|
|
289
306
|
</div>
|
|
307
|
+
{imageNotice && <p className="mt-1 text-xs text-red-600">{imageNotice}</p>}
|
|
290
308
|
|
|
291
309
|
{/* Thumbnail previews */}
|
|
292
310
|
{images.length > 0 && (
|
|
@@ -31,7 +31,7 @@ import { EditorModal } from "./EditorModal";
|
|
|
31
31
|
import { SiteSettingsModal } from "./SiteSettingsModal";
|
|
32
32
|
import ChatWidget from "../chat/ChatWidget";
|
|
33
33
|
import { MediaLibraryModal } from "./MediaLibraryModal";
|
|
34
|
-
import { MediaLibraryContext, type LibraryKind } from "./MediaLibraryContext";
|
|
34
|
+
import { MediaLibraryContext, type LibraryKind, type MediaLibraryContextValue } from "./MediaLibraryContext";
|
|
35
35
|
import { ProcessingIndicator } from "./ProcessingIndicator";
|
|
36
36
|
import { SectionSkeleton } from "./SectionSkeleton";
|
|
37
37
|
import { SectionTypePicker } from "./SectionTypePicker";
|
|
@@ -179,6 +179,12 @@ interface Props {
|
|
|
179
179
|
collabBootstrap?: CollabBootstrap | null;
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
/** Stable empty map for the closed-modal case, so the memos below don't churn. */
|
|
183
|
+
const EMPTY_COUNTS: Record<string, number> = {};
|
|
184
|
+
|
|
185
|
+
/** Short enough that a colour-picker drag still reads as live. */
|
|
186
|
+
const CONFIG_PREVIEW_DEBOUNCE_MS = 120;
|
|
187
|
+
|
|
182
188
|
export default function EditorShell({
|
|
183
189
|
headSha,
|
|
184
190
|
draftHeadSha,
|
|
@@ -208,6 +214,18 @@ export default function EditorShell({
|
|
|
208
214
|
// documents): U6's registry publish skips null so document rows keep the
|
|
209
215
|
// overlay island's SSR seed instead of flashing "File missing" on mount.
|
|
210
216
|
const [documentManifest, setDocumentManifest] = useState<DocumentManifest | null>(null);
|
|
217
|
+
// Read by the post-await cacheContent calls below. Every one of them re-primes
|
|
218
|
+
// the cache at a NEW sha, so omitting the manifest doesn't just skip a field —
|
|
219
|
+
// it writes null, and the next reload takes the cache hit and reports every
|
|
220
|
+
// document missing until the cache is invalidated. The collab handlers read it
|
|
221
|
+
// too: reading the state directly there would either capture a stale closure
|
|
222
|
+
// copy or force documentManifest into their dep arrays, churning the callback
|
|
223
|
+
// identity the collab wiring subscribes with.
|
|
224
|
+
//
|
|
225
|
+
// Assigned synchronously on render, NOT in an effect — the readers above run
|
|
226
|
+
// after an await in the same tick, which an effect-updated ref serves stale.
|
|
227
|
+
const documentManifestRef = useRef<DocumentManifest | null>(documentManifest);
|
|
228
|
+
documentManifestRef.current = documentManifest;
|
|
211
229
|
const [showMediaLibrary, setShowMediaLibrary] = useState(false);
|
|
212
230
|
const [mediaModalMode, setMediaModalMode] = useState<"select" | "manage">("manage");
|
|
213
231
|
const [mediaSelectCallback, setMediaSelectCallback] = useState<((id: string) => void) | null>(null);
|
|
@@ -309,13 +327,11 @@ export default function EditorShell({
|
|
|
309
327
|
// immediate 409 retry assembles the saved manifest against the rebased copy.
|
|
310
328
|
const mediaManifestRef = useRef<MediaManifest>(mediaManifest);
|
|
311
329
|
useEffect(() => { mediaManifestRef.current = mediaManifest; }, [mediaManifest]);
|
|
312
|
-
//
|
|
313
|
-
//
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
|
|
317
|
-
const documentManifestRef = useRef<DocumentManifest | null>(documentManifest);
|
|
318
|
-
useEffect(() => { documentManifestRef.current = documentManifest; }, [documentManifest]);
|
|
330
|
+
// The manifest our working copy diverged from — the last one we fetched, and
|
|
331
|
+
// the only way rebaseMediaManifest can tell "we deleted this folder" from
|
|
332
|
+
// "they created it". Advanced on every load and every rebase, never on a local
|
|
333
|
+
// folder edit.
|
|
334
|
+
const baseMediaManifestRef = useRef<MediaManifest | null>(null);
|
|
319
335
|
// Guards the debounced preview-image composition effect (below): skips
|
|
320
336
|
// recomposing on the very first run when a preview already exists, and
|
|
321
337
|
// dedupes concurrent runs against the same composition signature.
|
|
@@ -548,6 +564,7 @@ export default function EditorShell({
|
|
|
548
564
|
dirtySectionIds: dirtyIds,
|
|
549
565
|
locallyAddedIds,
|
|
550
566
|
locallyDeletedIds: deletedSections,
|
|
567
|
+
baselineIndex: cached?.index,
|
|
551
568
|
remoteSections: remote.sections,
|
|
552
569
|
remoteIndex: remote.index,
|
|
553
570
|
// rebaseContent self-detects remote deletions of dirty sections, so we
|
|
@@ -584,9 +601,14 @@ export default function EditorShell({
|
|
|
584
601
|
}
|
|
585
602
|
|
|
586
603
|
const rebasedManifest = remote.mediaManifest
|
|
587
|
-
? rebaseMediaManifest(
|
|
604
|
+
? rebaseMediaManifest(
|
|
605
|
+
mediaManifestRef.current,
|
|
606
|
+
remote.mediaManifest,
|
|
607
|
+
baseMediaManifestRef.current ?? undefined,
|
|
608
|
+
)
|
|
588
609
|
: null;
|
|
589
610
|
if (rebasedManifest) setMediaManifest(rebasedManifest);
|
|
611
|
+
if (remote.mediaManifest) baseMediaManifestRef.current = remote.mediaManifest;
|
|
590
612
|
|
|
591
613
|
setSections(mergedSections);
|
|
592
614
|
setSiteIndex(mergedIndex);
|
|
@@ -603,7 +625,12 @@ export default function EditorShell({
|
|
|
603
625
|
siteIndexRef.current = mergedIndex;
|
|
604
626
|
changedSectionIdsRef.current = new Set(remote.changedSectionIds);
|
|
605
627
|
mainIndexRef.current = remote.mainIndex;
|
|
606
|
-
|
|
628
|
+
// From the FETCHED remote index, not `mergedIndex` — the rebase
|
|
629
|
+
// deliberately preserves locallyAddedIds, and a never-saved local section
|
|
630
|
+
// is by definition not on the remote. Taking it from the merge would let a
|
|
631
|
+
// later delete of that section enqueue a file deletion for a path that was
|
|
632
|
+
// never committed.
|
|
633
|
+
remoteSectionIdsRef.current = new Set(remote.index.pages.flatMap((p) => p.order));
|
|
607
634
|
// C2: the cache is the CLEAN baseline — always the fetched remote
|
|
608
635
|
// snapshot, never the rebased result. Caching the rebase (local dirty
|
|
609
636
|
// overlays folded in) would poison the next recovery's diff: dirty
|
|
@@ -686,6 +713,7 @@ export default function EditorShell({
|
|
|
686
713
|
dirtySectionIds: dirtyIds,
|
|
687
714
|
locallyAddedIds,
|
|
688
715
|
locallyDeletedIds: deletedSections,
|
|
716
|
+
baselineIndex: cached?.index,
|
|
689
717
|
remoteSections: remote.sections,
|
|
690
718
|
remoteIndex: remote.index,
|
|
691
719
|
remoteChangedIds,
|
|
@@ -693,9 +721,14 @@ export default function EditorShell({
|
|
|
693
721
|
|
|
694
722
|
// The rebased result goes into React state (live view)...
|
|
695
723
|
if (remote.mediaManifest) {
|
|
696
|
-
const rebasedManifest = rebaseMediaManifest(
|
|
724
|
+
const rebasedManifest = rebaseMediaManifest(
|
|
725
|
+
mediaManifestRef.current,
|
|
726
|
+
remote.mediaManifest,
|
|
727
|
+
baseMediaManifestRef.current ?? undefined,
|
|
728
|
+
);
|
|
697
729
|
setMediaManifest(rebasedManifest);
|
|
698
730
|
mediaManifestRef.current = rebasedManifest;
|
|
731
|
+
baseMediaManifestRef.current = remote.mediaManifest;
|
|
699
732
|
}
|
|
700
733
|
setSections(result.sections);
|
|
701
734
|
setSiteIndex(result.index);
|
|
@@ -707,7 +740,9 @@ export default function EditorShell({
|
|
|
707
740
|
savedShaRef.current = remote.savedBranchSha;
|
|
708
741
|
changedSectionIdsRef.current = new Set(remote.changedSectionIds);
|
|
709
742
|
mainIndexRef.current = remote.mainIndex;
|
|
710
|
-
|
|
743
|
+
// Fetched remote, not the rebased `result` — see the identical note in
|
|
744
|
+
// recoverConflict.
|
|
745
|
+
remoteSectionIdsRef.current = new Set(remote.index.pages.flatMap((p) => p.order));
|
|
711
746
|
|
|
712
747
|
// ...but the CLEAN fetched remote snapshot is what gets cached as the
|
|
713
748
|
// baseline (amendment 1: caching the rebased result would poison the next
|
|
@@ -808,6 +843,16 @@ export default function EditorShell({
|
|
|
808
843
|
});
|
|
809
844
|
useEffect(() => { collabRef.current = collab; }, [collab]);
|
|
810
845
|
|
|
846
|
+
// Cached closures for sections that no longer exist are dead weight for the
|
|
847
|
+
// rest of the session — a long editing run of adds and deletes accumulates one
|
|
848
|
+
// per id ever seen.
|
|
849
|
+
useEffect(() => {
|
|
850
|
+
const live = new Set(sections.map((l) => l.section.id));
|
|
851
|
+
for (const id of editIntentByIdRef.current.keys()) {
|
|
852
|
+
if (!live.has(id)) editIntentByIdRef.current.delete(id);
|
|
853
|
+
}
|
|
854
|
+
}, [sections]);
|
|
855
|
+
|
|
811
856
|
// Stable per-section onEditIntent: focus + lock request. The cached closure
|
|
812
857
|
// always calls the LIVE session via collabRef, so it needn't change identity.
|
|
813
858
|
const getEditIntent = useCallback((id: string): (() => Promise<boolean>) => {
|
|
@@ -1191,6 +1236,10 @@ export default function EditorShell({
|
|
|
1191
1236
|
setMainSha(headSha);
|
|
1192
1237
|
setChangedSectionIds(new Set(cached.changedSectionIds ?? []));
|
|
1193
1238
|
setMainIndex(cached.mainIndex ?? loadedIndex);
|
|
1239
|
+
// Cached alongside the content: without it a cache-hit load showed no
|
|
1240
|
+
// damaged-section banner at all, so a broken section file stayed
|
|
1241
|
+
// invisible until the next fresh fetch.
|
|
1242
|
+
setUnparseableSectionIds(cached.unparseableSectionIds ?? []);
|
|
1194
1243
|
// Without this, a cache-hit load left documentManifest null forever —
|
|
1195
1244
|
// the siteDocuments memo's null-guard then never lifted and every
|
|
1196
1245
|
// document row (committed or staged) read "File missing". The row's copy
|
|
@@ -1209,6 +1258,9 @@ export default function EditorShell({
|
|
|
1209
1258
|
savedBranchSha: data.savedBranchSha,
|
|
1210
1259
|
changedSectionIds: data.changedSectionIds,
|
|
1211
1260
|
mainIndex: data.mainIndex,
|
|
1261
|
+
// Rides the same row as the content it describes — a cache-hit load
|
|
1262
|
+
// showed no damaged-section banner at all until this came along.
|
|
1263
|
+
unparseableSectionIds: data.unparseableSectionIds,
|
|
1212
1264
|
},
|
|
1213
1265
|
)
|
|
1214
1266
|
setDocumentManifest(data.documentManifest ?? {});
|
|
@@ -1237,6 +1289,7 @@ export default function EditorShell({
|
|
|
1237
1289
|
remoteSectionIdsRef.current = new Set(loadedIndex.pages.flatMap((p) => p.order));
|
|
1238
1290
|
setSiteConfig(loadedConfig);
|
|
1239
1291
|
setMediaManifest(loadedManifest);
|
|
1292
|
+
baseMediaManifestRef.current = loadedManifest;
|
|
1240
1293
|
siteIndexRef.current = loadedIndex;
|
|
1241
1294
|
applySiteConfigPreview(loadedConfig);
|
|
1242
1295
|
|
|
@@ -1353,6 +1406,69 @@ export default function EditorShell({
|
|
|
1353
1406
|
setShellState({ phase: "ready" });
|
|
1354
1407
|
}, [sections, applySiteConfigPreview, persistence]);
|
|
1355
1408
|
|
|
1409
|
+
// Usage counts are only read by the media modal, and computing them walks every
|
|
1410
|
+
// section (plus the whole siteConfig for images). Gate them on the modal being
|
|
1411
|
+
// open so a keystroke doesn't pay for two deep structural passes.
|
|
1412
|
+
const referenceCountMap = useMemo(
|
|
1413
|
+
() => (showMediaLibrary ? mediaPipeline.computeReferenceCountMap() : EMPTY_COUNTS),
|
|
1414
|
+
[showMediaLibrary, mediaPipeline.computeReferenceCountMap],
|
|
1415
|
+
);
|
|
1416
|
+
const documentReferenceCountMap = useMemo(
|
|
1417
|
+
() => (showMediaLibrary ? mediaPipeline.computeDocumentReferenceCountMap() : EMPTY_COUNTS),
|
|
1418
|
+
[showMediaLibrary, mediaPipeline.computeDocumentReferenceCountMap],
|
|
1419
|
+
);
|
|
1420
|
+
|
|
1421
|
+
// Memoized: rebuilt inline, these two arrays changed identity on every shell
|
|
1422
|
+
// render, so a keystroke in the modal's alt-text field (which is controlled off
|
|
1423
|
+
// mediaManifest) re-rendered the entire media grid.
|
|
1424
|
+
const mediaLibraryItems = useMemo(
|
|
1425
|
+
() => [
|
|
1426
|
+
...Object.values(mediaManifest.images),
|
|
1427
|
+
...mediaPipeline.pendingMediaItems,
|
|
1428
|
+
].filter((item) => !mediaPipeline.pendingDeletions.includes(item.id) && !item.hidden),
|
|
1429
|
+
[mediaManifest.images, mediaPipeline.pendingMediaItems, mediaPipeline.pendingDeletions],
|
|
1430
|
+
);
|
|
1431
|
+
|
|
1432
|
+
const mediaLibraryDocuments = useMemo(
|
|
1433
|
+
() =>
|
|
1434
|
+
Object.entries({
|
|
1435
|
+
...documentManifest,
|
|
1436
|
+
...Object.fromEntries(mediaPipeline.pendingDocuments.map((d) => [d.id, d.entry])),
|
|
1437
|
+
})
|
|
1438
|
+
// Deleted-but-unsaved documents stay in the manifest until the save
|
|
1439
|
+
// commits them; the library must not offer them again.
|
|
1440
|
+
.filter(([id]) => !mediaPipeline.pendingDocumentDeletions.includes(id))
|
|
1441
|
+
.map(([id, entry]) => ({
|
|
1442
|
+
id,
|
|
1443
|
+
filename: entry.filename,
|
|
1444
|
+
size: entry.size,
|
|
1445
|
+
kind: entry.kind,
|
|
1446
|
+
})),
|
|
1447
|
+
[documentManifest, mediaPipeline.pendingDocuments, mediaPipeline.pendingDocumentDeletions],
|
|
1448
|
+
);
|
|
1449
|
+
|
|
1450
|
+
// The two overrides EditorShell layers onto the pipeline's context, hoisted so
|
|
1451
|
+
// they have stable identities.
|
|
1452
|
+
const openSelectModal = useCallback<MediaLibraryContextValue["openSelectModal"]>((onSelect, opts) => {
|
|
1453
|
+
setMediaModalMode("select");
|
|
1454
|
+
setMediaSelectCallback(() => onSelect);
|
|
1455
|
+
setMediaSelectKinds(opts?.kinds ?? null);
|
|
1456
|
+
setShowMediaLibrary(true);
|
|
1457
|
+
}, []);
|
|
1458
|
+
|
|
1459
|
+
const reportRejectedUploads = useCallback<MediaLibraryContextValue["reportRejectedUploads"]>((files) => {
|
|
1460
|
+
setRejectedUploads(files);
|
|
1461
|
+
}, []);
|
|
1462
|
+
|
|
1463
|
+
// Memoized: an inline object literal with two inline arrows defeated
|
|
1464
|
+
// useMediaPipeline's own memoised contextValue entirely. useResolvedMedia and
|
|
1465
|
+
// useResolvedDocument memoise on [id, ctx], so a fresh context per render made
|
|
1466
|
+
// EVERY image on the page re-run provider.resolve() on every keystroke.
|
|
1467
|
+
const mediaLibraryValue = useMemo(
|
|
1468
|
+
() => ({ ...mediaPipeline.contextValue, openSelectModal, reportRejectedUploads }),
|
|
1469
|
+
[mediaPipeline.contextValue, openSelectModal, reportRejectedUploads],
|
|
1470
|
+
);
|
|
1471
|
+
|
|
1356
1472
|
const handleDiscard = useCallback(async () => {
|
|
1357
1473
|
await discardLocalChanges();
|
|
1358
1474
|
setLocalChangesExist(false);
|
|
@@ -1365,6 +1481,12 @@ export default function EditorShell({
|
|
|
1365
1481
|
mediaPipeline.setPendingMediaItems([]);
|
|
1366
1482
|
mediaPipeline.setPendingLocalUrls({});
|
|
1367
1483
|
mediaPipeline.setPendingDeletions([]);
|
|
1484
|
+
// Documents are a separate pipeline with its own two pending states, and
|
|
1485
|
+
// both outlive discard unless cleared here: a doomed committed document
|
|
1486
|
+
// would still be sent as `deletedDocumentIds` by the next save, and a
|
|
1487
|
+
// staged upload would still be committed by it.
|
|
1488
|
+
mediaPipeline.setPendingDocumentDeletions([]);
|
|
1489
|
+
await mediaPipeline.unstageAllDocuments();
|
|
1368
1490
|
const cached = await getCachedContent();
|
|
1369
1491
|
if (cached) {
|
|
1370
1492
|
setSections(cached.sections);
|
|
@@ -1378,7 +1500,12 @@ export default function EditorShell({
|
|
|
1378
1500
|
}
|
|
1379
1501
|
// Discard restores the cached snapshot, so the sidecars must come from that
|
|
1380
1502
|
// same row — anything else could disagree with the sections we just set.
|
|
1381
|
-
if (cached.mediaManifest)
|
|
1503
|
+
if (cached.mediaManifest) {
|
|
1504
|
+
setMediaManifest(cached.mediaManifest);
|
|
1505
|
+
// Discard re-seeds the working copy from this row, so it is also the new
|
|
1506
|
+
// divergence base for rebaseMediaManifest.
|
|
1507
|
+
baseMediaManifestRef.current = cached.mediaManifest;
|
|
1508
|
+
}
|
|
1382
1509
|
if (cached.documentManifest) setDocumentManifest(cached.documentManifest);
|
|
1383
1510
|
}
|
|
1384
1511
|
persistence.clearConfigDirty();
|
|
@@ -1638,12 +1765,37 @@ export default function EditorShell({
|
|
|
1638
1765
|
[persistence],
|
|
1639
1766
|
);
|
|
1640
1767
|
|
|
1768
|
+
// Debounced side effects for Site Settings edits. `setSiteConfig` stays
|
|
1769
|
+
// immediate (the form is controlled off it), but the preview writes nine CSS
|
|
1770
|
+
// custom properties, four synchronous localStorage.setItem calls and two event
|
|
1771
|
+
// dispatches, and persistConfig is a Dexie write — all of which ran on every
|
|
1772
|
+
// keystroke in the site-name field.
|
|
1773
|
+
const configFlushTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
1774
|
+
const pendingConfigRef = useRef<SiteConfig | null>(null);
|
|
1775
|
+
|
|
1776
|
+
const flushSiteConfigSideEffects = useCallback(() => {
|
|
1777
|
+
if (configFlushTimerRef.current) {
|
|
1778
|
+
clearTimeout(configFlushTimerRef.current);
|
|
1779
|
+
configFlushTimerRef.current = null;
|
|
1780
|
+
}
|
|
1781
|
+
const config = pendingConfigRef.current;
|
|
1782
|
+
if (!config) return;
|
|
1783
|
+
pendingConfigRef.current = null;
|
|
1784
|
+
applySiteConfigPreview(config);
|
|
1785
|
+
void persistence.persistConfig(config);
|
|
1786
|
+
}, [applySiteConfigPreview, persistence]);
|
|
1787
|
+
|
|
1788
|
+
// Trailing edge always fires, and unmount flushes — so the last keystroke
|
|
1789
|
+
// before closing Site Settings is never the one that gets dropped.
|
|
1790
|
+
useEffect(() => flushSiteConfigSideEffects, [flushSiteConfigSideEffects]);
|
|
1791
|
+
|
|
1641
1792
|
const handleSiteConfigChange = useCallback((config: SiteConfig) => {
|
|
1642
1793
|
setSiteConfig(config);
|
|
1643
|
-
applySiteConfigPreview(config);
|
|
1644
|
-
persistence.persistConfig(config);
|
|
1645
1794
|
setLocalChangesExist(true);
|
|
1646
|
-
|
|
1795
|
+
pendingConfigRef.current = config;
|
|
1796
|
+
if (configFlushTimerRef.current) clearTimeout(configFlushTimerRef.current);
|
|
1797
|
+
configFlushTimerRef.current = setTimeout(flushSiteConfigSideEffects, CONFIG_PREVIEW_DEBOUNCE_MS);
|
|
1798
|
+
}, [flushSiteConfigSideEffects]);
|
|
1647
1799
|
|
|
1648
1800
|
// Recompose the social-preview raster (siteConfig.previewImage) when brand
|
|
1649
1801
|
// inputs change. Debounced so text edits don't compose per keystroke. The
|
|
@@ -1680,7 +1832,10 @@ export default function EditorShell({
|
|
|
1680
1832
|
const blob = await composePreviewImage(inputs);
|
|
1681
1833
|
const file = new File([blob], "site-preview.webp", { type: "image/webp" });
|
|
1682
1834
|
const { width, height } = servedPreviewSize(siteConfigRef.current!.media.sizes);
|
|
1683
|
-
|
|
1835
|
+
// enqueueFile, not uploadFileWithCallback: the latter mints a blob URL
|
|
1836
|
+
// for an immediate local preview, and this call site discards it — so
|
|
1837
|
+
// it was only ever revoked if the upload happened to succeed.
|
|
1838
|
+
mediaPipeline.enqueueFile(file, (imageId) => {
|
|
1684
1839
|
// Ignore a stale upload: a newer compose has superseded this one.
|
|
1685
1840
|
if (gen !== previewGenRef.current) return;
|
|
1686
1841
|
handleSiteConfigChange({
|
|
@@ -1811,19 +1966,11 @@ export default function EditorShell({
|
|
|
1811
1966
|
<EditorProvider>
|
|
1812
1967
|
<ViewBranchWatcher
|
|
1813
1968
|
setViewSections={setViewSections}
|
|
1969
|
+
onError={setRemoteNotice}
|
|
1814
1970
|
/>
|
|
1815
1971
|
<HistoryWatcher onHistoryActiveChange={setHistoryActive} />
|
|
1816
1972
|
<EditorModalProvider>
|
|
1817
|
-
<MediaLibraryContext.Provider value={
|
|
1818
|
-
...mediaPipeline.contextValue,
|
|
1819
|
-
openSelectModal: (onSelect, opts) => {
|
|
1820
|
-
setMediaModalMode("select");
|
|
1821
|
-
setMediaSelectCallback(() => onSelect);
|
|
1822
|
-
setMediaSelectKinds(opts?.kinds ?? null);
|
|
1823
|
-
setShowMediaLibrary(true);
|
|
1824
|
-
},
|
|
1825
|
-
reportRejectedUploads: (files) => setRejectedUploads(files),
|
|
1826
|
-
}}>
|
|
1973
|
+
<MediaLibraryContext.Provider value={mediaLibraryValue}>
|
|
1827
1974
|
<div className="editor-shell relative">
|
|
1828
1975
|
<EditorToolbar
|
|
1829
1976
|
buttonState={buttonState}
|
|
@@ -2094,23 +2241,8 @@ export default function EditorShell({
|
|
|
2094
2241
|
>
|
|
2095
2242
|
<MediaLibraryModal
|
|
2096
2243
|
mode={mediaModalMode}
|
|
2097
|
-
items={
|
|
2098
|
-
|
|
2099
|
-
...mediaPipeline.pendingMediaItems,
|
|
2100
|
-
].filter((item) => !mediaPipeline.pendingDeletions.includes(item.id) && !item.hidden)}
|
|
2101
|
-
documents={Object.entries({
|
|
2102
|
-
...documentManifest,
|
|
2103
|
-
...Object.fromEntries(mediaPipeline.pendingDocuments.map((d) => [d.id, d.entry])),
|
|
2104
|
-
})
|
|
2105
|
-
// Deleted-but-unsaved documents stay in the manifest until the
|
|
2106
|
-
// save commits them; the library must not offer them again.
|
|
2107
|
-
.filter(([id]) => !mediaPipeline.pendingDocumentDeletions.includes(id))
|
|
2108
|
-
.map(([id, entry]) => ({
|
|
2109
|
-
id,
|
|
2110
|
-
filename: entry.filename,
|
|
2111
|
-
size: entry.size,
|
|
2112
|
-
kind: entry.kind,
|
|
2113
|
-
}))}
|
|
2244
|
+
items={mediaLibraryItems}
|
|
2245
|
+
documents={mediaLibraryDocuments}
|
|
2114
2246
|
kinds={mediaModalMode === "manage" ? undefined : (mediaSelectKinds ?? undefined)}
|
|
2115
2247
|
onSelect={(id) => {
|
|
2116
2248
|
if (mediaSelectCallback) {
|
|
@@ -2124,8 +2256,8 @@ export default function EditorShell({
|
|
|
2124
2256
|
onDelete={mediaPipeline.handleMediaDelete}
|
|
2125
2257
|
onDeleteDocuments={(ids) => { void mediaPipeline.handleDocumentDelete(ids); }}
|
|
2126
2258
|
onAltChange={mediaPipeline.handleMediaAltChange}
|
|
2127
|
-
referenceCountMap={
|
|
2128
|
-
documentReferenceCountMap={
|
|
2259
|
+
referenceCountMap={referenceCountMap}
|
|
2260
|
+
documentReferenceCountMap={documentReferenceCountMap}
|
|
2129
2261
|
localUrlMap={mediaPipeline.pendingLocalUrls}
|
|
2130
2262
|
maxFileSize={siteConfig?.media.maxFileSize}
|
|
2131
2263
|
uploadProgress={mediaPipeline.uploadProgress}
|
|
@@ -2219,8 +2351,10 @@ export default function EditorShell({
|
|
|
2219
2351
|
|
|
2220
2352
|
function ViewBranchWatcher({
|
|
2221
2353
|
setViewSections,
|
|
2354
|
+
onError,
|
|
2222
2355
|
}: {
|
|
2223
2356
|
setViewSections: (sections: LoadedSection[] | null) => void;
|
|
2357
|
+
onError: (message: string) => void;
|
|
2224
2358
|
}) {
|
|
2225
2359
|
const { isEditMode, viewBranch } = useEditorContext();
|
|
2226
2360
|
|
|
@@ -2234,13 +2368,29 @@ function ViewBranchWatcher({
|
|
|
2234
2368
|
return;
|
|
2235
2369
|
}
|
|
2236
2370
|
|
|
2237
|
-
|
|
2238
|
-
|
|
2371
|
+
// Cancelled on switch-away: without this a late resolve re-entered
|
|
2372
|
+
// branch-preview over the draft canvas after the user had already left it.
|
|
2373
|
+
const controller = new AbortController();
|
|
2374
|
+
fetch(`/api/content?branch=main`, { signal: controller.signal })
|
|
2375
|
+
.then((res) => {
|
|
2376
|
+
// A non-JSON 5xx used to parse to `undefined` and render an empty
|
|
2377
|
+
// "Published" view with nothing explaining it.
|
|
2378
|
+
if (!res.ok) throw new Error(`Failed to load the published view: ${res.status}`);
|
|
2379
|
+
return res.json();
|
|
2380
|
+
})
|
|
2239
2381
|
.then((data) => {
|
|
2382
|
+
if (!Array.isArray(data?.sections)) throw new Error("Malformed published-view response");
|
|
2240
2383
|
setViewSections(data.sections);
|
|
2241
2384
|
})
|
|
2242
|
-
.catch(
|
|
2243
|
-
|
|
2385
|
+
.catch((err) => {
|
|
2386
|
+
if (err?.name === "AbortError") return;
|
|
2387
|
+
console.error(err);
|
|
2388
|
+
// Fall back to the in-memory path rather than showing an empty page.
|
|
2389
|
+
setViewSections(null);
|
|
2390
|
+
onError("Couldn't load the published view — showing your current edits.");
|
|
2391
|
+
});
|
|
2392
|
+
return () => controller.abort();
|
|
2393
|
+
}, [viewBranch, isEditMode, setViewSections, onError]);
|
|
2244
2394
|
|
|
2245
2395
|
return null;
|
|
2246
2396
|
}
|
|
@@ -260,6 +260,50 @@ function UploadZone({ onUpload, onFolderUpload }: {
|
|
|
260
260
|
);
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
+
// A local draft, committed on blur. Bound straight to the manifest it fired a
|
|
264
|
+
// manifest write per keystroke, and every one of those re-rendered the whole
|
|
265
|
+
// editor shell — visible typing lag on a large library.
|
|
266
|
+
function AltTextField({ id, alt, onAltChange }: {
|
|
267
|
+
id: string;
|
|
268
|
+
alt: string;
|
|
269
|
+
onAltChange?: (id: string, alt: string) => void;
|
|
270
|
+
}) {
|
|
271
|
+
const [draft, setDraft] = useState(alt);
|
|
272
|
+
const committedRef = useRef(alt);
|
|
273
|
+
|
|
274
|
+
// Adopt an external change (a peer's save, a manifest reload); our own commit
|
|
275
|
+
// comes back through this prop too, hence the comparison against `committed`.
|
|
276
|
+
useEffect(() => {
|
|
277
|
+
if (alt !== committedRef.current) {
|
|
278
|
+
committedRef.current = alt;
|
|
279
|
+
setDraft(alt);
|
|
280
|
+
}
|
|
281
|
+
}, [alt]);
|
|
282
|
+
|
|
283
|
+
const commit = () => {
|
|
284
|
+
if (draft === committedRef.current) return;
|
|
285
|
+
committedRef.current = draft;
|
|
286
|
+
onAltChange?.(id, draft);
|
|
287
|
+
};
|
|
288
|
+
// Closing the modal unmounts the field without a blur; without this the last
|
|
289
|
+
// edit would be silently dropped.
|
|
290
|
+
const commitRef = useRef(commit);
|
|
291
|
+
commitRef.current = commit;
|
|
292
|
+
useEffect(() => () => commitRef.current(), []);
|
|
293
|
+
|
|
294
|
+
return (
|
|
295
|
+
<textarea
|
|
296
|
+
value={draft}
|
|
297
|
+
placeholder="Describe this image for accessibility"
|
|
298
|
+
rows={2}
|
|
299
|
+
onClick={(e) => e.stopPropagation()}
|
|
300
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
301
|
+
onBlur={commit}
|
|
302
|
+
className="w-full resize-none border-0 bg-transparent p-0 text-xs leading-snug text-base-contrast placeholder:text-base-contrast-light/40 focus:outline-none focus:ring-0"
|
|
303
|
+
/>
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
263
307
|
function MediaCard({ item, manage, isSelected, dragIds, onClick, children }: {
|
|
264
308
|
item: MediaItem; manage: boolean; isSelected: boolean;
|
|
265
309
|
dragIds: string[]; onClick: (e: React.MouseEvent) => void; children: React.ReactNode;
|
|
@@ -528,7 +572,10 @@ export function MediaLibraryModal({
|
|
|
528
572
|
}
|
|
529
573
|
|
|
530
574
|
function handleBatchDelete() {
|
|
531
|
-
|
|
575
|
+
// Confirm for anything in use, and for any multi-selection: an unused
|
|
576
|
+
// multi-delete went straight through, so one mis-aimed click could take out
|
|
577
|
+
// a whole rubber-banded range with nothing to stop it.
|
|
578
|
+
if (totalUsage > 0 || selectedIds.length > 1) {
|
|
532
579
|
setConfirmDelete(true);
|
|
533
580
|
setFolderToDelete(null); // the two delete confirmations are mutually exclusive
|
|
534
581
|
return;
|
|
@@ -723,7 +770,9 @@ export function MediaLibraryModal({
|
|
|
723
770
|
{confirmDelete && (
|
|
724
771
|
<div className="flex flex-col items-center rounded-lg border border-red-200 bg-red-50 px-4 py-3 lg:flex-row lg:justify-between dark:border-red-900/50 dark:bg-red-950/30">
|
|
725
772
|
<p className="mb-2 text-center text-sm text-red-800 lg:mb-0 lg:text-left dark:text-red-200">
|
|
726
|
-
|
|
773
|
+
{totalUsage > 0
|
|
774
|
+
? `Some of these ${usedNoun} are being used. Deleting them will remove them from the sections that use them.`
|
|
775
|
+
: `Delete ${selectedIds.length} ${usedNoun}? No section uses them.`}
|
|
727
776
|
</p>
|
|
728
777
|
<div className="flex shrink-0 items-center gap-2">
|
|
729
778
|
<Button variant="secondary" size="sm" onClick={() => setConfirmDelete(false)}>
|
|
@@ -982,14 +1031,7 @@ export function MediaLibraryModal({
|
|
|
982
1031
|
<label className="text-[10px] font-medium uppercase leading-none tracking-wide text-base-contrast-light/50">
|
|
983
1032
|
Alt text
|
|
984
1033
|
</label>
|
|
985
|
-
<
|
|
986
|
-
value={item.alt}
|
|
987
|
-
placeholder="Describe this image for accessibility"
|
|
988
|
-
rows={2}
|
|
989
|
-
onClick={(e) => e.stopPropagation()}
|
|
990
|
-
onChange={(e) => onAltChange?.(item.id, e.target.value)}
|
|
991
|
-
className="w-full resize-none border-0 bg-transparent p-0 text-xs leading-snug text-base-contrast placeholder:text-base-contrast-light/40 focus:outline-none focus:ring-0"
|
|
992
|
-
/>
|
|
1034
|
+
<AltTextField id={item.id} alt={item.alt} onAltChange={onAltChange} />
|
|
993
1035
|
</div>
|
|
994
1036
|
</MediaCard>
|
|
995
1037
|
);
|
|
@@ -32,6 +32,9 @@ export function SiteSettingsUsers({ currentUser }: Props) {
|
|
|
32
32
|
}
|
|
33
33
|
const data = await res.json();
|
|
34
34
|
setUsers(data.users);
|
|
35
|
+
// A successful reload has to clear the previous failure, or a transient
|
|
36
|
+
// network blip leaves its message on screen for the rest of the session.
|
|
37
|
+
setError(null);
|
|
35
38
|
} catch {
|
|
36
39
|
setError("Network error while loading users");
|
|
37
40
|
setUsers([]);
|