@drawnagency/primitives 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/dist/auth/index.js +26 -3
  2. package/dist/chunk-2VTPWODA.js +60 -0
  3. package/dist/chunk-CS7F6IOY.js +39 -0
  4. package/dist/chunk-HOJAF4VD.js +264 -0
  5. package/dist/chunk-IP6ODLXX.js +341 -0
  6. package/dist/chunk-T4BJ6RSB.js +58 -0
  7. package/dist/chunk-UKEVUCIZ.js +200 -0
  8. package/dist/chunk-UMSFICAC.js +36 -0
  9. package/dist/index.js +156 -4
  10. package/dist/lib/index.js +62 -12
  11. package/dist/lib/sanitize.d.ts.map +1 -1
  12. package/dist/media/index.js +36 -9
  13. package/dist/schemas/index.js +52 -7
  14. package/package.json +4 -4
  15. package/src/lib/sanitize.ts +6 -2
  16. package/dist/auth/cookies.js +0 -44
  17. package/dist/auth/errors.js +0 -10
  18. package/dist/auth/security.js +0 -48
  19. package/dist/auth/types.js +0 -1
  20. package/dist/components/brandguide/ColorSwatchSettings.js +0 -10
  21. package/dist/components/brandguide/Colors.js +0 -79
  22. package/dist/components/brandguide/DoDontList.js +0 -22
  23. package/dist/components/brandguide/DoDontMediaGrid.js +0 -5
  24. package/dist/components/editor/AudiencePicker.js +0 -24
  25. package/dist/components/editor/DeleteButton.js +0 -6
  26. package/dist/components/editor/DragHandle.js +0 -8
  27. package/dist/components/editor/InsertButton.js +0 -7
  28. package/dist/components/editor/SectionWrapper.js +0 -135
  29. package/dist/components/editor/SettingsButton.js +0 -6
  30. package/dist/components/editor/SettingsForm.js +0 -64
  31. package/dist/components/editor/StatusBadge.js +0 -10
  32. package/dist/components/editor/StatusPicker.js +0 -30
  33. package/dist/components/editor/index.js +0 -7
  34. package/dist/components/primitives/CustomParagraph.js +0 -24
  35. package/dist/components/primitives/EditableGrid.js +0 -90
  36. package/dist/components/primitives/EditableList.js +0 -54
  37. package/dist/components/primitives/EditablePlainText.js +0 -52
  38. package/dist/components/primitives/EditableRichText.js +0 -86
  39. package/dist/components/primitives/HeadingSection.js +0 -7
  40. package/dist/components/primitives/IconPicker.js +0 -21
  41. package/dist/components/primitives/LinkPopover.js +0 -48
  42. package/dist/components/primitives/MediaSettingsForms.js +0 -42
  43. package/dist/components/primitives/ResolvedMedia.js +0 -9
  44. package/dist/components/primitives/RichTextToolbar.js +0 -26
  45. package/dist/components/primitives/tiptap-presets.js +0 -44
  46. package/dist/components/primitives/useEditableCollection.js +0 -61
  47. package/dist/components/primitives/useEditablePlainText.js +0 -27
  48. package/dist/components/primitives/useEditableRichText.js +0 -52
  49. package/dist/components/sections/Button/CTAButton.js +0 -18
  50. package/dist/components/sections/Button/index.js +0 -28
  51. package/dist/components/sections/Colors/index.js +0 -34
  52. package/dist/components/sections/DoDontList/index.js +0 -33
  53. package/dist/components/sections/DoDontMediaGrid/index.js +0 -41
  54. package/dist/components/sections/IconList/IconList.js +0 -131
  55. package/dist/components/sections/IconList/IconListSettings.js +0 -22
  56. package/dist/components/sections/IconList/index.js +0 -27
  57. package/dist/components/sections/LinkHeading/index.js +0 -15
  58. package/dist/components/sections/MediaGrid/MediaGrid.js +0 -62
  59. package/dist/components/sections/MediaGrid/index.js +0 -35
  60. package/dist/components/sections/Prose/Prose.js +0 -11
  61. package/dist/components/sections/Prose/index.js +0 -15
  62. package/dist/components/sections/SectionLayout.js +0 -15
  63. package/dist/components/sections/SplitContent/SplitContent.js +0 -31
  64. package/dist/components/sections/SplitContent/SplitContentSettings.js +0 -17
  65. package/dist/components/sections/SplitContent/index.js +0 -27
  66. package/dist/components/sections/SubHeading/index.js +0 -18
  67. package/dist/components/sections/SubSubHeading/index.js +0 -18
  68. package/dist/components/sections/ViewRenderer.js +0 -13
  69. package/dist/components/sections/register-schemas.js +0 -15
  70. package/dist/components/sections/register.js +0 -15
  71. package/dist/components/shared/Button.js +0 -27
  72. package/dist/components/shared/Checkbox.js +0 -10
  73. package/dist/components/shared/ColorPicker.js +0 -5
  74. package/dist/components/shared/ErrorBoundary.js +0 -30
  75. package/dist/components/shared/FontPicker.js +0 -190
  76. package/dist/components/shared/FormLabel.js +0 -5
  77. package/dist/components/shared/IconButton.js +0 -16
  78. package/dist/components/shared/Input.js +0 -8
  79. package/dist/components/shared/Navigation.js +0 -71
  80. package/dist/components/shared/PasswordInput.js +0 -11
  81. package/dist/components/shared/Popover.js +0 -33
  82. package/dist/components/shared/PopoverItem.js +0 -6
  83. package/dist/components/shared/Select.js +0 -9
  84. package/dist/components/shared/Textarea.js +0 -8
  85. package/dist/components/shared/Toggle.js +0 -5
  86. package/dist/components/shared/Tooltip.js +0 -8
  87. package/dist/components/shared/icons.js +0 -23
  88. package/dist/components/shell/AudienceAddForm.js +0 -43
  89. package/dist/components/shell/AudienceRow.js +0 -74
  90. package/dist/components/shell/EditorContext.js +0 -24
  91. package/dist/components/shell/EditorLoginForm.js +0 -46
  92. package/dist/components/shell/EditorModal.js +0 -43
  93. package/dist/components/shell/EditorModalContext.js +0 -20
  94. package/dist/components/shell/EditorShell.js +0 -483
  95. package/dist/components/shell/MediaLibraryContext.js +0 -5
  96. package/dist/components/shell/MediaLibraryModal.js +0 -145
  97. package/dist/components/shell/ProcessingIndicator.js +0 -15
  98. package/dist/components/shell/SectionSkeleton.js +0 -22
  99. package/dist/components/shell/SectionTypePicker.js +0 -15
  100. package/dist/components/shell/SiteSettingsDisplay.js +0 -28
  101. package/dist/components/shell/SiteSettingsModal.js +0 -40
  102. package/dist/components/shell/SiteSettingsUsers.js +0 -87
  103. package/dist/components/shell/SiteSettingsViewerAccess.js +0 -94
  104. package/dist/components/shell/ViewerLoginForm.js +0 -40
  105. package/dist/data/google-fonts.json +0 -7718
  106. package/dist/hooks/index.js +0 -6
  107. package/dist/hooks/useActiveHeadings.js +0 -99
  108. package/dist/hooks/useEditorPersistence.js +0 -73
  109. package/dist/hooks/useEditorPublish.js +0 -145
  110. package/dist/hooks/useFocusTrap.js +0 -51
  111. package/dist/hooks/useMediaPipeline.js +0 -253
  112. package/dist/hooks/useResolvedMedia.js +0 -39
  113. package/dist/lib/cn.js +0 -5
  114. package/dist/lib/contrast.js +0 -11
  115. package/dist/lib/dexie.js +0 -236
  116. package/dist/lib/events.js +0 -15
  117. package/dist/lib/google-fonts.js +0 -11
  118. package/dist/lib/grid.js +0 -7
  119. package/dist/lib/icons.js +0 -27
  120. package/dist/lib/loader.js +0 -57
  121. package/dist/lib/nav.js +0 -58
  122. package/dist/lib/registry.js +0 -64
  123. package/dist/lib/safeRedirect.js +0 -11
  124. package/dist/lib/sanitize.js +0 -6
  125. package/dist/lib/timestamp.js +0 -28
  126. package/dist/media/github.js +0 -60
  127. package/dist/media/queue.js +0 -116
  128. package/dist/media/resolve.js +0 -50
  129. package/dist/media/types.js +0 -1
  130. package/dist/media/utils.js +0 -41
  131. package/dist/media/videoPoster.js +0 -44
  132. package/dist/media/worker.js +0 -73
  133. package/dist/schemas/audience.js +0 -19
  134. package/dist/schemas/auth.js +0 -22
  135. package/dist/schemas/media-grid-options.js +0 -7
  136. package/dist/schemas/media.js +0 -28
  137. package/dist/schemas/sections.js +0 -12
  138. package/dist/schemas/shared.js +0 -71
  139. package/dist/schemas/site-config.js +0 -26
@@ -1,6 +0,0 @@
1
- export { useActiveHeadings } from "./useActiveHeadings";
2
- export { useEditorPersistence } from "./useEditorPersistence";
3
- export { useEditorPublish } from "./useEditorPublish";
4
- export { useFocusTrap } from "./useFocusTrap";
5
- export { useMediaPipeline } from "./useMediaPipeline";
6
- export { useResolvedMedia } from "./useResolvedMedia";
@@ -1,99 +0,0 @@
1
- import { useEffect, useMemo, useRef, useState, useCallback } from "react";
2
- export function useActiveHeadings(parentIds, childIdsByParent, grandchildIdsByChild, headerOffset = 80) {
3
- const [activeParentId, setActiveParentId] = useState("");
4
- const [activeChildId, setActiveChildId] = useState("");
5
- const [activeGrandchildId, setActiveGrandchildId] = useState("");
6
- const childKey = useMemo(() => Object.entries(childIdsByParent).map(([k, v]) => `${k}:${v.join(",")}`).join("|"), [childIdsByParent]);
7
- const grandchildKey = useMemo(() => Object.entries(grandchildIdsByChild).map(([k, v]) => `${k}:${v.join(",")}`).join("|"), [grandchildIdsByChild]);
8
- // Use refs for comparison inside the effect to avoid re-render loops
9
- const activeParentRef = useRef("");
10
- const activeChildRef = useRef("");
11
- const activeGrandchildRef = useRef("");
12
- const ticking = useRef(false);
13
- const transitionTimeoutRef = useRef(null);
14
- useEffect(() => {
15
- if (parentIds.length === 0)
16
- return;
17
- let parentEls = [];
18
- const collect = () => {
19
- parentEls = parentIds
20
- .map((id) => ({ id, el: document.getElementById(id) }))
21
- .filter((x) => !!x.el);
22
- };
23
- collect();
24
- const findActive = (ids) => {
25
- let active = "";
26
- for (const id of ids) {
27
- const el = document.getElementById(id);
28
- if (!el)
29
- continue;
30
- if (el.getBoundingClientRect().top - headerOffset <= 0)
31
- active = id;
32
- else
33
- break;
34
- }
35
- return active;
36
- };
37
- const computeActive = () => {
38
- const parent = findActive(parentIds) || parentEls[0]?.id || "";
39
- if (!parent)
40
- return;
41
- if (activeParentRef.current !== parent) {
42
- if (transitionTimeoutRef.current)
43
- clearTimeout(transitionTimeoutRef.current);
44
- if (activeParentRef.current) {
45
- // Delay parent transitions to prevent flashing
46
- transitionTimeoutRef.current = setTimeout(() => {
47
- activeParentRef.current = parent;
48
- setActiveParentId(parent);
49
- }, 250);
50
- }
51
- else {
52
- activeParentRef.current = parent;
53
- setActiveParentId(parent);
54
- }
55
- }
56
- const child = findActive(childIdsByParent[parent] || []);
57
- if (activeChildRef.current !== child) {
58
- activeChildRef.current = child;
59
- setActiveChildId(child);
60
- }
61
- const grandchild = findActive(grandchildIdsByChild[child] || []);
62
- if (activeGrandchildRef.current !== grandchild) {
63
- activeGrandchildRef.current = grandchild;
64
- setActiveGrandchildId(grandchild);
65
- }
66
- };
67
- const onScroll = () => {
68
- if (!ticking.current) {
69
- ticking.current = true;
70
- requestAnimationFrame(() => {
71
- computeActive();
72
- ticking.current = false;
73
- });
74
- }
75
- };
76
- const onResize = () => {
77
- collect();
78
- computeActive();
79
- };
80
- window.addEventListener("scroll", onScroll, { passive: true });
81
- window.addEventListener("resize", onResize);
82
- // Initial compute
83
- computeActive();
84
- return () => {
85
- window.removeEventListener("scroll", onScroll);
86
- window.removeEventListener("resize", onResize);
87
- if (transitionTimeoutRef.current)
88
- clearTimeout(transitionTimeoutRef.current);
89
- };
90
- // Only stable values in dep array — NO state variables
91
- }, [parentIds.join("|"), childKey, grandchildKey, headerOffset]);
92
- const setActiveSection = useCallback((id) => {
93
- const newHash = `#${id}`;
94
- if (window.location.hash !== newHash) {
95
- window.history.replaceState(null, "", newHash);
96
- }
97
- }, []);
98
- return { activeParentId, activeChildId, activeGrandchildId, setActiveSection };
99
- }
@@ -1,73 +0,0 @@
1
- import { useCallback, useRef, useEffect } from "react";
2
- import { persistAll, persistSiteConfig } from "../lib/dexie";
3
- export function useEditorPersistence(siteIndexRef) {
4
- const state = useRef({
5
- pendingSections: new Map(),
6
- pendingDeletes: new Set(),
7
- indexDirty: false,
8
- configDirty: false,
9
- });
10
- const flushTimerRef = useRef(null);
11
- const flushToDexie = useCallback(async () => {
12
- const s = state.current;
13
- if (s.pendingSections.size === 0 && !s.indexDirty && s.pendingDeletes.size === 0)
14
- return;
15
- const entries = Array.from(s.pendingSections.entries()).map(([sectionId, content]) => ({
16
- sectionId,
17
- content,
18
- }));
19
- const deletedIds = Array.from(s.pendingDeletes);
20
- s.pendingSections = new Map();
21
- s.pendingDeletes = new Set();
22
- const wasIndexDirty = s.indexDirty;
23
- s.indexDirty = false;
24
- await persistAll(entries, wasIndexDirty ? siteIndexRef.current : undefined, deletedIds.length > 0 ? deletedIds : undefined);
25
- }, [siteIndexRef]);
26
- const scheduleFlush = useCallback(() => {
27
- if (flushTimerRef.current)
28
- clearTimeout(flushTimerRef.current);
29
- flushTimerRef.current = setTimeout(flushToDexie, 500);
30
- }, [flushToDexie]);
31
- useEffect(() => {
32
- return () => {
33
- if (flushTimerRef.current)
34
- clearTimeout(flushTimerRef.current);
35
- };
36
- }, []);
37
- const markSectionDirty = useCallback((sectionId, content) => {
38
- state.current.pendingSections.set(sectionId, content);
39
- scheduleFlush();
40
- }, [scheduleFlush]);
41
- const markSectionDeleted = useCallback((sectionId) => {
42
- state.current.pendingSections.delete(sectionId);
43
- state.current.pendingDeletes.add(sectionId);
44
- state.current.indexDirty = true;
45
- scheduleFlush();
46
- }, [scheduleFlush]);
47
- const markIndexDirty = useCallback(() => {
48
- state.current.indexDirty = true;
49
- scheduleFlush();
50
- }, [scheduleFlush]);
51
- const persistConfig = useCallback(async (config) => {
52
- state.current.configDirty = true;
53
- await persistSiteConfig(config);
54
- }, []);
55
- const cancelPendingFlush = useCallback(() => {
56
- if (flushTimerRef.current) {
57
- clearTimeout(flushTimerRef.current);
58
- flushTimerRef.current = null;
59
- }
60
- }, []);
61
- const isConfigDirty = useCallback(() => state.current.configDirty, []);
62
- const clearConfigDirty = useCallback(() => { state.current.configDirty = false; }, []);
63
- return {
64
- markSectionDirty,
65
- markSectionDeleted,
66
- markIndexDirty,
67
- persistConfig,
68
- flushNow: flushToDexie,
69
- cancelPendingFlush,
70
- isConfigDirty,
71
- clearConfigDirty,
72
- };
73
- }
@@ -1,145 +0,0 @@
1
- import { useState, useCallback, useRef, useEffect } from "react";
2
- import { getDirtySections, hasLocalChanges, discardLocalChanges, cacheContent, getPendingMediaLocalUrls, clearPendingMedia } from "../lib/dexie";
3
- function blobToBase64(blob) {
4
- return new Promise((resolve, reject) => {
5
- const reader = new FileReader();
6
- reader.onload = () => {
7
- const dataUrl = reader.result;
8
- resolve(dataUrl.split(",")[1]);
9
- };
10
- reader.onerror = reject;
11
- reader.readAsDataURL(blob);
12
- });
13
- }
14
- export function useEditorPublish({ flushNow, cancelPendingFlush, isConfigDirty, clearConfigDirty, siteIndexRef, siteConfig, sections, onSuccess, mediaManifest, pendingMediaItems, pendingMediaDeletions, onMediaPublished, }) {
15
- const [isPublishing, setIsPublishing] = useState(false);
16
- const [publishFeedback, setPublishFeedback] = useState(null);
17
- const feedbackTimerRef = useRef(null);
18
- useEffect(() => {
19
- return () => {
20
- if (feedbackTimerRef.current)
21
- clearTimeout(feedbackTimerRef.current);
22
- };
23
- }, []);
24
- const handlePublish = useCallback(async () => {
25
- if (!siteConfig)
26
- return;
27
- setIsPublishing(true);
28
- setPublishFeedback(null);
29
- try {
30
- cancelPendingFlush();
31
- await flushNow();
32
- const hasChanges = await hasLocalChanges();
33
- const hasMediaChanges = pendingMediaItems.length > 0 || pendingMediaDeletions.length > 0;
34
- if (!hasChanges && !isConfigDirty() && !hasMediaChanges) {
35
- setIsPublishing(false);
36
- return;
37
- }
38
- const dirty = await getDirtySections();
39
- // Gather pending media for the commit
40
- const mediaUploads = [];
41
- const blobUrlsToRevoke = [];
42
- for (const item of pendingMediaItems) {
43
- const localUrls = await getPendingMediaLocalUrls(item.id);
44
- if (!localUrls)
45
- continue;
46
- for (const url of Object.values(localUrls)) {
47
- blobUrlsToRevoke.push(url);
48
- }
49
- const blobs = [];
50
- const failedBlobFetches = [];
51
- const mimeExt = {
52
- "image/gif": "gif", "image/apng": "apng", "video/mp4": "mp4", "video/webm": "webm",
53
- };
54
- for (const [key, url] of Object.entries(localUrls)) {
55
- if (key === "primary" && item.kind === "image")
56
- continue;
57
- try {
58
- const resp = await fetch(url);
59
- const blob = await resp.blob();
60
- const base64 = await blobToBase64(blob);
61
- if (key === "primary") {
62
- const ext = mimeExt[item.mimeType] ?? "bin";
63
- blobs.push({ path: `assets/images/${item.folder}/original.${ext}`, base64 });
64
- }
65
- else {
66
- blobs.push({ path: `assets/images/${item.folder}/${key}.webp`, base64 });
67
- }
68
- }
69
- catch {
70
- failedBlobFetches.push(`${item.id}/${key}`);
71
- }
72
- }
73
- if (failedBlobFetches.length > 0) {
74
- throw new Error(`Media upload failed: could not read blob data for ${failedBlobFetches.join(", ")}`);
75
- }
76
- if (blobs.length > 0) {
77
- mediaUploads.push({ item, blobs });
78
- }
79
- }
80
- // Build updated manifest
81
- let updatedManifest;
82
- if (hasMediaChanges) {
83
- const images = { ...mediaManifest.images };
84
- for (const upload of mediaUploads) {
85
- images[upload.item.id] = upload.item;
86
- }
87
- for (const id of pendingMediaDeletions) {
88
- delete images[id];
89
- }
90
- updatedManifest = { images };
91
- }
92
- const response = await fetch("/api/save", {
93
- method: "POST",
94
- headers: { "Content-Type": "application/json" },
95
- body: JSON.stringify({
96
- sections: dirty.map(({ sectionId, content }) => ({
97
- id: sectionId,
98
- content,
99
- })),
100
- siteIndex: siteIndexRef.current,
101
- ...(isConfigDirty() ? { siteConfig } : {}),
102
- ...(hasMediaChanges ? {
103
- media: {
104
- uploads: mediaUploads.map(({ item, blobs }) => ({ item, blobs })),
105
- deletions: pendingMediaDeletions.map((id) => ({
106
- id,
107
- folder: mediaManifest.images[id]?.folder,
108
- })).filter((d) => d.folder),
109
- manifest: updatedManifest,
110
- },
111
- } : {}),
112
- }),
113
- });
114
- if (!response.ok) {
115
- const errorBody = await response.json().catch(() => ({}));
116
- throw new Error(errorBody.error || "Publish failed");
117
- }
118
- const { sha } = await response.json();
119
- await discardLocalChanges();
120
- await clearPendingMedia();
121
- await cacheContent(sha, sections, siteIndexRef.current, siteConfig);
122
- clearConfigDirty();
123
- onSuccess();
124
- onMediaPublished(pendingMediaItems, pendingMediaDeletions);
125
- for (const url of blobUrlsToRevoke) {
126
- URL.revokeObjectURL(url);
127
- }
128
- setPublishFeedback("Published");
129
- if (feedbackTimerRef.current)
130
- clearTimeout(feedbackTimerRef.current);
131
- feedbackTimerRef.current = setTimeout(() => setPublishFeedback(null), 3000);
132
- }
133
- catch (error) {
134
- console.error("Publish failed:", error);
135
- setPublishFeedback("Publish failed");
136
- if (feedbackTimerRef.current)
137
- clearTimeout(feedbackTimerRef.current);
138
- feedbackTimerRef.current = setTimeout(() => setPublishFeedback(null), 5000);
139
- }
140
- finally {
141
- setIsPublishing(false);
142
- }
143
- }, [flushNow, cancelPendingFlush, isConfigDirty, clearConfigDirty, siteIndexRef, siteConfig, sections, onSuccess, mediaManifest, pendingMediaItems, pendingMediaDeletions, onMediaPublished]);
144
- return { isPublishing, publishFeedback, handlePublish };
145
- }
@@ -1,51 +0,0 @@
1
- import { useEffect } from "react";
2
- const FOCUSABLE_SELECTOR = [
3
- "a[href]",
4
- "button:not([disabled])",
5
- "input:not([disabled])",
6
- "select:not([disabled])",
7
- "textarea:not([disabled])",
8
- '[tabindex]:not([tabindex="-1"])',
9
- ].join(", ");
10
- export function useFocusTrap(ref, active) {
11
- useEffect(() => {
12
- if (!active || !ref.current)
13
- return;
14
- const container = ref.current;
15
- const previouslyFocused = document.activeElement;
16
- const getFocusable = () => Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR));
17
- // Focus first element on activation
18
- const focusable = getFocusable();
19
- if (focusable.length > 0) {
20
- focusable[0].focus();
21
- }
22
- const handleKeyDown = (e) => {
23
- if (e.key !== "Tab")
24
- return;
25
- const elements = getFocusable();
26
- if (elements.length === 0)
27
- return;
28
- const first = elements[0];
29
- const last = elements[elements.length - 1];
30
- if (e.shiftKey) {
31
- if (document.activeElement === first) {
32
- e.preventDefault();
33
- last.focus();
34
- }
35
- }
36
- else {
37
- if (document.activeElement === last) {
38
- e.preventDefault();
39
- first.focus();
40
- }
41
- }
42
- };
43
- container.addEventListener("keydown", handleKeyDown);
44
- return () => {
45
- container.removeEventListener("keydown", handleKeyDown);
46
- if (previouslyFocused && document.body.contains(previouslyFocused)) {
47
- previouslyFocused.focus();
48
- }
49
- };
50
- }, [active, ref]);
51
- }
@@ -1,253 +0,0 @@
1
- import { useState, useCallback, useEffect, useRef, useMemo } from "react";
2
- import { hashFileBuffer, sanitizeMediaName } from "../media/utils";
3
- import { ProcessingQueue } from "../media/queue";
4
- import { addPendingMediaItem, markPendingMediaDeleted, } from "../lib/dexie";
5
- import { generateVideoPoster } from "../media/videoPoster";
6
- function clearImageIds(obj, ids) {
7
- if (Array.isArray(obj))
8
- return obj.map((item) => clearImageIds(item, ids));
9
- if (obj !== null && typeof obj === "object") {
10
- const record = obj;
11
- const result = {};
12
- for (const [key, value] of Object.entries(record)) {
13
- if (key === "imageId" && typeof value === "string" && ids.has(value)) {
14
- result[key] = "";
15
- }
16
- else {
17
- result[key] = clearImageIds(value, ids);
18
- }
19
- }
20
- return result;
21
- }
22
- return obj;
23
- }
24
- export function useMediaPipeline({ siteConfig, mediaManifest, setMediaManifest, sections, setSections, setLocalChangesExist, setDirtySectionIds, markSectionDirty, }) {
25
- const [processingItems, setProcessingItems] = useState([]);
26
- const [pendingMediaItems, setPendingMediaItems] = useState([]);
27
- const [pendingLocalUrls, setPendingLocalUrls] = useState({});
28
- const [pendingDeletions, setPendingDeletions] = useState([]);
29
- const queueRef = useRef(null);
30
- const uploadCallbacksRef = useRef(new Map());
31
- // --- Processing queue ---
32
- const mediaConfigKey = siteConfig
33
- ? `${siteConfig.media.sizes.join(",")}_${siteConfig.media.quality}_${siteConfig.media.maxFileSize}`
34
- : "";
35
- useEffect(() => {
36
- if (!siteConfig)
37
- return;
38
- const mediaConfig = siteConfig.media;
39
- const queue = new ProcessingQueue({
40
- sizes: mediaConfig.sizes,
41
- quality: mediaConfig.quality,
42
- maxConcurrent: 3,
43
- createWorker: () => new Worker(new URL("../media/worker.ts", import.meta.url), { type: "module" }),
44
- onEvent: (event) => {
45
- setProcessingItems((prev) => {
46
- const next = prev.filter((i) => i.id !== event.item.id);
47
- if (event.item.state !== "complete")
48
- next.push(event.item);
49
- return next;
50
- });
51
- if (event.type === "complete" && event.item.result) {
52
- const result = event.item.result;
53
- const kind = event.item.kind;
54
- const sanitizedName = sanitizeMediaName(event.item.originalName);
55
- const finalize = (localUrls, width, height) => {
56
- const item = {
57
- id: event.item.hash,
58
- hash: event.item.hash,
59
- kind,
60
- originalName: sanitizedName,
61
- width,
62
- height,
63
- mimeType: kind === "image" ? "image/webp" : event.item.mimeType,
64
- size: result.primaryBlob.size,
65
- folder: `${sanitizedName}-${event.item.hash}`,
66
- variants: result.variants.map((v) => ({
67
- width: v.width,
68
- height: v.height,
69
- size: v.size,
70
- })),
71
- alt: "",
72
- };
73
- addPendingMediaItem(item, localUrls);
74
- setPendingMediaItems((prev) => [...prev, item]);
75
- const displayKey = kind === "video"
76
- ? "primary"
77
- : Object.keys(localUrls).find((k) => k !== "primary" && k !== "poster") ?? "primary";
78
- if (localUrls[displayKey]) {
79
- setPendingLocalUrls((prev) => ({ ...prev, [item.id]: localUrls[displayKey] }));
80
- }
81
- setLocalChangesExist(true);
82
- const callback = uploadCallbacksRef.current.get(event.item.hash);
83
- if (callback) {
84
- callback(event.item.hash);
85
- uploadCallbacksRef.current.delete(event.item.hash);
86
- }
87
- };
88
- const localUrls = {};
89
- for (const v of result.variants) {
90
- localUrls[String(v.width)] = URL.createObjectURL(v.blob);
91
- }
92
- if (result.primaryBlob) {
93
- localUrls["primary"] = URL.createObjectURL(result.primaryBlob);
94
- }
95
- if (kind === "video") {
96
- generateVideoPoster(result.primaryBlob, mediaConfig.quality).then(({ posterBlob, width, height }) => {
97
- localUrls["poster"] = URL.createObjectURL(posterBlob);
98
- finalize(localUrls, width, height);
99
- }, () => {
100
- finalize(localUrls, result.width, result.height);
101
- });
102
- }
103
- else {
104
- if (result.posterBlob) {
105
- localUrls["poster"] = URL.createObjectURL(result.posterBlob);
106
- }
107
- finalize(localUrls, result.width, result.height);
108
- }
109
- }
110
- },
111
- });
112
- queueRef.current = queue;
113
- return () => queue.destroy();
114
- // eslint-disable-next-line react-hooks/exhaustive-deps -- rebuild only when processing params change, not on every siteConfig reference change
115
- }, [mediaConfigKey, setLocalChangesExist]);
116
- // --- Media handlers ---
117
- const handleMediaUpload = useCallback(async (files) => {
118
- if (!siteConfig)
119
- return;
120
- const mediaConfig = siteConfig.media;
121
- const queue = queueRef.current;
122
- if (!queue)
123
- return;
124
- for (const file of files) {
125
- if (file.size > mediaConfig.maxFileSize)
126
- continue;
127
- const buffer = await file.arrayBuffer();
128
- const hash = await hashFileBuffer(buffer);
129
- const existsInManifest = hash in mediaManifest.images;
130
- const existsInPending = pendingMediaItems.some((i) => i.hash === hash);
131
- if (existsInManifest || existsInPending)
132
- continue;
133
- let kind = "image";
134
- if (file.type === "image/gif" || file.type === "image/apng")
135
- kind = "animated";
136
- if (file.type.startsWith("video/"))
137
- kind = "video";
138
- queue.add({ buffer, originalName: file.name, mimeType: file.type, hash, kind });
139
- }
140
- }, [siteConfig, mediaManifest, pendingMediaItems]);
141
- const handleMediaDelete = useCallback(async (ids) => {
142
- for (const id of ids) {
143
- await markPendingMediaDeleted(id);
144
- }
145
- setPendingDeletions((prev) => [...prev, ...ids]);
146
- const idSet = new Set(ids);
147
- setSections((prev) => prev.map((loaded) => {
148
- const json = JSON.stringify(loaded.section);
149
- const hasRef = ids.some((id) => json.includes(`"${id}"`));
150
- if (!hasRef)
151
- return loaded;
152
- const cleared = clearImageIds(loaded.section, idSet);
153
- markSectionDirty(loaded.section.id, cleared);
154
- setDirtySectionIds((prev) => new Set(prev).add(loaded.section.id));
155
- return { ...loaded, section: { id: loaded.section.id, ...cleared } };
156
- }));
157
- setLocalChangesExist(true);
158
- }, [markSectionDirty, setSections, setDirtySectionIds, setLocalChangesExist]);
159
- const handleMediaAltChange = useCallback((id, alt) => {
160
- setMediaManifest((prev) => {
161
- const item = prev.images[id];
162
- if (!item)
163
- return prev;
164
- return { images: { ...prev.images, [id]: { ...item, alt } } };
165
- });
166
- setPendingMediaItems((prev) => prev.map((item) => (item.id === id ? { ...item, alt } : item)));
167
- setLocalChangesExist(true);
168
- }, [setMediaManifest, setLocalChangesExist]);
169
- const referenceCountMap = useMemo(() => {
170
- const counts = {};
171
- const allIds = [
172
- ...Object.keys(mediaManifest.images),
173
- ...pendingMediaItems.map((i) => i.id),
174
- ];
175
- for (const { section } of sections) {
176
- const json = JSON.stringify(section);
177
- for (const id of allIds) {
178
- const regex = new RegExp(`"imageId"\\s*:\\s*"${id}"`, "g");
179
- const matches = json.match(regex);
180
- if (matches)
181
- counts[id] = (counts[id] ?? 0) + matches.length;
182
- }
183
- }
184
- return counts;
185
- }, [sections, mediaManifest, pendingMediaItems]);
186
- // --- Unified enqueue ---
187
- const enqueueFile = useCallback((file, onComplete) => {
188
- if (!siteConfig)
189
- return;
190
- const mediaConfig = siteConfig.media;
191
- if (file.size > mediaConfig.maxFileSize)
192
- return;
193
- (async () => {
194
- try {
195
- const buffer = await file.arrayBuffer();
196
- const hash = await hashFileBuffer(buffer);
197
- const existsInManifest = hash in mediaManifest.images;
198
- const existsInPending = pendingMediaItems.some((i) => i.hash === hash);
199
- if (existsInManifest || existsInPending) {
200
- if (onComplete)
201
- onComplete(hash);
202
- return;
203
- }
204
- if (onComplete) {
205
- uploadCallbacksRef.current.set(hash, onComplete);
206
- }
207
- let kind = "image";
208
- if (file.type === "image/gif" || file.type === "image/apng")
209
- kind = "animated";
210
- if (file.type.startsWith("video/"))
211
- kind = "video";
212
- queueRef.current?.add({ buffer, originalName: file.name, mimeType: file.type, hash, kind });
213
- }
214
- catch (err) {
215
- console.error(`[useMediaPipeline] Failed to enqueue file "${file.name}":`, err);
216
- }
217
- })();
218
- }, [siteConfig, mediaManifest, pendingMediaItems]);
219
- // --- Context value ---
220
- const contextValue = useMemo(() => ({
221
- openSelectModal: () => {
222
- // Placeholder — EditorShell overrides this
223
- },
224
- uploadFile: (file) => handleMediaUpload([file]),
225
- uploadFileWithCallback: (file, onComplete) => {
226
- const blobUrl = URL.createObjectURL(file);
227
- enqueueFile(file, (imageId) => {
228
- URL.revokeObjectURL(blobUrl);
229
- onComplete(imageId);
230
- });
231
- return blobUrl;
232
- },
233
- manifest: mediaManifest,
234
- pendingItems: pendingMediaItems,
235
- pendingLocalUrls: pendingLocalUrls,
236
- siteConfig,
237
- }), [handleMediaUpload, enqueueFile, mediaManifest, pendingMediaItems, pendingLocalUrls, siteConfig]);
238
- return {
239
- processingItems,
240
- pendingMediaItems,
241
- setPendingMediaItems,
242
- pendingLocalUrls,
243
- setPendingLocalUrls,
244
- pendingDeletions,
245
- setPendingDeletions,
246
- handleMediaUpload,
247
- handleMediaDelete,
248
- handleMediaAltChange,
249
- referenceCountMap,
250
- contextValue,
251
- enqueueFile,
252
- };
253
- }
@@ -1,39 +0,0 @@
1
- import { useMemo } from "react";
2
- import { useMediaLibrary } from "../components/shell/MediaLibraryContext";
3
- import { createMediaAdapter } from "../media";
4
- export function useResolvedMedia(imageId) {
5
- const ctx = useMediaLibrary();
6
- return useMemo(() => {
7
- if (!imageId || !ctx) {
8
- return { src: undefined, srcset: undefined, poster: undefined, alt: "", kind: "image" };
9
- }
10
- const { manifest, pendingItems, pendingLocalUrls, siteConfig } = ctx;
11
- const sizes = siteConfig?.media?.sizes ?? [640, 1080, 1920];
12
- const manifestItem = manifest.images[imageId];
13
- const pendingItem = pendingItems.find((i) => i.id === imageId);
14
- const item = manifestItem ?? pendingItem;
15
- const alt = item?.alt ?? "";
16
- const kind = item?.kind ?? "image";
17
- const localUrl = pendingLocalUrls[imageId];
18
- if (localUrl) {
19
- return { src: localUrl, srcset: undefined, poster: undefined, alt, kind };
20
- }
21
- if (manifestItem) {
22
- const adapter = createMediaAdapter(manifest);
23
- const resolved = adapter.resolve(imageId, sizes);
24
- if (resolved && resolved.tag === "img") {
25
- return {
26
- src: resolved.src,
27
- srcset: "srcSet" in resolved ? resolved.srcSet : undefined,
28
- poster: undefined,
29
- alt,
30
- kind,
31
- };
32
- }
33
- if (resolved && resolved.tag === "video") {
34
- return { src: resolved.src, srcset: undefined, poster: resolved.poster, alt, kind };
35
- }
36
- }
37
- return { src: undefined, srcset: undefined, poster: undefined, alt, kind };
38
- }, [imageId, ctx]);
39
- }
package/dist/lib/cn.js DELETED
@@ -1,5 +0,0 @@
1
- import { clsx } from "clsx";
2
- import { twMerge } from "tailwind-merge";
3
- export function cn(...inputs) {
4
- return twMerge(clsx(inputs));
5
- }