@drawnagency/primitives 0.1.93 → 0.1.95

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 (84) hide show
  1. package/dist/{chunk-UFOWXOG6.js → chunk-ABV7OZB3.js} +6 -0
  2. package/dist/{chunk-LQQY2RW6.js → chunk-DJAJRBPD.js} +1 -0
  3. package/dist/{chunk-677NTHAZ.js → chunk-PTOLUMGV.js} +3 -0
  4. package/dist/{chunk-RBKBNQGB.js → chunk-QQBBYQYD.js} +7 -6
  5. package/dist/{chunk-YI2HUFE3.js → chunk-RQPHPAT2.js} +1 -1
  6. package/dist/chunk-VAOW4WXQ.js +15 -0
  7. package/dist/{chunk-XYFZ57AS.js → chunk-YUUUCDXN.js} +59 -5
  8. package/dist/{chunk-HLKAQLTV.js → chunk-ZKGMLLUQ.js} +1 -1
  9. package/dist/components/brandguide/Colors.d.ts +2 -2
  10. package/dist/components/brandguide/Colors.d.ts.map +1 -1
  11. package/dist/components/brandguide/ColorsEditable.d.ts +2 -2
  12. package/dist/components/brandguide/ColorsEditable.d.ts.map +1 -1
  13. package/dist/components/editor/SectionWrapper.d.ts.map +1 -1
  14. package/dist/components/editor/SettingsForm.d.ts.map +1 -1
  15. package/dist/components/editor/index.js +4 -3
  16. package/dist/components/primitives/ResolvedMedia.d.ts.map +1 -1
  17. package/dist/components/primitives/index.js +2 -2
  18. package/dist/components/sections/Container/Container.d.ts +2 -2
  19. package/dist/components/sections/Container/Container.d.ts.map +1 -1
  20. package/dist/components/sections/Video/VideoBlock.d.ts.map +1 -1
  21. package/dist/components/sections/brand-guide.js +2 -2
  22. package/dist/components/sections/register-schemas.js +5 -5
  23. package/dist/components/sections/register.js +5 -5
  24. package/dist/components/shared/HistoryChangesModal.d.ts +39 -0
  25. package/dist/components/shared/HistoryChangesModal.d.ts.map +1 -0
  26. package/dist/components/shared/HistoryPopover.d.ts +3 -1
  27. package/dist/components/shared/HistoryPopover.d.ts.map +1 -1
  28. package/dist/components/shared/Navigation.d.ts.map +1 -1
  29. package/dist/components/shared/Popover.d.ts +10 -1
  30. package/dist/components/shared/Popover.d.ts.map +1 -1
  31. package/dist/components/shell/EditorModal.d.ts +6 -1
  32. package/dist/components/shell/EditorModal.d.ts.map +1 -1
  33. package/dist/components/shell/EditorModalContext.d.ts +3 -2
  34. package/dist/components/shell/EditorModalContext.d.ts.map +1 -1
  35. package/dist/components/shell/EditorShell.d.ts.map +1 -1
  36. package/dist/hooks/useEditorPublish.d.ts +5 -1
  37. package/dist/hooks/useEditorPublish.d.ts.map +1 -1
  38. package/dist/hooks/useMediaPipeline.d.ts.map +1 -1
  39. package/dist/index.js +9 -12
  40. package/dist/lib/events.d.ts +18 -0
  41. package/dist/lib/events.d.ts.map +1 -1
  42. package/dist/lib/history-buckets.d.ts +30 -0
  43. package/dist/lib/history-buckets.d.ts.map +1 -0
  44. package/dist/lib/index.d.ts +1 -1
  45. package/dist/lib/index.d.ts.map +1 -1
  46. package/dist/lib/index.js +4 -2
  47. package/dist/lib/media-folders.d.ts +13 -0
  48. package/dist/lib/media-folders.d.ts.map +1 -1
  49. package/dist/lib/media-folders.js +172 -0
  50. package/dist/lib/registry.d.ts +7 -1
  51. package/dist/lib/registry.d.ts.map +1 -1
  52. package/dist/lib/theme-vars.d.ts +19 -4
  53. package/dist/lib/theme-vars.d.ts.map +1 -1
  54. package/dist/storage/index.d.ts +1 -1
  55. package/dist/storage/index.d.ts.map +1 -1
  56. package/dist/storage/index.js +9 -0
  57. package/dist/storage/types.d.ts +14 -0
  58. package/dist/storage/types.d.ts.map +1 -1
  59. package/package.json +9 -1
  60. package/src/components/brandguide/Colors.tsx +2 -1
  61. package/src/components/brandguide/ColorsEditable.tsx +2 -1
  62. package/src/components/editor/SectionWrapper.tsx +1 -0
  63. package/src/components/editor/SettingsForm.tsx +5 -2
  64. package/src/components/primitives/ResolvedMedia.tsx +6 -0
  65. package/src/components/sections/Container/Container.tsx +5 -4
  66. package/src/components/sections/Video/VideoBlock.tsx +9 -1
  67. package/src/components/shared/HistoryChangesModal.tsx +256 -0
  68. package/src/components/shared/HistoryPopover.tsx +255 -76
  69. package/src/components/shared/Navigation.tsx +30 -9
  70. package/src/components/shared/Popover.tsx +95 -2
  71. package/src/components/shell/EditorModal.tsx +26 -4
  72. package/src/components/shell/EditorModalContext.tsx +5 -4
  73. package/src/components/shell/EditorShell.tsx +51 -2
  74. package/src/components/shell/SiteSettingsModal.tsx +1 -1
  75. package/src/hooks/useEditorPublish.ts +15 -6
  76. package/src/hooks/useMediaPipeline.ts +4 -0
  77. package/src/lib/events.ts +11 -0
  78. package/src/lib/history-buckets.ts +190 -0
  79. package/src/lib/index.ts +1 -1
  80. package/src/lib/media-folders.ts +16 -0
  81. package/src/lib/registry.ts +9 -1
  82. package/src/lib/theme-vars.ts +25 -4
  83. package/src/storage/index.ts +1 -1
  84. package/src/storage/types.ts +15 -0
@@ -74,6 +74,12 @@ export function ResolvedMedia({
74
74
  playsInline
75
75
  loop
76
76
  autoPlay
77
+ // Defer the fetch until the loop nears the viewport. Without
78
+ // this, every media-section video on a page downloads in full
79
+ // during initial load. `autoplay` still overrides the hint when
80
+ // the browser decides to play, so playback is unchanged — see
81
+ // the matching note in sections/Video/VideoBlock.tsx.
82
+ preload="none"
77
83
  className={mediaClass}
78
84
  onLoadedData={() => setLoaded(true)}
79
85
  />
@@ -1,6 +1,6 @@
1
1
  import { type ComponentType, type ReactNode } from "react";
2
2
  import type { Edge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge";
3
- import { getSection } from "../../../lib/registry";
3
+ import { getSection, type OpenModal } from "../../../lib/registry";
4
4
  import { cn } from "../../../lib/cn";
5
5
  import { containerGridClass, colSpanClass, spacerHiddenClass, masonryColumnsClass } from "../../../lib/container-grid";
6
6
  import { ChildBlockWrapper } from "../../editor/ChildBlockWrapper";
@@ -27,7 +27,7 @@ type ChildComponent = ComponentType<{
27
27
  options: Record<string, unknown>;
28
28
  isEditMode: boolean;
29
29
  onChange?: (content: unknown) => void;
30
- openModal?: (title: string, content: ReactNode) => void;
30
+ openModal?: OpenModal;
31
31
  }>;
32
32
 
33
33
  interface ContainerChildProps {
@@ -48,7 +48,7 @@ interface ContainerChildProps {
48
48
  onColSpanChange: (span: number) => void;
49
49
  onOpenSettings?: () => void;
50
50
  onChildContentChange?: (content: unknown) => void;
51
- openModal?: (title: string, content: ReactNode) => void;
51
+ openModal?: OpenModal;
52
52
  /** Drag/drop hitbox edges: ["left","right"] in grid (row flow), ["top","bottom"] in masonry
53
53
  * (CSS multi-column stacks items vertically). Defaults to ROW_EDGES. */
54
54
  allowedEdges?: Edge[];
@@ -134,7 +134,7 @@ export interface ContainerProps {
134
134
  content: { id?: string; content: ContainerContent };
135
135
  isEditMode: boolean;
136
136
  onChange?: (content: unknown) => void;
137
- openModal?: (title: string, content: ReactNode) => void;
137
+ openModal?: OpenModal;
138
138
  }
139
139
 
140
140
  /**
@@ -296,6 +296,7 @@ export function Container({ content, isEditMode, onChange, openModal }: Containe
296
296
  onChange={applyResult}
297
297
  tabs={def.settingsTabs}
298
298
  />,
299
+ { tabbed: (def.settingsTabs?.length ?? 0) > 0 },
299
300
  );
300
301
  }
301
302
  };
@@ -90,9 +90,17 @@ export function VideoBlock({ source, caption, options, isEditMode, onSourceChang
90
90
  const height = bakedHeight ?? resolved.height;
91
91
  aspect = width && height ? `${width} / ${height}` : undefined;
92
92
 
93
+ // `preload="none"` on the ambient branch is deliberate and load-bearing.
94
+ // An autoplaying loop with no preload hint fetches the whole file during
95
+ // initial page load, even far below the fold — on the Siete portal that was
96
+ // 246 MB of video downloading before the viewer scrolled to any of it.
97
+ // `preload` is only a hint and `autoplay` overrides it once the browser
98
+ // decides to play, so playback is unaffected; what changes is that the
99
+ // fetch is deferred until the element approaches the viewport instead of
100
+ // racing everything else on the page. The poster still renders immediately.
93
101
  const playbackProps =
94
102
  playback === "ambient"
95
- ? { autoPlay: true, muted: true, loop: true, playsInline: true }
103
+ ? { autoPlay: true, muted: true, loop: true, playsInline: true, preload: "none" as const }
96
104
  : { controls: true, preload: "metadata" as const };
97
105
 
98
106
  const videoEl = imageId ? (
@@ -0,0 +1,256 @@
1
+ import { useEffect, useState } from "react";
2
+ import { EditorModal } from "../shell/EditorModal";
3
+ import { Button } from "./Button";
4
+
5
+ export interface ChangedSection {
6
+ id: string;
7
+ /** The section's heading, or null when it has none / wasn't resolved. */
8
+ title: string | null;
9
+ type: string;
10
+ page: string | null;
11
+ }
12
+
13
+ /** "icon_list" → "Icon list". */
14
+ function humanizeType(type: string) {
15
+ const spaced = type.replace(/[-_]/g, " ");
16
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1);
17
+ }
18
+
19
+ /**
20
+ * Section ids are UUIDs, and plenty of section types (media, spacer) have no
21
+ * heading to show. Fall back to the type name alone — the page shown alongside
22
+ * it carries enough context, and an id fragment reads as noise. Two headless
23
+ * sections of the same type on one page will look alike; that's accepted.
24
+ */
25
+ function displayName(s: ChangedSection) {
26
+ return s.title ?? humanizeType(s.type);
27
+ }
28
+
29
+ export interface ChangeSummary {
30
+ edited: ChangedSection[];
31
+ added: ChangedSection[];
32
+ removed: ChangedSection[];
33
+ structure: string[];
34
+ /** Too many sections changed to resolve real titles — ids shown instead. */
35
+ truncated: boolean;
36
+ }
37
+
38
+ interface HistoryChangesModalProps {
39
+ isOpen: boolean;
40
+ onClose: () => void;
41
+ /** Node label, e.g. "Friday · Jul 24" or "Edit at 4:47 PM". */
42
+ title: string;
43
+ base: string | null;
44
+ head: string;
45
+ commitCount: number;
46
+ onBrowse: () => void;
47
+ onRestore: () => void;
48
+ /**
49
+ * True when the editor holds unsaved local changes. Restoring discards them,
50
+ * and unlike published content they are NOT recoverable from history — so the
51
+ * confirmation has to say so rather than reassure.
52
+ */
53
+ hasUnsavedChanges?: boolean;
54
+ /** Disables the restore controls while a restore is in flight. */
55
+ isRestoring?: boolean;
56
+ /** Set when a dispatched restore came back failed. */
57
+ restoreError?: string | null;
58
+ }
59
+
60
+ // Commit ranges are immutable, so a fetched summary stays valid for the session.
61
+ const cache = new Map<string, ChangeSummary>();
62
+
63
+ /**
64
+ * Bump whenever the response shape or the meaning of a field changes.
65
+ *
66
+ * The endpoint sends `immutable` with a one-year max-age — correct for the
67
+ * *content* of a commit range, which genuinely never changes, but it means a
68
+ * browser that cached an older shape would keep serving it and no reload could
69
+ * dislodge it. Versioning the URL makes a shape change a different cache entry.
70
+ * v2: `title` became nullable (was the section id, which is a UUID).
71
+ */
72
+ const API_VERSION = 2;
73
+
74
+ function queryFor(head: string, base: string | null) {
75
+ const q = new URLSearchParams({ v: String(API_VERSION), head });
76
+ if (base) q.set("base", base);
77
+ return `/api/history/changes?${q.toString()}`;
78
+ }
79
+
80
+ function Group({ label, items, sign }: { label: string; items: ChangedSection[]; sign: string }) {
81
+ if (items.length === 0) return null;
82
+ return (
83
+ <>
84
+ <p className="mb-1.5 text-[10px] font-medium tracking-wider text-base-contrast-light uppercase">
85
+ {label} ({items.length})
86
+ </p>
87
+ <ul className="mb-4 list-none p-0">
88
+ {items.map((s) => (
89
+ <li
90
+ key={s.id}
91
+ className="flex items-baseline gap-2 border-b border-base-200 py-1 last:border-b-0"
92
+ >
93
+ <span aria-hidden="true" className="w-3 shrink-0 text-center text-xs font-bold text-base-contrast-light">
94
+ {sign}
95
+ </span>
96
+ <span className="text-sm text-base-contrast">{displayName(s)}</span>
97
+ {s.page && <span className="text-xs text-base-contrast-light">· {s.page}</span>}
98
+ <span className="ml-auto shrink-0 pl-2 text-xs text-base-contrast-light">{s.type}</span>
99
+ </li>
100
+ ))}
101
+ </ul>
102
+ </>
103
+ );
104
+ }
105
+
106
+ export function HistoryChangesModal({
107
+ isOpen, onClose, title, base, head, commitCount, onBrowse, onRestore,
108
+ hasUnsavedChanges = false, isRestoring = false, restoreError = null,
109
+ }: HistoryChangesModalProps) {
110
+ const [summary, setSummary] = useState<ChangeSummary | null>(null);
111
+ const [error, setError] = useState(false);
112
+ const [confirmingRestore, setConfirmingRestore] = useState(false);
113
+
114
+ // Reopening on a different range must not inherit the previous confirm state.
115
+ useEffect(() => {
116
+ if (!isOpen) setConfirmingRestore(false);
117
+ }, [isOpen]);
118
+
119
+ useEffect(() => {
120
+ if (!isOpen) return;
121
+ const url = queryFor(head, base);
122
+ const hit = cache.get(url);
123
+ if (hit) {
124
+ setSummary(hit);
125
+ setError(false);
126
+ return;
127
+ }
128
+ let live = true;
129
+ setSummary(null);
130
+ setError(false);
131
+ (async () => {
132
+ try {
133
+ const res = await fetch(url);
134
+ if (!res.ok) throw new Error(String(res.status));
135
+ const data = (await res.json()) as ChangeSummary;
136
+ cache.set(url, data);
137
+ if (live) setSummary(data);
138
+ } catch {
139
+ if (live) setError(true);
140
+ }
141
+ })();
142
+ return () => { live = false; };
143
+ }, [isOpen, head, base]);
144
+
145
+ const total = summary
146
+ ? summary.edited.length + summary.added.length + summary.removed.length
147
+ : 0;
148
+
149
+ return (
150
+ // `tabbed` locks the dialog to one height so it doesn't jump when the
151
+ // summary arrives, and gives an overflow-hidden flex column — which is what
152
+ // lets the body scroll under a pinned footer. `noPadding` because the
153
+ // scrolling region owns its own padding.
154
+ <EditorModal
155
+ isOpen={isOpen}
156
+ onClose={onClose}
157
+ title={confirmingRestore ? "Restore this version?" : title}
158
+ size="standard"
159
+ tabbed
160
+ noPadding
161
+ >
162
+ <div className="flex min-h-0 flex-1 flex-col">
163
+ <div className="flex-1 overflow-y-auto px-6 py-4">
164
+ {confirmingRestore ? (
165
+ <>
166
+ <p className="text-sm text-base-contrast-light">
167
+ This will publish the content from this version as a new update.
168
+ Your currently published content stays available in the history.
169
+ </p>
170
+ {restoreError && (
171
+ <p role="alert" className="mt-3 text-sm font-medium text-red-600">
172
+ {restoreError} Nothing was published — you can try again.
173
+ </p>
174
+ )}
175
+ {hasUnsavedChanges && (
176
+ <p className="mt-3 text-sm font-medium text-red-600">
177
+ You have unsaved changes. Restoring discards them, and unlike
178
+ published content they cannot be recovered from history.
179
+ </p>
180
+ )}
181
+ </>
182
+ ) : (
183
+ <>
184
+ {error && (
185
+ <p className="py-6 text-center text-sm text-base-contrast-light">
186
+ Couldn&rsquo;t load the change summary.
187
+ </p>
188
+ )}
189
+
190
+ {!error && !summary && (
191
+ <p className="py-6 text-center text-sm text-base-contrast-light">
192
+ Loading changes&hellip;
193
+ </p>
194
+ )}
195
+
196
+ {summary && (
197
+ <>
198
+ <p className="mb-4 text-xs text-base-contrast-light">
199
+ {commitCount === 1 ? "Single edit" : `${commitCount} edits`}
200
+ {" · "}
201
+ {total} section{total === 1 ? "" : "s"} affected
202
+ </p>
203
+
204
+ <Group label="Edited" items={summary.edited} sign="~" />
205
+ <Group label="Added" items={summary.added} sign="+" />
206
+ <Group label="Removed" items={summary.removed} sign="−" />
207
+
208
+ {summary.structure.length > 0 && (
209
+ <>
210
+ <p className="mb-1.5 text-[10px] font-medium tracking-wider text-base-contrast-light uppercase">
211
+ Page structure
212
+ </p>
213
+ <ul className="mb-4 list-none p-0">
214
+ {summary.structure.map((s) => (
215
+ <li key={s} className="py-1 text-sm text-base-contrast">{s}</li>
216
+ ))}
217
+ </ul>
218
+ </>
219
+ )}
220
+
221
+ {summary.truncated && (
222
+ <p className="mb-4 text-xs text-base-contrast-light">
223
+ Too many sections changed to resolve names &mdash; showing section
224
+ types instead.
225
+ </p>
226
+ )}
227
+ </>
228
+ )}
229
+ </>
230
+ )}
231
+ </div>
232
+
233
+ <div className="flex shrink-0 justify-end gap-2 border-t border-base-200 px-6 py-3">
234
+ {confirmingRestore ? (
235
+ <>
236
+ <Button variant="secondary" size="sm" disabled={isRestoring}
237
+ onClick={() => setConfirmingRestore(false)}>
238
+ Cancel
239
+ </Button>
240
+ <Button variant="destructive" size="sm" disabled={isRestoring} onClick={onRestore}>
241
+ {isRestoring ? "Restoring…" : "Restore"}
242
+ </Button>
243
+ </>
244
+ ) : (
245
+ <>
246
+ <Button variant="destructive" size="sm" onClick={() => setConfirmingRestore(true)}>
247
+ Restore&hellip;
248
+ </Button>
249
+ <Button size="sm" onClick={onBrowse}>Browse this version</Button>
250
+ </>
251
+ )}
252
+ </div>
253
+ </div>
254
+ </EditorModal>
255
+ );
256
+ }