@drawnagency/primitives 0.1.119 → 0.1.120
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-G6O3TYPJ.js → chunk-HZTPF6OK.js} +10 -3
- package/dist/{chunk-YKKGVSJH.js → chunk-UZBFVVTC.js} +1 -1
- package/dist/components/editor/AudienceIndicator.d.ts +3 -1
- package/dist/components/editor/AudienceIndicator.d.ts.map +1 -1
- package/dist/components/editor/IndicatorPill.d.ts +3 -1
- package/dist/components/editor/IndicatorPill.d.ts.map +1 -1
- package/dist/components/editor/SectionBulkEditModal.d.ts +25 -0
- package/dist/components/editor/SectionBulkEditModal.d.ts.map +1 -0
- 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 +14 -8
- package/dist/components/sections/register-schemas.js +2 -2
- package/dist/components/sections/register.js +2 -2
- package/dist/components/shared/Popover.d.ts +1 -1
- package/dist/components/shared/Popover.d.ts.map +1 -1
- package/dist/components/shared/SectionSummaryRow.d.ts +1 -1
- package/dist/components/shell/EditorModal.d.ts.map +1 -1
- package/dist/components/shell/EditorShell.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/editor/AudienceIndicator.tsx +8 -4
- package/src/components/editor/IndicatorPill.tsx +4 -0
- package/src/components/editor/SectionBulkEditModal.tsx +328 -0
- package/src/components/editor/StatusIndicator.tsx +3 -2
- package/src/components/shared/Popover.tsx +17 -4
- package/src/components/shared/SectionSummaryRow.tsx +1 -1
- package/src/components/shell/EditorModal.tsx +5 -2
- package/src/components/shell/EditorShell.tsx +11 -7
- package/dist/components/editor/SectionOrderingModal.d.ts +0 -10
- package/dist/components/editor/SectionOrderingModal.d.ts.map +0 -1
- package/src/components/editor/SectionOrderingModal.tsx +0 -80
|
@@ -712,14 +712,18 @@ function Popover({
|
|
|
712
712
|
panel.style.left = "";
|
|
713
713
|
panel.style.right = "";
|
|
714
714
|
}, [isOpen, align]);
|
|
715
|
-
useStackedEscape(onClose, { enabled: isOpen });
|
|
715
|
+
const escapeLayerRef = useStackedEscape(onClose, { enabled: isOpen });
|
|
716
|
+
useLayoutEffect(() => {
|
|
717
|
+
escapeLayerRef.current = isOpen ? anchorRef.current?.closest("[data-modal-layer]") ?? null : null;
|
|
718
|
+
}, [isOpen, anchorRef, escapeLayerRef]);
|
|
716
719
|
useEffect3(() => {
|
|
717
720
|
if (!isOpen) return;
|
|
718
721
|
function onMouseDown(e) {
|
|
719
722
|
const target = e.target;
|
|
720
723
|
if (panelRef.current?.contains(target)) return;
|
|
721
724
|
if (anchorRef.current?.contains(target)) return;
|
|
722
|
-
|
|
725
|
+
const layer = target instanceof Element ? target.closest("[data-modal-layer]") : null;
|
|
726
|
+
if (layer && !layer.contains(anchorRef.current)) return;
|
|
723
727
|
onClose();
|
|
724
728
|
}
|
|
725
729
|
document.addEventListener("mousedown", onMouseDown);
|
|
@@ -738,6 +742,7 @@ function Popover({
|
|
|
738
742
|
ref: panelRef,
|
|
739
743
|
role: "dialog",
|
|
740
744
|
"aria-modal": "false",
|
|
745
|
+
"data-popover": "",
|
|
741
746
|
style: {
|
|
742
747
|
position: "fixed",
|
|
743
748
|
left: `${fixedPos.left}px`,
|
|
@@ -757,8 +762,10 @@ function Popover({
|
|
|
757
762
|
ref: panelRef,
|
|
758
763
|
role: "dialog",
|
|
759
764
|
"aria-modal": "false",
|
|
765
|
+
"data-popover": "",
|
|
760
766
|
className: cn(
|
|
761
|
-
"absolute
|
|
767
|
+
"absolute z-50 rounded-md border border-base-200 bg-base shadow-lg",
|
|
768
|
+
placement === "top" ? "bottom-full mb-1" : "top-full mt-1",
|
|
762
769
|
effectiveAlign === "end" ? "right-0" : "left-0",
|
|
763
770
|
className
|
|
764
771
|
),
|
|
@@ -3,8 +3,10 @@ interface AudienceChecklistProps {
|
|
|
3
3
|
access: string[];
|
|
4
4
|
audiences: Audience[];
|
|
5
5
|
onToggle: (name: string) => void;
|
|
6
|
+
/** Bulk edit: audiences only some of the selected sections have. */
|
|
7
|
+
partial?: string[];
|
|
6
8
|
}
|
|
7
|
-
export declare function AudienceChecklist({ access, audiences, onToggle }: AudienceChecklistProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export declare function AudienceChecklist({ access, audiences, onToggle, partial }: AudienceChecklistProps): import("react/jsx-runtime").JSX.Element;
|
|
8
10
|
interface AudienceIndicatorProps {
|
|
9
11
|
access: string[];
|
|
10
12
|
audiences: Audience[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AudienceIndicator.d.ts","sourceRoot":"","sources":["../../../src/components/editor/AudienceIndicator.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAEjD,UAAU,sBAAsB;IAC9B,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"AudienceIndicator.d.ts","sourceRoot":"","sources":["../../../src/components/editor/AudienceIndicator.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAEjD,UAAU,sBAAsB;IAC9B,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,oEAAoE;IACpE,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,wBAAgB,iBAAiB,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAY,EAAE,EAAE,sBAAsB,2CA2CtG;AAED,UAAU,sBAAsB;IAC9B,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IACrC,qFAAqF;IACrF,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,wBAAgB,iBAAiB,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,sBAAsB,2CA+BlG"}
|
|
@@ -15,7 +15,9 @@ interface IndicatorPillProps {
|
|
|
15
15
|
/** Display-only mode: dots/label still render but the popovers never open
|
|
16
16
|
* (e.g. while another editor holds the section lock). */
|
|
17
17
|
disabled?: boolean;
|
|
18
|
+
/** Open the click popover above the pill — for pills in a bottom-pinned bar. */
|
|
19
|
+
placement?: "bottom" | "top";
|
|
18
20
|
}
|
|
19
|
-
export declare function IndicatorPill({ dots, label, hoverContent, clickContent, className, buttonClassName, ariaLabel, disabled, }: IndicatorPillProps): import("react/jsx-runtime").JSX.Element;
|
|
21
|
+
export declare function IndicatorPill({ dots, label, hoverContent, clickContent, className, buttonClassName, ariaLabel, disabled, placement, }: IndicatorPillProps): import("react/jsx-runtime").JSX.Element;
|
|
20
22
|
export {};
|
|
21
23
|
//# sourceMappingURL=IndicatorPill.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IndicatorPill.d.ts","sourceRoot":"","sources":["../../../src/components/editor/IndicatorPill.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA4C,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAIjF,UAAU,MAAM;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,UAAU,kBAAkB;IAC1B,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,0FAA0F;IAC1F,YAAY,CAAC,EAAE,SAAS,CAAC;IACzB,YAAY,CAAC,EAAE,SAAS,GAAG,CAAC,CAAC,OAAO,EAAE,MAAM,IAAI,KAAK,SAAS,CAAC,CAAC;IAChE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;8DAC0D;IAC1D,QAAQ,CAAC,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"IndicatorPill.d.ts","sourceRoot":"","sources":["../../../src/components/editor/IndicatorPill.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA4C,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAIjF,UAAU,MAAM;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,UAAU,kBAAkB;IAC1B,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,0FAA0F;IAC1F,YAAY,CAAC,EAAE,SAAS,CAAC;IACzB,YAAY,CAAC,EAAE,SAAS,GAAG,CAAC,CAAC,OAAO,EAAE,MAAM,IAAI,KAAK,SAAS,CAAC,CAAC;IAChE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;8DAC0D;IAC1D,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gFAAgF;IAChF,SAAS,CAAC,EAAE,QAAQ,GAAG,KAAK,CAAC;CAC9B;AAED,wBAAgB,aAAa,CAAC,EAC5B,IAAI,EACJ,KAAK,EACL,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,eAAe,EACf,SAAS,EACT,QAAQ,EACR,SAAoB,GACrB,EAAE,kBAAkB,2CAoGpB"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { LoadedSection } from "../../lib/loader";
|
|
2
|
+
import type { MediaManifest } from "../../media/types";
|
|
3
|
+
import type { Audience } from "../../auth/types";
|
|
4
|
+
type Status = "draft" | "live" | "archived";
|
|
5
|
+
interface SectionBulkEditModalProps {
|
|
6
|
+
sections: LoadedSection[];
|
|
7
|
+
mediaManifest: MediaManifest;
|
|
8
|
+
audiences: Audience[];
|
|
9
|
+
/** Sections another editor holds. They stay draggable but can't be selected,
|
|
10
|
+
* matching the canvas, where a held section's status and audience are locked. */
|
|
11
|
+
locks: ReadonlyMap<string, {
|
|
12
|
+
email: string;
|
|
13
|
+
}>;
|
|
14
|
+
onReorder: (fromIndex: number, toIndex: number) => void;
|
|
15
|
+
onStatusChange: (sectionId: string, status: Status) => void;
|
|
16
|
+
onAccessChange: (sectionId: string, access: string[]) => void;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The toolbar's "Edit sections" panel: drag rows to reorder, or tick several and
|
|
20
|
+
* set their status and audience at once from the bar that appears. Every change
|
|
21
|
+
* applies to the working copy immediately, like the per-section pills it reuses.
|
|
22
|
+
*/
|
|
23
|
+
export declare function SectionBulkEditModal({ sections, mediaManifest, audiences, locks, onReorder, onStatusChange, onAccessChange, }: SectionBulkEditModalProps): import("react/jsx-runtime").JSX.Element;
|
|
24
|
+
export {};
|
|
25
|
+
//# sourceMappingURL=SectionBulkEditModal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SectionBulkEditModal.d.ts","sourceRoot":"","sources":["../../../src/components/editor/SectionBulkEditModal.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAajD,KAAK,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,UAAU,CAAC;AAI5C,UAAU,yBAAyB;IACjC,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,aAAa,EAAE,aAAa,CAAC;IAC7B,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB;sFACkF;IAClF,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC9C,SAAS,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACxD,cAAc,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5D,cAAc,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;CAC/D;AAiHD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,EACnC,QAAQ,EACR,aAAa,EACb,SAAS,EACT,KAAK,EACL,SAAS,EACT,cAAc,EACd,cAAc,GACf,EAAE,yBAAyB,2CAyK3B"}
|
|
@@ -15,8 +15,10 @@ interface DeriveResult {
|
|
|
15
15
|
description: string;
|
|
16
16
|
}
|
|
17
17
|
export declare function deriveStatusDisplay({ mainStatus, savedStatus, contentDiffers, isLocalOnly }: DeriveInput): DeriveResult;
|
|
18
|
+
export declare const dotColorClasses: Record<StatusColor, string>;
|
|
18
19
|
interface StatusRadioListProps {
|
|
19
|
-
|
|
20
|
+
/** null when several sections with different statuses are being set at once. */
|
|
21
|
+
status: Status | null;
|
|
20
22
|
onChange: (status: Status) => void;
|
|
21
23
|
onClose?: () => void;
|
|
22
24
|
ariaLabel?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StatusIndicator.d.ts","sourceRoot":"","sources":["../../../src/components/editor/StatusIndicator.tsx"],"names":[],"mappings":"AAKA,KAAK,WAAW,GAAG,OAAO,GAAG,MAAM,GAAG,UAAU,GAAG,UAAU,CAAC;AAC9D,KAAK,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,UAAU,CAAC;AAE5C,UAAU,MAAM;IACd,KAAK,EAAE,WAAW,CAAC;CACpB;AAED,UAAU,WAAW;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,OAAO,CAAC;IACxB,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,UAAU,YAAY;IACpB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACrB;AAgBD,wBAAgB,mBAAmB,CAAC,EAAE,UAAU,EAAE,WAAW,EAAE,cAAc,EAAE,WAAW,EAAE,EAAE,WAAW,GAAG,YAAY,CAkCvH;
|
|
1
|
+
{"version":3,"file":"StatusIndicator.d.ts","sourceRoot":"","sources":["../../../src/components/editor/StatusIndicator.tsx"],"names":[],"mappings":"AAKA,KAAK,WAAW,GAAG,OAAO,GAAG,MAAM,GAAG,UAAU,GAAG,UAAU,CAAC;AAC9D,KAAK,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,UAAU,CAAC;AAE5C,UAAU,MAAM;IACd,KAAK,EAAE,WAAW,CAAC;CACpB;AAED,UAAU,WAAW;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,OAAO,CAAC;IACxB,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,UAAU,YAAY;IACpB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACrB;AAgBD,wBAAgB,mBAAmB,CAAC,EAAE,UAAU,EAAE,WAAW,EAAE,cAAc,EAAE,WAAW,EAAE,EAAE,WAAW,GAAG,YAAY,CAkCvH;AAED,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,WAAW,EAAE,MAAM,CAKvD,CAAC;AAKF,UAAU,oBAAoB;IAC5B,gFAAgF;IAChF,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,eAAe,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,SAA4B,EAAE,EAAE,oBAAoB,2CA2BhH;AAED,UAAU,oBAAoB;IAC5B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,OAAO,CAAC;IACxB,WAAW,EAAE,OAAO,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,kFAAkF;IAClF,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,wBAAgB,eAAe,CAAC,EAC9B,UAAU,EACV,WAAW,EACX,cAAc,EACd,WAAW,EACX,MAAM,EACN,KAAK,EACL,QAAQ,EACR,QAAQ,GACT,EAAE,oBAAoB,2CAsBtB"}
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
canDropBlock,
|
|
13
13
|
isBlockDragData,
|
|
14
14
|
useEditorContext
|
|
15
|
-
} from "../../chunk-
|
|
15
|
+
} from "../../chunk-HZTPF6OK.js";
|
|
16
16
|
import "../../chunk-GGYY3JQJ.js";
|
|
17
17
|
import {
|
|
18
18
|
CheckboxBox
|
|
@@ -71,7 +71,8 @@ function IndicatorPill({
|
|
|
71
71
|
className,
|
|
72
72
|
buttonClassName,
|
|
73
73
|
ariaLabel,
|
|
74
|
-
disabled
|
|
74
|
+
disabled,
|
|
75
|
+
placement = "bottom"
|
|
75
76
|
}) {
|
|
76
77
|
const [hoverOpen, setHoverOpen] = useState(false);
|
|
77
78
|
const [clickOpen, setClickOpen] = useState(false);
|
|
@@ -154,6 +155,7 @@ function IndicatorPill({
|
|
|
154
155
|
isOpen: clickOpen,
|
|
155
156
|
onClose: handleClickClose,
|
|
156
157
|
anchorRef: buttonRef,
|
|
158
|
+
placement,
|
|
157
159
|
className: "min-w-full max-w-xs",
|
|
158
160
|
children: typeof clickContent === "function" ? clickContent(handleClickClose) : clickContent
|
|
159
161
|
}
|
|
@@ -272,30 +274,34 @@ function StatusIndicator({
|
|
|
272
274
|
}
|
|
273
275
|
|
|
274
276
|
// src/components/editor/AudienceIndicator.tsx
|
|
275
|
-
import { Check as Check2 } from "lucide-react";
|
|
277
|
+
import { Check as Check2, Minus } from "lucide-react";
|
|
276
278
|
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
277
|
-
function AudienceChecklist({ access, audiences, onToggle }) {
|
|
279
|
+
function AudienceChecklist({ access, audiences, onToggle, partial = [] }) {
|
|
278
280
|
if (audiences.length === 0) {
|
|
279
281
|
return /* @__PURE__ */ jsx4("div", { className: "px-3 py-2 text-xs text-base-contrast-light whitespace-nowrap", children: "No audiences configured. Add audiences in Site Settings \u2192 Viewer Access." });
|
|
280
282
|
}
|
|
281
283
|
return /* @__PURE__ */ jsx4("ul", { role: "list", className: "w-full overflow-hidden py-1", children: audiences.map((a) => {
|
|
282
284
|
const checked = access.includes(a.name);
|
|
285
|
+
const mixed = !checked && partial.includes(a.name);
|
|
283
286
|
return /* @__PURE__ */ jsx4("li", { children: /* @__PURE__ */ jsxs4(
|
|
284
287
|
PopoverItem,
|
|
285
288
|
{
|
|
286
289
|
role: "checkbox",
|
|
287
|
-
"aria-checked": checked,
|
|
290
|
+
"aria-checked": mixed ? "mixed" : checked,
|
|
288
291
|
onClick: () => onToggle(a.name),
|
|
289
292
|
children: [
|
|
290
|
-
/* @__PURE__ */
|
|
293
|
+
/* @__PURE__ */ jsxs4(
|
|
291
294
|
"span",
|
|
292
295
|
{
|
|
293
296
|
"aria-hidden": "true",
|
|
294
297
|
className: cn(
|
|
295
298
|
"flex h-5 w-5 shrink-0 items-center justify-center rounded border transition-colors",
|
|
296
|
-
checked ? "border-primary bg-primary text-primary-contrast" : "border-base-300 bg-base"
|
|
299
|
+
checked || mixed ? "border-primary bg-primary text-primary-contrast" : "border-base-300 bg-base"
|
|
297
300
|
),
|
|
298
|
-
children:
|
|
301
|
+
children: [
|
|
302
|
+
checked && /* @__PURE__ */ jsx4(Check2, { size: 14, strokeWidth: 3 }),
|
|
303
|
+
mixed && /* @__PURE__ */ jsx4(Minus, { size: 14, strokeWidth: 3 })
|
|
304
|
+
]
|
|
299
305
|
}
|
|
300
306
|
),
|
|
301
307
|
/* @__PURE__ */ jsx4(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
allSectionDefs
|
|
3
|
-
} from "../../chunk-
|
|
4
|
-
import "../../chunk-
|
|
3
|
+
} from "../../chunk-UZBFVVTC.js";
|
|
4
|
+
import "../../chunk-HZTPF6OK.js";
|
|
5
5
|
import "../../chunk-UNFPATDE.js";
|
|
6
6
|
import "../../chunk-GGYY3JQJ.js";
|
|
7
7
|
import "../../chunk-RIN2QCYJ.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
allSectionDefs
|
|
3
|
-
} from "../../chunk-
|
|
4
|
-
import "../../chunk-
|
|
3
|
+
} from "../../chunk-UZBFVVTC.js";
|
|
4
|
+
import "../../chunk-HZTPF6OK.js";
|
|
5
5
|
import "../../chunk-UNFPATDE.js";
|
|
6
6
|
import "../../chunk-GGYY3JQJ.js";
|
|
7
7
|
import "../../chunk-RIN2QCYJ.js";
|
|
@@ -13,7 +13,7 @@ interface PopoverProps {
|
|
|
13
13
|
* panel wider than the sidebar is clipped and adds a horizontal scrollbar.
|
|
14
14
|
*/
|
|
15
15
|
portal?: boolean;
|
|
16
|
-
/** Which side of the anchor to open toward.
|
|
16
|
+
/** Which side of the anchor to open toward. */
|
|
17
17
|
placement?: "bottom" | "top";
|
|
18
18
|
}
|
|
19
19
|
export declare function Popover({ isOpen, onClose, anchorRef, children, align, className, portal, placement, }: PopoverProps): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Popover.d.ts","sourceRoot":"","sources":["../../../src/components/shared/Popover.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAgD,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAMrG,UAAU,YAAY;IACpB,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,SAAS,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IACzC,QAAQ,EAAE,SAAS,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,GAAG,KAAK,GAAG,MAAM,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB
|
|
1
|
+
{"version":3,"file":"Popover.d.ts","sourceRoot":"","sources":["../../../src/components/shared/Popover.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAgD,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAMrG,UAAU,YAAY;IACpB,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,SAAS,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IACzC,QAAQ,EAAE,SAAS,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,GAAG,KAAK,GAAG,MAAM,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,+CAA+C;IAC/C,SAAS,CAAC,EAAE,QAAQ,GAAG,KAAK,CAAC;CAC9B;AAED,wBAAgB,OAAO,CAAC,EACtB,MAAM,EACN,OAAO,EACP,SAAS,EACT,QAAQ,EACR,KAAc,EACd,SAAS,EACT,MAAc,EACd,SAAoB,GACrB,EAAE,YAAY,kDA2Jd"}
|
|
@@ -15,7 +15,7 @@ interface Props {
|
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
17
|
* One section as a compact card: type over label, with a thumbnail cluster.
|
|
18
|
-
* Shared by the
|
|
18
|
+
* Shared by the Edit sections modal (which wraps it in a drag handle + drop
|
|
19
19
|
* indicator) and the Audience Details page (which dims unavailable rows), so
|
|
20
20
|
* both read as the same list of the same site.
|
|
21
21
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EditorModal.d.ts","sourceRoot":"","sources":["../../../src/components/shell/EditorModal.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"EditorModal.d.ts","sourceRoot":"","sources":["../../../src/components/shell/EditorModal.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAyC,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAS9E,UAAU,gBAAgB;IACxB,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,UAAU,GAAG,OAAO,GAAG,UAAU,CAAC;IACzC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED,wBAAgB,WAAW,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,QAAgB,EAAE,IAAiB,EAAE,SAAiB,EAAE,MAAc,EAAE,QAAQ,EAAE,EAAE,gBAAgB,sCAwFzJ"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EditorShell.d.ts","sourceRoot":"","sources":["../../../src/components/shell/EditorShell.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAWjD,OAAO,EAAoC,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAC;AA4CtF,OAAO,KAAK,EAAE,eAAe,EAA2D,MAAM,oBAAoB,CAAC;AA6DnH,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AA8BxD,UAAU,KAAK;IACb,OAAO,EAAE,MAAM,CAAC;IAIhB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,YAAY,EAAE;QACZ,KAAK,EAAE,OAAO,CAAC;QACf,aAAa,EAAE,OAAO,CAAC;QACvB,YAAY,EAAE,OAAO,CAAC;QACtB,cAAc,EAAE,OAAO,CAAC;QACxB,kBAAkB,EAAE,OAAO,CAAC;QAC5B,cAAc,EAAE,OAAO,CAAC;KACzB,CAAC;IACF,WAAW,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,GAAG,QAAQ,CAAA;KAAE,GAAG,IAAI,CAAC;IAIhE,sBAAsB,CAAC,EAAE,OAAO,CAAC;IAKjC,eAAe,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;CAC1C;AAQD,MAAM,CAAC,OAAO,UAAU,WAAW,CAAC,EAClC,OAAO,EACP,YAAY,EACZ,MAAM,EACN,SAAS,EAAE,gBAAgB,EAC3B,YAAY,EACZ,WAAW,EACX,sBAA8B,EAC9B,eAAe,GAChB,EAAE,KAAK,
|
|
1
|
+
{"version":3,"file":"EditorShell.d.ts","sourceRoot":"","sources":["../../../src/components/shell/EditorShell.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAWjD,OAAO,EAAoC,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAC;AA4CtF,OAAO,KAAK,EAAE,eAAe,EAA2D,MAAM,oBAAoB,CAAC;AA6DnH,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AA8BxD,UAAU,KAAK;IACb,OAAO,EAAE,MAAM,CAAC;IAIhB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,YAAY,EAAE;QACZ,KAAK,EAAE,OAAO,CAAC;QACf,aAAa,EAAE,OAAO,CAAC;QACvB,YAAY,EAAE,OAAO,CAAC;QACtB,cAAc,EAAE,OAAO,CAAC;QACxB,kBAAkB,EAAE,OAAO,CAAC;QAC5B,cAAc,EAAE,OAAO,CAAC;KACzB,CAAC;IACF,WAAW,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,GAAG,QAAQ,CAAA;KAAE,GAAG,IAAI,CAAC;IAIhE,sBAAsB,CAAC,EAAE,OAAO,CAAC;IAKjC,eAAe,CAAC,EAAE,eAAe,GAAG,IAAI,CAAC;CAC1C;AAQD,MAAM,CAAC,OAAO,UAAU,WAAW,CAAC,EAClC,OAAO,EACP,YAAY,EACZ,MAAM,EACN,SAAS,EAAE,gBAAgB,EAC3B,YAAY,EACZ,WAAW,EACX,sBAA8B,EAC9B,eAAe,GAChB,EAAE,KAAK,2CAgsEP;AA8ED;6EAC6E;AAC7E,wBAAgB,kBAAkB,CAChC,YAAY,EAAE,WAAW,CAAC,YAAY,CAAC,EACvC,aAAa,EAAE,MAAM,GAAG,IAAI,EAC5B,eAAe,EAAE,MAAM,GAAG,IAAI;;;;;;;;;EAK/B"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drawnagency/primitives",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.120",
|
|
4
4
|
"description": "Browser-safe foundation of the Brand Portal framework: Zod schemas, section components, editor shell, hooks, pure libs, collab contract.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Check } from "lucide-react";
|
|
1
|
+
import { Check, Minus } from "lucide-react";
|
|
2
2
|
import { cn } from "../../lib/cn";
|
|
3
3
|
import { IndicatorPill } from "./IndicatorPill";
|
|
4
4
|
import { PopoverItem } from "../shared/PopoverItem";
|
|
@@ -8,9 +8,11 @@ interface AudienceChecklistProps {
|
|
|
8
8
|
access: string[];
|
|
9
9
|
audiences: Audience[];
|
|
10
10
|
onToggle: (name: string) => void;
|
|
11
|
+
/** Bulk edit: audiences only some of the selected sections have. */
|
|
12
|
+
partial?: string[];
|
|
11
13
|
}
|
|
12
14
|
|
|
13
|
-
export function AudienceChecklist({ access, audiences, onToggle }: AudienceChecklistProps) {
|
|
15
|
+
export function AudienceChecklist({ access, audiences, onToggle, partial = [] }: AudienceChecklistProps) {
|
|
14
16
|
if (audiences.length === 0) {
|
|
15
17
|
return (
|
|
16
18
|
<div className="px-3 py-2 text-xs text-base-contrast-light whitespace-nowrap">
|
|
@@ -22,23 +24,25 @@ export function AudienceChecklist({ access, audiences, onToggle }: AudienceCheck
|
|
|
22
24
|
<ul role="list" className="w-full overflow-hidden py-1">
|
|
23
25
|
{audiences.map((a) => {
|
|
24
26
|
const checked = access.includes(a.name);
|
|
27
|
+
const mixed = !checked && partial.includes(a.name);
|
|
25
28
|
return (
|
|
26
29
|
<li key={a.name}>
|
|
27
30
|
<PopoverItem
|
|
28
31
|
role="checkbox"
|
|
29
|
-
aria-checked={checked}
|
|
32
|
+
aria-checked={mixed ? "mixed" : checked}
|
|
30
33
|
onClick={() => onToggle(a.name)}
|
|
31
34
|
>
|
|
32
35
|
<span
|
|
33
36
|
aria-hidden="true"
|
|
34
37
|
className={cn(
|
|
35
38
|
"flex h-5 w-5 shrink-0 items-center justify-center rounded border transition-colors",
|
|
36
|
-
checked
|
|
39
|
+
checked || mixed
|
|
37
40
|
? "border-primary bg-primary text-primary-contrast"
|
|
38
41
|
: "border-base-300 bg-base",
|
|
39
42
|
)}
|
|
40
43
|
>
|
|
41
44
|
{checked && <Check size={14} strokeWidth={3} />}
|
|
45
|
+
{mixed && <Minus size={14} strokeWidth={3} />}
|
|
42
46
|
</span>
|
|
43
47
|
<span
|
|
44
48
|
className="h-3 w-3 shrink-0 rounded-full border border-base-200"
|
|
@@ -19,6 +19,8 @@ interface IndicatorPillProps {
|
|
|
19
19
|
/** Display-only mode: dots/label still render but the popovers never open
|
|
20
20
|
* (e.g. while another editor holds the section lock). */
|
|
21
21
|
disabled?: boolean;
|
|
22
|
+
/** Open the click popover above the pill — for pills in a bottom-pinned bar. */
|
|
23
|
+
placement?: "bottom" | "top";
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
export function IndicatorPill({
|
|
@@ -30,6 +32,7 @@ export function IndicatorPill({
|
|
|
30
32
|
buttonClassName,
|
|
31
33
|
ariaLabel,
|
|
32
34
|
disabled,
|
|
35
|
+
placement = "bottom",
|
|
33
36
|
}: IndicatorPillProps) {
|
|
34
37
|
const [hoverOpen, setHoverOpen] = useState(false);
|
|
35
38
|
const [clickOpen, setClickOpen] = useState(false);
|
|
@@ -123,6 +126,7 @@ export function IndicatorPill({
|
|
|
123
126
|
isOpen={clickOpen}
|
|
124
127
|
onClose={handleClickClose}
|
|
125
128
|
anchorRef={buttonRef}
|
|
129
|
+
placement={placement}
|
|
126
130
|
className="min-w-full max-w-xs"
|
|
127
131
|
>
|
|
128
132
|
{typeof clickContent === "function" ? clickContent(handleClickClose) : clickContent}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { useRef, useState } from "react";
|
|
2
|
+
import { Check, ChevronDown, Lock, Minus, X } from "lucide-react";
|
|
3
|
+
import type { LoadedSection } from "../../lib/loader";
|
|
4
|
+
import { getSection } from "../../lib/registry";
|
|
5
|
+
import type { MediaManifest } from "../../media/types";
|
|
6
|
+
import type { Audience } from "../../auth/types";
|
|
7
|
+
import { DragHandle } from "./DragHandle";
|
|
8
|
+
import { useSortableRow } from "../../hooks/useSortableRow";
|
|
9
|
+
import { DropEdgeIndicator } from "./DropEdgeIndicator";
|
|
10
|
+
import { IndicatorPill } from "./IndicatorPill";
|
|
11
|
+
import { StatusRadioList, dotColorClasses } from "./StatusIndicator";
|
|
12
|
+
import { AudienceChecklist } from "./AudienceIndicator";
|
|
13
|
+
import { SectionSummaryRow } from "../shared/SectionSummaryRow";
|
|
14
|
+
import { Popover } from "../shared/Popover";
|
|
15
|
+
import { PopoverItem } from "../shared/PopoverItem";
|
|
16
|
+
import { toThumbViews } from "../../lib/thumb-views";
|
|
17
|
+
import { cn } from "../../lib/cn";
|
|
18
|
+
|
|
19
|
+
type Status = "draft" | "live" | "archived";
|
|
20
|
+
const STATUSES: Status[] = ["draft", "live", "archived"];
|
|
21
|
+
type Coverage = "all" | "some" | "none";
|
|
22
|
+
|
|
23
|
+
interface SectionBulkEditModalProps {
|
|
24
|
+
sections: LoadedSection[];
|
|
25
|
+
mediaManifest: MediaManifest;
|
|
26
|
+
audiences: Audience[];
|
|
27
|
+
/** Sections another editor holds. They stay draggable but can't be selected,
|
|
28
|
+
* matching the canvas, where a held section's status and audience are locked. */
|
|
29
|
+
locks: ReadonlyMap<string, { email: string }>;
|
|
30
|
+
onReorder: (fromIndex: number, toIndex: number) => void;
|
|
31
|
+
onStatusChange: (sectionId: string, status: Status) => void;
|
|
32
|
+
onAccessChange: (sectionId: string, access: string[]) => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function audienceLabel(selected: Audience[]): string {
|
|
36
|
+
if (selected.length === 0) return "No audience";
|
|
37
|
+
if (selected.length === 1) return selected[0].displayName;
|
|
38
|
+
return `${selected.length} audiences`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function coverage(sections: LoadedSection[], name: string): Coverage {
|
|
42
|
+
const n = sections.filter((s) => s.meta.access.includes(name)).length;
|
|
43
|
+
if (n === 0) return "none";
|
|
44
|
+
return n === sections.length ? "all" : "some";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function CheckBox({ state }: { state: Coverage }) {
|
|
48
|
+
return (
|
|
49
|
+
<span
|
|
50
|
+
aria-hidden="true"
|
|
51
|
+
className={cn(
|
|
52
|
+
"flex h-4 w-4 shrink-0 items-center justify-center rounded border transition-colors",
|
|
53
|
+
state === "none" ? "border-base-300 bg-base" : "border-primary bg-primary text-primary-contrast",
|
|
54
|
+
)}
|
|
55
|
+
>
|
|
56
|
+
{state === "all" && <Check size={12} strokeWidth={3} />}
|
|
57
|
+
{state === "some" && <Minus size={12} strokeWidth={3} />}
|
|
58
|
+
</span>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function SectionRow({
|
|
63
|
+
section,
|
|
64
|
+
index,
|
|
65
|
+
mediaManifest,
|
|
66
|
+
audiences,
|
|
67
|
+
selected,
|
|
68
|
+
lockedBy,
|
|
69
|
+
onToggle,
|
|
70
|
+
onReorder,
|
|
71
|
+
}: {
|
|
72
|
+
section: LoadedSection;
|
|
73
|
+
index: number;
|
|
74
|
+
mediaManifest: MediaManifest;
|
|
75
|
+
audiences: Audience[];
|
|
76
|
+
selected: boolean;
|
|
77
|
+
lockedBy: string | null;
|
|
78
|
+
onToggle: (range: boolean) => void;
|
|
79
|
+
onReorder: (fromIndex: number, toIndex: number) => void;
|
|
80
|
+
}) {
|
|
81
|
+
const { rowRef, handleRef, isDragging, closestEdge } = useSortableRow<HTMLDivElement, HTMLButtonElement>({
|
|
82
|
+
index,
|
|
83
|
+
dragType: "ordering-row",
|
|
84
|
+
onReorder,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const definition = getSection(section.section.type);
|
|
88
|
+
const label = definition?.getLabel?.(section.section) ?? definition?.label ?? section.section.type;
|
|
89
|
+
const typeLabel = definition?.label ?? section.section.type;
|
|
90
|
+
const status = section.meta.status as Status;
|
|
91
|
+
const access = audiences.filter((a) => section.meta.access.includes(a.name));
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<div ref={rowRef} className={cn("relative flex items-center gap-1", isDragging && "opacity-50")}>
|
|
95
|
+
<DropEdgeIndicator edge={closestEdge} />
|
|
96
|
+
|
|
97
|
+
{lockedBy ? (
|
|
98
|
+
<span title={`Being edited by ${lockedBy}`} className="flex h-8 w-8 shrink-0 items-center justify-center text-base-contrast-light">
|
|
99
|
+
<Lock size={14} aria-label={`${label} is being edited by ${lockedBy}`} />
|
|
100
|
+
</span>
|
|
101
|
+
) : (
|
|
102
|
+
<button
|
|
103
|
+
type="button"
|
|
104
|
+
role="checkbox"
|
|
105
|
+
aria-checked={selected}
|
|
106
|
+
aria-label={`Select ${label}`}
|
|
107
|
+
onClick={(e) => onToggle(e.shiftKey)}
|
|
108
|
+
className="flex h-8 w-8 shrink-0 cursor-pointer items-center justify-center"
|
|
109
|
+
>
|
|
110
|
+
<CheckBox state={selected ? "all" : "none"} />
|
|
111
|
+
</button>
|
|
112
|
+
)}
|
|
113
|
+
|
|
114
|
+
<div className="shrink-0 [&_[role=tooltip]]:hidden">
|
|
115
|
+
<DragHandle ref={handleRef} />
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<SectionSummaryRow
|
|
119
|
+
label={label}
|
|
120
|
+
typeLabel={typeLabel}
|
|
121
|
+
thumbnails={toThumbViews(definition?.getThumbnails?.(section.section) ?? [], mediaManifest)}
|
|
122
|
+
className={cn(selected && "border-primary/60 bg-primary/5", isDragging && "outline outline-2 outline-primary/50")}
|
|
123
|
+
trailing={
|
|
124
|
+
<div className="flex shrink-0 flex-col items-end gap-0.5 text-xs text-base-contrast-light">
|
|
125
|
+
<span className="inline-flex items-center gap-1.5 capitalize">
|
|
126
|
+
<span aria-hidden="true" className={cn("h-2.5 w-2.5 rounded-full border border-base-200", dotColorClasses[status])} />
|
|
127
|
+
{status}
|
|
128
|
+
</span>
|
|
129
|
+
<span className="inline-flex items-center gap-1.5">
|
|
130
|
+
{access.length > 0 && (
|
|
131
|
+
<span aria-hidden="true" className="flex -space-x-1">
|
|
132
|
+
{access.slice(0, 3).map((a) => (
|
|
133
|
+
<span key={a.name} className="h-2.5 w-2.5 rounded-full border border-base" style={{ backgroundColor: a.color ?? "#9ca3af" }} />
|
|
134
|
+
))}
|
|
135
|
+
</span>
|
|
136
|
+
)}
|
|
137
|
+
{audienceLabel(access)}
|
|
138
|
+
</span>
|
|
139
|
+
</div>
|
|
140
|
+
}
|
|
141
|
+
/>
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The toolbar's "Edit sections" panel: drag rows to reorder, or tick several and
|
|
148
|
+
* set their status and audience at once from the bar that appears. Every change
|
|
149
|
+
* applies to the working copy immediately, like the per-section pills it reuses.
|
|
150
|
+
*/
|
|
151
|
+
export function SectionBulkEditModal({
|
|
152
|
+
sections,
|
|
153
|
+
mediaManifest,
|
|
154
|
+
audiences,
|
|
155
|
+
locks,
|
|
156
|
+
onReorder,
|
|
157
|
+
onStatusChange,
|
|
158
|
+
onAccessChange,
|
|
159
|
+
}: SectionBulkEditModalProps) {
|
|
160
|
+
const [selectedIds, setSelectedIds] = useState<ReadonlySet<string>>(new Set());
|
|
161
|
+
const [anchorId, setAnchorId] = useState<string | null>(null);
|
|
162
|
+
const [selectMenuOpen, setSelectMenuOpen] = useState(false);
|
|
163
|
+
const selectMenuRef = useRef<HTMLButtonElement>(null);
|
|
164
|
+
|
|
165
|
+
// Derived, so a section deleted or newly locked by a peer drops out of the selection.
|
|
166
|
+
const selectable = sections.filter((s) => !locks.has(s.section.id));
|
|
167
|
+
const selected = selectable.filter((s) => selectedIds.has(s.section.id));
|
|
168
|
+
const allSelected = selectable.length > 0 && selected.length === selectable.length;
|
|
169
|
+
|
|
170
|
+
function select(ids: string[]) {
|
|
171
|
+
setSelectedIds(new Set(ids));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function toggle(id: string, range: boolean) {
|
|
175
|
+
const ids = selectable.map((s) => s.section.id);
|
|
176
|
+
const next = new Set(selectedIds);
|
|
177
|
+
if (range && anchorId && ids.includes(anchorId)) {
|
|
178
|
+
const [a, b] = [ids.indexOf(anchorId), ids.indexOf(id)].sort((x, y) => x - y);
|
|
179
|
+
for (const rangeId of ids.slice(a, b + 1)) next.add(rangeId);
|
|
180
|
+
} else {
|
|
181
|
+
if (next.has(id)) next.delete(id);
|
|
182
|
+
else next.add(id);
|
|
183
|
+
setAnchorId(id);
|
|
184
|
+
}
|
|
185
|
+
setSelectedIds(next);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const statuses = STATUSES.filter((s) => selected.some((x) => x.meta.status === s));
|
|
189
|
+
const commonStatus = statuses.length === 1 ? statuses[0] : null;
|
|
190
|
+
const coverageByName = new Map(audiences.map((a) => [a.name, coverage(selected, a.name)]));
|
|
191
|
+
const fullAudiences = audiences.filter((a) => coverageByName.get(a.name) === "all");
|
|
192
|
+
const partialAudiences = audiences.filter((a) => coverageByName.get(a.name) === "some");
|
|
193
|
+
|
|
194
|
+
function toggleAudience(name: string) {
|
|
195
|
+
// A partly-covered audience is added to every selected section; a fully
|
|
196
|
+
// covered one is removed from all of them.
|
|
197
|
+
const add = coverageByName.get(name) !== "all";
|
|
198
|
+
for (const s of selected) {
|
|
199
|
+
const has = s.meta.access.includes(name);
|
|
200
|
+
if (add && !has) onAccessChange(s.section.id, [...s.meta.access, name]);
|
|
201
|
+
if (!add && has) onAccessChange(s.section.id, s.meta.access.filter((n) => n !== name));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const selectMenu: { label: string; ids: () => string[]; status?: Status }[] = [
|
|
206
|
+
{ label: "All", ids: () => selectable.map((s) => s.section.id) },
|
|
207
|
+
{ label: "None", ids: () => [] },
|
|
208
|
+
...STATUSES.map((status) => ({
|
|
209
|
+
label: status,
|
|
210
|
+
status,
|
|
211
|
+
ids: () => selectable.filter((s) => s.meta.status === status).map((s) => s.section.id),
|
|
212
|
+
})),
|
|
213
|
+
];
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
<div className="flex min-h-0 flex-1 flex-col">
|
|
217
|
+
<div className="-mx-6 mb-3 flex items-center border-b border-base-200 px-6 pb-2 text-xs text-base-contrast-light">
|
|
218
|
+
<div className="relative flex items-center rounded hover:bg-base-accent">
|
|
219
|
+
<button
|
|
220
|
+
type="button"
|
|
221
|
+
role="checkbox"
|
|
222
|
+
aria-checked={allSelected ? true : selected.length > 0 ? "mixed" : false}
|
|
223
|
+
aria-label={allSelected ? "Deselect all" : "Select all"}
|
|
224
|
+
onClick={() => select(allSelected ? [] : selectable.map((s) => s.section.id))}
|
|
225
|
+
className="flex cursor-pointer items-center py-1.5 pr-1 pl-2"
|
|
226
|
+
>
|
|
227
|
+
<CheckBox state={allSelected ? "all" : selected.length > 0 ? "some" : "none"} />
|
|
228
|
+
</button>
|
|
229
|
+
<button
|
|
230
|
+
ref={selectMenuRef}
|
|
231
|
+
type="button"
|
|
232
|
+
aria-haspopup="dialog"
|
|
233
|
+
aria-expanded={selectMenuOpen}
|
|
234
|
+
onClick={() => setSelectMenuOpen((open) => !open)}
|
|
235
|
+
className="flex cursor-pointer items-center gap-1 py-1.5 pr-2 pl-1 hover:text-base-contrast"
|
|
236
|
+
>
|
|
237
|
+
Select
|
|
238
|
+
<ChevronDown size={12} aria-hidden="true" />
|
|
239
|
+
</button>
|
|
240
|
+
<Popover isOpen={selectMenuOpen} onClose={() => setSelectMenuOpen(false)} anchorRef={selectMenuRef} align="start" className="min-w-40 py-1">
|
|
241
|
+
{selectMenu.map(({ label, ids, status }) => (
|
|
242
|
+
<PopoverItem
|
|
243
|
+
key={label}
|
|
244
|
+
className={cn(status === "draft" && "mt-1 border-t border-base-200 pt-2")}
|
|
245
|
+
onClick={() => {
|
|
246
|
+
select(ids());
|
|
247
|
+
setSelectMenuOpen(false);
|
|
248
|
+
}}
|
|
249
|
+
>
|
|
250
|
+
{status && <span aria-hidden="true" className={cn("h-2.5 w-2.5 rounded-full border border-base-200", dotColorClasses[status])} />}
|
|
251
|
+
<span className="flex-1 capitalize">{label}</span>
|
|
252
|
+
{status && <span className="tabular-nums text-base-contrast-light">{ids().length}</span>}
|
|
253
|
+
</PopoverItem>
|
|
254
|
+
))}
|
|
255
|
+
</Popover>
|
|
256
|
+
</div>
|
|
257
|
+
{selected.length > 0 && <span className="ml-2">{selected.length} of {sections.length} selected</span>}
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
<div className="flex flex-col gap-2 pb-2">
|
|
261
|
+
{sections.map((loadedSection, index) => {
|
|
262
|
+
const id = loadedSection.section.id;
|
|
263
|
+
return (
|
|
264
|
+
<SectionRow
|
|
265
|
+
key={id}
|
|
266
|
+
section={loadedSection}
|
|
267
|
+
index={index}
|
|
268
|
+
mediaManifest={mediaManifest}
|
|
269
|
+
audiences={audiences}
|
|
270
|
+
selected={selectedIds.has(id)}
|
|
271
|
+
lockedBy={locks.get(id)?.email ?? null}
|
|
272
|
+
onToggle={(range) => toggle(id, range)}
|
|
273
|
+
onReorder={onReorder}
|
|
274
|
+
/>
|
|
275
|
+
);
|
|
276
|
+
})}
|
|
277
|
+
</div>
|
|
278
|
+
|
|
279
|
+
{selected.length > 0 && (
|
|
280
|
+
<div className="sticky bottom-0 -mx-6 -mb-4 mt-auto flex items-center gap-2 border-t border-base-200 bg-base px-6 py-3 text-sm shadow-[0_-4px_12px_rgba(0,0,0,0.06)]">
|
|
281
|
+
<span className="font-medium text-base-contrast">{selected.length} selected</span>
|
|
282
|
+
<button
|
|
283
|
+
type="button"
|
|
284
|
+
onClick={() => select([])}
|
|
285
|
+
aria-label="Clear selection"
|
|
286
|
+
className="cursor-pointer rounded p-0.5 text-base-contrast-light hover:bg-base-accent"
|
|
287
|
+
>
|
|
288
|
+
<X size={14} />
|
|
289
|
+
</button>
|
|
290
|
+
<div className="ml-auto flex items-center gap-2">
|
|
291
|
+
<IndicatorPill
|
|
292
|
+
placement="top"
|
|
293
|
+
dots={statuses.map((s) => ({ className: dotColorClasses[s] }))}
|
|
294
|
+
label={commonStatus ? commonStatus.charAt(0).toUpperCase() + commonStatus.slice(1) : "Mixed status"}
|
|
295
|
+
ariaLabel="Status of selected sections"
|
|
296
|
+
buttonClassName="text-base-contrast-light"
|
|
297
|
+
clickContent={(onClose) => (
|
|
298
|
+
<StatusRadioList
|
|
299
|
+
status={commonStatus}
|
|
300
|
+
ariaLabel="Status for selected sections"
|
|
301
|
+
onClose={onClose}
|
|
302
|
+
onChange={(status) => {
|
|
303
|
+
for (const s of selected) if (s.meta.status !== status) onStatusChange(s.section.id, status);
|
|
304
|
+
}}
|
|
305
|
+
/>
|
|
306
|
+
)}
|
|
307
|
+
/>
|
|
308
|
+
<IndicatorPill
|
|
309
|
+
placement="top"
|
|
310
|
+
dots={[...fullAudiences, ...partialAudiences].map((a) => ({ color: a.color ?? "#9ca3af" }))}
|
|
311
|
+
label={partialAudiences.length > 0 ? "Mixed audiences" : audienceLabel(fullAudiences)}
|
|
312
|
+
ariaLabel="Audience of selected sections"
|
|
313
|
+
buttonClassName={fullAudiences.length + partialAudiences.length === 0 ? "text-base-contrast-light" : undefined}
|
|
314
|
+
clickContent={
|
|
315
|
+
<AudienceChecklist
|
|
316
|
+
audiences={audiences}
|
|
317
|
+
access={fullAudiences.map((a) => a.name)}
|
|
318
|
+
partial={partialAudiences.map((a) => a.name)}
|
|
319
|
+
onToggle={toggleAudience}
|
|
320
|
+
/>
|
|
321
|
+
}
|
|
322
|
+
/>
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
)}
|
|
326
|
+
</div>
|
|
327
|
+
);
|
|
328
|
+
}
|
|
@@ -73,7 +73,7 @@ export function deriveStatusDisplay({ mainStatus, savedStatus, contentDiffers, i
|
|
|
73
73
|
};
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
const dotColorClasses: Record<StatusColor, string> = {
|
|
76
|
+
export const dotColorClasses: Record<StatusColor, string> = {
|
|
77
77
|
live: "bg-status-live-dot",
|
|
78
78
|
draft: "bg-status-draft-dot",
|
|
79
79
|
archived: "bg-white border-status-archived-dot-border",
|
|
@@ -84,7 +84,8 @@ const dotColorClasses: Record<StatusColor, string> = {
|
|
|
84
84
|
const STATUSES: Status[] = ["draft", "live", "archived"];
|
|
85
85
|
|
|
86
86
|
interface StatusRadioListProps {
|
|
87
|
-
|
|
87
|
+
/** null when several sections with different statuses are being set at once. */
|
|
88
|
+
status: Status | null;
|
|
88
89
|
onChange: (status: Status) => void;
|
|
89
90
|
onClose?: () => void;
|
|
90
91
|
ariaLabel?: string;
|
|
@@ -18,7 +18,7 @@ interface PopoverProps {
|
|
|
18
18
|
* panel wider than the sidebar is clipped and adds a horizontal scrollbar.
|
|
19
19
|
*/
|
|
20
20
|
portal?: boolean;
|
|
21
|
-
/** Which side of the anchor to open toward.
|
|
21
|
+
/** Which side of the anchor to open toward. */
|
|
22
22
|
placement?: "bottom" | "top";
|
|
23
23
|
}
|
|
24
24
|
|
|
@@ -111,7 +111,14 @@ export function Popover({
|
|
|
111
111
|
// this the one keypress dismisses both, losing the navigator's zoom stack
|
|
112
112
|
// — inconsistent with the X button, and with the mousedown exemption below
|
|
113
113
|
// that exists for exactly this stacking.
|
|
114
|
-
useStackedEscape(onClose, { enabled: isOpen });
|
|
114
|
+
const escapeLayerRef = useStackedEscape<HTMLElement>(onClose, { enabled: isOpen });
|
|
115
|
+
// A popover living INSIDE an EditorModal treats that modal's layer as its
|
|
116
|
+
// own, so only a modal stacked above it (one opened from this popover) takes
|
|
117
|
+
// Escape away. The modal, for its part, yields Escape while one of its
|
|
118
|
+
// popovers is open (EditorModal's `before`).
|
|
119
|
+
useLayoutEffect(() => {
|
|
120
|
+
escapeLayerRef.current = isOpen ? anchorRef.current?.closest<HTMLElement>("[data-modal-layer]") ?? null : null;
|
|
121
|
+
}, [isOpen, anchorRef, escapeLayerRef]);
|
|
115
122
|
|
|
116
123
|
useEffect(() => {
|
|
117
124
|
if (!isOpen) return;
|
|
@@ -122,7 +129,10 @@ export function Popover({
|
|
|
122
129
|
// A modal opened from inside this popover portals to <body>, so it is
|
|
123
130
|
// "outside" by DOM containment. Closing on its mousedowns would unmount
|
|
124
131
|
// the modal along with the popover, and would break text selection in it.
|
|
125
|
-
|
|
132
|
+
// The modal this popover itself sits in doesn't count: a click elsewhere
|
|
133
|
+
// in it is an ordinary outside click.
|
|
134
|
+
const layer = target instanceof Element ? target.closest("[data-modal-layer]") : null;
|
|
135
|
+
if (layer && !layer.contains(anchorRef.current)) return;
|
|
126
136
|
onClose();
|
|
127
137
|
}
|
|
128
138
|
document.addEventListener("mousedown", onMouseDown);
|
|
@@ -145,6 +155,7 @@ export function Popover({
|
|
|
145
155
|
ref={panelRef}
|
|
146
156
|
role="dialog"
|
|
147
157
|
aria-modal="false"
|
|
158
|
+
data-popover=""
|
|
148
159
|
style={{
|
|
149
160
|
position: "fixed",
|
|
150
161
|
left: `${fixedPos.left}px`,
|
|
@@ -164,8 +175,10 @@ export function Popover({
|
|
|
164
175
|
ref={panelRef}
|
|
165
176
|
role="dialog"
|
|
166
177
|
aria-modal="false"
|
|
178
|
+
data-popover=""
|
|
167
179
|
className={cn(
|
|
168
|
-
"absolute
|
|
180
|
+
"absolute z-50 rounded-md border border-base-200 bg-base shadow-lg",
|
|
181
|
+
placement === "top" ? "bottom-full mb-1" : "top-full mt-1",
|
|
169
182
|
effectiveAlign === "end" ? "right-0" : "left-0",
|
|
170
183
|
className,
|
|
171
184
|
)}
|
|
@@ -20,7 +20,7 @@ const MAX_THUMBS = 3;
|
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* One section as a compact card: type over label, with a thumbnail cluster.
|
|
23
|
-
* Shared by the
|
|
23
|
+
* Shared by the Edit sections modal (which wraps it in a drag handle + drop
|
|
24
24
|
* indicator) and the Audience Details page (which dims unavailable rows), so
|
|
25
25
|
* both read as the same list of the same site.
|
|
26
26
|
*/
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useId, useRef, type ReactNode } from "react";
|
|
1
|
+
import { useCallback, useEffect, useId, useRef, type ReactNode } from "react";
|
|
2
2
|
import { createPortal } from "react-dom";
|
|
3
3
|
import { X } from "lucide-react";
|
|
4
4
|
import { IconButton } from "../shared/IconButton";
|
|
@@ -31,7 +31,10 @@ export function EditorModal({ isOpen, onClose, title, blocking = false, size = "
|
|
|
31
31
|
// Another EditorModal opened on top of this one (a ConfirmDialog, say) owns
|
|
32
32
|
// Escape — attaching layerRef to our own data-modal-layer element below is
|
|
33
33
|
// what lets the hook tell "another layer" from "just me".
|
|
34
|
-
|
|
34
|
+
// An open Popover inside this modal owns Escape: it closes first, the modal on
|
|
35
|
+
// the next press. Stable, so the keydown listener isn't re-bound every render.
|
|
36
|
+
const popoverOpenInside = useCallback(() => Boolean(panelRef.current?.querySelector("[data-popover]")), []);
|
|
37
|
+
const layerRef = useStackedEscape<HTMLDivElement>(onClose, { enabled: isOpen && !blocking, before: popoverOpenInside });
|
|
35
38
|
|
|
36
39
|
useEffect(() => {
|
|
37
40
|
if (!isOpen) return;
|
|
@@ -42,7 +42,7 @@ import { buildInsertMenuOptions, resolveInsertedContent } from "../../lib/insert
|
|
|
42
42
|
ensureSectionsRegistered();
|
|
43
43
|
import { BugReportFAB } from "./BugReportFAB";
|
|
44
44
|
import { SectionWrapper } from "../editor/SectionWrapper";
|
|
45
|
-
import {
|
|
45
|
+
import { SectionBulkEditModal } from "../editor/SectionBulkEditModal";
|
|
46
46
|
import { MoveSectionModal } from "../editor/MoveSectionModal";
|
|
47
47
|
import { SectionLayout } from "../sections/SectionLayout";
|
|
48
48
|
import {
|
|
@@ -113,7 +113,7 @@ import { FabStack } from "../shared/FabStack";
|
|
|
113
113
|
import { Tooltip } from "../shared/Tooltip";
|
|
114
114
|
import { SegmentedControl } from "../shared/SegmentedControl";
|
|
115
115
|
import { computeDisplaySections } from "./view-branch";
|
|
116
|
-
import { ImageIcon,
|
|
116
|
+
import { ImageIcon, ListChecks, FilesIcon, JoystickIcon, Settings, X, Eye, Pencil } from "lucide-react";
|
|
117
117
|
import { ErrorBoundary } from "../shared/ErrorBoundary";
|
|
118
118
|
import { HistoryToolbar } from "./HistoryToolbar";
|
|
119
119
|
import { RestoreModal } from "./RestoreModal";
|
|
@@ -244,7 +244,7 @@ export default function EditorShell({
|
|
|
244
244
|
useEffect(() => { deletedSectionsRef.current = deletedSections; }, [deletedSections]);
|
|
245
245
|
const [pendingDeleteSectionId, setPendingDeleteSectionId] = useState<string | null>(null);
|
|
246
246
|
// One shared aria-live region for every reorder — sections (via
|
|
247
|
-
// onReorderSections, the dispatch point shared by
|
|
247
|
+
// onReorderSections, the dispatch point shared by SectionBulkEditModal's
|
|
248
248
|
// drag and SectionWrapper's Move up/down) and pages/groups (via
|
|
249
249
|
// handleMoveNavEntry, shared the same way by PagesModal/NavGroupRow's drag
|
|
250
250
|
// and their own Move up/down). The section canvas's own per-block drag
|
|
@@ -2361,13 +2361,17 @@ export default function EditorShell({
|
|
|
2361
2361
|
<EditorModal
|
|
2362
2362
|
isOpen={showOrderingModal}
|
|
2363
2363
|
onClose={() => setShowOrderingModal(false)}
|
|
2364
|
-
title="
|
|
2364
|
+
title="Edit sections"
|
|
2365
2365
|
size="settings"
|
|
2366
2366
|
>
|
|
2367
|
-
<
|
|
2367
|
+
<SectionBulkEditModal
|
|
2368
2368
|
sections={activeSections}
|
|
2369
2369
|
mediaManifest={mediaManifest}
|
|
2370
|
+
audiences={pageAudiences}
|
|
2371
|
+
locks={collab.locks}
|
|
2370
2372
|
onReorder={onReorderSections}
|
|
2373
|
+
onStatusChange={onStatusChange}
|
|
2374
|
+
onAccessChange={onAccessChange}
|
|
2371
2375
|
/>
|
|
2372
2376
|
</EditorModal>
|
|
2373
2377
|
<EditorModal isOpen={showPagesModal} onClose={() => setShowPagesModal(false)} title="Pages" size="settings">
|
|
@@ -3132,8 +3136,8 @@ function EditorToolbar({
|
|
|
3132
3136
|
<Tooltip side="bottom" content="Media library">
|
|
3133
3137
|
<IconButton icon={<ImageIcon size={16} />} label="Media library" size="md" onClick={onMediaClick} className="border border-base-200 bg-base-accent" />
|
|
3134
3138
|
</Tooltip>
|
|
3135
|
-
<Tooltip side="bottom" content="
|
|
3136
|
-
<IconButton icon={<
|
|
3139
|
+
<Tooltip side="bottom" content="Edit sections">
|
|
3140
|
+
<IconButton icon={<ListChecks size={16} />} label="Edit sections" size="md" onClick={onOrderingClick} className="border border-base-200 bg-base-accent" />
|
|
3137
3141
|
</Tooltip>
|
|
3138
3142
|
<Tooltip side="bottom" content="Pages">
|
|
3139
3143
|
<IconButton icon={<FilesIcon size={16} />} label="Pages" size="md" onClick={onPagesClick} className="border border-base-200 bg-base-accent" />
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import type { LoadedSection } from "../../lib/loader";
|
|
2
|
-
import type { MediaManifest } from "../../media/types";
|
|
3
|
-
interface SectionOrderingModalProps {
|
|
4
|
-
sections: LoadedSection[];
|
|
5
|
-
mediaManifest: MediaManifest;
|
|
6
|
-
onReorder: (fromIndex: number, toIndex: number) => void;
|
|
7
|
-
}
|
|
8
|
-
export declare function SectionOrderingModal({ sections, mediaManifest, onReorder, }: SectionOrderingModalProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
-
export {};
|
|
10
|
-
//# sourceMappingURL=SectionOrderingModal.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"SectionOrderingModal.d.ts","sourceRoot":"","sources":["../../../src/components/editor/SectionOrderingModal.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAQvD,UAAU,yBAAyB;IACjC,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,aAAa,EAAE,aAAa,CAAC;IAC7B,SAAS,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACzD;AA+CD,wBAAgB,oBAAoB,CAAC,EACnC,QAAQ,EACR,aAAa,EACb,SAAS,GACV,EAAE,yBAAyB,2CAc3B"}
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import type { LoadedSection } from "../../lib/loader";
|
|
2
|
-
import { getSection } from "../../lib/registry";
|
|
3
|
-
import type { MediaManifest } from "../../media/types";
|
|
4
|
-
import { DragHandle } from "./DragHandle";
|
|
5
|
-
import { useSortableRow } from "../../hooks/useSortableRow";
|
|
6
|
-
import { DropEdgeIndicator } from "./DropEdgeIndicator";
|
|
7
|
-
import { SectionSummaryRow } from "../shared/SectionSummaryRow";
|
|
8
|
-
import { toThumbViews } from "../../lib/thumb-views";
|
|
9
|
-
import { cn } from "../../lib/cn";
|
|
10
|
-
|
|
11
|
-
interface SectionOrderingModalProps {
|
|
12
|
-
sections: LoadedSection[];
|
|
13
|
-
mediaManifest: MediaManifest;
|
|
14
|
-
onReorder: (fromIndex: number, toIndex: number) => void;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function SectionRow({
|
|
18
|
-
section,
|
|
19
|
-
index,
|
|
20
|
-
mediaManifest,
|
|
21
|
-
onReorder,
|
|
22
|
-
}: {
|
|
23
|
-
section: LoadedSection;
|
|
24
|
-
index: number;
|
|
25
|
-
mediaManifest: MediaManifest;
|
|
26
|
-
onReorder: (fromIndex: number, toIndex: number) => void;
|
|
27
|
-
}) {
|
|
28
|
-
const { rowRef, handleRef, isDragging, closestEdge } = useSortableRow<HTMLDivElement, HTMLButtonElement>({
|
|
29
|
-
index,
|
|
30
|
-
dragType: "ordering-row",
|
|
31
|
-
onReorder,
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
const definition = getSection(section.section.type);
|
|
35
|
-
const label = definition?.getLabel?.(section.section) ?? definition?.label ?? section.section.type;
|
|
36
|
-
const typeLabel = definition?.label ?? section.section.type;
|
|
37
|
-
|
|
38
|
-
return (
|
|
39
|
-
<div
|
|
40
|
-
ref={rowRef}
|
|
41
|
-
className={cn(
|
|
42
|
-
"relative flex items-center gap-1",
|
|
43
|
-
isDragging && "opacity-50",
|
|
44
|
-
)}
|
|
45
|
-
>
|
|
46
|
-
<DropEdgeIndicator edge={closestEdge} />
|
|
47
|
-
|
|
48
|
-
<div className="shrink-0 [&_[role=tooltip]]:hidden">
|
|
49
|
-
<DragHandle ref={handleRef} />
|
|
50
|
-
</div>
|
|
51
|
-
|
|
52
|
-
<SectionSummaryRow
|
|
53
|
-
label={label}
|
|
54
|
-
typeLabel={typeLabel}
|
|
55
|
-
thumbnails={toThumbViews(definition?.getThumbnails?.(section.section) ?? [], mediaManifest)}
|
|
56
|
-
className={cn(isDragging && "outline outline-2 outline-primary/50")}
|
|
57
|
-
/>
|
|
58
|
-
</div>
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function SectionOrderingModal({
|
|
63
|
-
sections,
|
|
64
|
-
mediaManifest,
|
|
65
|
-
onReorder,
|
|
66
|
-
}: SectionOrderingModalProps) {
|
|
67
|
-
return (
|
|
68
|
-
<div className="flex flex-col gap-2 overflow-y-auto">
|
|
69
|
-
{sections.map((loadedSection, index) => (
|
|
70
|
-
<SectionRow
|
|
71
|
-
key={loadedSection.section.id}
|
|
72
|
-
section={loadedSection}
|
|
73
|
-
index={index}
|
|
74
|
-
mediaManifest={mediaManifest}
|
|
75
|
-
onReorder={onReorder}
|
|
76
|
-
/>
|
|
77
|
-
))}
|
|
78
|
-
</div>
|
|
79
|
-
);
|
|
80
|
-
}
|