@drawnagency/primitives 0.1.118 → 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-O37AV27P.js → chunk-5JYHTHVR.js} +28 -0
- package/dist/{chunk-W72BPGR3.js → chunk-BZKHHUIN.js} +44 -8
- package/dist/{chunk-4NSOKEA7.js → chunk-HFHM4MNK.js} +12 -7
- package/dist/{chunk-CEFFP3CH.js → chunk-HZTPF6OK.js} +304 -83
- package/dist/{chunk-RIITGTV3.js → chunk-LQHBC7C6.js} +37 -2
- package/dist/{chunk-VY3IFGE3.js → chunk-QAMAIDXZ.js} +1 -1
- package/dist/{chunk-HRX3XJBU.js → chunk-RIN2QCYJ.js} +9 -11
- package/dist/{chunk-ZOTRHHUB.js → chunk-UNFPATDE.js} +8 -10
- package/dist/{chunk-ERLIL4RU.js → chunk-UZBFVVTC.js} +35 -37
- package/dist/{chunk-67P34WNW.js → chunk-ZKVQMIMJ.js} +3 -20
- package/dist/collab/rebase.js +4 -5
- package/dist/components/chat/ChatComposer.d.ts +0 -4
- package/dist/components/chat/ChatComposer.d.ts.map +1 -1
- package/dist/components/chat/ChatPanel.d.ts +2 -3
- package/dist/components/chat/ChatPanel.d.ts.map +1 -1
- package/dist/components/chat/markdown.d.ts +2 -3
- package/dist/components/chat/markdown.d.ts.map +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/PagesModal.d.ts +0 -2
- package/dist/components/editor/PagesModal.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 +28 -26
- package/dist/components/primitives/index.js +11 -15
- package/dist/components/sections/brand-guide.js +10 -13
- package/dist/components/sections/register-schemas.js +16 -21
- package/dist/components/sections/register.js +16 -21
- 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/AudienceChips.d.ts +1 -3
- package/dist/components/shell/AudienceChips.d.ts.map +1 -1
- package/dist/components/shell/EditorModal.d.ts.map +1 -1
- package/dist/components/shell/EditorShell.d.ts +0 -34
- package/dist/components/shell/EditorShell.d.ts.map +1 -1
- package/dist/components/shell/SiteSettingsModal.d.ts.map +1 -1
- package/dist/hooks/useSortableNavRow.d.ts +1 -1
- package/dist/hooks/useSortableRow.d.ts +1 -1
- package/dist/index.js +83 -90
- package/dist/lib/index-ops.js +5 -6
- package/dist/lib/index.js +27 -32
- package/dist/lib/pages.d.ts +0 -2
- package/dist/lib/pages.d.ts.map +1 -1
- package/dist/lib/sidecars.js +2 -2
- package/dist/lib/site-validation.js +13 -14
- package/dist/lib/storage-tier.js +4 -4
- package/dist/schemas/index.js +42 -45
- package/dist/schemas/site-config.d.ts +1 -277
- package/dist/schemas/site-config.d.ts.map +1 -1
- package/package.json +2 -42
- package/src/components/chat/ChatComposer.tsx +0 -4
- package/src/components/chat/ChatPanel.tsx +2 -3
- package/src/components/chat/markdown.ts +2 -3
- package/src/components/editor/AudienceIndicator.tsx +8 -4
- package/src/components/editor/IndicatorPill.tsx +4 -0
- package/src/components/editor/PagesModal.tsx +1 -15
- package/src/components/editor/SectionBulkEditModal.tsx +328 -0
- package/src/components/editor/StatusIndicator.tsx +7 -6
- package/src/components/shared/Popover.tsx +17 -4
- package/src/components/shared/SectionSummaryRow.tsx +1 -1
- package/src/components/shell/AudienceChips.tsx +1 -3
- package/src/components/shell/EditorModal.tsx +5 -2
- package/src/components/shell/EditorShell.tsx +15 -119
- package/src/components/shell/SiteSettingsModal.tsx +0 -11
- package/src/lib/pages.ts +0 -22
- package/src/schemas/index.ts +0 -4
- package/src/schemas/site-config.ts +4 -32
- package/dist/chunk-4TZJZU2T.js +0 -92
- package/dist/chunk-DIMDRBFK.js +0 -11
- package/dist/chunk-FV7JQRCN.js +0 -94
- package/dist/chunk-IIDKW7AV.js +0 -41
- package/dist/chunk-JCDZAFX2.js +0 -224
- package/dist/chunk-MM7KZS5C.js +0 -30
- package/dist/chunk-QQ3S3P5P.js +0 -38
- package/dist/chunk-TEOAKGZA.js +0 -784
- package/dist/chunk-VAYZJKVR.js +0 -209
- package/dist/components/editor/SectionOrderingModal.d.ts +0 -10
- package/dist/components/editor/SectionOrderingModal.d.ts.map +0 -1
- package/dist/components/shell/DocInspector.d.ts +0 -18
- package/dist/components/shell/DocInspector.d.ts.map +0 -1
- package/dist/components/shell/DocumentEditorOverlay.d.ts +0 -29
- package/dist/components/shell/DocumentEditorOverlay.d.ts.map +0 -1
- package/dist/components/shell/DocumentPresentOverlay.d.ts +0 -30
- package/dist/components/shell/DocumentPresentOverlay.d.ts.map +0 -1
- package/dist/components/shell/SiteSettingsDecks.d.ts +0 -13
- package/dist/components/shell/SiteSettingsDecks.d.ts.map +0 -1
- package/dist/deck/DeckComponentView.d.ts +0 -13
- package/dist/deck/DeckComponentView.d.ts.map +0 -1
- package/dist/deck/DeckPlayer.d.ts +0 -26
- package/dist/deck/DeckPlayer.d.ts.map +0 -1
- package/dist/deck/DeckSlideView.d.ts +0 -17
- package/dist/deck/DeckSlideView.d.ts.map +0 -1
- package/dist/deck/DeckStage.d.ts +0 -16
- package/dist/deck/DeckStage.d.ts.map +0 -1
- package/dist/deck/components/DeckImage.d.ts +0 -14
- package/dist/deck/components/DeckImage.d.ts.map +0 -1
- package/dist/deck/components/DeckImageGrid.d.ts +0 -27
- package/dist/deck/components/DeckImageGrid.d.ts.map +0 -1
- package/dist/deck/components/DeckRichText.d.ts +0 -22
- package/dist/deck/components/DeckRichText.d.ts.map +0 -1
- package/dist/deck/components/DeckVideo.d.ts +0 -15
- package/dist/deck/components/DeckVideo.d.ts.map +0 -1
- package/dist/deck/editing.d.ts +0 -50
- package/dist/deck/editing.d.ts.map +0 -1
- package/dist/deck/flip.d.ts +0 -11
- package/dist/deck/flip.d.ts.map +0 -1
- package/dist/deck/index.d.ts +0 -24
- package/dist/deck/index.d.ts.map +0 -1
- package/dist/deck/index.js +0 -51
- package/dist/deck/layouts/CenteredTextLayout.d.ts +0 -12
- package/dist/deck/layouts/CenteredTextLayout.d.ts.map +0 -1
- package/dist/deck/layouts/ColumnSplitLayout.d.ts +0 -14
- package/dist/deck/layouts/ColumnSplitLayout.d.ts.map +0 -1
- package/dist/deck/layouts/FullBleedImageLayout.d.ts +0 -14
- package/dist/deck/layouts/FullBleedImageLayout.d.ts.map +0 -1
- package/dist/deck/media.d.ts +0 -33
- package/dist/deck/media.d.ts.map +0 -1
- package/dist/deck/tokens.d.ts +0 -22
- package/dist/deck/tokens.d.ts.map +0 -1
- package/dist/deck/useStageScale.d.ts +0 -10
- package/dist/deck/useStageScale.d.ts.map +0 -1
- package/dist/deck-builder/DeckBuilderApp.d.ts +0 -30
- package/dist/deck-builder/DeckBuilderApp.d.ts.map +0 -1
- package/dist/deck-builder/DeckBuilderTopBar.d.ts +0 -24
- package/dist/deck-builder/DeckBuilderTopBar.d.ts.map +0 -1
- package/dist/deck-builder/DeckChatPanel.d.ts +0 -45
- package/dist/deck-builder/DeckChatPanel.d.ts.map +0 -1
- package/dist/deck-builder/DeckEditor.d.ts +0 -37
- package/dist/deck-builder/DeckEditor.d.ts.map +0 -1
- package/dist/deck-builder/DeckLibrary.d.ts +0 -22
- package/dist/deck-builder/DeckLibrary.d.ts.map +0 -1
- package/dist/deck-builder/DeckSlideSurface.d.ts +0 -31
- package/dist/deck-builder/DeckSlideSurface.d.ts.map +0 -1
- package/dist/deck-builder/deck-api.d.ts +0 -76
- package/dist/deck-builder/deck-api.d.ts.map +0 -1
- package/dist/deck-builder/deck-chat-client.d.ts +0 -44
- package/dist/deck-builder/deck-chat-client.d.ts.map +0 -1
- package/dist/deck-builder/deck-chat-history.d.ts +0 -10
- package/dist/deck-builder/deck-chat-history.d.ts.map +0 -1
- package/dist/deck-builder/index.d.ts +0 -13
- package/dist/deck-builder/index.d.ts.map +0 -1
- package/dist/deck-builder/index.js +0 -1498
- package/dist/deck-builder/starter-deck.d.ts +0 -23
- package/dist/deck-builder/starter-deck.d.ts.map +0 -1
- package/dist/deck-builder/types.d.ts +0 -29
- package/dist/deck-builder/types.d.ts.map +0 -1
- package/dist/deck-builder/useSortableSlide.d.ts +0 -29
- package/dist/deck-builder/useSortableSlide.d.ts.map +0 -1
- package/dist/document/assets/fonts/albra-grotesk-bold.woff +0 -0
- package/dist/document/assets/fonts/albra-grotesk-regular.woff +0 -0
- package/dist/document/assets/fonts/kapra-neue-expanded-medium.woff +0 -0
- package/dist/document/assets/fonts/kapra-neue-expanded-regular.woff +0 -0
- package/dist/document/components/DocCoverPage.d.ts +0 -11
- package/dist/document/components/DocCoverPage.d.ts.map +0 -1
- package/dist/document/components/DocPage.d.ts +0 -10
- package/dist/document/components/DocPage.d.ts.map +0 -1
- package/dist/document/components/DocRail.d.ts +0 -28
- package/dist/document/components/DocRail.d.ts.map +0 -1
- package/dist/document/components/DocSectionPage.d.ts +0 -12
- package/dist/document/components/DocSectionPage.d.ts.map +0 -1
- package/dist/document/components/DocViewer.d.ts +0 -21
- package/dist/document/components/DocViewer.d.ts.map +0 -1
- package/dist/document/components/Wordmark.d.ts +0 -4
- package/dist/document/components/Wordmark.d.ts.map +0 -1
- package/dist/document/components/measure.d.ts +0 -7
- package/dist/document/components/measure.d.ts.map +0 -1
- package/dist/document/components/useDocPages.d.ts +0 -12
- package/dist/document/components/useDocPages.d.ts.map +0 -1
- package/dist/document/engine/blocks.d.ts +0 -2
- package/dist/document/engine/blocks.d.ts.map +0 -1
- package/dist/document/engine/harvest.d.ts +0 -3
- package/dist/document/engine/harvest.d.ts.map +0 -1
- package/dist/document/engine/masonry.d.ts +0 -26
- package/dist/document/engine/masonry.d.ts.map +0 -1
- package/dist/document/engine/paginate.d.ts +0 -43
- package/dist/document/engine/paginate.d.ts.map +0 -1
- package/dist/document/engine/settings.d.ts +0 -91
- package/dist/document/engine/settings.d.ts.map +0 -1
- package/dist/document/engine/types.d.ts +0 -44
- package/dist/document/engine/types.d.ts.map +0 -1
- package/dist/document/index.d.ts +0 -13
- package/dist/document/index.d.ts.map +0 -1
- package/dist/document/index.js +0 -1030
- package/dist/document/styles/document.css +0 -217
- package/dist/lib/deck-access.d.ts +0 -86
- package/dist/lib/deck-access.d.ts.map +0 -1
- package/dist/lib/deck-access.js +0 -58
- package/dist/lib/deck-dexie.d.ts +0 -88
- package/dist/lib/deck-dexie.d.ts.map +0 -1
- package/dist/lib/deck-dexie.js +0 -22
- package/dist/lib/deck-edit.d.ts +0 -67
- package/dist/lib/deck-edit.d.ts.map +0 -1
- package/dist/lib/deck-media-assets.d.ts +0 -27
- package/dist/lib/deck-media-assets.d.ts.map +0 -1
- package/dist/lib/deck-media-assets.js +0 -25
- package/dist/lib/deck-media-refs.d.ts +0 -35
- package/dist/lib/deck-media-refs.d.ts.map +0 -1
- package/dist/lib/deck-media-refs.js +0 -36
- package/dist/lib/deck-tool.d.ts +0 -25
- package/dist/lib/deck-tool.d.ts.map +0 -1
- package/dist/lib/deck-tool.js +0 -58
- package/dist/lib/document-input.d.ts +0 -25
- package/dist/lib/document-input.d.ts.map +0 -1
- package/dist/schemas/deck-meta.d.ts +0 -85
- package/dist/schemas/deck-meta.d.ts.map +0 -1
- package/dist/schemas/deck-meta.js +0 -64
- package/dist/schemas/deck.d.ts +0 -954
- package/dist/schemas/deck.d.ts.map +0 -1
- package/dist/schemas/deck.js +0 -65
- package/src/components/editor/SectionOrderingModal.tsx +0 -80
- package/src/components/shell/DocInspector.tsx +0 -314
- package/src/components/shell/DocumentEditorOverlay.tsx +0 -179
- package/src/components/shell/DocumentPresentOverlay.tsx +0 -87
- package/src/components/shell/SiteSettingsDecks.tsx +0 -41
- package/src/deck/DeckComponentView.tsx +0 -28
- package/src/deck/DeckPlayer.tsx +0 -166
- package/src/deck/DeckSlideView.tsx +0 -38
- package/src/deck/DeckStage.tsx +0 -39
- package/src/deck/components/DeckImage.tsx +0 -38
- package/src/deck/components/DeckImageGrid.tsx +0 -254
- package/src/deck/components/DeckRichText.tsx +0 -111
- package/src/deck/components/DeckVideo.tsx +0 -81
- package/src/deck/editing.tsx +0 -201
- package/src/deck/flip.ts +0 -70
- package/src/deck/index.ts +0 -30
- package/src/deck/layouts/CenteredTextLayout.tsx +0 -29
- package/src/deck/layouts/ColumnSplitLayout.tsx +0 -69
- package/src/deck/layouts/FullBleedImageLayout.tsx +0 -58
- package/src/deck/media.tsx +0 -46
- package/src/deck/tokens.ts +0 -57
- package/src/deck/useStageScale.ts +0 -44
- package/src/deck-builder/DeckBuilderApp.tsx +0 -572
- package/src/deck-builder/DeckBuilderTopBar.tsx +0 -62
- package/src/deck-builder/DeckChatPanel.tsx +0 -334
- package/src/deck-builder/DeckEditor.tsx +0 -258
- package/src/deck-builder/DeckLibrary.tsx +0 -195
- package/src/deck-builder/DeckSlideSurface.tsx +0 -55
- package/src/deck-builder/deck-api.ts +0 -177
- package/src/deck-builder/deck-chat-client.ts +0 -129
- package/src/deck-builder/deck-chat-history.ts +0 -62
- package/src/deck-builder/index.ts +0 -12
- package/src/deck-builder/starter-deck.ts +0 -86
- package/src/deck-builder/types.ts +0 -24
- package/src/deck-builder/useSortableSlide.ts +0 -102
- package/src/document/assets/fonts/albra-grotesk-bold.woff +0 -0
- package/src/document/assets/fonts/albra-grotesk-regular.woff +0 -0
- package/src/document/assets/fonts/kapra-neue-expanded-medium.woff +0 -0
- package/src/document/assets/fonts/kapra-neue-expanded-regular.woff +0 -0
- package/src/document/components/DocCoverPage.tsx +0 -45
- package/src/document/components/DocPage.tsx +0 -40
- package/src/document/components/DocRail.tsx +0 -189
- package/src/document/components/DocSectionPage.tsx +0 -48
- package/src/document/components/DocViewer.tsx +0 -313
- package/src/document/components/Wordmark.tsx +0 -18
- package/src/document/components/measure.tsx +0 -133
- package/src/document/components/useDocPages.ts +0 -95
- package/src/document/engine/blocks.ts +0 -116
- package/src/document/engine/harvest.ts +0 -95
- package/src/document/engine/masonry.ts +0 -158
- package/src/document/engine/paginate.ts +0 -232
- package/src/document/engine/settings.ts +0 -125
- package/src/document/engine/types.ts +0 -64
- package/src/document/index.ts +0 -18
- package/src/document/styles/document.css +0 -217
- package/src/lib/deck-access.ts +0 -132
- package/src/lib/deck-dexie.ts +0 -190
- package/src/lib/deck-edit.ts +0 -196
- package/src/lib/deck-media-assets.ts +0 -57
- package/src/lib/deck-media-refs.ts +0 -67
- package/src/lib/deck-tool.ts +0 -95
- package/src/lib/document-input.ts +0 -57
- package/src/schemas/deck-meta.ts +0 -88
- package/src/schemas/deck.ts +0 -478
- package/dist/{chunk-DJABADOO.js → chunk-AJ2QOAZI.js} +3 -3
|
@@ -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,18 +73,19 @@ export function deriveStatusDisplay({ mainStatus, savedStatus, contentDiffers, i
|
|
|
73
73
|
};
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
const dotColorClasses: Record<StatusColor, string> = {
|
|
77
|
-
live: "bg-status-live-
|
|
78
|
-
draft: "bg-status-draft-
|
|
79
|
-
archived: "bg-white border-status-
|
|
80
|
-
modified: "bg-status-modified-
|
|
76
|
+
export const dotColorClasses: Record<StatusColor, string> = {
|
|
77
|
+
live: "bg-status-live-dot",
|
|
78
|
+
draft: "bg-status-draft-dot",
|
|
79
|
+
archived: "bg-white border-status-archived-dot-border",
|
|
80
|
+
modified: "bg-status-modified-dot",
|
|
81
81
|
};
|
|
82
82
|
|
|
83
83
|
|
|
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
|
*/
|
|
@@ -2,9 +2,7 @@ import { cn } from "../../lib/cn";
|
|
|
2
2
|
import { contrastColor } from "../../lib/contrast";
|
|
3
3
|
import type { Audience } from "../../auth/types";
|
|
4
4
|
|
|
5
|
-
/** Multi-select audience chips — the site-settings idiom for "who sees this".
|
|
6
|
-
* Shared by the chatbot and deck-builder opt-ins so the two gates, which have
|
|
7
|
-
* identical strict-opt-in semantics, cannot come to look like different rules. */
|
|
5
|
+
/** Multi-select audience chips — the site-settings idiom for "who sees this". */
|
|
8
6
|
export function AudienceChips({
|
|
9
7
|
audiences,
|
|
10
8
|
selected,
|
|
@@ -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;
|