@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.
- package/dist/{chunk-UFOWXOG6.js → chunk-ABV7OZB3.js} +6 -0
- package/dist/{chunk-LQQY2RW6.js → chunk-DJAJRBPD.js} +1 -0
- package/dist/{chunk-677NTHAZ.js → chunk-PTOLUMGV.js} +3 -0
- package/dist/{chunk-RBKBNQGB.js → chunk-QQBBYQYD.js} +7 -6
- package/dist/{chunk-YI2HUFE3.js → chunk-RQPHPAT2.js} +1 -1
- package/dist/chunk-VAOW4WXQ.js +15 -0
- package/dist/{chunk-XYFZ57AS.js → chunk-YUUUCDXN.js} +59 -5
- package/dist/{chunk-HLKAQLTV.js → chunk-ZKGMLLUQ.js} +1 -1
- package/dist/components/brandguide/Colors.d.ts +2 -2
- package/dist/components/brandguide/Colors.d.ts.map +1 -1
- package/dist/components/brandguide/ColorsEditable.d.ts +2 -2
- package/dist/components/brandguide/ColorsEditable.d.ts.map +1 -1
- package/dist/components/editor/SectionWrapper.d.ts.map +1 -1
- package/dist/components/editor/SettingsForm.d.ts.map +1 -1
- package/dist/components/editor/index.js +4 -3
- package/dist/components/primitives/ResolvedMedia.d.ts.map +1 -1
- package/dist/components/primitives/index.js +2 -2
- package/dist/components/sections/Container/Container.d.ts +2 -2
- package/dist/components/sections/Container/Container.d.ts.map +1 -1
- package/dist/components/sections/Video/VideoBlock.d.ts.map +1 -1
- package/dist/components/sections/brand-guide.js +2 -2
- package/dist/components/sections/register-schemas.js +5 -5
- package/dist/components/sections/register.js +5 -5
- package/dist/components/shared/HistoryChangesModal.d.ts +39 -0
- package/dist/components/shared/HistoryChangesModal.d.ts.map +1 -0
- package/dist/components/shared/HistoryPopover.d.ts +3 -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 +10 -1
- package/dist/components/shared/Popover.d.ts.map +1 -1
- package/dist/components/shell/EditorModal.d.ts +6 -1
- package/dist/components/shell/EditorModal.d.ts.map +1 -1
- package/dist/components/shell/EditorModalContext.d.ts +3 -2
- package/dist/components/shell/EditorModalContext.d.ts.map +1 -1
- package/dist/components/shell/EditorShell.d.ts.map +1 -1
- package/dist/hooks/useEditorPublish.d.ts +5 -1
- package/dist/hooks/useEditorPublish.d.ts.map +1 -1
- package/dist/hooks/useMediaPipeline.d.ts.map +1 -1
- package/dist/index.js +9 -12
- package/dist/lib/events.d.ts +18 -0
- package/dist/lib/events.d.ts.map +1 -1
- package/dist/lib/history-buckets.d.ts +30 -0
- package/dist/lib/history-buckets.d.ts.map +1 -0
- package/dist/lib/index.d.ts +1 -1
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +4 -2
- package/dist/lib/media-folders.d.ts +13 -0
- package/dist/lib/media-folders.d.ts.map +1 -1
- package/dist/lib/media-folders.js +172 -0
- package/dist/lib/registry.d.ts +7 -1
- package/dist/lib/registry.d.ts.map +1 -1
- package/dist/lib/theme-vars.d.ts +19 -4
- package/dist/lib/theme-vars.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 +9 -0
- package/dist/storage/types.d.ts +14 -0
- package/dist/storage/types.d.ts.map +1 -1
- package/package.json +9 -1
- package/src/components/brandguide/Colors.tsx +2 -1
- package/src/components/brandguide/ColorsEditable.tsx +2 -1
- package/src/components/editor/SectionWrapper.tsx +1 -0
- package/src/components/editor/SettingsForm.tsx +5 -2
- package/src/components/primitives/ResolvedMedia.tsx +6 -0
- package/src/components/sections/Container/Container.tsx +5 -4
- package/src/components/sections/Video/VideoBlock.tsx +9 -1
- package/src/components/shared/HistoryChangesModal.tsx +256 -0
- package/src/components/shared/HistoryPopover.tsx +255 -76
- package/src/components/shared/Navigation.tsx +30 -9
- package/src/components/shared/Popover.tsx +95 -2
- package/src/components/shell/EditorModal.tsx +26 -4
- package/src/components/shell/EditorModalContext.tsx +5 -4
- package/src/components/shell/EditorShell.tsx +51 -2
- package/src/components/shell/SiteSettingsModal.tsx +1 -1
- package/src/hooks/useEditorPublish.ts +15 -6
- package/src/hooks/useMediaPipeline.ts +4 -0
- package/src/lib/events.ts +11 -0
- package/src/lib/history-buckets.ts +190 -0
- package/src/lib/index.ts +1 -1
- package/src/lib/media-folders.ts +16 -0
- package/src/lib/registry.ts +9 -1
- package/src/lib/theme-vars.ts +25 -4
- package/src/storage/index.ts +1 -1
- 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?:
|
|
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?:
|
|
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?:
|
|
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’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…
|
|
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 — 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…
|
|
248
|
+
</Button>
|
|
249
|
+
<Button size="sm" onClick={onBrowse}>Browse this version</Button>
|
|
250
|
+
</>
|
|
251
|
+
)}
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
</EditorModal>
|
|
255
|
+
);
|
|
256
|
+
}
|