@drawnagency/primitives 0.1.89 → 0.1.90
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-75P6HWPN.js → chunk-5BM2Y3NJ.js} +5 -3
- package/dist/{chunk-VK5KNH73.js → chunk-AGWE2LJG.js} +2 -2
- package/dist/{chunk-2QXJ2KGB.js → chunk-KBQNJZUX.js} +1 -1
- package/dist/{chunk-WCBYXNAG.js → chunk-KMQZYORS.js} +1 -0
- package/dist/{chunk-Y24T75HT.js → chunk-MK7YB2A4.js} +1 -1
- package/dist/{chunk-QFV5TSPO.js → chunk-SIYQ37QY.js} +12 -4
- package/dist/{chunk-J75XGXYD.js → chunk-XKRQMTE4.js} +9 -0
- package/dist/{chunk-L2MAJKKC.js → chunk-XUIJHYKI.js} +2 -2
- package/dist/{chunk-MVX4ULYE.js → chunk-ZLG7S54V.js} +241 -82
- package/dist/components/editor/AudienceIndicator.d.ts +6 -0
- package/dist/components/editor/AudienceIndicator.d.ts.map +1 -1
- package/dist/components/editor/ChildBlockWrapper.d.ts.map +1 -1
- package/dist/components/editor/ColSpanControl.d.ts.map +1 -1
- package/dist/components/editor/DragHandle.d.ts +3 -1
- package/dist/components/editor/DragHandle.d.ts.map +1 -1
- package/dist/components/editor/SectionWrapper.d.ts.map +1 -1
- package/dist/components/editor/StatusIndicator.d.ts +7 -0
- package/dist/components/editor/StatusIndicator.d.ts.map +1 -1
- package/dist/components/editor/index.js +98 -76
- package/dist/components/primitives/index.js +3 -3
- package/dist/components/sections/Container/Container.d.ts +3 -2
- package/dist/components/sections/Container/Container.d.ts.map +1 -1
- package/dist/components/sections/Container/ContainerSettingsForm.d.ts +4 -2
- package/dist/components/sections/Container/ContainerSettingsForm.d.ts.map +1 -1
- package/dist/components/sections/Container/index.d.ts +8 -2
- package/dist/components/sections/Container/index.d.ts.map +1 -1
- package/dist/components/sections/SectionLayout.d.ts +3 -1
- package/dist/components/sections/SectionLayout.d.ts.map +1 -1
- package/dist/components/sections/ViewRenderer.d.ts +3 -1
- package/dist/components/sections/ViewRenderer.d.ts.map +1 -1
- package/dist/components/sections/all-sections.d.ts +8 -2
- package/dist/components/sections/all-sections.d.ts.map +1 -1
- package/dist/components/sections/brand-guide.js +3 -3
- package/dist/components/sections/core-sections.d.ts +8 -2
- package/dist/components/sections/core-sections.d.ts.map +1 -1
- package/dist/components/sections/register-schemas.js +6 -6
- package/dist/components/sections/register.js +6 -6
- package/dist/components/shared/Tooltip.d.ts +8 -1
- package/dist/components/shared/Tooltip.d.ts.map +1 -1
- package/dist/components/shell/AudiencePreviewMenu.d.ts +7 -0
- package/dist/components/shell/AudiencePreviewMenu.d.ts.map +1 -0
- package/dist/components/shell/AuthoringDefaultsMenu.d.ts +10 -0
- package/dist/components/shell/AuthoringDefaultsMenu.d.ts.map +1 -0
- package/dist/components/shell/EditorContext.d.ts +3 -0
- package/dist/components/shell/EditorContext.d.ts.map +1 -1
- package/dist/components/shell/EditorShell.d.ts.map +1 -1
- package/dist/components/shell/SiteSettingsChatbot.d.ts.map +1 -1
- package/dist/hooks/useBuildStatus.d.ts.map +1 -1
- package/dist/index.js +6 -4
- package/dist/lib/authoring-defaults.d.ts +17 -0
- package/dist/lib/authoring-defaults.d.ts.map +1 -0
- package/dist/lib/container-grid.d.ts +6 -0
- package/dist/lib/container-grid.d.ts.map +1 -1
- package/dist/lib/index.js +3 -3
- package/dist/lib/migrate-sections-transform.js +1 -1
- package/dist/media/index.js +1 -1
- package/dist/schemas/index.js +4 -2
- package/dist/schemas/site-config.d.ts +6 -0
- package/dist/schemas/site-config.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/editor/AudienceIndicator.tsx +50 -37
- package/src/components/editor/ChildBlockWrapper.tsx +8 -7
- package/src/components/editor/ColSpanControl.tsx +14 -11
- package/src/components/editor/DragHandle.tsx +5 -2
- package/src/components/editor/PagesModal.tsx +10 -10
- package/src/components/editor/SectionWrapper.tsx +32 -21
- package/src/components/editor/StatusIndicator.tsx +37 -24
- package/src/components/sections/Container/Container.tsx +185 -43
- package/src/components/sections/Container/ContainerSettingsForm.tsx +64 -24
- package/src/components/sections/Container/index.tsx +45 -20
- package/src/components/sections/SectionLayout.tsx +13 -3
- package/src/components/sections/ViewRenderer.tsx +11 -2
- package/src/components/shared/Tooltip.tsx +18 -2
- package/src/components/shell/AudiencePreviewMenu.tsx +73 -0
- package/src/components/shell/AuthoringDefaultsMenu.tsx +54 -0
- package/src/components/shell/EditorContext.tsx +5 -1
- package/src/components/shell/EditorShell.tsx +59 -12
- package/src/components/shell/SiteSettingsChatbot.tsx +2 -1
- package/src/hooks/useBuildStatus.ts +35 -4
- package/src/lib/authoring-defaults.ts +42 -0
- package/src/lib/container-grid.ts +14 -0
- package/src/lib/migrate-sections-transform.ts +5 -3
- package/src/schemas/site-config.ts +1 -1
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { useRef, useState } from "react";
|
|
2
|
+
import { Check, UsersIcon } from "lucide-react";
|
|
3
|
+
import { IconButton } from "../shared/IconButton";
|
|
4
|
+
import { Popover } from "../shared/Popover";
|
|
5
|
+
import { PopoverItem } from "../shared/PopoverItem";
|
|
6
|
+
import { useEditorContext } from "./EditorContext";
|
|
7
|
+
import type { Audience } from "../../auth/types";
|
|
8
|
+
|
|
9
|
+
interface AudiencePreviewMenuProps {
|
|
10
|
+
audiences: Audience[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function AudiencePreviewMenu({ audiences }: AudiencePreviewMenuProps) {
|
|
14
|
+
const { previewAudience, setPreviewAudience } = useEditorContext();
|
|
15
|
+
const [open, setOpen] = useState(false);
|
|
16
|
+
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
17
|
+
// A previewed audience can be deleted mid-session; the badge falls back grey.
|
|
18
|
+
const active = previewAudience !== null ? audiences.find((a) => a.name === previewAudience) : undefined;
|
|
19
|
+
|
|
20
|
+
function select(name: string | null) {
|
|
21
|
+
setPreviewAudience(name);
|
|
22
|
+
setOpen(false);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div ref={wrapperRef} className="relative">
|
|
27
|
+
<IconButton
|
|
28
|
+
icon={<UsersIcon size={16} />}
|
|
29
|
+
label="Preview as audience"
|
|
30
|
+
size="md"
|
|
31
|
+
onClick={() => setOpen((v) => !v)}
|
|
32
|
+
aria-pressed={previewAudience !== null}
|
|
33
|
+
aria-haspopup="true"
|
|
34
|
+
aria-expanded={open}
|
|
35
|
+
className="border border-base-200 bg-base-accent"
|
|
36
|
+
/>
|
|
37
|
+
{previewAudience !== null && (
|
|
38
|
+
<span
|
|
39
|
+
aria-hidden="true"
|
|
40
|
+
data-testid="audience-preview-badge"
|
|
41
|
+
className="pointer-events-none absolute -top-0.5 -right-0.5 h-2.5 w-2.5 rounded-full border border-base"
|
|
42
|
+
style={{ backgroundColor: active?.color ?? "#9ca3af" }}
|
|
43
|
+
/>
|
|
44
|
+
)}
|
|
45
|
+
<Popover isOpen={open} onClose={() => setOpen(false)} anchorRef={wrapperRef} className="w-48">
|
|
46
|
+
<ul role="radiogroup" aria-label="Preview as audience" className="w-full overflow-hidden py-1">
|
|
47
|
+
<li>
|
|
48
|
+
<PopoverItem role="radio" aria-checked={previewAudience === null} onClick={() => select(null)}>
|
|
49
|
+
<span className="flex-1 font-medium text-base-contrast">All audiences</span>
|
|
50
|
+
{previewAudience === null && <Check size={14} strokeWidth={3} className="text-primary" />}
|
|
51
|
+
</PopoverItem>
|
|
52
|
+
</li>
|
|
53
|
+
{audiences.map((a) => {
|
|
54
|
+
const checked = previewAudience === a.name;
|
|
55
|
+
return (
|
|
56
|
+
<li key={a.name}>
|
|
57
|
+
<PopoverItem role="radio" aria-checked={checked} onClick={() => select(a.name)}>
|
|
58
|
+
<span
|
|
59
|
+
aria-hidden="true"
|
|
60
|
+
className="h-3 w-3 shrink-0 rounded-full border border-base-200"
|
|
61
|
+
style={{ backgroundColor: a.color ?? "#9ca3af" }}
|
|
62
|
+
/>
|
|
63
|
+
<span className="flex-1 font-medium text-base-contrast">{a.displayName}</span>
|
|
64
|
+
{checked && <Check size={14} strokeWidth={3} className="text-primary" />}
|
|
65
|
+
</PopoverItem>
|
|
66
|
+
</li>
|
|
67
|
+
);
|
|
68
|
+
})}
|
|
69
|
+
</ul>
|
|
70
|
+
</Popover>
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { useRef, useState } from "react";
|
|
2
|
+
import { SlidersHorizontalIcon } from "lucide-react";
|
|
3
|
+
import { IconButton } from "../shared/IconButton";
|
|
4
|
+
import { Popover } from "../shared/Popover";
|
|
5
|
+
import { StatusRadioList } from "../editor/StatusIndicator";
|
|
6
|
+
import { AudienceChecklist } from "../editor/AudienceIndicator";
|
|
7
|
+
import type { AuthoringDefaults } from "../../lib/authoring-defaults";
|
|
8
|
+
import type { Audience } from "../../auth/types";
|
|
9
|
+
|
|
10
|
+
interface AuthoringDefaultsMenuProps {
|
|
11
|
+
defaults: AuthoringDefaults;
|
|
12
|
+
audiences: Audience[];
|
|
13
|
+
onChange: (next: AuthoringDefaults) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function AuthoringDefaultsMenu({ defaults, audiences, onChange }: AuthoringDefaultsMenuProps) {
|
|
17
|
+
const [open, setOpen] = useState(false);
|
|
18
|
+
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
19
|
+
|
|
20
|
+
function toggleAudience(name: string) {
|
|
21
|
+
const access = defaults.access.includes(name)
|
|
22
|
+
? defaults.access.filter((n) => n !== name)
|
|
23
|
+
: [...defaults.access, name];
|
|
24
|
+
onChange({ ...defaults, access });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div ref={wrapperRef} className="relative">
|
|
29
|
+
<IconButton
|
|
30
|
+
icon={<SlidersHorizontalIcon size={16} />}
|
|
31
|
+
label="Authoring defaults"
|
|
32
|
+
size="md"
|
|
33
|
+
onClick={() => setOpen((v) => !v)}
|
|
34
|
+
aria-haspopup="true"
|
|
35
|
+
aria-expanded={open}
|
|
36
|
+
className="border border-base-200 bg-base-accent"
|
|
37
|
+
/>
|
|
38
|
+
<Popover isOpen={open} onClose={() => setOpen(false)} anchorRef={wrapperRef} className="w-56">
|
|
39
|
+
<div className="px-3 pt-2 pb-1 text-xs font-semibold text-base-contrast-light">
|
|
40
|
+
New sections start as
|
|
41
|
+
</div>
|
|
42
|
+
<StatusRadioList
|
|
43
|
+
status={defaults.status}
|
|
44
|
+
onChange={(status) => onChange({ ...defaults, status })}
|
|
45
|
+
ariaLabel="Default status for new sections"
|
|
46
|
+
/>
|
|
47
|
+
<div className="border-t border-base-200 px-3 pt-2 pb-1 text-xs font-semibold text-base-contrast-light">
|
|
48
|
+
Default audiences
|
|
49
|
+
</div>
|
|
50
|
+
<AudienceChecklist access={defaults.access} audiences={audiences} onToggle={toggleAudience} />
|
|
51
|
+
</Popover>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -17,11 +17,14 @@ interface EditorContextValue {
|
|
|
17
17
|
viewBranch: "saved" | "live";
|
|
18
18
|
historyState: HistoryState;
|
|
19
19
|
historyPageId: string | null;
|
|
20
|
+
/** Audience-preview mode: dim sections this audience can't access. null = off. */
|
|
21
|
+
previewAudience: string | null;
|
|
20
22
|
toggleEditMode: () => void;
|
|
21
23
|
toggleShowAllChrome: () => void;
|
|
22
24
|
setViewBranch: (branch: "saved" | "live") => void;
|
|
23
25
|
setHistoryState: (state: HistoryState) => void;
|
|
24
26
|
setHistoryPageId: (pageId: string | null) => void;
|
|
27
|
+
setPreviewAudience: (audience: string | null) => void;
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
const EditorContext = createContext<EditorContextValue | null>(null);
|
|
@@ -36,6 +39,7 @@ export function EditorProvider({ children }: EditorProviderProps) {
|
|
|
36
39
|
const [viewBranch, setViewBranchState] = useState<"saved" | "live">("saved");
|
|
37
40
|
const [historyState, setHistoryStateRaw] = useState<HistoryState>(null);
|
|
38
41
|
const [historyPageId, setHistoryPageId] = useState<string | null>(null);
|
|
42
|
+
const [previewAudience, setPreviewAudience] = useState<string | null>(null);
|
|
39
43
|
const setHistoryState = useCallback((state: HistoryState) => {
|
|
40
44
|
// null = "auto": resolved to the current page (or home) at render time
|
|
41
45
|
setHistoryPageId(null);
|
|
@@ -54,7 +58,7 @@ export function EditorProvider({ children }: EditorProviderProps) {
|
|
|
54
58
|
const setViewBranch = useCallback((b: "saved" | "live") => setViewBranchState(b), []);
|
|
55
59
|
|
|
56
60
|
return (
|
|
57
|
-
<EditorContext.Provider value={{ isEditMode, showAllChrome, viewBranch, historyState, historyPageId, toggleEditMode, toggleShowAllChrome, setViewBranch, setHistoryState, setHistoryPageId }}>
|
|
61
|
+
<EditorContext.Provider value={{ isEditMode, showAllChrome, viewBranch, historyState, historyPageId, previewAudience, toggleEditMode, toggleShowAllChrome, setViewBranch, setHistoryState, setHistoryPageId, setPreviewAudience }}>
|
|
58
62
|
{children}
|
|
59
63
|
</EditorContext.Provider>
|
|
60
64
|
);
|
|
@@ -90,7 +90,7 @@ import { generateSiteNav } from "../../lib/nav";
|
|
|
90
90
|
import { siteNavChangeEvent, pageSelectEvent, darkModeEvent, siteNameChangeEvent, historySelectEvent } from "../../lib/events";
|
|
91
91
|
import {
|
|
92
92
|
homePage, pageById, pageBySlug, pageDisplayTitle, addSectionToPage, removeSectionFromPages,
|
|
93
|
-
reorderSectionInPage,
|
|
93
|
+
reorderSectionInPage, audiencePasses,
|
|
94
94
|
setHomePage, setPageArchived, setPageFields, setPageAudience, setPageDocument, deletePage, addPage, reorderPages,
|
|
95
95
|
moveSection as moveSectionReducer,
|
|
96
96
|
} from "../../lib/pages";
|
|
@@ -101,6 +101,7 @@ import { UploadRejectionAlert, formatRejections } from "../shared/UploadRejectio
|
|
|
101
101
|
import { Button } from "../shared/Button";
|
|
102
102
|
import { SplitButton } from "../shared/SplitButton";
|
|
103
103
|
import { IconButton } from "../shared/IconButton";
|
|
104
|
+
import { Tooltip } from "../shared/Tooltip";
|
|
104
105
|
import { SegmentedControl } from "../shared/SegmentedControl";
|
|
105
106
|
import { computeDisplaySections } from "./view-branch";
|
|
106
107
|
import { SettingsIcon } from "../shared/icons";
|
|
@@ -110,6 +111,9 @@ import { HistoryToolbar } from "./HistoryToolbar";
|
|
|
110
111
|
import { RestoreModal } from "./RestoreModal";
|
|
111
112
|
import { EditorLoading } from "./EditorLoading";
|
|
112
113
|
import ViewRenderer from "../sections/ViewRenderer";
|
|
114
|
+
import { AuthoringDefaultsMenu } from "./AuthoringDefaultsMenu";
|
|
115
|
+
import { AudiencePreviewMenu } from "./AudiencePreviewMenu";
|
|
116
|
+
import { loadAuthoringDefaults, saveAuthoringDefaults, filterAccessToKnown, type AuthoringDefaults } from "../../lib/authoring-defaults";
|
|
113
117
|
|
|
114
118
|
export { useMediaLibrary } from "./MediaLibraryContext";
|
|
115
119
|
|
|
@@ -173,6 +177,11 @@ export default function EditorShell({
|
|
|
173
177
|
const [sections, setSections] = useState<LoadedSection[]>([]);
|
|
174
178
|
const [siteIndex, setSiteIndex] = useState<SiteIndex>({ siteId, pages: [], sections: {} });
|
|
175
179
|
const [audiences, setAudiences] = useState<Audience[]>(initialAudiences);
|
|
180
|
+
const [authoringDefaults, setAuthoringDefaults] = useState<AuthoringDefaults>(loadAuthoringDefaults);
|
|
181
|
+
const onAuthoringDefaultsChange = useCallback((next: AuthoringDefaults) => {
|
|
182
|
+
setAuthoringDefaults(next);
|
|
183
|
+
saveAuthoringDefaults(next);
|
|
184
|
+
}, []);
|
|
176
185
|
const [localChangesExist, setLocalChangesExist] = useState(false);
|
|
177
186
|
const [showDiscardConfirm, setShowDiscardConfirm] = useState(false);
|
|
178
187
|
const [showSiteSettings, setShowSiteSettings] = useState(false);
|
|
@@ -1157,7 +1166,14 @@ export default function EditorShell({
|
|
|
1157
1166
|
const content = resolveInsertedContent(selection, getAllSections(), newId);
|
|
1158
1167
|
if (!content) return;
|
|
1159
1168
|
const id = newId();
|
|
1160
|
-
|
|
1169
|
+
// Access is constrained to the audiences this page can offer (all site
|
|
1170
|
+
// audiences on an unrestricted page), so stale or out-of-scope default
|
|
1171
|
+
// names never land in a section's access list.
|
|
1172
|
+
const meta = {
|
|
1173
|
+
type: content.type,
|
|
1174
|
+
status: authoringDefaults.status,
|
|
1175
|
+
access: filterAccessToKnown(authoringDefaults.access, pageAudiences),
|
|
1176
|
+
};
|
|
1161
1177
|
const newSection: LoadedSection = { section: { id, ...content } as LoadedSection["section"], meta };
|
|
1162
1178
|
|
|
1163
1179
|
setSections((prev) => [...prev, newSection]);
|
|
@@ -1173,7 +1189,7 @@ export default function EditorShell({
|
|
|
1173
1189
|
// save-time sync by design).
|
|
1174
1190
|
void collabRef.current?.requestEdit(id);
|
|
1175
1191
|
},
|
|
1176
|
-
[persistence, resolvedActivePage],
|
|
1192
|
+
[persistence, resolvedActivePage, authoringDefaults, pageAudiences],
|
|
1177
1193
|
);
|
|
1178
1194
|
|
|
1179
1195
|
// Runs after the confirmation modal is accepted. Removes the section from the
|
|
@@ -1512,6 +1528,9 @@ export default function EditorShell({
|
|
|
1512
1528
|
collabPeers={collab.peers}
|
|
1513
1529
|
collabSelfId={collabBootstrap?.userId ?? null}
|
|
1514
1530
|
collabStatus={collab.status}
|
|
1531
|
+
audiences={audiences}
|
|
1532
|
+
authoringDefaults={authoringDefaults}
|
|
1533
|
+
onAuthoringDefaultsChange={onAuthoringDefaultsChange}
|
|
1515
1534
|
/>
|
|
1516
1535
|
|
|
1517
1536
|
{/* Bug reports persist via Supabase. On the password-only (zero-Supabase)
|
|
@@ -1889,7 +1908,7 @@ function HistoryOrEditorContent({
|
|
|
1889
1908
|
activeSections: LoadedSection[];
|
|
1890
1909
|
activePageId: string | null;
|
|
1891
1910
|
}) {
|
|
1892
|
-
const { historyState, historyPageId, setHistoryPageId } = useEditorContext();
|
|
1911
|
+
const { historyState, historyPageId, setHistoryPageId, previewAudience } = useEditorContext();
|
|
1893
1912
|
const wasInHistory = useRef(false);
|
|
1894
1913
|
|
|
1895
1914
|
const historyPage = historyState ? resolveHistoryPage(historyState, historyPageId, activePageId) : null;
|
|
@@ -1922,7 +1941,7 @@ function HistoryOrEditorContent({
|
|
|
1922
1941
|
const pageSections = historyState.sections
|
|
1923
1942
|
.filter((s) => position.has(s.section.id))
|
|
1924
1943
|
.sort((a, b) => (position.get(a.section.id) ?? 0) - (position.get(b.section.id) ?? 0));
|
|
1925
|
-
return <ViewRenderer sections={pageSections} />;
|
|
1944
|
+
return <ViewRenderer sections={pageSections} previewAudience={previewAudience} />;
|
|
1926
1945
|
}
|
|
1927
1946
|
return <>{children}</>;
|
|
1928
1947
|
}
|
|
@@ -2001,7 +2020,7 @@ function EditorContent({
|
|
|
2001
2020
|
getEditIntent: (id: string) => () => Promise<boolean>;
|
|
2002
2021
|
onTakeover: (id: string) => void;
|
|
2003
2022
|
}) {
|
|
2004
|
-
const { isEditMode, viewBranch } = useEditorContext();
|
|
2023
|
+
const { isEditMode, viewBranch, previewAudience } = useEditorContext();
|
|
2005
2024
|
const { openModal, closeModal } = useEditorModal();
|
|
2006
2025
|
const [pendingInsertIndex, setPendingInsertIndex] = useState<number | null>(null);
|
|
2007
2026
|
const dismissPendingInsert = useCallback(() => setPendingInsertIndex(null), []);
|
|
@@ -2092,7 +2111,12 @@ function EditorContent({
|
|
|
2092
2111
|
)}
|
|
2093
2112
|
|
|
2094
2113
|
<ErrorBoundary label={`${definition.label} (${section.type})`}>
|
|
2095
|
-
<SectionLayout
|
|
2114
|
+
<SectionLayout
|
|
2115
|
+
type={section.type}
|
|
2116
|
+
status={meta.status}
|
|
2117
|
+
dimNonPublished={!isEditMode}
|
|
2118
|
+
dimmed={previewAudience !== null && !audiencePasses(meta.access, previewAudience)}
|
|
2119
|
+
>
|
|
2096
2120
|
<SectionWrapper
|
|
2097
2121
|
sectionId={section.id}
|
|
2098
2122
|
sectionType={section.type}
|
|
@@ -2340,6 +2364,9 @@ function EditorToolbar({
|
|
|
2340
2364
|
collabPeers,
|
|
2341
2365
|
collabSelfId,
|
|
2342
2366
|
collabStatus,
|
|
2367
|
+
audiences,
|
|
2368
|
+
authoringDefaults,
|
|
2369
|
+
onAuthoringDefaultsChange,
|
|
2343
2370
|
}: {
|
|
2344
2371
|
buttonState: "synced" | "publish" | "saveAndPublish";
|
|
2345
2372
|
localChangesExist: boolean;
|
|
@@ -2362,6 +2389,9 @@ function EditorToolbar({
|
|
|
2362
2389
|
collabPeers: CollabPeer[];
|
|
2363
2390
|
collabSelfId: string | null;
|
|
2364
2391
|
collabStatus: CollabStatus;
|
|
2392
|
+
audiences: Audience[];
|
|
2393
|
+
authoringDefaults: AuthoringDefaults;
|
|
2394
|
+
onAuthoringDefaultsChange: (next: AuthoringDefaults) => void;
|
|
2365
2395
|
}) {
|
|
2366
2396
|
const { isEditMode, viewBranch, setViewBranch, toggleEditMode, historyState, setHistoryState, historyPageId, setHistoryPageId } = useEditorContext();
|
|
2367
2397
|
|
|
@@ -2432,11 +2462,28 @@ function EditorToolbar({
|
|
|
2432
2462
|
</div>
|
|
2433
2463
|
<div className="flex items-center justify-end gap-2">
|
|
2434
2464
|
<ProcessingIndicator items={processingItems} />
|
|
2435
|
-
<
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
<
|
|
2439
|
-
|
|
2465
|
+
<Tooltip side="bottom" content="Authoring defaults">
|
|
2466
|
+
<AuthoringDefaultsMenu defaults={authoringDefaults} audiences={audiences} onChange={onAuthoringDefaultsChange} />
|
|
2467
|
+
</Tooltip>
|
|
2468
|
+
<Tooltip side="bottom" content="Preview as audience">
|
|
2469
|
+
<AudiencePreviewMenu audiences={audiences} />
|
|
2470
|
+
</Tooltip>
|
|
2471
|
+
<div aria-hidden="true" className="mx-1 h-6 w-px self-center bg-base-200" />
|
|
2472
|
+
<Tooltip side="bottom" content="Media library">
|
|
2473
|
+
<IconButton icon={<ImageIcon size={16} />} label="Media library" size="md" onClick={onMediaClick} className="border border-base-200 bg-base-accent" />
|
|
2474
|
+
</Tooltip>
|
|
2475
|
+
<Tooltip side="bottom" content="Reorder sections">
|
|
2476
|
+
<IconButton icon={<ListOrderedIcon size={16} />} label="Reorder sections" size="md" onClick={onOrderingClick} className="border border-base-200 bg-base-accent" />
|
|
2477
|
+
</Tooltip>
|
|
2478
|
+
<Tooltip side="bottom" content="Pages">
|
|
2479
|
+
<IconButton icon={<FilesIcon size={16} />} label="Pages" size="md" onClick={onPagesClick} className="border border-base-200 bg-base-accent" />
|
|
2480
|
+
</Tooltip>
|
|
2481
|
+
<Tooltip side="bottom" content="Toggle controls">
|
|
2482
|
+
<ShowAllChromeToggle />
|
|
2483
|
+
</Tooltip>
|
|
2484
|
+
<Tooltip side="bottom-clamp" content="Site settings">
|
|
2485
|
+
<IconButton icon={<SettingsIcon size={16} />} label="Site settings" size="md" onClick={onSettingsClick} className="border border-base-200 bg-base-accent" />
|
|
2486
|
+
</Tooltip>
|
|
2440
2487
|
</div>
|
|
2441
2488
|
</div>
|
|
2442
2489
|
</div>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
2
|
import { Button } from "../shared/Button";
|
|
3
3
|
import { cn } from "../../lib/cn";
|
|
4
|
+
import { contrastColor } from "../../lib/contrast";
|
|
4
5
|
import ChatPanel from "../chat/ChatPanel";
|
|
5
6
|
import type { SiteConfig, ChatbotConfig, ChatbotTask } from "../../schemas/site-config";
|
|
6
7
|
import type { Audience } from "../../auth/types";
|
|
@@ -35,7 +36,7 @@ function AudienceChips({ audiences, selected, onToggle }: { audiences: Audience[
|
|
|
35
36
|
<button key={a.name} type="button" onClick={() => onToggle(a.name)}
|
|
36
37
|
className={cn("rounded-full border px-2 py-0.5 text-xs",
|
|
37
38
|
selected.includes(a.name) ? "border-primary bg-primary text-primary-contrast" : "border-base-200 bg-base-accent text-base-contrast")}
|
|
38
|
-
style={a.color && selected.includes(a.name) ? { backgroundColor: a.color, borderColor: a.color } : undefined}>
|
|
39
|
+
style={a.color && selected.includes(a.name) ? { backgroundColor: a.color, borderColor: a.color, color: contrastColor(a.color) } : undefined}>
|
|
39
40
|
{a.displayName}
|
|
40
41
|
</button>
|
|
41
42
|
))}
|
|
@@ -23,6 +23,14 @@ const FADE_DURATION = 1000;
|
|
|
23
23
|
// whose deploy was never confirmed (e.g. superseded by a later build) — show
|
|
24
24
|
// idle rather than a phantom in-progress publish.
|
|
25
25
|
const STALE_BUILD_MS = 15 * 60 * 1000;
|
|
26
|
+
// Hard cap on an ACTIVE tracking session. Completion normally arrives via a terminal
|
|
27
|
+
// poll (ready/error), which the Netlify webhook writes to the deploy-status row. If
|
|
28
|
+
// that signal is lost/delayed (webhook dropped or unconfigured) the row stays
|
|
29
|
+
// "building" forever and the elapsed counter would climb indefinitely. This backstop
|
|
30
|
+
// stops the runaway counter and hides the indicator after the cap — the same "a build
|
|
31
|
+
// shouldn't run longer than this" invariant as STALE_BUILD_MS, applied to the live
|
|
32
|
+
// session rather than only on mount.
|
|
33
|
+
const MAX_TRACKING_MS = STALE_BUILD_MS;
|
|
26
34
|
|
|
27
35
|
export function useBuildStatus(): BuildStatusResult {
|
|
28
36
|
const [state, setState] = useState<BuildState>("idle");
|
|
@@ -31,6 +39,7 @@ export function useBuildStatus(): BuildStatusResult {
|
|
|
31
39
|
const clearRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
32
40
|
const fadeRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
33
41
|
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
42
|
+
const giveUpRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
34
43
|
const isPolling = useRef(false);
|
|
35
44
|
const mountedRef = useRef(true);
|
|
36
45
|
|
|
@@ -52,6 +61,23 @@ export function useBuildStatus(): BuildStatusResult {
|
|
|
52
61
|
isPolling.current = false;
|
|
53
62
|
}, []);
|
|
54
63
|
|
|
64
|
+
const clearGiveUp = useCallback(() => {
|
|
65
|
+
if (giveUpRef.current) { clearTimeout(giveUpRef.current); giveUpRef.current = null; }
|
|
66
|
+
}, []);
|
|
67
|
+
|
|
68
|
+
// Arm the runaway backstop: if the cap elapses before a terminal signal, stop
|
|
69
|
+
// polling + the timer and drop back to idle (hide the indicator). `elapsedMs` lets a
|
|
70
|
+
// build already in progress at mount count against the same absolute cap.
|
|
71
|
+
const armGiveUp = useCallback((elapsedMs = 0) => {
|
|
72
|
+
clearGiveUp();
|
|
73
|
+
giveUpRef.current = setTimeout(() => {
|
|
74
|
+
if (!mountedRef.current) return;
|
|
75
|
+
stopPolling();
|
|
76
|
+
stopTimer();
|
|
77
|
+
setState("idle");
|
|
78
|
+
}, Math.max(0, MAX_TRACKING_MS - elapsedMs));
|
|
79
|
+
}, [clearGiveUp, stopPolling, stopTimer]);
|
|
80
|
+
|
|
55
81
|
const fetchStatus = useCallback(async (): Promise<BuildStatusResponse | null> => {
|
|
56
82
|
try {
|
|
57
83
|
const res = await fetch("/api/build-status");
|
|
@@ -86,6 +112,7 @@ export function useBuildStatus(): BuildStatusResult {
|
|
|
86
112
|
if (data.state === "ready" || data.state === "error") {
|
|
87
113
|
stopPolling();
|
|
88
114
|
stopTimer();
|
|
115
|
+
clearGiveUp();
|
|
89
116
|
if (data.state === "ready") {
|
|
90
117
|
clearRef.current = setTimeout(() => {
|
|
91
118
|
if (!mountedRef.current) return;
|
|
@@ -98,7 +125,7 @@ export function useBuildStatus(): BuildStatusResult {
|
|
|
98
125
|
}
|
|
99
126
|
}
|
|
100
127
|
},
|
|
101
|
-
[stopPolling, stopTimer],
|
|
128
|
+
[stopPolling, stopTimer, clearGiveUp],
|
|
102
129
|
);
|
|
103
130
|
|
|
104
131
|
const startPolling = useCallback(() => {
|
|
@@ -130,6 +157,7 @@ export function useBuildStatus(): BuildStatusResult {
|
|
|
130
157
|
// counts real elapsed time instead of sitting frozen at 0:00.
|
|
131
158
|
startTimer(Number.isFinite(ageMs) ? Math.max(0, Math.floor(ageMs / 1000)) : 0);
|
|
132
159
|
startPolling();
|
|
160
|
+
armGiveUp(Number.isFinite(ageMs) ? ageMs : 0);
|
|
133
161
|
return;
|
|
134
162
|
}
|
|
135
163
|
|
|
@@ -143,24 +171,27 @@ export function useBuildStatus(): BuildStatusResult {
|
|
|
143
171
|
mountedRef.current = false;
|
|
144
172
|
stopPolling();
|
|
145
173
|
stopTimer();
|
|
174
|
+
clearGiveUp();
|
|
146
175
|
if (clearRef.current) clearTimeout(clearRef.current);
|
|
147
176
|
if (fadeRef.current) clearTimeout(fadeRef.current);
|
|
148
177
|
};
|
|
149
|
-
}, [fetchStatus, handleStatusUpdate, startPolling, startTimer, stopPolling, stopTimer]);
|
|
178
|
+
}, [fetchStatus, handleStatusUpdate, startPolling, startTimer, stopPolling, stopTimer, armGiveUp, clearGiveUp]);
|
|
150
179
|
|
|
151
180
|
const startTracking = useCallback(() => {
|
|
152
181
|
if (clearRef.current) { clearTimeout(clearRef.current); clearRef.current = null; }
|
|
153
182
|
setState("building");
|
|
154
183
|
startTimer();
|
|
155
184
|
startPolling();
|
|
156
|
-
|
|
185
|
+
armGiveUp(0);
|
|
186
|
+
}, [startPolling, startTimer, armGiveUp]);
|
|
157
187
|
|
|
158
188
|
const dismiss = useCallback(() => {
|
|
159
189
|
if (clearRef.current) { clearTimeout(clearRef.current); clearRef.current = null; }
|
|
160
190
|
setState("idle");
|
|
161
191
|
stopPolling();
|
|
162
192
|
stopTimer();
|
|
163
|
-
|
|
193
|
+
clearGiveUp();
|
|
194
|
+
}, [stopPolling, stopTimer, clearGiveUp]);
|
|
164
195
|
|
|
165
196
|
return { state, elapsedSeconds, startTracking, dismiss };
|
|
166
197
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { StatusSchema } from "../schemas/site-config";
|
|
3
|
+
import type { Audience } from "../auth/types";
|
|
4
|
+
|
|
5
|
+
export const AuthoringDefaultsSchema = z.object({
|
|
6
|
+
status: StatusSchema,
|
|
7
|
+
access: z.array(z.string()),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export type AuthoringDefaults = z.infer<typeof AuthoringDefaultsSchema>;
|
|
11
|
+
|
|
12
|
+
export const FALLBACK_AUTHORING_DEFAULTS: AuthoringDefaults = { status: "draft", access: [] };
|
|
13
|
+
|
|
14
|
+
// Per-origin key — every client site is its own domain, so the preference is
|
|
15
|
+
// automatically per-site.
|
|
16
|
+
const STORAGE_KEY = "portal-authoring-defaults";
|
|
17
|
+
|
|
18
|
+
export function loadAuthoringDefaults(): AuthoringDefaults {
|
|
19
|
+
if (typeof localStorage === "undefined") return FALLBACK_AUTHORING_DEFAULTS;
|
|
20
|
+
try {
|
|
21
|
+
const raw = localStorage.getItem(STORAGE_KEY);
|
|
22
|
+
if (!raw) return FALLBACK_AUTHORING_DEFAULTS;
|
|
23
|
+
return AuthoringDefaultsSchema.parse(JSON.parse(raw));
|
|
24
|
+
} catch {
|
|
25
|
+
return FALLBACK_AUTHORING_DEFAULTS;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function saveAuthoringDefaults(defaults: AuthoringDefaults): void {
|
|
30
|
+
if (typeof localStorage === "undefined") return;
|
|
31
|
+
try {
|
|
32
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(defaults));
|
|
33
|
+
} catch {
|
|
34
|
+
// Quota/private-mode failures just lose the preference.
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Stored default audiences can outlive the audiences themselves — drop any
|
|
39
|
+
// name the site no longer has before stamping it onto a new section's access.
|
|
40
|
+
export function filterAccessToKnown(access: string[], audiences: Audience[]): string[] {
|
|
41
|
+
return access.filter((name) => audiences.some((a) => a.name === name));
|
|
42
|
+
}
|
|
@@ -42,6 +42,20 @@ export const gapClass: Record<string, string> = {
|
|
|
42
42
|
lg: "gap-8",
|
|
43
43
|
};
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Column count -> `columns-N` classes for masonry (CSS multi-column), mirroring
|
|
47
|
+
* `containerGridClass`'s collapse curve (`grid-cols-`→`columns-`). Full static
|
|
48
|
+
* strings so Tailwind's source scan generates them.
|
|
49
|
+
*/
|
|
50
|
+
export const masonryColumnsClass: Record<number, string> = {
|
|
51
|
+
1: "columns-1",
|
|
52
|
+
2: "columns-1 @sm:columns-2",
|
|
53
|
+
3: "columns-1 @sm:columns-2 @lg:columns-3",
|
|
54
|
+
4: "columns-1 @xs:columns-2 @lg:columns-4",
|
|
55
|
+
5: "columns-1 @xs:columns-2 @md:columns-3 @xl:columns-5",
|
|
56
|
+
6: "columns-1 @xs:columns-2 @md:columns-3 @2xl:columns-6",
|
|
57
|
+
};
|
|
58
|
+
|
|
45
59
|
/**
|
|
46
60
|
* Per-column-count container-query classes that HIDE a spacer cell once the container
|
|
47
61
|
* collapses to a single column (mobile). Mirrors `containerGridClass`'s collapse curve:
|
|
@@ -99,7 +99,8 @@ function upgradeGrid(section: Obj): Obj {
|
|
|
99
99
|
type: "container",
|
|
100
100
|
content: {
|
|
101
101
|
columns: content.columns,
|
|
102
|
-
|
|
102
|
+
layout: "grid",
|
|
103
|
+
equalizeRowHeight: false,
|
|
103
104
|
children: media.map((item, i) => mapRefItem(item, id, i)),
|
|
104
105
|
},
|
|
105
106
|
};
|
|
@@ -126,7 +127,7 @@ function upgradeSplitContent(section: Obj): Obj {
|
|
|
126
127
|
};
|
|
127
128
|
const children = options.imagePosition === "right" ? [proseChild, mediaChild] : [mediaChild, proseChild];
|
|
128
129
|
|
|
129
|
-
const out: Obj = { id, type: "container", content: { columns: 2,
|
|
130
|
+
const out: Obj = { id, type: "container", content: { columns: 2, layout: "grid", equalizeRowHeight: false, children } };
|
|
130
131
|
if (section.layout !== undefined) out.layout = section.layout;
|
|
131
132
|
return out;
|
|
132
133
|
}
|
|
@@ -163,7 +164,8 @@ function upgradeDoDont(section: Obj): Obj {
|
|
|
163
164
|
type: "container",
|
|
164
165
|
content: {
|
|
165
166
|
columns: 2,
|
|
166
|
-
|
|
167
|
+
layout: "grid",
|
|
168
|
+
equalizeRowHeight: false,
|
|
167
169
|
children: [
|
|
168
170
|
dodontChild(doItems, id, 0, "do", childOptions),
|
|
169
171
|
dodontChild(dontItems, id, 1, "dont", childOptions),
|
|
@@ -7,7 +7,7 @@ import { CORNER_RADIUS_OPTIONS } from "../lib/corner-radius";
|
|
|
7
7
|
// document options block onto PageSchema.
|
|
8
8
|
import { documentOptionsSchema } from "../document/engine/settings";
|
|
9
9
|
|
|
10
|
-
const StatusSchema = z.enum(["draft", "live", "archived", "published"]).transform(
|
|
10
|
+
export const StatusSchema = z.enum(["draft", "live", "archived", "published"]).transform(
|
|
11
11
|
(val) => val === "published" ? "live" as const : val,
|
|
12
12
|
);
|
|
13
13
|
|