@drawnagency/primitives 0.1.49 → 0.1.50
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-P3HO76OS.js → chunk-DLXIYIG2.js} +6 -3
- package/dist/{chunk-5XYUO4HP.js → chunk-ICRCH3GI.js} +19 -2
- package/dist/{chunk-BJ6FYGYP.js → chunk-ONBJG426.js} +95 -9
- package/dist/components/editor/MoveSectionModal.d.ts +12 -0
- package/dist/components/editor/MoveSectionModal.d.ts.map +1 -0
- package/dist/components/editor/PagesModal.d.ts +18 -0
- package/dist/components/editor/PagesModal.d.ts.map +1 -0
- package/dist/components/editor/SectionWrapper.d.ts +1 -1
- package/dist/components/editor/SectionWrapper.d.ts.map +1 -1
- package/dist/components/editor/SettingsForm.d.ts.map +1 -1
- package/dist/components/sections/Button/CTAButton.d.ts +3 -3
- package/dist/components/sections/Button/CTAButton.d.ts.map +1 -1
- package/dist/components/sections/Button/index.d.ts +10 -2
- package/dist/components/sections/Button/index.d.ts.map +1 -1
- package/dist/components/shared/Input.d.ts +1 -0
- package/dist/components/shared/Input.d.ts.map +1 -1
- package/dist/components/shared/LinkField.d.ts +9 -0
- package/dist/components/shared/LinkField.d.ts.map +1 -0
- package/dist/components/shared/Navigation.d.ts +4 -3
- package/dist/components/shared/Navigation.d.ts.map +1 -1
- package/dist/components/shared/PagesContext.d.ts +13 -0
- package/dist/components/shared/PagesContext.d.ts.map +1 -0
- package/dist/components/shell/EditorShell.d.ts.map +1 -1
- package/dist/index.js +17 -3
- package/dist/lib/dexie.d.ts.map +1 -1
- package/dist/lib/dexie.js +16 -3
- package/dist/lib/events.d.ts +3 -1
- package/dist/lib/events.d.ts.map +1 -1
- package/dist/lib/index.js +2 -2
- package/dist/lib/links.d.ts +14 -0
- package/dist/lib/links.d.ts.map +1 -0
- package/dist/lib/loader.d.ts +2 -2
- package/dist/lib/loader.d.ts.map +1 -1
- package/dist/lib/nav.d.ts +23 -0
- package/dist/lib/nav.d.ts.map +1 -1
- package/dist/lib/pages.d.ts +31 -0
- package/dist/lib/pages.d.ts.map +1 -0
- package/dist/lib/registry.d.ts +7 -0
- package/dist/lib/registry.d.ts.map +1 -1
- package/dist/schemas/index.d.ts +1 -0
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +17 -3
- package/dist/schemas/link.d.ts +24 -0
- package/dist/schemas/link.d.ts.map +1 -0
- package/dist/schemas/site-config.d.ts +128 -3
- package/dist/schemas/site-config.d.ts.map +1 -1
- package/package.json +5 -1
- package/src/components/editor/MoveSectionModal.tsx +38 -0
- package/src/components/editor/PagesModal.tsx +392 -0
- package/src/components/editor/SectionWrapper.tsx +13 -0
- package/src/components/editor/SettingsForm.tsx +12 -0
- package/src/components/sections/Button/CTAButton.tsx +10 -11
- package/src/components/sections/Button/index.tsx +4 -9
- package/src/components/shared/Input.tsx +14 -3
- package/src/components/shared/LinkField.tsx +87 -0
- package/src/components/shared/Navigation.tsx +131 -136
- package/src/components/shared/PagesContext.tsx +12 -0
- package/src/components/shell/EditorShell.tsx +273 -78
- package/src/hooks/useEditorPublish.ts +1 -1
- package/src/lib/dexie.ts +18 -5
- package/src/lib/events.ts +3 -1
- package/src/lib/links.ts +41 -0
- package/src/lib/loader.ts +5 -4
- package/src/lib/nav.ts +59 -0
- package/src/lib/pages.ts +209 -0
- package/src/lib/registry.ts +8 -0
- package/src/schemas/index.ts +1 -0
- package/src/schemas/link.ts +17 -0
- package/src/schemas/site-config.ts +113 -11
|
@@ -1,38 +1,36 @@
|
|
|
1
1
|
import { useState, useCallback, useEffect, useRef } from "react";
|
|
2
2
|
import { cn } from "../../lib/cn";
|
|
3
3
|
import { Toggle } from "./Toggle";
|
|
4
|
-
import type { NavItem } from "../../lib/nav";
|
|
5
|
-
import { editModeEvent,
|
|
4
|
+
import type { SiteNav, NavItem } from "../../lib/nav";
|
|
5
|
+
import { editModeEvent, siteNavChangeEvent, pageSelectEvent, darkModeEvent, historySelectEvent } from "../../lib/events";
|
|
6
6
|
import { useActiveHeadings } from "../../hooks/useActiveHeadings";
|
|
7
7
|
import { formatDate } from "../../lib/timestamp";
|
|
8
8
|
import { Popover } from "./Popover";
|
|
9
9
|
import { HistoryPopover } from "./HistoryPopover";
|
|
10
10
|
|
|
11
11
|
interface Props {
|
|
12
|
-
|
|
12
|
+
siteNav: SiteNav;
|
|
13
13
|
siteName: string;
|
|
14
14
|
darkMode: "light" | "dark" | "optional";
|
|
15
15
|
lastUpdated: string | null;
|
|
16
|
+
// Optional same-island handler (tests). In production the editor nav is a
|
|
17
|
+
// separate island from EditorShell, so a click dispatches pageSelectEvent.
|
|
18
|
+
onPageSelect?: (pageId: string) => void;
|
|
16
19
|
}
|
|
17
20
|
|
|
18
|
-
export default function Navigation({
|
|
21
|
+
export default function Navigation({ siteNav: initialNav, siteName, darkMode, lastUpdated, onPageSelect }: Props) {
|
|
19
22
|
const [isOpen, setIsOpen] = useState(false);
|
|
20
|
-
// Must start false to match SSR (where `window` is undefined). Reading window
|
|
21
|
-
// in the initializer makes the first client render disagree with the server
|
|
22
|
-
// (edit branch vs view branch) → React hydration mismatch (#418). Set it from
|
|
23
|
-
// the pathname in the mount effect below instead.
|
|
24
23
|
const [isEditMode, setIsEditMode] = useState(false);
|
|
25
24
|
const [currentDarkMode, setCurrentDarkMode] = useState(darkMode);
|
|
26
25
|
const [isDark, setIsDark] = useState(false);
|
|
27
|
-
const [
|
|
26
|
+
const [siteNav, setSiteNav] = useState<SiteNav>(initialNav);
|
|
28
27
|
const [showHistory, setShowHistory] = useState(false);
|
|
29
28
|
const historyButtonRef = useRef<HTMLButtonElement>(null);
|
|
30
29
|
|
|
31
30
|
useEffect(() => {
|
|
32
|
-
// Resolve edit mode after mount (post-hydration) to avoid an SSR/client mismatch.
|
|
33
31
|
setIsEditMode(window.location.pathname.startsWith("/edit"));
|
|
34
32
|
const unlistenEdit = editModeEvent.listen(({ isEditMode }) => setIsEditMode(isEditMode));
|
|
35
|
-
const unlistenNav =
|
|
33
|
+
const unlistenNav = siteNavChangeEvent.listen((n) => setSiteNav(n));
|
|
36
34
|
const unlistenDark = darkModeEvent.listen((mode) => setCurrentDarkMode(mode as typeof darkMode));
|
|
37
35
|
return () => { unlistenEdit(); unlistenNav(); unlistenDark(); };
|
|
38
36
|
}, []);
|
|
@@ -40,9 +38,7 @@ export default function Navigation({ navLinks: initialNavLinks, siteName, darkMo
|
|
|
40
38
|
useEffect(() => {
|
|
41
39
|
const root = document.documentElement;
|
|
42
40
|
setIsDark(root.classList.contains("dark"));
|
|
43
|
-
const observer = new MutationObserver(() =>
|
|
44
|
-
setIsDark(root.classList.contains("dark"));
|
|
45
|
-
});
|
|
41
|
+
const observer = new MutationObserver(() => setIsDark(root.classList.contains("dark")));
|
|
46
42
|
observer.observe(root, { attributes: true, attributeFilter: ["class"] });
|
|
47
43
|
return () => observer.disconnect();
|
|
48
44
|
}, []);
|
|
@@ -55,142 +51,157 @@ export default function Navigation({ navLinks: initialNavLinks, siteName, darkMo
|
|
|
55
51
|
setTimeout(() => root.classList.remove("theme-changing"), 200);
|
|
56
52
|
}, []);
|
|
57
53
|
|
|
58
|
-
//
|
|
59
|
-
|
|
54
|
+
// The active page can live in any tier (editors navigate to hidden and
|
|
55
|
+
// archived pages via the Pages modal).
|
|
56
|
+
const activePage =
|
|
57
|
+
[...siteNav.pages, ...siteNav.hidden, ...siteNav.archive].find((p) => p.isActive) ?? null;
|
|
58
|
+
const headings: NavItem[] = activePage?.headings ?? [];
|
|
59
|
+
|
|
60
|
+
const parentIds = headings.map((h) => h.href.replace("#", ""));
|
|
60
61
|
const childIdsByParent: Record<string, string[]> = {};
|
|
61
62
|
const grandchildIdsByChild: Record<string, string[]> = {};
|
|
62
|
-
|
|
63
|
-
for (const parent of navLinks) {
|
|
63
|
+
for (const parent of headings) {
|
|
64
64
|
const pid = parent.href.replace("#", "");
|
|
65
65
|
childIdsByParent[pid] = parent.children.map((c) => c.href.replace("#", ""));
|
|
66
66
|
for (const child of parent.children) {
|
|
67
|
-
|
|
68
|
-
grandchildIdsByChild[cid] = child.children.map((gc) => gc.href.replace("#", ""));
|
|
67
|
+
grandchildIdsByChild[child.href.replace("#", "")] = child.children.map((gc) => gc.href.replace("#", ""));
|
|
69
68
|
}
|
|
70
69
|
}
|
|
71
|
-
|
|
72
70
|
const { activeParentId, activeChildId, activeGrandchildId, setActiveSection } =
|
|
73
71
|
useActiveHeadings(parentIds, childIdsByParent, grandchildIdsByChild);
|
|
74
72
|
|
|
75
|
-
const
|
|
73
|
+
const handleHeadingNav = useCallback((href: string) => {
|
|
76
74
|
const id = href.replace("#", "");
|
|
77
75
|
const el = document.getElementById(id);
|
|
78
|
-
if (el) {
|
|
79
|
-
el.scrollIntoView({ behavior: "smooth" });
|
|
80
|
-
setActiveSection(id);
|
|
81
|
-
}
|
|
76
|
+
if (el) { el.scrollIntoView({ behavior: "smooth" }); setActiveSection(id); }
|
|
82
77
|
setIsOpen(false);
|
|
83
78
|
}, [setActiveSection]);
|
|
84
79
|
|
|
80
|
+
const renderHeadings = () => (
|
|
81
|
+
<ul className="ml-3 mt-1 space-y-1 border-l border-base-200 pl-3">
|
|
82
|
+
{headings.map((parent) => {
|
|
83
|
+
const pid = parent.href.replace("#", "");
|
|
84
|
+
return (
|
|
85
|
+
<li key={pid}>
|
|
86
|
+
<button
|
|
87
|
+
onClick={() => handleHeadingNav(parent.href)}
|
|
88
|
+
className={cn("cursor-pointer w-full rounded px-2 py-1 text-left text-sm transition-colors",
|
|
89
|
+
activeParentId === pid ? "font-bold text-primary" : "text-base-contrast-light hover:text-primary",
|
|
90
|
+
!isEditMode && parent.status && parent.status !== "live" && "opacity-50")}
|
|
91
|
+
>
|
|
92
|
+
{parent.label}
|
|
93
|
+
</button>
|
|
94
|
+
{activeParentId === pid && parent.children.length > 0 && (
|
|
95
|
+
<ul className="ml-3 mt-1 space-y-1">
|
|
96
|
+
{parent.children.map((child) => {
|
|
97
|
+
const cid = child.href.replace("#", "");
|
|
98
|
+
return (
|
|
99
|
+
<li key={cid}>
|
|
100
|
+
<button onClick={() => handleHeadingNav(child.href)}
|
|
101
|
+
className={cn("cursor-pointer w-full rounded px-2 py-1 text-left text-sm transition-colors",
|
|
102
|
+
activeChildId === cid ? "font-bold text-primary" : "text-base-contrast-light hover:text-primary",
|
|
103
|
+
!isEditMode && child.status && child.status !== "live" && "opacity-50")}>
|
|
104
|
+
{child.label}
|
|
105
|
+
</button>
|
|
106
|
+
{activeChildId === cid && child.children.length > 0 && (
|
|
107
|
+
<ul className="ml-3 mt-1 space-y-1">
|
|
108
|
+
{child.children.map((gc) => {
|
|
109
|
+
const gid = gc.href.replace("#", "");
|
|
110
|
+
return (
|
|
111
|
+
<li key={gid}>
|
|
112
|
+
<button onClick={() => handleHeadingNav(gc.href)}
|
|
113
|
+
className={cn("cursor-pointer block w-full px-2 py-1 text-left text-xs transition-colors",
|
|
114
|
+
activeGrandchildId === gid ? "font-bold text-primary" : "text-base-contrast-light hover:text-primary",
|
|
115
|
+
!isEditMode && gc.status && gc.status !== "live" && "opacity-50")}>
|
|
116
|
+
{gc.label}
|
|
117
|
+
</button>
|
|
118
|
+
</li>
|
|
119
|
+
);
|
|
120
|
+
})}
|
|
121
|
+
</ul>
|
|
122
|
+
)}
|
|
123
|
+
</li>
|
|
124
|
+
);
|
|
125
|
+
})}
|
|
126
|
+
</ul>
|
|
127
|
+
)}
|
|
128
|
+
</li>
|
|
129
|
+
);
|
|
130
|
+
})}
|
|
131
|
+
</ul>
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const handlePageClick = useCallback((id: string) => {
|
|
135
|
+
if (onPageSelect) onPageSelect(id);
|
|
136
|
+
else pageSelectEvent.dispatch(id);
|
|
137
|
+
setIsOpen(false);
|
|
138
|
+
}, [onPageSelect]);
|
|
139
|
+
|
|
140
|
+
// Render an interactive button (no full navigation) when in the editor or when
|
|
141
|
+
// a same-island handler is supplied; otherwise a real <a> for the viewer.
|
|
142
|
+
const asButton = isEditMode || !!onPageSelect;
|
|
143
|
+
|
|
144
|
+
const renderPage = (page: SiteNav["pages"][number]) => (
|
|
145
|
+
<li key={page.id}>
|
|
146
|
+
{asButton ? (
|
|
147
|
+
<button
|
|
148
|
+
onClick={() => handlePageClick(page.id)}
|
|
149
|
+
className={cn("cursor-pointer w-full rounded px-3 py-2 text-left text-sm font-bold transition-colors",
|
|
150
|
+
page.isActive ? "text-primary" : "text-base-contrast hover:text-primary")}
|
|
151
|
+
>
|
|
152
|
+
{page.title}
|
|
153
|
+
</button>
|
|
154
|
+
) : (
|
|
155
|
+
<a
|
|
156
|
+
href={page.href}
|
|
157
|
+
className={cn("block w-full rounded px-3 py-2 text-left text-sm font-bold transition-colors",
|
|
158
|
+
page.isActive ? "text-primary" : "text-base-contrast hover:text-primary")}
|
|
159
|
+
>
|
|
160
|
+
{page.title}
|
|
161
|
+
</a>
|
|
162
|
+
)}
|
|
163
|
+
{page.isActive && headings.length > 0 && renderHeadings()}
|
|
164
|
+
</li>
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const renderPageGroup = (label: string, pages: SiteNav["pages"]) =>
|
|
168
|
+
pages.length > 0 && (
|
|
169
|
+
<div className="mx-4 mt-2 border-t border-base-200 pt-2">
|
|
170
|
+
<p className="px-3 py-1 text-xs font-semibold uppercase tracking-wide text-base-contrast/70">{label}</p>
|
|
171
|
+
<ul className="space-y-1">{pages.map(renderPage)}</ul>
|
|
172
|
+
</div>
|
|
173
|
+
);
|
|
174
|
+
|
|
85
175
|
return (
|
|
86
176
|
<>
|
|
87
|
-
{/* Mobile header bar */}
|
|
88
177
|
<header className="fixed top-0 left-0 right-0 z-50 bg-base lg:hidden">
|
|
89
178
|
<div className="mx-auto max-w-screen-xl flex h-16 items-center justify-between px-4">
|
|
90
179
|
<span className="text-lg font-bold text-primary">{siteName}</span>
|
|
91
|
-
<button
|
|
92
|
-
onClick={() => setIsOpen(!isOpen)}
|
|
93
|
-
className="cursor-pointer p-2 text-base-contrast"
|
|
94
|
-
aria-label="Toggle navigation"
|
|
95
|
-
>
|
|
180
|
+
<button onClick={() => setIsOpen(!isOpen)} className="cursor-pointer p-2 text-base-contrast" aria-label="Toggle navigation">
|
|
96
181
|
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
|
97
182
|
{isOpen
|
|
98
183
|
? <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
99
|
-
: <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
|
100
|
-
}
|
|
184
|
+
: <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />}
|
|
101
185
|
</svg>
|
|
102
186
|
</button>
|
|
103
187
|
</div>
|
|
104
188
|
</header>
|
|
105
189
|
|
|
106
|
-
{
|
|
107
|
-
{isOpen && (
|
|
108
|
-
<div
|
|
109
|
-
className="fixed inset-0 z-40 bg-black/50 lg:hidden"
|
|
110
|
-
onClick={() => setIsOpen(false)}
|
|
111
|
-
/>
|
|
112
|
-
)}
|
|
190
|
+
{isOpen && <div className="fixed inset-0 z-40 bg-black/50 lg:hidden" onClick={() => setIsOpen(false)} />}
|
|
113
191
|
|
|
114
|
-
{
|
|
115
|
-
|
|
116
|
-
className={cn(
|
|
117
|
-
"fixed top-0 left-0 lg:left-auto z-40 h-full w-64 flex flex-col overflow-y-auto border-r border-base-200 bg-base pt-16 transition-transform lg:translate-x-0 nav-sidebar",
|
|
118
|
-
isOpen ? "translate-x-0" : "-translate-x-full",
|
|
119
|
-
)}
|
|
120
|
-
>
|
|
121
|
-
{/* Desktop header */}
|
|
192
|
+
<nav className={cn("fixed top-0 left-0 lg:left-auto z-40 h-full w-64 flex flex-col overflow-y-auto border-r border-base-200 bg-base pt-16 transition-transform lg:translate-x-0 nav-sidebar",
|
|
193
|
+
isOpen ? "translate-x-0" : "-translate-x-full")}>
|
|
122
194
|
<div className="hidden px-4 py-4 lg:block">
|
|
123
195
|
<span className="text-lg font-bold text-primary">{siteName}</span>
|
|
124
196
|
</div>
|
|
125
197
|
|
|
126
198
|
<ul className="space-y-1 px-4 py-2">
|
|
127
|
-
{
|
|
128
|
-
const pid = parent.href.replace("#", "");
|
|
129
|
-
const isActiveParent = activeParentId === pid;
|
|
130
|
-
|
|
131
|
-
return (
|
|
132
|
-
<li key={pid}>
|
|
133
|
-
<button
|
|
134
|
-
onClick={() => handleNav(parent.href)}
|
|
135
|
-
className={cn(
|
|
136
|
-
"cursor-pointer w-full rounded px-3 py-2 text-left text-sm font-bold transition-colors",
|
|
137
|
-
isActiveParent ? "text-primary" : "text-base-contrast hover:text-primary",
|
|
138
|
-
!isEditMode && parent.status && parent.status !== "live" && "opacity-50",
|
|
139
|
-
)}
|
|
140
|
-
>
|
|
141
|
-
{parent.label}
|
|
142
|
-
</button>
|
|
143
|
-
|
|
144
|
-
{isActiveParent && parent.children.length > 0 && (
|
|
145
|
-
<ul className="ml-3 mt-1 space-y-1 border-l border-base-200 pl-3">
|
|
146
|
-
{parent.children.map((child) => {
|
|
147
|
-
const cid = child.href.replace("#", "");
|
|
148
|
-
const isActiveChild = activeChildId === cid;
|
|
149
|
-
|
|
150
|
-
return (
|
|
151
|
-
<li key={cid}>
|
|
152
|
-
<button
|
|
153
|
-
onClick={() => handleNav(child.href)}
|
|
154
|
-
className={cn(
|
|
155
|
-
"cursor-pointer w-full rounded px-2 py-1 text-left text-sm transition-colors",
|
|
156
|
-
isActiveChild ? "font-bold text-primary" : "text-base-contrast-light hover:text-primary",
|
|
157
|
-
!isEditMode && child.status && child.status !== "live" && "opacity-50",
|
|
158
|
-
)}
|
|
159
|
-
>
|
|
160
|
-
{child.label}
|
|
161
|
-
</button>
|
|
162
|
-
|
|
163
|
-
{isActiveChild && child.children.length > 0 && (
|
|
164
|
-
<ul className="ml-3 mt-1 space-y-1">
|
|
165
|
-
{child.children.map((gc) => {
|
|
166
|
-
const gid = gc.href.replace("#", "");
|
|
167
|
-
return (
|
|
168
|
-
<li key={gid}>
|
|
169
|
-
<button
|
|
170
|
-
onClick={() => handleNav(gc.href)}
|
|
171
|
-
className={cn(
|
|
172
|
-
"cursor-pointer block w-full px-2 py-1 text-left text-xs transition-colors",
|
|
173
|
-
activeGrandchildId === gid ? "font-bold text-primary" : "text-base-contrast-light hover:text-primary",
|
|
174
|
-
!isEditMode && gc.status && gc.status !== "live" && "opacity-50",
|
|
175
|
-
)}
|
|
176
|
-
>
|
|
177
|
-
{gc.label}
|
|
178
|
-
</button>
|
|
179
|
-
</li>
|
|
180
|
-
);
|
|
181
|
-
})}
|
|
182
|
-
</ul>
|
|
183
|
-
)}
|
|
184
|
-
</li>
|
|
185
|
-
);
|
|
186
|
-
})}
|
|
187
|
-
</ul>
|
|
188
|
-
)}
|
|
189
|
-
</li>
|
|
190
|
-
);
|
|
191
|
-
})}
|
|
199
|
+
{siteNav.pages.map(renderPage)}
|
|
192
200
|
</ul>
|
|
193
201
|
|
|
202
|
+
{renderPageGroup("Hidden from Menu", siteNav.hidden)}
|
|
203
|
+
{renderPageGroup("Archive", siteNav.archive)}
|
|
204
|
+
|
|
194
205
|
<div className="mt-auto">
|
|
195
206
|
{currentDarkMode === "optional" && (
|
|
196
207
|
<div className="mx-4 border-t border-base-200 py-4">
|
|
@@ -201,37 +212,21 @@ export default function Navigation({ navLinks: initialNavLinks, siteName, darkMo
|
|
|
201
212
|
</div>
|
|
202
213
|
</div>
|
|
203
214
|
)}
|
|
204
|
-
|
|
205
215
|
{lastUpdated && (
|
|
206
216
|
<div className={cn(currentDarkMode !== "optional" && "border-t border-base-200", "mx-4 py-4 text-center")}>
|
|
207
217
|
{isEditMode ? (
|
|
208
218
|
<div className="relative">
|
|
209
|
-
<button
|
|
210
|
-
|
|
211
|
-
onClick={() => setShowHistory((prev) => !prev)}
|
|
212
|
-
className="cursor-pointer text-xs text-base-contrast-light hover:text-primary transition-colors"
|
|
213
|
-
aria-label="View history"
|
|
214
|
-
>
|
|
219
|
+
<button ref={historyButtonRef} onClick={() => setShowHistory((p) => !p)}
|
|
220
|
+
className="cursor-pointer text-xs text-base-contrast-light hover:text-primary transition-colors" aria-label="View history">
|
|
215
221
|
Last updated {formatDate(lastUpdated)}
|
|
216
222
|
</button>
|
|
217
|
-
<Popover
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
anchorRef={historyButtonRef}
|
|
221
|
-
className="w-56 !bottom-full !top-auto !mb-1 !mt-0"
|
|
222
|
-
>
|
|
223
|
-
<HistoryPopover
|
|
224
|
-
onSelectCommit={(sha, date) => {
|
|
225
|
-
setShowHistory(false);
|
|
226
|
-
historySelectEvent.dispatch({ sha, date });
|
|
227
|
-
}}
|
|
228
|
-
/>
|
|
223
|
+
<Popover isOpen={showHistory} onClose={() => setShowHistory(false)} anchorRef={historyButtonRef}
|
|
224
|
+
className="w-56 !bottom-full !top-auto !mb-1 !mt-0">
|
|
225
|
+
<HistoryPopover onSelectCommit={(sha, date) => { setShowHistory(false); historySelectEvent.dispatch({ sha, date }); }} />
|
|
229
226
|
</Popover>
|
|
230
227
|
</div>
|
|
231
228
|
) : (
|
|
232
|
-
<p className="text-xs text-base-contrast-light">
|
|
233
|
-
Last updated {formatDate(lastUpdated)}
|
|
234
|
-
</p>
|
|
229
|
+
<p className="text-xs text-base-contrast-light">Last updated {formatDate(lastUpdated)}</p>
|
|
235
230
|
)}
|
|
236
231
|
</div>
|
|
237
232
|
)}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
|
|
3
|
+
export interface PagesContextValue {
|
|
4
|
+
pages: { id: string; title: string }[];
|
|
5
|
+
getPageHeadings: (pageId: string) => { id: string; label: string }[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const PagesContext = createContext<PagesContextValue>({ pages: [], getPageHeadings: () => [] });
|
|
9
|
+
|
|
10
|
+
export function usePages(): PagesContextValue {
|
|
11
|
+
return useContext(PagesContext);
|
|
12
|
+
}
|