@drawnagency/primitives 0.1.58 → 0.1.60
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/capabilities.d.ts +16 -0
- package/dist/auth/capabilities.d.ts.map +1 -0
- package/dist/auth/index.d.ts +2 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +3 -1
- package/dist/auth/types.d.ts +26 -22
- package/dist/auth/types.d.ts.map +1 -1
- package/dist/{chunk-ICLXLWQ5.js → chunk-BT7STGDW.js} +31 -23
- package/dist/{chunk-L2JJFOXD.js → chunk-CBWK3KPV.js} +5 -1
- package/dist/{chunk-V7JN2DDU.js → chunk-ODCJQOVO.js} +1 -1
- package/dist/{chunk-AN62WPW7.js → chunk-Q5EEYBMJ.js} +8 -5
- package/dist/chunk-TZQPOR5A.js +24 -0
- package/dist/{chunk-XTK4BR27.js → chunk-UNWNT52N.js} +13 -0
- package/dist/chunk-ZGFAYZWB.js +8 -0
- package/dist/components/editor/DropEdgeIndicator.d.ts +8 -0
- package/dist/components/editor/DropEdgeIndicator.d.ts.map +1 -0
- package/dist/components/editor/PagesModal.d.ts.map +1 -1
- package/dist/components/editor/SectionOrderingModal.d.ts.map +1 -1
- package/dist/components/primitives/defineHeadingSection.d.ts +28 -0
- package/dist/components/primitives/defineHeadingSection.d.ts.map +1 -0
- package/dist/components/sections/Colors/index.d.ts.map +1 -1
- package/dist/components/sections/Container/index.d.ts.map +1 -1
- package/dist/components/sections/IconList/IconList.d.ts +1 -6
- package/dist/components/sections/IconList/IconList.d.ts.map +1 -1
- package/dist/components/sections/IconList/index.d.ts +11 -0
- package/dist/components/sections/IconList/index.d.ts.map +1 -1
- package/dist/components/sections/LinkHeading/index.d.ts.map +1 -1
- package/dist/components/sections/SubHeading/index.d.ts +0 -1
- package/dist/components/sections/SubHeading/index.d.ts.map +1 -1
- package/dist/components/sections/SubSubHeading/index.d.ts +0 -1
- package/dist/components/sections/SubSubHeading/index.d.ts.map +1 -1
- package/dist/components/sections/all-sections.d.ts +0 -2
- package/dist/components/sections/all-sections.d.ts.map +1 -1
- package/dist/components/sections/register-schemas.js +409 -420
- package/dist/components/shared/Navigation.d.ts.map +1 -1
- package/dist/components/shell/BugReportFAB.d.ts.map +1 -1
- package/dist/components/shell/EditorShell.d.ts.map +1 -1
- package/dist/hooks/useEditorPublish.d.ts.map +1 -1
- package/dist/hooks/useMediaPipeline.d.ts.map +1 -1
- package/dist/hooks/useSortableRow.d.ts +25 -0
- package/dist/hooks/useSortableRow.d.ts.map +1 -0
- package/dist/index.js +27 -24
- package/dist/lib/events.d.ts +1 -0
- package/dist/lib/events.d.ts.map +1 -1
- package/dist/lib/image-refs.d.ts +21 -0
- package/dist/lib/image-refs.d.ts.map +1 -0
- package/dist/lib/index.js +8 -7
- package/dist/lib/platform-broker.d.ts +14 -0
- package/dist/lib/platform-broker.d.ts.map +1 -0
- package/dist/lib/platform-broker.js +60 -0
- package/dist/lib/reorder.d.ts +12 -0
- package/dist/lib/reorder.d.ts.map +1 -0
- package/dist/lib/text.d.ts +2 -0
- package/dist/lib/text.d.ts.map +1 -1
- package/dist/media/index.js +3 -2
- package/dist/media/utils.d.ts.map +1 -1
- package/dist/schemas/index.js +11 -11
- package/dist/types/database.d.ts +46 -0
- package/dist/types/database.d.ts.map +1 -1
- package/package.json +6 -2
- package/src/auth/capabilities.ts +26 -0
- package/src/auth/index.ts +3 -0
- package/src/auth/types.ts +36 -26
- package/src/components/editor/DropEdgeIndicator.tsx +13 -0
- package/src/components/editor/PagesModal.tsx +12 -80
- package/src/components/editor/SectionOrderingModal.tsx +8 -78
- package/src/components/primitives/defineHeadingSection.tsx +56 -0
- package/src/components/sections/Colors/index.tsx +2 -4
- package/src/components/sections/Container/index.tsx +2 -4
- package/src/components/sections/IconList/IconList.tsx +1 -7
- package/src/components/sections/IconList/index.tsx +11 -5
- package/src/components/sections/LinkHeading/index.tsx +5 -20
- package/src/components/sections/SubHeading/index.tsx +6 -23
- package/src/components/sections/SubSubHeading/index.tsx +6 -23
- package/src/components/shared/Navigation.tsx +6 -4
- package/src/components/shell/BugReportFAB.tsx +13 -9
- package/src/components/shell/EditorShell.tsx +5 -1
- package/src/hooks/useEditorPublish.ts +71 -49
- package/src/hooks/useMediaPipeline.ts +11 -34
- package/src/hooks/useSortableRow.ts +99 -0
- package/src/lib/events.ts +1 -0
- package/src/lib/image-refs.ts +53 -0
- package/src/lib/platform-broker.ts +72 -0
- package/src/lib/reorder.ts +20 -0
- package/src/lib/text.ts +5 -0
- package/src/media/utils.ts +6 -1
- package/src/types/database.ts +41 -0
- package/dist/chunk-DKOUFIP6.js +0 -35
package/src/auth/types.ts
CHANGED
|
@@ -21,40 +21,50 @@ export type TokenExchangeResult =
|
|
|
21
21
|
| { success: false; error: string };
|
|
22
22
|
|
|
23
23
|
export interface AuthProvider {
|
|
24
|
-
|
|
25
|
-
oauth: boolean;
|
|
26
|
-
emailPassword: boolean;
|
|
27
|
-
passwordOnly: boolean;
|
|
28
|
-
userManagement: boolean;
|
|
29
|
-
audienceManagement: boolean;
|
|
30
|
-
passwordToggle: boolean;
|
|
31
|
-
};
|
|
32
|
-
oauthProviders?: ("google" | "github")[];
|
|
33
|
-
|
|
24
|
+
// --- Required: every provider authenticates a session. ---
|
|
34
25
|
resolveSession(ctx: AuthContext): Promise<Session | null>;
|
|
35
26
|
signIn(method: SignInMethod, ctx: AuthContext): Promise<SignInResult>;
|
|
36
27
|
signOut(ctx: AuthContext): Promise<void>;
|
|
37
28
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
29
|
+
// --- Declarative sign-in surface (drives the login UI only). ---
|
|
30
|
+
// signIn() is polymorphic and handles all of these; there is no backing method
|
|
31
|
+
// to derive presence from, so these stay declared rather than presence-derived.
|
|
32
|
+
signInMethods: {
|
|
33
|
+
oauth?: ("google" | "github")[]; // non-empty → render OAuth buttons
|
|
34
|
+
emailPassword?: boolean; // render the email + password form
|
|
35
|
+
passwordOnly?: boolean; // single shared-password mode
|
|
36
|
+
};
|
|
44
37
|
|
|
45
|
-
//
|
|
46
|
-
|
|
38
|
+
// --- Viewer access control ("audiences"). Read required; manage optional. ---
|
|
39
|
+
audiences: {
|
|
40
|
+
list(): Promise<Audience[]>;
|
|
41
|
+
verify(name: string, password: string): Promise<boolean>;
|
|
42
|
+
get?(name: string): Promise<Audience | null>; // optional single-row optimization
|
|
43
|
+
// Presence of `manage` === the old capabilities.audienceManagement.
|
|
44
|
+
manage?: {
|
|
45
|
+
create(displayName: string, color: string | null, password: string): Promise<void>;
|
|
46
|
+
setPassword(name: string, password: string): Promise<void>;
|
|
47
|
+
setColor(name: string, color: string | null): Promise<void>;
|
|
48
|
+
setDisplayName(name: string, displayName: string): Promise<void>;
|
|
49
|
+
delete(name: string): Promise<void>;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
47
52
|
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
// --- Shared-password toggle. Read required; `set` optional. ---
|
|
54
|
+
// Presence of `set` === the old capabilities.passwordToggle.
|
|
55
|
+
passwordEnabled: {
|
|
56
|
+
get(): Promise<boolean>;
|
|
57
|
+
set?(enabled: boolean): Promise<void>;
|
|
58
|
+
};
|
|
54
59
|
|
|
55
|
-
|
|
56
|
-
|
|
60
|
+
// --- Site user management. Presence === the old capabilities.userManagement. ---
|
|
61
|
+
userManagement?: {
|
|
62
|
+
list(): Promise<SiteUser[]>;
|
|
63
|
+
invite(email: string, role: Role): Promise<void>;
|
|
64
|
+
revoke(userId: string): Promise<void>;
|
|
65
|
+
};
|
|
57
66
|
|
|
67
|
+
// --- Provider-specific auth flows (optional; shape unchanged). ---
|
|
58
68
|
handleCallback?(request: Request, ctx: AuthContext): Promise<Response>;
|
|
59
69
|
handleTokenExchange?(tokens: { accessToken: string; refreshToken: string }, type: TokenExchangeType, ctx: AuthContext): Promise<TokenExchangeResult>;
|
|
60
70
|
updatePassword?(password: string, ctx: AuthContext): Promise<void>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The horizontal insertion line shown at the top or bottom edge of a row during
|
|
3
|
+
* a closest-edge drag-to-reorder, paired with `useSortableRow`'s `closestEdge`.
|
|
4
|
+
*/
|
|
5
|
+
export function DropEdgeIndicator({ edge }: { edge: "top" | "bottom" | null }) {
|
|
6
|
+
if (edge === "top") {
|
|
7
|
+
return <div className="absolute top-0 right-0 left-0 z-10 h-0.5 -translate-y-1/2 bg-primary" />;
|
|
8
|
+
}
|
|
9
|
+
if (edge === "bottom") {
|
|
10
|
+
return <div className="absolute right-0 bottom-0 left-0 z-10 h-0.5 translate-y-1/2 bg-primary" />;
|
|
11
|
+
}
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useEffect
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
2
|
import { Check } from "lucide-react";
|
|
3
3
|
import type { SiteIndex, Page } from "../../schemas/site-config";
|
|
4
4
|
import type { Audience } from "../../auth/types";
|
|
@@ -7,7 +7,10 @@ import { Input } from "../shared/Input";
|
|
|
7
7
|
import { Checkbox } from "../shared/Checkbox";
|
|
8
8
|
import { AudienceIndicator } from "./AudienceIndicator";
|
|
9
9
|
import { DragHandle } from "./DragHandle";
|
|
10
|
+
import { useSortableRow } from "../../hooks/useSortableRow";
|
|
11
|
+
import { DropEdgeIndicator } from "./DropEdgeIndicator";
|
|
10
12
|
import { slugifyPageSlug, uniquePageSlug, pageDisplayTitle } from "../../lib/pages";
|
|
13
|
+
import { pluralize } from "../../lib/text";
|
|
11
14
|
import { RESERVED_SLUGS } from "../../schemas/site-config";
|
|
12
15
|
import { cn } from "../../lib/cn";
|
|
13
16
|
|
|
@@ -111,11 +114,13 @@ function PageRow({
|
|
|
111
114
|
onSetFields: (patch: Partial<Pick<Page, "title" | "slug" | "showInNav">>) => void;
|
|
112
115
|
onSetAudience: (access: string[]) => void; onRequestDelete: () => void;
|
|
113
116
|
}) {
|
|
114
|
-
const rowRef = useRef<HTMLDivElement>(null);
|
|
115
|
-
const handleRef = useRef<HTMLButtonElement>(null);
|
|
116
|
-
const [isDragging, setIsDragging] = useState(false);
|
|
117
|
-
const [closestEdge, setClosestEdge] = useState<"top" | "bottom" | null>(null);
|
|
118
117
|
const draggable = dragIndex !== undefined;
|
|
118
|
+
const { rowRef, handleRef, isDragging, closestEdge } = useSortableRow<HTMLDivElement, HTMLButtonElement>({
|
|
119
|
+
index: dragIndex ?? 0,
|
|
120
|
+
dragType: "page-ordering-row",
|
|
121
|
+
onReorder,
|
|
122
|
+
enabled: draggable,
|
|
123
|
+
});
|
|
119
124
|
|
|
120
125
|
// The slug input edits a local draft; only valid values commit to the index,
|
|
121
126
|
// which must always hold a non-empty, unique, non-reserved slug.
|
|
@@ -189,85 +194,12 @@ function PageRow({
|
|
|
189
194
|
onToggleExpand();
|
|
190
195
|
};
|
|
191
196
|
|
|
192
|
-
useEffect(() => {
|
|
193
|
-
if (dragIndex === undefined) return;
|
|
194
|
-
const row = rowRef.current;
|
|
195
|
-
const handle = handleRef.current;
|
|
196
|
-
if (!row || !handle) return;
|
|
197
|
-
|
|
198
|
-
let cleanup: (() => void) | undefined;
|
|
199
|
-
let cancelled = false;
|
|
200
|
-
|
|
201
|
-
Promise.all([
|
|
202
|
-
import("@atlaskit/pragmatic-drag-and-drop/element/adapter"),
|
|
203
|
-
import("@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge"),
|
|
204
|
-
]).then(([{ draggable, dropTargetForElements }, { attachClosestEdge, extractClosestEdge }]) => {
|
|
205
|
-
if (cancelled) return;
|
|
206
|
-
|
|
207
|
-
const cleanupDraggable = draggable({
|
|
208
|
-
element: row,
|
|
209
|
-
dragHandle: handle,
|
|
210
|
-
getInitialData: () => ({ dragType: "page-ordering-row", index: dragIndex }),
|
|
211
|
-
onGenerateDragPreview: () => {
|
|
212
|
-
row.style.opacity = "0.4";
|
|
213
|
-
requestAnimationFrame(() => {
|
|
214
|
-
row.style.opacity = "";
|
|
215
|
-
});
|
|
216
|
-
},
|
|
217
|
-
onDragStart: () => setIsDragging(true),
|
|
218
|
-
onDrop: () => setIsDragging(false),
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
const cleanupDropTarget = dropTargetForElements({
|
|
222
|
-
element: row,
|
|
223
|
-
canDrop: ({ source }) => source.data.dragType === "page-ordering-row",
|
|
224
|
-
getData: ({ input, element }) =>
|
|
225
|
-
attachClosestEdge({ index: dragIndex }, { input, element, allowedEdges: ["top", "bottom"] }),
|
|
226
|
-
onDragEnter: ({ self }) => {
|
|
227
|
-
const edge = extractClosestEdge(self.data);
|
|
228
|
-
setClosestEdge(edge === "top" || edge === "bottom" ? edge : null);
|
|
229
|
-
},
|
|
230
|
-
onDrag: ({ self }) => {
|
|
231
|
-
const edge = extractClosestEdge(self.data);
|
|
232
|
-
setClosestEdge(edge === "top" || edge === "bottom" ? edge : null);
|
|
233
|
-
},
|
|
234
|
-
onDragLeave: () => setClosestEdge(null),
|
|
235
|
-
onDrop: ({ source, self }) => {
|
|
236
|
-
setClosestEdge(null);
|
|
237
|
-
const fromIndex = source.data.index as number;
|
|
238
|
-
const edge = extractClosestEdge(self.data);
|
|
239
|
-
let toIndex = dragIndex;
|
|
240
|
-
if (edge === "bottom") toIndex = dragIndex + 1;
|
|
241
|
-
if (fromIndex < toIndex) toIndex--;
|
|
242
|
-
if (fromIndex !== toIndex) {
|
|
243
|
-
onReorder(fromIndex, toIndex);
|
|
244
|
-
}
|
|
245
|
-
},
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
cleanup = () => {
|
|
249
|
-
cleanupDraggable();
|
|
250
|
-
cleanupDropTarget();
|
|
251
|
-
};
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
return () => {
|
|
255
|
-
cancelled = true;
|
|
256
|
-
cleanup?.();
|
|
257
|
-
};
|
|
258
|
-
}, [dragIndex, onReorder]);
|
|
259
|
-
|
|
260
197
|
return (
|
|
261
198
|
<div
|
|
262
199
|
ref={rowRef}
|
|
263
200
|
className={cn("relative rounded-md border border-base-200", isDragging && "opacity-50")}
|
|
264
201
|
>
|
|
265
|
-
{closestEdge
|
|
266
|
-
<div className="absolute top-0 right-0 left-0 z-10 h-0.5 -translate-y-1/2 bg-primary" />
|
|
267
|
-
)}
|
|
268
|
-
{closestEdge === "bottom" && (
|
|
269
|
-
<div className="absolute right-0 bottom-0 left-0 z-10 h-0.5 translate-y-1/2 bg-primary" />
|
|
270
|
-
)}
|
|
202
|
+
<DropEdgeIndicator edge={closestEdge} />
|
|
271
203
|
<div className="flex items-center gap-2 px-3 py-2">
|
|
272
204
|
{draggable && (
|
|
273
205
|
<div className="shrink-0 [&_[role=tooltip]]:hidden">
|
|
@@ -375,7 +307,7 @@ function DeleteConfirm({ page, onCancel, onConfirm }: { page: Page; onCancel: ()
|
|
|
375
307
|
<div className="flex flex-col gap-3">
|
|
376
308
|
<p className="text-sm text-base-contrast">
|
|
377
309
|
Deleting <strong>{confirmName}</strong> permanently removes the page and its{" "}
|
|
378
|
-
<strong>{n
|
|
310
|
+
<strong>{pluralize(n, "section")}</strong>. This is recoverable only via GitHub version history.
|
|
379
311
|
</p>
|
|
380
312
|
<Input
|
|
381
313
|
label={`Type the page name "${confirmName}" to confirm`}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { useState, useEffect, useRef } from "react";
|
|
2
1
|
import type { LoadedSection } from "../../lib/loader";
|
|
3
2
|
import type { Thumbnail } from "../../lib/registry";
|
|
4
3
|
import { getSection } from "../../lib/registry";
|
|
5
4
|
import { getMediaProvider } from "../../media";
|
|
6
5
|
import type { MediaManifest } from "../../media/types";
|
|
7
6
|
import { DragHandle } from "./DragHandle";
|
|
7
|
+
import { useSortableRow } from "../../hooks/useSortableRow";
|
|
8
|
+
import { DropEdgeIndicator } from "./DropEdgeIndicator";
|
|
8
9
|
import { cn } from "../../lib/cn";
|
|
9
10
|
|
|
10
11
|
interface SectionOrderingModalProps {
|
|
@@ -34,10 +35,11 @@ function SectionRow({
|
|
|
34
35
|
mediaManifest: MediaManifest;
|
|
35
36
|
onReorder: (fromIndex: number, toIndex: number) => void;
|
|
36
37
|
}) {
|
|
37
|
-
const rowRef =
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
const { rowRef, handleRef, isDragging, closestEdge } = useSortableRow<HTMLDivElement, HTMLButtonElement>({
|
|
39
|
+
index,
|
|
40
|
+
dragType: "ordering-row",
|
|
41
|
+
onReorder,
|
|
42
|
+
});
|
|
41
43
|
|
|
42
44
|
const definition = getSection(section.section.type);
|
|
43
45
|
const label = definition?.getLabel?.(section.section) ?? definition?.label ?? section.section.type;
|
|
@@ -46,73 +48,6 @@ function SectionRow({
|
|
|
46
48
|
const thumbnails = allThumbnails.slice(0, 3);
|
|
47
49
|
const remainingCount = Math.max(0, allThumbnails.length - 3);
|
|
48
50
|
|
|
49
|
-
useEffect(() => {
|
|
50
|
-
const row = rowRef.current;
|
|
51
|
-
const handle = handleRef.current;
|
|
52
|
-
if (!row || !handle) return;
|
|
53
|
-
|
|
54
|
-
let cleanup: (() => void) | undefined;
|
|
55
|
-
let cancelled = false;
|
|
56
|
-
|
|
57
|
-
Promise.all([
|
|
58
|
-
import("@atlaskit/pragmatic-drag-and-drop/element/adapter"),
|
|
59
|
-
import("@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge"),
|
|
60
|
-
]).then(([{ draggable, dropTargetForElements }, { attachClosestEdge, extractClosestEdge }]) => {
|
|
61
|
-
if (cancelled) return;
|
|
62
|
-
|
|
63
|
-
const cleanupDraggable = draggable({
|
|
64
|
-
element: row,
|
|
65
|
-
dragHandle: handle,
|
|
66
|
-
getInitialData: () => ({ dragType: "ordering-row", index }),
|
|
67
|
-
onGenerateDragPreview: () => {
|
|
68
|
-
row.style.opacity = "0.4";
|
|
69
|
-
requestAnimationFrame(() => {
|
|
70
|
-
row.style.opacity = "";
|
|
71
|
-
});
|
|
72
|
-
},
|
|
73
|
-
onDragStart: () => setIsDragging(true),
|
|
74
|
-
onDrop: () => setIsDragging(false),
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
const cleanupDropTarget = dropTargetForElements({
|
|
78
|
-
element: row,
|
|
79
|
-
canDrop: ({ source }) => source.data.dragType === "ordering-row",
|
|
80
|
-
getData: ({ input, element }) =>
|
|
81
|
-
attachClosestEdge({ index }, { input, element, allowedEdges: ["top", "bottom"] }),
|
|
82
|
-
onDragEnter: ({ self }) => {
|
|
83
|
-
const edge = extractClosestEdge(self.data);
|
|
84
|
-
setClosestEdge(edge === "top" || edge === "bottom" ? edge : null);
|
|
85
|
-
},
|
|
86
|
-
onDrag: ({ self }) => {
|
|
87
|
-
const edge = extractClosestEdge(self.data);
|
|
88
|
-
setClosestEdge(edge === "top" || edge === "bottom" ? edge : null);
|
|
89
|
-
},
|
|
90
|
-
onDragLeave: () => setClosestEdge(null),
|
|
91
|
-
onDrop: ({ source, self }) => {
|
|
92
|
-
setClosestEdge(null);
|
|
93
|
-
const fromIndex = source.data.index as number;
|
|
94
|
-
const edge = extractClosestEdge(self.data);
|
|
95
|
-
let toIndex = index;
|
|
96
|
-
if (edge === "bottom") toIndex = index + 1;
|
|
97
|
-
if (fromIndex < toIndex) toIndex--;
|
|
98
|
-
if (fromIndex !== toIndex) {
|
|
99
|
-
onReorder(fromIndex, toIndex);
|
|
100
|
-
}
|
|
101
|
-
},
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
cleanup = () => {
|
|
105
|
-
cleanupDraggable();
|
|
106
|
-
cleanupDropTarget();
|
|
107
|
-
};
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
return () => {
|
|
111
|
-
cancelled = true;
|
|
112
|
-
cleanup?.();
|
|
113
|
-
};
|
|
114
|
-
}, [index, onReorder]);
|
|
115
|
-
|
|
116
51
|
return (
|
|
117
52
|
<div
|
|
118
53
|
ref={rowRef}
|
|
@@ -121,12 +56,7 @@ function SectionRow({
|
|
|
121
56
|
isDragging && "opacity-50",
|
|
122
57
|
)}
|
|
123
58
|
>
|
|
124
|
-
{closestEdge
|
|
125
|
-
<div className="absolute top-0 right-0 left-0 z-10 h-0.5 -translate-y-1/2 bg-primary" />
|
|
126
|
-
)}
|
|
127
|
-
{closestEdge === "bottom" && (
|
|
128
|
-
<div className="absolute right-0 bottom-0 left-0 z-10 h-0.5 translate-y-1/2 bg-primary" />
|
|
129
|
-
)}
|
|
59
|
+
<DropEdgeIndicator edge={closestEdge} />
|
|
130
60
|
|
|
131
61
|
<div className="shrink-0 [&_[role=tooltip]]:hidden">
|
|
132
62
|
<DragHandle ref={handleRef} />
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { defineSection } from "../../lib/registry";
|
|
4
|
+
import { HeadingSection } from "./HeadingSection";
|
|
5
|
+
|
|
6
|
+
interface HeadingSectionConfig<T extends string> {
|
|
7
|
+
type: T;
|
|
8
|
+
label: string;
|
|
9
|
+
icon: ReactNode;
|
|
10
|
+
/** Heading level reported to nav generation (one above the rendered tag by design). */
|
|
11
|
+
navRole: "h1" | "h2" | "h3";
|
|
12
|
+
/** The actual HTML tag rendered. */
|
|
13
|
+
tag: "h2" | "h3" | "h4";
|
|
14
|
+
placeholder: string;
|
|
15
|
+
defaultHeading: string;
|
|
16
|
+
/** When true, adds the `excludeFromNav` content field plus its settings toggle. */
|
|
17
|
+
navExclusion?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Factory for the heading section family (link / sub / sub-sub). They differ only
|
|
22
|
+
* in their type tag, label, icon, nav level, rendered tag, placeholder, default
|
|
23
|
+
* text, and whether they expose the nav-exclusion toggle — all three render the
|
|
24
|
+
* shared HeadingSection primitive through the identical edit/view plumbing.
|
|
25
|
+
*/
|
|
26
|
+
export function defineHeadingSection<T extends string>(config: HeadingSectionConfig<T>) {
|
|
27
|
+
const content = config.navExclusion
|
|
28
|
+
? z.object({ heading: z.string(), excludeFromNav: z.boolean().optional() })
|
|
29
|
+
: z.object({ heading: z.string() });
|
|
30
|
+
const schema = z.object({ type: z.literal(config.type), content });
|
|
31
|
+
|
|
32
|
+
return defineSection({
|
|
33
|
+
type: config.type,
|
|
34
|
+
label: config.label,
|
|
35
|
+
icon: config.icon,
|
|
36
|
+
schema,
|
|
37
|
+
navRole: config.navRole,
|
|
38
|
+
component: ({ content, onChange }) => (
|
|
39
|
+
<HeadingSection
|
|
40
|
+
heading={content.content.heading}
|
|
41
|
+
tag={config.tag}
|
|
42
|
+
placeholder={config.placeholder}
|
|
43
|
+
onChange={onChange ? (heading: string) => onChange({ ...content, content: { ...content.content, heading } }) : undefined}
|
|
44
|
+
/>
|
|
45
|
+
),
|
|
46
|
+
defaults: () => ({ type: config.type, content: { heading: config.defaultHeading } }),
|
|
47
|
+
getLabel: (content) => content.content.heading,
|
|
48
|
+
...(config.navExclusion
|
|
49
|
+
? {
|
|
50
|
+
settings: {
|
|
51
|
+
excludeFromNav: { type: "checkbox" as const, label: "Exclude from navigation", default: false, target: "content" as const },
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
: {}),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
@@ -3,6 +3,7 @@ import { z } from "zod";
|
|
|
3
3
|
import { Palette } from "lucide-react";
|
|
4
4
|
import { ColorItemSchema } from "../../../schemas/shared";
|
|
5
5
|
import Colors from "../../brandguide/Colors";
|
|
6
|
+
import { pluralize } from "../../../lib/text";
|
|
6
7
|
|
|
7
8
|
const schema = z.object({
|
|
8
9
|
type: z.literal("colors"),
|
|
@@ -35,10 +36,7 @@ export default defineSection({
|
|
|
35
36
|
type: "colors" as const,
|
|
36
37
|
content: { colors: [{ spaces: [{ hex: "#000000" }] }] },
|
|
37
38
|
}),
|
|
38
|
-
getLabel: (content) =>
|
|
39
|
-
const n = content.content.colors.length;
|
|
40
|
-
return `${n} color${n === 1 ? "" : "s"}`;
|
|
41
|
-
},
|
|
39
|
+
getLabel: (content) => pluralize(content.content.colors.length, "color"),
|
|
42
40
|
getThumbnails: (content) =>
|
|
43
41
|
content.content.colors
|
|
44
42
|
.filter((c) => c.spaces[0]?.hex)
|
|
@@ -5,6 +5,7 @@ import { getSectionSchema } from "../../../schemas/sections";
|
|
|
5
5
|
import { MAX_CONTAINER_COLUMNS } from "../../../lib/container-grid";
|
|
6
6
|
import { Container } from "./Container";
|
|
7
7
|
import { ContainerSettingsForm } from "./ContainerSettingsForm";
|
|
8
|
+
import { pluralize } from "../../../lib/text";
|
|
8
9
|
|
|
9
10
|
const schema = z.object({
|
|
10
11
|
type: z.literal("container"),
|
|
@@ -43,9 +44,6 @@ export default defineSection({
|
|
|
43
44
|
type: "container" as const,
|
|
44
45
|
content: { columns: 1, flow: "row" as const, children: [] },
|
|
45
46
|
}),
|
|
46
|
-
getLabel: (content) => {
|
|
47
|
-
const n = content.content.children.length;
|
|
48
|
-
return `Container (${n} item${n === 1 ? "" : "s"})`;
|
|
49
|
-
},
|
|
47
|
+
getLabel: (content) => `Container (${pluralize(content.content.children.length, "item")})`,
|
|
50
48
|
settingsForm: ContainerSettingsForm,
|
|
51
49
|
});
|
|
@@ -8,13 +8,7 @@ import { DragHandle, DeleteIcon, AddIcon } from "../../shared/icons";
|
|
|
8
8
|
import { CopyPlus, Pencil } from "lucide-react";
|
|
9
9
|
import { IconButton } from "../../shared/IconButton";
|
|
10
10
|
import { IconPicker } from "../../primitives/IconPicker";
|
|
11
|
-
|
|
12
|
-
interface IconListItem {
|
|
13
|
-
label: string;
|
|
14
|
-
text: string;
|
|
15
|
-
icon?: string;
|
|
16
|
-
dodont?: "do" | "dont";
|
|
17
|
-
}
|
|
11
|
+
import type { IconListItem } from "./index";
|
|
18
12
|
|
|
19
13
|
interface Props {
|
|
20
14
|
items: IconListItem[];
|
|
@@ -3,11 +3,20 @@ import { z } from "zod";
|
|
|
3
3
|
import { List } from "lucide-react";
|
|
4
4
|
import IconList from "./IconList";
|
|
5
5
|
import { IconListSettings } from "./IconListSettings";
|
|
6
|
+
import { pluralize } from "../../../lib/text";
|
|
7
|
+
|
|
8
|
+
export const IconListItemSchema = z.object({
|
|
9
|
+
label: z.string(),
|
|
10
|
+
text: z.string(),
|
|
11
|
+
icon: z.string().optional(),
|
|
12
|
+
dodont: z.enum(["do", "dont"]).optional(),
|
|
13
|
+
});
|
|
14
|
+
export type IconListItem = z.infer<typeof IconListItemSchema>;
|
|
6
15
|
|
|
7
16
|
const schema = z.object({
|
|
8
17
|
type: z.literal("icon_list"),
|
|
9
18
|
content: z.object({
|
|
10
|
-
items: z.array(
|
|
19
|
+
items: z.array(IconListItemSchema),
|
|
11
20
|
}),
|
|
12
21
|
options: z.object({
|
|
13
22
|
icon: z.string().nullable().optional(),
|
|
@@ -34,9 +43,6 @@ export default defineSection({
|
|
|
34
43
|
type: "icon_list" as const,
|
|
35
44
|
content: { items: [{ label: "", text: "" }] },
|
|
36
45
|
}),
|
|
37
|
-
getLabel: (content) =>
|
|
38
|
-
const n = content.content.items.length;
|
|
39
|
-
return `${n} icon${n === 1 ? "" : "s"}`;
|
|
40
|
-
},
|
|
46
|
+
getLabel: (content) => pluralize(content.content.items.length, "icon"),
|
|
41
47
|
settingsForm: IconListSettings,
|
|
42
48
|
});
|
|
@@ -1,27 +1,12 @@
|
|
|
1
|
-
import { defineSection } from "../../../lib/registry";
|
|
2
|
-
import { z } from "zod";
|
|
3
1
|
import { Heading1 } from "lucide-react";
|
|
4
|
-
import {
|
|
2
|
+
import { defineHeadingSection } from "../../primitives/defineHeadingSection";
|
|
5
3
|
|
|
6
|
-
|
|
7
|
-
type: z.literal("link_heading"),
|
|
8
|
-
content: z.object({ heading: z.string() }),
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
export default defineSection({
|
|
4
|
+
export default defineHeadingSection({
|
|
12
5
|
type: "link_heading",
|
|
13
6
|
label: "Link Heading",
|
|
14
7
|
icon: <Heading1 size={18} />,
|
|
15
|
-
schema,
|
|
16
8
|
navRole: "h1",
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
tag="h2"
|
|
21
|
-
placeholder="Section heading"
|
|
22
|
-
onChange={onChange ? (heading: string) => onChange({ type: "link_heading", content: { heading } }) : undefined}
|
|
23
|
-
/>
|
|
24
|
-
),
|
|
25
|
-
defaults: () => ({ type: "link_heading" as const, content: { heading: "New Section" } }),
|
|
26
|
-
getLabel: (content) => content.content.heading,
|
|
9
|
+
tag: "h2",
|
|
10
|
+
placeholder: "Section heading",
|
|
11
|
+
defaultHeading: "New Section",
|
|
27
12
|
});
|
|
@@ -1,30 +1,13 @@
|
|
|
1
|
-
import { defineSection } from "../../../lib/registry";
|
|
2
|
-
import { z } from "zod";
|
|
3
1
|
import { Heading2 } from "lucide-react";
|
|
4
|
-
import {
|
|
2
|
+
import { defineHeadingSection } from "../../primitives/defineHeadingSection";
|
|
5
3
|
|
|
6
|
-
|
|
7
|
-
type: z.literal("sub_heading"),
|
|
8
|
-
content: z.object({ heading: z.string(), excludeFromNav: z.boolean().optional() }),
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
export default defineSection({
|
|
4
|
+
export default defineHeadingSection({
|
|
12
5
|
type: "sub_heading",
|
|
13
6
|
label: "Sub Heading",
|
|
14
7
|
icon: <Heading2 size={18} />,
|
|
15
|
-
schema,
|
|
16
8
|
navRole: "h2",
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
placeholder="Sub heading"
|
|
22
|
-
onChange={onChange ? (heading: string) => onChange({ ...content, content: { ...content.content, heading } }) : undefined}
|
|
23
|
-
/>
|
|
24
|
-
),
|
|
25
|
-
defaults: () => ({ type: "sub_heading" as const, content: { heading: "New Sub Heading" } }),
|
|
26
|
-
getLabel: (content) => content.content.heading,
|
|
27
|
-
settings: {
|
|
28
|
-
excludeFromNav: { type: "checkbox", label: "Exclude from navigation", default: false, target: "content" },
|
|
29
|
-
},
|
|
9
|
+
tag: "h3",
|
|
10
|
+
placeholder: "Sub heading",
|
|
11
|
+
defaultHeading: "New Sub Heading",
|
|
12
|
+
navExclusion: true,
|
|
30
13
|
});
|
|
@@ -1,30 +1,13 @@
|
|
|
1
|
-
import { defineSection } from "../../../lib/registry";
|
|
2
|
-
import { z } from "zod";
|
|
3
1
|
import { Heading3 } from "lucide-react";
|
|
4
|
-
import {
|
|
2
|
+
import { defineHeadingSection } from "../../primitives/defineHeadingSection";
|
|
5
3
|
|
|
6
|
-
|
|
7
|
-
type: z.literal("sub_sub_heading"),
|
|
8
|
-
content: z.object({ heading: z.string(), excludeFromNav: z.boolean().optional() }),
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
export default defineSection({
|
|
4
|
+
export default defineHeadingSection({
|
|
12
5
|
type: "sub_sub_heading",
|
|
13
6
|
label: "Sub Sub Heading",
|
|
14
7
|
icon: <Heading3 size={18} />,
|
|
15
|
-
schema,
|
|
16
8
|
navRole: "h3",
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
placeholder="Sub sub heading"
|
|
22
|
-
onChange={onChange ? (heading: string) => onChange({ ...content, content: { ...content.content, heading } }) : undefined}
|
|
23
|
-
/>
|
|
24
|
-
),
|
|
25
|
-
defaults: () => ({ type: "sub_sub_heading" as const, content: { heading: "New Sub Sub Heading" } }),
|
|
26
|
-
getLabel: (content) => content.content.heading,
|
|
27
|
-
settings: {
|
|
28
|
-
excludeFromNav: { type: "checkbox", label: "Exclude from navigation", default: false, target: "content" },
|
|
29
|
-
},
|
|
9
|
+
tag: "h4",
|
|
10
|
+
placeholder: "Sub sub heading",
|
|
11
|
+
defaultHeading: "New Sub Sub Heading",
|
|
12
|
+
navExclusion: true,
|
|
30
13
|
});
|
|
@@ -3,7 +3,7 @@ import { ChevronRight } from "lucide-react";
|
|
|
3
3
|
import { cn } from "../../lib/cn";
|
|
4
4
|
import { Toggle } from "./Toggle";
|
|
5
5
|
import type { SiteNav, NavItem } from "../../lib/nav";
|
|
6
|
-
import { editModeEvent, siteNavChangeEvent, pageSelectEvent, darkModeEvent, historySelectEvent } from "../../lib/events";
|
|
6
|
+
import { editModeEvent, siteNavChangeEvent, pageSelectEvent, darkModeEvent, siteNameChangeEvent, historySelectEvent } from "../../lib/events";
|
|
7
7
|
import { useActiveHeadings } from "../../hooks/useActiveHeadings";
|
|
8
8
|
import { formatDate } from "../../lib/timestamp";
|
|
9
9
|
import { Popover } from "./Popover";
|
|
@@ -23,6 +23,7 @@ export default function Navigation({ siteNav: initialNav, siteName, darkMode, la
|
|
|
23
23
|
const [isOpen, setIsOpen] = useState(false);
|
|
24
24
|
const [isEditMode, setIsEditMode] = useState(false);
|
|
25
25
|
const [currentDarkMode, setCurrentDarkMode] = useState(darkMode);
|
|
26
|
+
const [currentSiteName, setCurrentSiteName] = useState(siteName);
|
|
26
27
|
const [isDark, setIsDark] = useState(false);
|
|
27
28
|
const [siteNav, setSiteNav] = useState<SiteNav>(initialNav);
|
|
28
29
|
const [showHistory, setShowHistory] = useState(false);
|
|
@@ -34,7 +35,8 @@ export default function Navigation({ siteNav: initialNav, siteName, darkMode, la
|
|
|
34
35
|
const unlistenEdit = editModeEvent.listen(({ isEditMode }) => setIsEditMode(isEditMode));
|
|
35
36
|
const unlistenNav = siteNavChangeEvent.listen((n) => setSiteNav(n));
|
|
36
37
|
const unlistenDark = darkModeEvent.listen((mode) => setCurrentDarkMode(mode as typeof darkMode));
|
|
37
|
-
|
|
38
|
+
const unlistenName = siteNameChangeEvent.listen((name) => setCurrentSiteName(name));
|
|
39
|
+
return () => { unlistenEdit(); unlistenNav(); unlistenDark(); unlistenName(); };
|
|
38
40
|
}, []);
|
|
39
41
|
|
|
40
42
|
useEffect(() => {
|
|
@@ -191,7 +193,7 @@ export default function Navigation({ siteNav: initialNav, siteName, darkMode, la
|
|
|
191
193
|
<>
|
|
192
194
|
<header className="fixed top-0 left-0 right-0 z-50 bg-base lg:hidden">
|
|
193
195
|
<div className="mx-auto max-w-screen-xl flex h-16 items-center justify-between px-4">
|
|
194
|
-
<span className="text-lg font-bold text-primary">{
|
|
196
|
+
<span className="text-lg font-bold text-primary">{currentSiteName}</span>
|
|
195
197
|
<button onClick={() => setIsOpen(!isOpen)} className="cursor-pointer p-2 text-base-contrast" aria-label="Toggle navigation">
|
|
196
198
|
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
|
197
199
|
{isOpen
|
|
@@ -207,7 +209,7 @@ export default function Navigation({ siteNav: initialNav, siteName, darkMode, la
|
|
|
207
209
|
<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",
|
|
208
210
|
isOpen ? "translate-x-0" : "-translate-x-full")}>
|
|
209
211
|
<div className="hidden px-4 py-4 lg:block">
|
|
210
|
-
<span className="text-lg font-bold text-primary">{
|
|
212
|
+
<span className="text-lg font-bold text-primary">{currentSiteName}</span>
|
|
211
213
|
</div>
|
|
212
214
|
|
|
213
215
|
<ul className="space-y-1 px-4 py-2">
|