@drawnagency/primitives 0.1.104 → 0.1.106
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/auth/cookies.d.ts +21 -1
- package/dist/auth/cookies.d.ts.map +1 -1
- package/dist/auth/index.d.ts +2 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +3 -1
- package/dist/auth/types.d.ts +28 -0
- package/dist/auth/types.d.ts.map +1 -1
- package/dist/{chunk-PTOLUMGV.js → chunk-4RNF2DRG.js} +1 -0
- package/dist/{chunk-MUTHF2GT.js → chunk-5IZ2KTMU.js} +2 -2
- package/dist/{chunk-LZKVV4K2.js → chunk-6P4CJCXB.js} +7 -1
- package/dist/{chunk-CZLYECHC.js → chunk-AB3XVXI3.js} +4 -4
- package/dist/chunk-B7VBSE5X.js +12 -0
- package/dist/{chunk-BGHWUJFV.js → chunk-BOZUEKWL.js} +107 -171
- package/dist/{chunk-TGNS7D4F.js → chunk-CPTFFLK2.js} +1 -1
- package/dist/{chunk-26ALJO4K.js → chunk-D6TLUVOP.js} +1 -1
- package/dist/{chunk-EM5DFHDZ.js → chunk-GEENI3OC.js} +13 -4
- package/dist/chunk-GGXZZ77Z.js +70 -0
- package/dist/{chunk-EWNDBKYK.js → chunk-LUAI5DWR.js} +1 -1
- package/dist/chunk-NLQ5F3RQ.js +43 -0
- package/dist/{chunk-X4PRBGB3.js → chunk-QLBHERK6.js} +9 -9
- package/dist/{chunk-3COSXTOY.js → chunk-XBYJQ5RS.js} +1 -1
- package/dist/{chunk-CXQWLG6X.js → chunk-Z7VXESB6.js} +18 -18
- package/dist/{chunk-WA6QPDJI.js → chunk-ZHHJVKU7.js} +6 -6
- package/dist/collab/rebase.js +2 -1
- package/dist/components/chat/ChatPanel.d.ts.map +1 -1
- package/dist/components/chat/ChatWidget.d.ts.map +1 -1
- package/dist/components/docfile/DocFileOverlay.d.ts.map +1 -1
- package/dist/components/docfile/DocFilePane.d.ts +4 -3
- package/dist/components/docfile/DocFilePane.d.ts.map +1 -1
- package/dist/components/editor/SectionOrderingModal.d.ts.map +1 -1
- package/dist/components/editor/index.js +8 -7
- package/dist/components/primitives/index.js +7 -6
- package/dist/components/sections/brand-guide.js +8 -7
- package/dist/components/sections/register-schemas.js +13 -12
- package/dist/components/sections/register.js +13 -12
- package/dist/components/shared/Navigation.d.ts +16 -1
- package/dist/components/shared/Navigation.d.ts.map +1 -1
- package/dist/components/shared/SectionSummaryRow.d.ts +24 -0
- package/dist/components/shared/SectionSummaryRow.d.ts.map +1 -0
- package/dist/components/shell/AudienceAddForm.d.ts +4 -1
- package/dist/components/shell/AudienceAddForm.d.ts.map +1 -1
- package/dist/components/shell/AudienceDetailsPanel.d.ts +28 -0
- package/dist/components/shell/AudienceDetailsPanel.d.ts.map +1 -0
- package/dist/components/shell/AudienceRow.d.ts +15 -1
- package/dist/components/shell/AudienceRow.d.ts.map +1 -1
- package/dist/components/shell/CredentialList.d.ts +18 -0
- package/dist/components/shell/CredentialList.d.ts.map +1 -0
- package/dist/components/shell/EditorShell.d.ts.map +1 -1
- package/dist/components/shell/SiteSettingsViewerAccess.d.ts.map +1 -1
- package/dist/components/shell/ViewerLoginForm.d.ts +5 -1
- package/dist/components/shell/ViewerLoginForm.d.ts.map +1 -1
- package/dist/index.js +47 -29
- package/dist/lib/audience-overview.d.ts +53 -0
- package/dist/lib/audience-overview.d.ts.map +1 -0
- package/dist/lib/audience-secret.d.ts +11 -0
- package/dist/lib/audience-secret.d.ts.map +1 -0
- package/dist/lib/audience-secret.js +70 -0
- package/dist/lib/events.d.ts +3 -0
- package/dist/lib/events.d.ts.map +1 -1
- package/dist/lib/index-ops.js +3 -2
- package/dist/lib/index.js +13 -12
- package/dist/lib/nav.d.ts.map +1 -1
- package/dist/lib/thumb-views.d.ts +17 -0
- package/dist/lib/thumb-views.d.ts.map +1 -0
- package/dist/media/index.js +4 -3
- package/dist/schemas/audience.d.ts +29 -0
- package/dist/schemas/audience.d.ts.map +1 -1
- package/dist/schemas/audience.js +24 -0
- package/dist/schemas/auth.d.ts +1 -0
- package/dist/schemas/auth.d.ts.map +1 -1
- package/dist/schemas/auth.js +1 -1
- package/dist/schemas/index.js +28 -12
- package/dist/schemas/site-config.d.ts +1 -1
- package/dist/schemas/site-config.d.ts.map +1 -1
- package/dist/types/database.d.ts +59 -5
- package/dist/types/database.d.ts.map +1 -1
- package/package.json +9 -1
- package/src/auth/cookies.ts +37 -4
- package/src/auth/index.ts +2 -1
- package/src/auth/types.ts +32 -0
- package/src/components/chat/ChatPanel.tsx +4 -2
- package/src/components/chat/ChatWidget.tsx +4 -1
- package/src/components/docfile/DocFileOverlay.tsx +28 -2
- package/src/components/docfile/DocFilePane.tsx +4 -3
- package/src/components/editor/SectionOrderingModal.tsx +8 -73
- package/src/components/shared/Navigation.tsx +113 -7
- package/src/components/shared/SectionSummaryRow.tsx +73 -0
- package/src/components/shell/AudienceAddForm.tsx +18 -9
- package/src/components/shell/AudienceDetailsPanel.tsx +263 -0
- package/src/components/shell/AudienceRow.tsx +55 -13
- package/src/components/shell/CredentialList.tsx +285 -0
- package/src/components/shell/EditorShell.tsx +94 -4
- package/src/components/shell/SiteSettingsViewerAccess.tsx +57 -6
- package/src/components/shell/ViewerLoginForm.tsx +35 -15
- package/src/lib/audience-overview.ts +110 -0
- package/src/lib/audience-secret.ts +99 -0
- package/src/lib/events.ts +3 -0
- package/src/lib/nav.ts +6 -2
- package/src/lib/thumb-views.ts +28 -0
- package/src/schemas/audience.ts +52 -0
- package/src/schemas/auth.ts +6 -0
- package/src/schemas/site-config.ts +1 -1
- package/src/types/database.ts +59 -5
- package/dist/chunk-ZDQ3TT47.js +0 -30
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { LoadedSection } from "../../lib/loader";
|
|
2
|
-
import type { Thumbnail } from "../../lib/registry";
|
|
3
2
|
import { getSection } from "../../lib/registry";
|
|
4
|
-
import { getMediaProvider } from "../../media";
|
|
5
3
|
import type { MediaManifest } from "../../media/types";
|
|
6
4
|
import { DragHandle } from "./DragHandle";
|
|
7
5
|
import { useSortableRow } from "../../hooks/useSortableRow";
|
|
8
6
|
import { DropEdgeIndicator } from "./DropEdgeIndicator";
|
|
7
|
+
import { SectionSummaryRow } from "../shared/SectionSummaryRow";
|
|
8
|
+
import { toThumbViews } from "../../lib/thumb-views";
|
|
9
9
|
import { cn } from "../../lib/cn";
|
|
10
10
|
|
|
11
11
|
interface SectionOrderingModalProps {
|
|
@@ -14,16 +14,6 @@ interface SectionOrderingModalProps {
|
|
|
14
14
|
onReorder: (fromIndex: number, toIndex: number) => void;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
function resolveThumbnailSrc(thumb: Thumbnail, manifest: MediaManifest): string | null {
|
|
18
|
-
if (thumb.type === "color") return null;
|
|
19
|
-
const item = manifest.images[thumb.src];
|
|
20
|
-
if (!item) return null;
|
|
21
|
-
const provider = getMediaProvider();
|
|
22
|
-
const resolved = provider.resolve(item, [200]);
|
|
23
|
-
if (!resolved) return null;
|
|
24
|
-
return resolved.tag === "img" || resolved.tag === "video" ? resolved.src : null;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
17
|
function SectionRow({
|
|
28
18
|
section,
|
|
29
19
|
index,
|
|
@@ -44,9 +34,6 @@ function SectionRow({
|
|
|
44
34
|
const definition = getSection(section.section.type);
|
|
45
35
|
const label = definition?.getLabel?.(section.section) ?? definition?.label ?? section.section.type;
|
|
46
36
|
const typeLabel = definition?.label ?? section.section.type;
|
|
47
|
-
const allThumbnails = definition?.getThumbnails?.(section.section) ?? [];
|
|
48
|
-
const thumbnails = allThumbnails.slice(0, 3);
|
|
49
|
-
const remainingCount = Math.max(0, allThumbnails.length - 3);
|
|
50
37
|
|
|
51
38
|
return (
|
|
52
39
|
<div
|
|
@@ -62,68 +49,16 @@ function SectionRow({
|
|
|
62
49
|
<DragHandle ref={handleRef} />
|
|
63
50
|
</div>
|
|
64
51
|
|
|
65
|
-
<
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
<div className="truncate text-sm font-medium text-base-contrast">
|
|
72
|
-
{label}
|
|
73
|
-
</div>
|
|
74
|
-
</div>
|
|
75
|
-
|
|
76
|
-
{thumbnails.length > 0 && (
|
|
77
|
-
<div className="flex items-center -space-x-2">
|
|
78
|
-
{thumbnails.map((thumb, i) => (
|
|
79
|
-
thumb.type === "color" ? (
|
|
80
|
-
<div
|
|
81
|
-
key={i}
|
|
82
|
-
className="h-8 w-8 rounded border-2 border-base"
|
|
83
|
-
style={{ backgroundColor: thumb.value }}
|
|
84
|
-
/>
|
|
85
|
-
) : (
|
|
86
|
-
<ResolvedImage
|
|
87
|
-
key={i}
|
|
88
|
-
thumb={thumb}
|
|
89
|
-
manifest={mediaManifest}
|
|
90
|
-
/>
|
|
91
|
-
)
|
|
92
|
-
))}
|
|
93
|
-
{remainingCount > 0 && (
|
|
94
|
-
<div className="ml-1 flex h-8 w-8 items-center justify-center rounded border-2 border-base bg-base-accent text-xs text-base-contrast-light">
|
|
95
|
-
+{remainingCount}
|
|
96
|
-
</div>
|
|
97
|
-
)}
|
|
98
|
-
</div>
|
|
99
|
-
)}
|
|
100
|
-
</div>
|
|
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
|
+
/>
|
|
101
58
|
</div>
|
|
102
59
|
);
|
|
103
60
|
}
|
|
104
61
|
|
|
105
|
-
function ResolvedImage({
|
|
106
|
-
thumb,
|
|
107
|
-
manifest,
|
|
108
|
-
}: {
|
|
109
|
-
thumb: Thumbnail & { type: "image" };
|
|
110
|
-
manifest: MediaManifest;
|
|
111
|
-
}) {
|
|
112
|
-
const src = resolveThumbnailSrc(thumb, manifest);
|
|
113
|
-
if (!src) {
|
|
114
|
-
return (
|
|
115
|
-
<div className="h-8 w-8 rounded border-2 border-base bg-base-accent" />
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
return (
|
|
119
|
-
<img
|
|
120
|
-
src={src}
|
|
121
|
-
alt={thumb.alt ?? ""}
|
|
122
|
-
className="h-8 w-8 rounded border-2 border-base object-cover"
|
|
123
|
-
/>
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
62
|
export function SectionOrderingModal({
|
|
128
63
|
sections,
|
|
129
64
|
mediaManifest,
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { useState, useCallback, useEffect, useRef, type ReactNode } from "react";
|
|
2
|
-
import { ChevronRight } from "lucide-react";
|
|
2
|
+
import { ChevronRight, Users } from "lucide-react";
|
|
3
3
|
import { cn } from "../../lib/cn";
|
|
4
4
|
import { Toggle } from "./Toggle";
|
|
5
5
|
import type { SiteNav, NavItem, GroupNavItem, PageNavItem, DocGroupNavItem, DocNavItem } from "../../lib/nav";
|
|
6
6
|
import { groupDisplayLabel } from "../../lib/nav-tree";
|
|
7
7
|
import { openDocFile, closeDocFile } from "../docfile/doc-file-state";
|
|
8
|
-
import { editModeEvent, siteNavChangeEvent, pageSelectEvent, darkModeEvent, siteNameChangeEvent, historySelectEvent, editorDirtyEvent } from "../../lib/events";
|
|
8
|
+
import { editModeEvent, siteNavChangeEvent, pageSelectEvent, darkModeEvent, siteNameChangeEvent, historySelectEvent, editorDirtyEvent, audienceDetailsEvent } from "../../lib/events";
|
|
9
9
|
import { useActiveHeadings } from "../../hooks/useActiveHeadings";
|
|
10
10
|
import { formatDate } from "../../lib/timestamp";
|
|
11
11
|
import { Popover } from "./Popover";
|
|
@@ -25,6 +25,17 @@ interface Props {
|
|
|
25
25
|
* the freed column to chat. Static per page render — deliberately keyed to
|
|
26
26
|
* chat being ENABLED, not open, so the nav never flaps when chat toggles. */
|
|
27
27
|
desktopBreakpoint?: "lg" | "xl";
|
|
28
|
+
/** Link to the auto-generated Audience Details page, or null to omit it.
|
|
29
|
+
* The caller owns the gate (editors and the default audience only), because
|
|
30
|
+
* only it knows the requester — this component just renders what it is told. */
|
|
31
|
+
audienceDetailsHref?: string | null;
|
|
32
|
+
/** Highlight the Audience details link as the current page. Passed by the
|
|
33
|
+
* /audiences route; the editor sets it locally when its own surface is open. */
|
|
34
|
+
audienceDetailsActive?: boolean;
|
|
35
|
+
/** Who is signed in, for the bottom identity row and its log-out button.
|
|
36
|
+
* Null on a public (password-disabled) site, where nobody is signed in.
|
|
37
|
+
* `kind` only picks the post-logout destination. */
|
|
38
|
+
identity?: { label: string; sublabel?: string; kind: "editor" | "viewer" } | null;
|
|
28
39
|
}
|
|
29
40
|
|
|
30
41
|
// Both branches are literal class strings so Tailwind's scanner sees them.
|
|
@@ -33,7 +44,7 @@ const BP_CLASSES = {
|
|
|
33
44
|
xl: { hidden: "xl:hidden", leftAuto: "xl:left-auto", translate: "xl:translate-x-0", block: "xl:block" },
|
|
34
45
|
} as const;
|
|
35
46
|
|
|
36
|
-
export default function Navigation({ siteNav: initialNav, siteName, darkMode, lastUpdated, logo, onPageSelect, desktopBreakpoint = "lg" }: Props) {
|
|
47
|
+
export default function Navigation({ siteNav: initialNav, siteName, darkMode, lastUpdated, logo, onPageSelect, desktopBreakpoint = "lg", audienceDetailsHref = null, audienceDetailsActive = false, identity = null }: Props) {
|
|
37
48
|
const bp = BP_CLASSES[desktopBreakpoint];
|
|
38
49
|
const [isOpen, setIsOpen] = useState(false);
|
|
39
50
|
const [isEditMode, setIsEditMode] = useState(false);
|
|
@@ -45,6 +56,13 @@ export default function Navigation({ siteNav: initialNav, siteName, darkMode, la
|
|
|
45
56
|
const [isDark, setIsDark] = useState(false);
|
|
46
57
|
const [siteNav, setSiteNav] = useState<SiteNav>(initialNav);
|
|
47
58
|
const [showHistory, setShowHistory] = useState(false);
|
|
59
|
+
// Editor only: the audience surface opens in place (EditorShell swaps its
|
|
60
|
+
// body) rather than navigating, so the nav owns the highlight. Cleared by any
|
|
61
|
+
// page selection — including one made from the Pages modal, hence the listener
|
|
62
|
+
// rather than only the local click handler.
|
|
63
|
+
const [editorAudienceOpen, setEditorAudienceOpen] = useState(false);
|
|
64
|
+
const [loggingOut, setLoggingOut] = useState(false);
|
|
65
|
+
const [logoutError, setLogoutError] = useState<string | null>(null);
|
|
48
66
|
// Keyed by group id for authored groups and by label for the derived editor
|
|
49
67
|
// buckets ("Hidden from Menu", "Archive"). Generated ids never collide with
|
|
50
68
|
// those two literals, so one record safely serves both.
|
|
@@ -61,7 +79,8 @@ export default function Navigation({ siteNav: initialNav, siteName, darkMode, la
|
|
|
61
79
|
const unlistenNav = siteNavChangeEvent.listen((n) => setSiteNav(n));
|
|
62
80
|
const unlistenDark = darkModeEvent.listen((mode) => setCurrentDarkMode(mode as typeof darkMode));
|
|
63
81
|
const unlistenName = siteNameChangeEvent.listen((name) => setCurrentSiteName(name));
|
|
64
|
-
|
|
82
|
+
const unlistenPage = pageSelectEvent.listen(() => setEditorAudienceOpen(false));
|
|
83
|
+
return () => { unlistenEdit(); unlistenDirty(); unlistenNav(); unlistenDark(); unlistenName(); unlistenPage(); };
|
|
65
84
|
}, []);
|
|
66
85
|
|
|
67
86
|
useEffect(() => {
|
|
@@ -72,6 +91,25 @@ export default function Navigation({ siteNav: initialNav, siteName, darkMode, la
|
|
|
72
91
|
return () => observer.disconnect();
|
|
73
92
|
}, []);
|
|
74
93
|
|
|
94
|
+
// /api/auth/sign-out clears BOTH the editor session and the audience cookie,
|
|
95
|
+
// so one call serves either kind of session; only the destination differs.
|
|
96
|
+
const handleLogout = useCallback(async () => {
|
|
97
|
+
setLogoutError(null);
|
|
98
|
+
setLoggingOut(true);
|
|
99
|
+
try {
|
|
100
|
+
const res = await fetch("/api/auth/sign-out", { method: "POST" });
|
|
101
|
+
if (!res.ok) {
|
|
102
|
+
setLogoutError("Log out failed");
|
|
103
|
+
setLoggingOut(false);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
window.location.assign(identity?.kind === "editor" ? "/edit/login" : "/login");
|
|
107
|
+
} catch {
|
|
108
|
+
setLogoutError("Network error");
|
|
109
|
+
setLoggingOut(false);
|
|
110
|
+
}
|
|
111
|
+
}, [identity?.kind]);
|
|
112
|
+
|
|
75
113
|
const handleThemeToggle = useCallback((next: boolean) => {
|
|
76
114
|
const root = document.documentElement;
|
|
77
115
|
root.classList.add("theme-changing");
|
|
@@ -178,6 +216,7 @@ export default function Navigation({ siteNav: initialNav, siteName, darkMode, la
|
|
|
178
216
|
) : null;
|
|
179
217
|
|
|
180
218
|
const handlePageClick = useCallback((id: string) => {
|
|
219
|
+
setEditorAudienceOpen(false);
|
|
181
220
|
if (onPageSelect) onPageSelect(id);
|
|
182
221
|
else pageSelectEvent.dispatch(id);
|
|
183
222
|
setIsOpen(false);
|
|
@@ -187,6 +226,15 @@ export default function Navigation({ siteNav: initialNav, siteName, darkMode, la
|
|
|
187
226
|
// a same-island handler is supplied; otherwise a real <a> for the viewer.
|
|
188
227
|
const asButton = isEditMode || !!onPageSelect;
|
|
189
228
|
|
|
229
|
+
// Active either because the /audiences route said so, or because the editor's
|
|
230
|
+
// in-place surface is open.
|
|
231
|
+
const audienceActive = audienceDetailsActive || editorAudienceOpen;
|
|
232
|
+
const audienceLinkClass = cn(
|
|
233
|
+
"inline-flex items-center gap-1.5 text-xs transition-colors hover:text-primary",
|
|
234
|
+
// Same active treatment as a nav page link.
|
|
235
|
+
audienceActive ? "font-bold text-primary" : "text-base-contrast-light",
|
|
236
|
+
);
|
|
237
|
+
|
|
190
238
|
const renderPage = (page: PageNavItem) => (
|
|
191
239
|
<li key={page.id}>
|
|
192
240
|
{asButton ? (
|
|
@@ -338,23 +386,57 @@ export default function Navigation({ siteNav: initialNav, siteName, darkMode, la
|
|
|
338
386
|
{isEditMode && renderPageGroup("Hidden from Menu", siteNav.hidden)}
|
|
339
387
|
{isEditMode && renderPageGroup("Archive", siteNav.archive, true)}
|
|
340
388
|
|
|
389
|
+
{/* Bottom stack, in order: dark-mode toggle, audience details, last
|
|
390
|
+
updated, then who you are. Every block carries its own top divider
|
|
391
|
+
and is centred, so the stack reads as evenly separated rows — and
|
|
392
|
+
every line shares one type size (text-xs). */}
|
|
341
393
|
<div className="mt-auto">
|
|
342
394
|
{currentDarkMode === "optional" && (
|
|
343
395
|
<div className="mx-4 border-t border-base-200 py-4">
|
|
344
|
-
<div className="flex items-center justify-center gap-2 text-
|
|
396
|
+
<div className="flex items-center justify-center gap-2 text-xs text-base-contrast-light">
|
|
345
397
|
<span className={cn(!isDark && "text-base-contrast")}>Light</span>
|
|
346
398
|
<Toggle checked={isDark} onChange={handleThemeToggle} label="Toggle dark mode" />
|
|
347
399
|
<span className={cn(isDark && "text-base-contrast")}>Dark</span>
|
|
348
400
|
</div>
|
|
349
401
|
</div>
|
|
350
402
|
)}
|
|
403
|
+
{/* A real anchor, not a pageSelectEvent: /audiences is a platform
|
|
404
|
+
route, and in the editor it deliberately navigates out of the shell
|
|
405
|
+
(drafts live in IndexedDB, so leaving and coming back loses
|
|
406
|
+
nothing). */}
|
|
407
|
+
{audienceDetailsHref && (
|
|
408
|
+
<div className="mx-4 border-t border-base-200 py-3 text-center">
|
|
409
|
+
{isEditMode ? (
|
|
410
|
+
// In the editor this is a surface, not a destination: EditorShell
|
|
411
|
+
// swaps its body so the toolbar (and unsaved work) stay put.
|
|
412
|
+
<button
|
|
413
|
+
onClick={() => { setEditorAudienceOpen(true); audienceDetailsEvent.dispatch(); setIsOpen(false); }}
|
|
414
|
+
aria-current={audienceActive ? "page" : undefined}
|
|
415
|
+
className={cn("cursor-pointer", audienceLinkClass)}
|
|
416
|
+
>
|
|
417
|
+
<Users className="h-3.5 w-3.5 shrink-0" aria-hidden />
|
|
418
|
+
Audience details
|
|
419
|
+
</button>
|
|
420
|
+
) : (
|
|
421
|
+
<a
|
|
422
|
+
href={audienceDetailsHref}
|
|
423
|
+
aria-current={audienceActive ? "page" : undefined}
|
|
424
|
+
className={audienceLinkClass}
|
|
425
|
+
>
|
|
426
|
+
<Users className="h-3.5 w-3.5 shrink-0" aria-hidden />
|
|
427
|
+
Audience details
|
|
428
|
+
</a>
|
|
429
|
+
)}
|
|
430
|
+
</div>
|
|
431
|
+
)}
|
|
432
|
+
|
|
351
433
|
{lastUpdated && (
|
|
352
|
-
<div ref={historyAnchorRef} className=
|
|
434
|
+
<div ref={historyAnchorRef} className="mx-4 border-t border-base-200 py-3 text-center">
|
|
353
435
|
{isEditMode ? (
|
|
354
436
|
<div className="relative">
|
|
355
437
|
<button onClick={() => setShowHistory((p) => !p)}
|
|
356
438
|
aria-label="View history" aria-expanded={showHistory}
|
|
357
|
-
className="inline-flex cursor-pointer items-center gap-
|
|
439
|
+
className="inline-flex cursor-pointer items-center gap-2 text-xs text-base-contrast-light transition-colors hover:text-primary">
|
|
358
440
|
Last updated {formatDate(lastUpdated)}
|
|
359
441
|
{/* The panel opens upward, so the caret points up when closed
|
|
360
442
|
and flips when open — the affordance that this is a toggle
|
|
@@ -380,6 +462,30 @@ export default function Navigation({ siteNav: initialNav, siteName, darkMode, la
|
|
|
380
462
|
)}
|
|
381
463
|
</div>
|
|
382
464
|
)}
|
|
465
|
+
|
|
466
|
+
{identity && (
|
|
467
|
+
<div className="mx-4 border-t border-base-200 py-3 text-center">
|
|
468
|
+
{/* Identity and sign-out share ONE line, dot-separated. The button
|
|
469
|
+
is phrasing content inside the <p> so it sits inline and
|
|
470
|
+
inherits the row's type size rather than declaring its own.
|
|
471
|
+
The audience moves to `title` — visible text stays terse. */}
|
|
472
|
+
<p
|
|
473
|
+
className="truncate text-xs text-base-contrast-light"
|
|
474
|
+
title={identity.sublabel ? `${identity.label} · ${identity.sublabel}` : identity.label}
|
|
475
|
+
>
|
|
476
|
+
<span className="text-base-contrast">{identity.label}</span>
|
|
477
|
+
{" · "}
|
|
478
|
+
<button
|
|
479
|
+
onClick={handleLogout}
|
|
480
|
+
disabled={loggingOut}
|
|
481
|
+
className="cursor-pointer underline-offset-2 transition-colors hover:text-primary hover:underline disabled:cursor-wait"
|
|
482
|
+
>
|
|
483
|
+
{loggingOut ? "Logging out..." : "Log out"}
|
|
484
|
+
</button>
|
|
485
|
+
</p>
|
|
486
|
+
{logoutError && <p className="mt-1 text-xs text-red-600">{logoutError}</p>}
|
|
487
|
+
</div>
|
|
488
|
+
)}
|
|
383
489
|
</div>
|
|
384
490
|
</nav>
|
|
385
491
|
</>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { cn } from "../../lib/cn";
|
|
3
|
+
import type { ThumbView } from "../../lib/thumb-views";
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
/** Section's own label (registry getLabel), e.g. its heading. */
|
|
7
|
+
label: string;
|
|
8
|
+
/** Section type's display name, e.g. "Colour palette". */
|
|
9
|
+
typeLabel: string;
|
|
10
|
+
/** Already resolved to URLs/colours by the caller. Rendered at most 3 + count. */
|
|
11
|
+
thumbnails: ThumbView[];
|
|
12
|
+
/** Dim the row (audience overview: this section isn't visible to them). */
|
|
13
|
+
muted?: boolean;
|
|
14
|
+
/** Slot at the right edge, before the thumbnails — badges, status pills. */
|
|
15
|
+
trailing?: ReactNode;
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const MAX_THUMBS = 3;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* One section as a compact card: type over label, with a thumbnail cluster.
|
|
23
|
+
* Shared by the reorder modal (which wraps it in a drag handle + drop
|
|
24
|
+
* indicator) and the Audience Details page (which dims unavailable rows), so
|
|
25
|
+
* both read as the same list of the same site.
|
|
26
|
+
*/
|
|
27
|
+
export function SectionSummaryRow({ label, typeLabel, thumbnails, muted, trailing, className }: Props) {
|
|
28
|
+
const shown = thumbnails.slice(0, MAX_THUMBS);
|
|
29
|
+
const remaining = Math.max(0, thumbnails.length - MAX_THUMBS);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className={cn(
|
|
33
|
+
"flex min-w-0 flex-1 items-center gap-3 rounded-md border border-base-200 bg-base px-3 py-2",
|
|
34
|
+
muted && "opacity-50",
|
|
35
|
+
className,
|
|
36
|
+
)}>
|
|
37
|
+
<div className="min-w-0 flex-1">
|
|
38
|
+
<div className="text-xs text-base-contrast-light">{typeLabel}</div>
|
|
39
|
+
<div className="truncate text-sm font-medium text-base-contrast">{label}</div>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
{trailing}
|
|
43
|
+
|
|
44
|
+
{shown.length > 0 && (
|
|
45
|
+
<div className="flex items-center -space-x-2">
|
|
46
|
+
{shown.map((thumb, i) =>
|
|
47
|
+
thumb.kind === "color" ? (
|
|
48
|
+
<div
|
|
49
|
+
key={i}
|
|
50
|
+
className="h-8 w-8 rounded border-2 border-base"
|
|
51
|
+
style={{ backgroundColor: thumb.value }}
|
|
52
|
+
/>
|
|
53
|
+
) : thumb.src ? (
|
|
54
|
+
<img
|
|
55
|
+
key={i}
|
|
56
|
+
src={thumb.src}
|
|
57
|
+
alt={thumb.alt}
|
|
58
|
+
className="h-8 w-8 rounded border-2 border-base object-cover"
|
|
59
|
+
/>
|
|
60
|
+
) : (
|
|
61
|
+
<div key={i} className="h-8 w-8 rounded border-2 border-base bg-base-accent" />
|
|
62
|
+
),
|
|
63
|
+
)}
|
|
64
|
+
{remaining > 0 && (
|
|
65
|
+
<div className="ml-1 flex h-8 w-8 items-center justify-center rounded border-2 border-base bg-base-accent text-xs text-base-contrast-light">
|
|
66
|
+
+{remaining}
|
|
67
|
+
</div>
|
|
68
|
+
)}
|
|
69
|
+
</div>
|
|
70
|
+
)}
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -8,11 +8,14 @@ import { ColorPicker } from "../shared/ColorPicker";
|
|
|
8
8
|
interface Props {
|
|
9
9
|
onSubmit: (data: { displayName: string; color: string | null; password: string }) => Promise<void>;
|
|
10
10
|
onCancel: () => void;
|
|
11
|
+
/** Credential mode: an audience has no password of its own — access comes from
|
|
12
|
+
* the named sign-ins added to it afterwards, so the field is hidden. */
|
|
13
|
+
requirePassword?: boolean;
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
const DEFAULT_COLOR = "#3b82f6";
|
|
14
17
|
|
|
15
|
-
export function AudienceAddForm({ onSubmit, onCancel }: Props) {
|
|
18
|
+
export function AudienceAddForm({ onSubmit, onCancel, requirePassword = true }: Props) {
|
|
16
19
|
const [name, setName] = useState("");
|
|
17
20
|
const [color, setColor] = useState(DEFAULT_COLOR);
|
|
18
21
|
const [password, setPassword] = useState("");
|
|
@@ -32,7 +35,7 @@ export function AudienceAddForm({ onSubmit, onCancel }: Props) {
|
|
|
32
35
|
setError("Invalid color");
|
|
33
36
|
return;
|
|
34
37
|
}
|
|
35
|
-
if (password.length === 0) {
|
|
38
|
+
if (requirePassword && password.length === 0) {
|
|
36
39
|
setError("Password is required");
|
|
37
40
|
return;
|
|
38
41
|
}
|
|
@@ -61,13 +64,19 @@ export function AudienceAddForm({ onSubmit, onCancel }: Props) {
|
|
|
61
64
|
value={name}
|
|
62
65
|
onChange={setName}
|
|
63
66
|
/>
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
{requirePassword ? (
|
|
68
|
+
<PasswordInput
|
|
69
|
+
label="Password"
|
|
70
|
+
value={password}
|
|
71
|
+
onChange={setPassword}
|
|
72
|
+
autoComplete="new-password"
|
|
73
|
+
showToggle
|
|
74
|
+
/>
|
|
75
|
+
) : (
|
|
76
|
+
<p className="text-[11px] text-base-contrast-light">
|
|
77
|
+
Add sign-ins to this audience once it exists — that is what grants access.
|
|
78
|
+
</p>
|
|
79
|
+
)}
|
|
71
80
|
</div>
|
|
72
81
|
</div>
|
|
73
82
|
{error && <p className="text-xs text-red-600">{error}</p>}
|