@drawnagency/primitives 0.1.76 → 0.1.77
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-2G2S36SY.js +13 -0
- package/dist/{chunk-7T2WQSWF.js → chunk-63BQQNZO.js} +9 -9
- package/dist/chunk-REM2NH7K.js +44 -0
- package/dist/{chunk-SWEVWC5K.js → chunk-RO4E5UJI.js} +21 -21
- package/dist/{chunk-NKVZGPR6.js → chunk-YP7IHMC3.js} +1 -1
- package/dist/collab/provider-registry.d.ts +4 -0
- package/dist/collab/provider-registry.d.ts.map +1 -0
- package/dist/collab/provider-registry.js +9 -0
- package/dist/collab/rebase.d.ts +37 -0
- package/dist/collab/rebase.d.ts.map +1 -0
- package/dist/collab/rebase.js +64 -0
- package/dist/collab/recovery.d.ts +89 -0
- package/dist/collab/recovery.d.ts.map +1 -0
- package/dist/collab/types.d.ts +93 -0
- package/dist/collab/types.d.ts.map +1 -0
- package/dist/collab/types.js +19 -0
- package/dist/components/editor/AudienceIndicator.d.ts +3 -1
- package/dist/components/editor/AudienceIndicator.d.ts.map +1 -1
- package/dist/components/editor/ConflictDialog.d.ts +14 -0
- package/dist/components/editor/ConflictDialog.d.ts.map +1 -0
- package/dist/components/editor/IndicatorPill.d.ts +4 -1
- package/dist/components/editor/IndicatorPill.d.ts.map +1 -1
- package/dist/components/editor/LockBadge.d.ts +7 -0
- package/dist/components/editor/LockBadge.d.ts.map +1 -0
- package/dist/components/editor/PresenceAvatars.d.ts +9 -0
- package/dist/components/editor/PresenceAvatars.d.ts.map +1 -0
- package/dist/components/editor/SectionWrapper.d.ts +1 -1
- package/dist/components/editor/SectionWrapper.d.ts.map +1 -1
- package/dist/components/editor/StatusIndicator.d.ts +3 -1
- package/dist/components/editor/StatusIndicator.d.ts.map +1 -1
- package/dist/components/editor/index.js +92 -44
- package/dist/components/primitives/index.js +3 -3
- package/dist/components/sections/brand-guide.js +3 -3
- package/dist/components/sections/register-schemas.js +9 -9
- package/dist/components/sections/register.js +9 -9
- package/dist/components/shell/EditorShell.d.ts +3 -1
- package/dist/components/shell/EditorShell.d.ts.map +1 -1
- package/dist/hooks/useCollabSession.d.ts +23 -0
- package/dist/hooks/useCollabSession.d.ts.map +1 -0
- package/dist/hooks/useEditorPersistence.d.ts +21 -1
- package/dist/hooks/useEditorPersistence.d.ts.map +1 -1
- package/dist/hooks/useEditorPublish.d.ts +39 -2
- package/dist/hooks/useEditorPublish.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +44 -22
- package/dist/lib/dexie.d.ts +12 -0
- package/dist/lib/dexie.d.ts.map +1 -1
- package/dist/lib/dexie.js +27 -0
- package/dist/lib/index.js +12 -12
- package/dist/lib/registry.d.ts +10 -0
- package/dist/lib/registry.d.ts.map +1 -1
- package/dist/schemas/index.js +1 -1
- package/dist/types/database.d.ts +70 -0
- package/dist/types/database.d.ts.map +1 -1
- package/package.json +13 -1
- package/src/collab/provider-registry.ts +14 -0
- package/src/collab/rebase.ts +140 -0
- package/src/collab/recovery.ts +149 -0
- package/src/collab/types.ts +96 -0
- package/src/components/editor/AudienceIndicator.tsx +4 -1
- package/src/components/editor/ConflictDialog.tsx +61 -0
- package/src/components/editor/IndicatorPill.tsx +12 -6
- package/src/components/editor/LockBadge.tsx +31 -0
- package/src/components/editor/PresenceAvatars.tsx +53 -0
- package/src/components/editor/SectionWrapper.tsx +60 -29
- package/src/components/editor/StatusIndicator.tsx +4 -0
- package/src/components/shell/EditorShell.tsx +561 -18
- package/src/hooks/useCollabSession.ts +220 -0
- package/src/hooks/useEditorPersistence.ts +104 -1
- package/src/hooks/useEditorPublish.ts +205 -63
- package/src/index.ts +2 -0
- package/src/lib/dexie.ts +44 -0
- package/src/lib/registry.ts +8 -0
- package/src/types/database.ts +55 -0
- package/dist/{chunk-Q3T23HVW.js → chunk-B2DLHZKC.js} +4 -4
- package/dist/{chunk-T7MP7ANP.js → chunk-UUCOJL7O.js} +0 -0
- package/dist/{chunk-ZRWGV5O6.js → chunk-XFDEUK37.js} +3 -3
|
@@ -7,6 +7,7 @@ import { DeleteButton } from "./DeleteButton";
|
|
|
7
7
|
import { SettingsButton } from "./SettingsButton";
|
|
8
8
|
import { StatusIndicator } from "./StatusIndicator";
|
|
9
9
|
import { AudienceIndicator } from "./AudienceIndicator";
|
|
10
|
+
import { LockBadge } from "./LockBadge";
|
|
10
11
|
import { SettingsForm } from "./SettingsForm";
|
|
11
12
|
import { useEditorContext } from "../shell/EditorContext";
|
|
12
13
|
import { useEditorModal } from "../shell/EditorModalContext";
|
|
@@ -38,8 +39,12 @@ export function SectionWrapper({
|
|
|
38
39
|
mainStatus,
|
|
39
40
|
contentDiffersFromMain,
|
|
40
41
|
isLocalOnly,
|
|
42
|
+
lockedBy,
|
|
43
|
+
onTakeover,
|
|
44
|
+
onEditIntent,
|
|
41
45
|
children,
|
|
42
46
|
}: WrapperProps) {
|
|
47
|
+
const isLocked = !!lockedBy;
|
|
43
48
|
const { isEditMode, showAllChrome } = useEditorContext();
|
|
44
49
|
const { openModal } = useEditorModal();
|
|
45
50
|
const [isActive, setIsActive] = useState(false);
|
|
@@ -66,7 +71,11 @@ export function SectionWrapper({
|
|
|
66
71
|
useEffect(() => {
|
|
67
72
|
const block = blockRef.current;
|
|
68
73
|
if (!block) return;
|
|
69
|
-
const onFocusIn = () =>
|
|
74
|
+
const onFocusIn = () => {
|
|
75
|
+
setIsActive(true);
|
|
76
|
+
// Fire-and-forget lock request; denial arrives via `lockedBy` in props.
|
|
77
|
+
if (!isLocked && onEditIntent) void onEditIntent();
|
|
78
|
+
};
|
|
70
79
|
const onFocusOut = (e: FocusEvent) => {
|
|
71
80
|
if (!block.contains(e.relatedTarget as Node)) setIsActive(false);
|
|
72
81
|
};
|
|
@@ -76,7 +85,10 @@ export function SectionWrapper({
|
|
|
76
85
|
block.removeEventListener("focusin", onFocusIn);
|
|
77
86
|
block.removeEventListener("focusout", onFocusOut);
|
|
78
87
|
};
|
|
79
|
-
|
|
88
|
+
// Caller contract: pass a memoized (useCallback) onEditIntent — it is a
|
|
89
|
+
// dependency here, so an inline closure re-binds these listeners on every
|
|
90
|
+
// render of the shell.
|
|
91
|
+
}, [isLocked, onEditIntent]);
|
|
80
92
|
|
|
81
93
|
useEffect(() => {
|
|
82
94
|
const block = blockRef.current;
|
|
@@ -164,7 +176,11 @@ export function SectionWrapper({
|
|
|
164
176
|
cancelled = true;
|
|
165
177
|
cleanup?.();
|
|
166
178
|
};
|
|
167
|
-
|
|
179
|
+
// isLocked: the DragHandle unmounts while locked, so DnD registration must
|
|
180
|
+
// be torn down on lock-acquire (cleanup above) and re-run against the fresh
|
|
181
|
+
// handle node on lock-release — without it, a locked-then-released section's
|
|
182
|
+
// registration stays bound to the old unmounted handle and reorder breaks.
|
|
183
|
+
}, [sectionId, index, containerId, isContainerBlock, isEditMode, isLocked]);
|
|
168
184
|
|
|
169
185
|
function handleSettingsClick() {
|
|
170
186
|
if (!onSectionChange) return;
|
|
@@ -229,6 +245,7 @@ export function SectionWrapper({
|
|
|
229
245
|
className={cn(
|
|
230
246
|
"relative",
|
|
231
247
|
isDragging && "opacity-50",
|
|
248
|
+
isLocked && "opacity-60",
|
|
232
249
|
isActive && "outline outline-2 outline-primary/50 rounded-sm",
|
|
233
250
|
!isActive &&
|
|
234
251
|
showAllChrome &&
|
|
@@ -242,7 +259,7 @@ export function SectionWrapper({
|
|
|
242
259
|
{/* Plus button — left gutter on xl+, hidden below. hover:z-[60] lifts the
|
|
243
260
|
whole slot (a z-30 stacking context the tooltip can't escape) above the
|
|
244
261
|
inline section-type picker (z-50) while the tooltip is showing. */}
|
|
245
|
-
{onRequestInsert && !isDragging && chromeReady && (
|
|
262
|
+
{!isLocked && onRequestInsert && !isDragging && chromeReady && (
|
|
246
263
|
<div
|
|
247
264
|
className={cn(
|
|
248
265
|
"absolute -left-16 bottom-full z-30 hover:z-[60] mb-1 hidden xl:block transition-opacity",
|
|
@@ -254,35 +271,40 @@ export function SectionWrapper({
|
|
|
254
271
|
)}
|
|
255
272
|
|
|
256
273
|
{/* Drag handle + section label */}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
>
|
|
263
|
-
<div className="relative -ml-[34px] flex flex-col items-center">
|
|
264
|
-
<DragHandle ref={handleRef} />
|
|
265
|
-
{onRequestInsert && !isDragging && chromeReady && (
|
|
266
|
-
// Below xl the gutter can't fit the plus beside the handle, so it
|
|
267
|
-
// hangs below it (absolute → no row-height contribution: the handle
|
|
268
|
-
// stays put and the pair stacks DOWN instead of centering).
|
|
269
|
-
<div className="absolute top-full xl:hidden">
|
|
270
|
-
<InsertButton index={index} onInsert={onRequestInsert} />
|
|
271
|
-
</div>
|
|
274
|
+
{!isLocked && (
|
|
275
|
+
<div
|
|
276
|
+
className={cn(
|
|
277
|
+
"absolute left-0 bottom-full z-30 hover:z-[60] mb-1 flex items-center gap-1.5 transition-opacity",
|
|
278
|
+
alwaysShow ? "opacity-100" : "opacity-0 group-hover:opacity-100",
|
|
272
279
|
)}
|
|
280
|
+
>
|
|
281
|
+
<div className="relative -ml-[34px] flex flex-col items-center">
|
|
282
|
+
<DragHandle ref={handleRef} />
|
|
283
|
+
{onRequestInsert && !isDragging && chromeReady && (
|
|
284
|
+
// Below xl the gutter can't fit the plus beside the handle, so it
|
|
285
|
+
// hangs below it (absolute → no row-height contribution: the handle
|
|
286
|
+
// stays put and the pair stacks DOWN instead of centering).
|
|
287
|
+
<div className="absolute top-full xl:hidden">
|
|
288
|
+
<InsertButton index={index} onInsert={onRequestInsert} />
|
|
289
|
+
</div>
|
|
290
|
+
)}
|
|
291
|
+
</div>
|
|
292
|
+
<span className="pointer-events-none select-none whitespace-nowrap text-sm capitalize text-base-contrast-light/80">
|
|
293
|
+
{definition.label}
|
|
294
|
+
</span>
|
|
273
295
|
</div>
|
|
274
|
-
|
|
275
|
-
{definition.label}
|
|
276
|
-
</span>
|
|
277
|
-
</div>
|
|
296
|
+
)}
|
|
278
297
|
|
|
279
298
|
{/* Status + settings + delete — top-right, above content */}
|
|
280
299
|
<div
|
|
281
300
|
className={cn(
|
|
282
301
|
"pointer-events-none absolute right-0 bottom-full z-30 mb-1 flex items-center gap-2 transition-opacity",
|
|
283
|
-
|
|
302
|
+
// A lock is important state — keep the cluster visible without hover.
|
|
303
|
+
alwaysShow || isLocked ? "opacity-100" : "opacity-0 group-hover:opacity-100",
|
|
284
304
|
)}
|
|
285
305
|
>
|
|
306
|
+
{lockedBy && <LockBadge email={lockedBy.email} onTakeover={onTakeover} />}
|
|
307
|
+
|
|
286
308
|
<div className="pointer-events-auto">
|
|
287
309
|
<StatusIndicator
|
|
288
310
|
mainStatus={mainStatus ?? null}
|
|
@@ -291,6 +313,7 @@ export function SectionWrapper({
|
|
|
291
313
|
isLocalOnly={isLocalOnly ?? false}
|
|
292
314
|
status={status as "draft" | "live" | "archived"}
|
|
293
315
|
dirty={dirty}
|
|
316
|
+
disabled={isLocked}
|
|
294
317
|
onChange={(s) => onStatusChange?.(s)}
|
|
295
318
|
/>
|
|
296
319
|
</div>
|
|
@@ -298,11 +321,12 @@ export function SectionWrapper({
|
|
|
298
321
|
<AudienceIndicator
|
|
299
322
|
access={access}
|
|
300
323
|
audiences={audiences}
|
|
324
|
+
disabled={isLocked}
|
|
301
325
|
onChange={(newAccess) => onAccessChange?.(newAccess)}
|
|
302
326
|
/>
|
|
303
327
|
</div>
|
|
304
328
|
|
|
305
|
-
{onAddChild && (
|
|
329
|
+
{!isLocked && onAddChild && (
|
|
306
330
|
<button
|
|
307
331
|
type="button"
|
|
308
332
|
onClick={onAddChild}
|
|
@@ -313,11 +337,11 @@ export function SectionWrapper({
|
|
|
313
337
|
</button>
|
|
314
338
|
)}
|
|
315
339
|
|
|
316
|
-
{hasSettings && (
|
|
340
|
+
{!isLocked && hasSettings && (
|
|
317
341
|
<SettingsButton onClick={handleSettingsClick} />
|
|
318
342
|
)}
|
|
319
343
|
|
|
320
|
-
{onMoveSection && (
|
|
344
|
+
{!isLocked && onMoveSection && (
|
|
321
345
|
<button
|
|
322
346
|
type="button"
|
|
323
347
|
onClick={onMoveSection}
|
|
@@ -328,7 +352,7 @@ export function SectionWrapper({
|
|
|
328
352
|
</button>
|
|
329
353
|
)}
|
|
330
354
|
|
|
331
|
-
{onDelete && <DeleteButton onDelete={onDelete} />}
|
|
355
|
+
{!isLocked && onDelete && <DeleteButton onDelete={onDelete} />}
|
|
332
356
|
</div>
|
|
333
357
|
|
|
334
358
|
{/* Drop indicator — centered in the section's own padding-top */}
|
|
@@ -345,7 +369,14 @@ export function SectionWrapper({
|
|
|
345
369
|
<div className="absolute right-0 bottom-0 left-0 z-10 h-0.5 bg-primary" />
|
|
346
370
|
)}
|
|
347
371
|
|
|
348
|
-
{
|
|
372
|
+
{isLocked ? (
|
|
373
|
+
// React 19 supports the boolean `inert` prop — blocks focus, pointer,
|
|
374
|
+
// and edits to another editor's locked content. Only wrapped when
|
|
375
|
+
// locked so the unlocked DOM is unchanged.
|
|
376
|
+
<div inert>{children}</div>
|
|
377
|
+
) : (
|
|
378
|
+
children
|
|
379
|
+
)}
|
|
349
380
|
</div>
|
|
350
381
|
);
|
|
351
382
|
}
|
|
@@ -91,6 +91,8 @@ interface StatusIndicatorProps {
|
|
|
91
91
|
status: Status;
|
|
92
92
|
dirty?: boolean;
|
|
93
93
|
onChange: (status: Status) => void;
|
|
94
|
+
/** Display-only mode — the pill still shows status but the picker never opens. */
|
|
95
|
+
disabled?: boolean;
|
|
94
96
|
}
|
|
95
97
|
|
|
96
98
|
export function StatusIndicator({
|
|
@@ -101,6 +103,7 @@ export function StatusIndicator({
|
|
|
101
103
|
status,
|
|
102
104
|
dirty,
|
|
103
105
|
onChange,
|
|
106
|
+
disabled,
|
|
104
107
|
}: StatusIndicatorProps) {
|
|
105
108
|
const display = deriveStatusDisplay({ mainStatus, savedStatus, contentDiffers, isLocalOnly });
|
|
106
109
|
|
|
@@ -117,6 +120,7 @@ export function StatusIndicator({
|
|
|
117
120
|
label={labelText}
|
|
118
121
|
ariaLabel={display.description}
|
|
119
122
|
buttonClassName="text-base-contrast-light"
|
|
123
|
+
disabled={disabled}
|
|
120
124
|
clickContent={(onClose) => (
|
|
121
125
|
<ul role="radiogroup" aria-label="Section status" className="w-full overflow-hidden py-1">
|
|
122
126
|
{STATUSES.map((s) => {
|