@drawnagency/primitives 0.1.75 → 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-U4E6S4PX.js → chunk-RO4E5UJI.js} +53 -39
- 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/ChildBlockWrapper.d.ts +7 -0
- package/dist/components/editor/ChildBlockWrapper.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/Container/Container.d.ts.map +1 -1
- 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/ChildBlockWrapper.tsx +15 -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/sections/Container/Container.tsx +6 -4
- 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
|
@@ -16,6 +16,9 @@ interface IndicatorPillProps {
|
|
|
16
16
|
className?: string;
|
|
17
17
|
buttonClassName?: string;
|
|
18
18
|
ariaLabel?: string;
|
|
19
|
+
/** Display-only mode: dots/label still render but the popovers never open
|
|
20
|
+
* (e.g. while another editor holds the section lock). */
|
|
21
|
+
disabled?: boolean;
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
export function IndicatorPill({
|
|
@@ -26,6 +29,7 @@ export function IndicatorPill({
|
|
|
26
29
|
className,
|
|
27
30
|
buttonClassName,
|
|
28
31
|
ariaLabel,
|
|
32
|
+
disabled,
|
|
29
33
|
}: IndicatorPillProps) {
|
|
30
34
|
const [hoverOpen, setHoverOpen] = useState(false);
|
|
31
35
|
const [clickOpen, setClickOpen] = useState(false);
|
|
@@ -41,7 +45,7 @@ export function IndicatorPill({
|
|
|
41
45
|
|
|
42
46
|
function handleMouseEnter() {
|
|
43
47
|
clearHoverTimeout();
|
|
44
|
-
if (!clickOpen && hoverContent) {
|
|
48
|
+
if (!disabled && !clickOpen && hoverContent) {
|
|
45
49
|
setHoverOpen(true);
|
|
46
50
|
}
|
|
47
51
|
}
|
|
@@ -54,7 +58,7 @@ export function IndicatorPill({
|
|
|
54
58
|
}
|
|
55
59
|
|
|
56
60
|
function handleClick() {
|
|
57
|
-
if (!clickContent) return;
|
|
61
|
+
if (disabled || !clickContent) return;
|
|
58
62
|
setHoverOpen(false);
|
|
59
63
|
clearHoverTimeout();
|
|
60
64
|
setClickOpen((v) => !v);
|
|
@@ -72,15 +76,17 @@ export function IndicatorPill({
|
|
|
72
76
|
onClick={handleClick}
|
|
73
77
|
onMouseEnter={handleMouseEnter}
|
|
74
78
|
onMouseLeave={handleMouseLeave}
|
|
75
|
-
aria-haspopup={clickContent ? "true" : undefined}
|
|
76
|
-
aria-expanded={clickContent ? clickOpen : undefined}
|
|
79
|
+
aria-haspopup={!disabled && clickContent ? "true" : undefined}
|
|
80
|
+
aria-expanded={!disabled && clickContent ? clickOpen : undefined}
|
|
81
|
+
aria-disabled={disabled || undefined}
|
|
77
82
|
aria-label={ariaLabel}
|
|
78
83
|
className={cn(
|
|
79
84
|
// align-middle keeps the inline-flex pill centred in its line box regardless of
|
|
80
85
|
// whether it leads with a status/audience dot — without it, the dot-less variant's
|
|
81
86
|
// text baseline sits ~2px lower than the dotted variant, misaligning the two pills.
|
|
82
|
-
"
|
|
83
|
-
"border border-base-200
|
|
87
|
+
"inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-xs font-medium align-middle",
|
|
88
|
+
"border border-base-200",
|
|
89
|
+
disabled ? "cursor-default opacity-60" : "cursor-pointer hover:bg-base-accent",
|
|
84
90
|
buttonClassName,
|
|
85
91
|
)}
|
|
86
92
|
>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Lock } from "lucide-react";
|
|
2
|
+
import { cn } from "../../lib/cn";
|
|
3
|
+
|
|
4
|
+
interface LockBadgeProps {
|
|
5
|
+
email: string;
|
|
6
|
+
onTakeover?: () => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Reuses the "modified" status tokens (an amber pair defined in base.css) so the
|
|
10
|
+
// lock badge sits in the same visual family as the other editor status pills
|
|
11
|
+
// rather than introducing raw amber-* literals.
|
|
12
|
+
export function LockBadge({ email, onTakeover }: LockBadgeProps) {
|
|
13
|
+
return (
|
|
14
|
+
<span className="pointer-events-auto inline-flex items-center gap-1.5 rounded-full bg-status-modified-bg px-2 py-0.5 text-xs font-medium text-status-modified-text align-middle">
|
|
15
|
+
<Lock size={12} strokeWidth={2.5} aria-hidden="true" />
|
|
16
|
+
<span className="whitespace-nowrap">{email} is editing</span>
|
|
17
|
+
{onTakeover && (
|
|
18
|
+
<button
|
|
19
|
+
type="button"
|
|
20
|
+
onClick={onTakeover}
|
|
21
|
+
className={cn(
|
|
22
|
+
"cursor-pointer rounded px-1.5 py-0.5 font-semibold",
|
|
23
|
+
"bg-status-modified-text/10 hover:bg-status-modified-text/20",
|
|
24
|
+
)}
|
|
25
|
+
>
|
|
26
|
+
Take over
|
|
27
|
+
</button>
|
|
28
|
+
)}
|
|
29
|
+
</span>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { CollabPeer, CollabStatus } from "../../collab/types";
|
|
2
|
+
import { cn } from "../../lib/cn";
|
|
3
|
+
|
|
4
|
+
// Vivid -600 fills read as distinct badge colors and keep white text legible
|
|
5
|
+
// in both light and dark editor chrome.
|
|
6
|
+
const AVATAR_COLORS = [
|
|
7
|
+
"bg-sky-600",
|
|
8
|
+
"bg-emerald-600",
|
|
9
|
+
"bg-amber-600",
|
|
10
|
+
"bg-fuchsia-600",
|
|
11
|
+
"bg-rose-600",
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
function initials(email: string): string {
|
|
15
|
+
return email.slice(0, 2).toUpperCase();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function colorFor(userId: string): string {
|
|
19
|
+
let h = 0;
|
|
20
|
+
for (const c of userId) h = (h * 31 + c.charCodeAt(0)) >>> 0;
|
|
21
|
+
return AVATAR_COLORS[h % AVATAR_COLORS.length];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface PresenceAvatarsProps {
|
|
25
|
+
peers: CollabPeer[];
|
|
26
|
+
selfUserId: string | null;
|
|
27
|
+
status: CollabStatus;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function PresenceAvatars({ peers, selfUserId, status }: PresenceAvatarsProps) {
|
|
31
|
+
const others = peers.filter((p) => p.userId !== selfUserId);
|
|
32
|
+
if (status !== "connected" || others.length === 0) return null;
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className="flex -space-x-2" aria-label="Editors online">
|
|
36
|
+
{others.map((p) => (
|
|
37
|
+
<span
|
|
38
|
+
key={p.userId}
|
|
39
|
+
title={p.focusSectionId ? `${p.email} — editing` : p.email}
|
|
40
|
+
className={cn(
|
|
41
|
+
// border-base-accent matches the toolbar surface behind the row in
|
|
42
|
+
// both themes (white in light, dark panel in dark), giving a clean
|
|
43
|
+
// ring where the avatars overlap.
|
|
44
|
+
"flex h-7 w-7 items-center justify-center rounded-full border-2 border-base-accent text-xs font-semibold text-white",
|
|
45
|
+
colorFor(p.userId),
|
|
46
|
+
)}
|
|
47
|
+
>
|
|
48
|
+
{initials(p.email)}
|
|
49
|
+
</span>
|
|
50
|
+
))}
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -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) => {
|
|
@@ -40,6 +40,8 @@ interface ContainerChildProps {
|
|
|
40
40
|
hasSettings: boolean;
|
|
41
41
|
mergedOptions: Record<string, unknown>;
|
|
42
42
|
isEditMode: boolean;
|
|
43
|
+
/** First cell only: shift the control rail below the section's stacked "+" (below xl). */
|
|
44
|
+
clearSectionChrome?: boolean;
|
|
43
45
|
ChildComp: ChildComponent;
|
|
44
46
|
onDelete: () => void;
|
|
45
47
|
onDuplicate: () => void;
|
|
@@ -65,6 +67,7 @@ function ContainerChild({
|
|
|
65
67
|
hasSettings,
|
|
66
68
|
mergedOptions,
|
|
67
69
|
isEditMode,
|
|
70
|
+
clearSectionChrome,
|
|
68
71
|
ChildComp,
|
|
69
72
|
onDelete,
|
|
70
73
|
onDuplicate,
|
|
@@ -97,6 +100,7 @@ function ContainerChild({
|
|
|
97
100
|
closestEdge={closestEdge}
|
|
98
101
|
isDragging={isDragging}
|
|
99
102
|
hasSettings={hasSettings}
|
|
103
|
+
clearSectionChrome={clearSectionChrome}
|
|
100
104
|
dragHandleRef={handleRef}
|
|
101
105
|
onDelete={onDelete}
|
|
102
106
|
onDuplicate={onDuplicate}
|
|
@@ -328,6 +332,7 @@ export function Container({ content, isEditMode, onChange, openModal }: Containe
|
|
|
328
332
|
hasSettings={hasSettings}
|
|
329
333
|
mergedOptions={mergedOptions}
|
|
330
334
|
isEditMode={isEditMode}
|
|
335
|
+
clearSectionChrome={index === 0}
|
|
331
336
|
ChildComp={def.component as ChildComponent}
|
|
332
337
|
onDelete={() => emit(trimTrailingSpacers(removeChildAt(container, index).container))}
|
|
333
338
|
onDuplicate={() => emit(duplicateChildAt(container, index, newId))}
|
|
@@ -348,10 +353,7 @@ export function Container({ content, isEditMode, onChange, openModal }: Containe
|
|
|
348
353
|
const emptyCols = occupied < columns ? columns - occupied : 0;
|
|
349
354
|
return (
|
|
350
355
|
<div className="@container">
|
|
351
|
-
{
|
|
352
|
-
into the left gutter, where the first child's control rail would collide
|
|
353
|
-
with it — push children down until the gutter is side-by-side again. */}
|
|
354
|
-
<div className={cn(gridClass, "pt-8 xl:pt-0")}>
|
|
356
|
+
<div className={gridClass}>
|
|
355
357
|
{children.map(renderEditChild)}
|
|
356
358
|
{Array.from({ length: emptyCols }, (_, i) => (
|
|
357
359
|
<EmptyColumnCell
|