@drawnagency/primitives 0.1.59 → 0.1.61
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-4OX3Z7SB.js +1421 -0
- package/dist/{chunk-RFZNNCAS.js → chunk-53DKVAB6.js} +1 -61
- package/dist/chunk-677NTHAZ.js +27 -0
- package/dist/chunk-7EOSDFS3.js +10 -0
- package/dist/{chunk-NTGSA3TI.js → chunk-AM5LZD6Q.js} +7 -30
- 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-TZQPOR5A.js +24 -0
- package/dist/{chunk-XTK4BR27.js → chunk-UNWNT52N.js} +13 -0
- package/dist/chunk-UQ4OYRW7.js +670 -0
- package/dist/chunk-WDSVHKAH.js +54 -0
- package/dist/chunk-WLMJCNU7.js +70 -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/editor/index.js +640 -0
- package/dist/components/primitives/defineHeadingSection.d.ts +28 -0
- package/dist/components/primitives/defineHeadingSection.d.ts.map +1 -0
- package/dist/components/primitives/index.d.ts +10 -0
- package/dist/components/primitives/index.d.ts.map +1 -0
- package/dist/components/primitives/index.js +166 -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 +376 -2453
- package/dist/components/shell/BugReportFAB.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 +46 -37
- package/dist/lib/image-refs.d.ts +21 -0
- package/dist/lib/image-refs.d.ts.map +1 -0
- package/dist/lib/index.js +21 -14
- 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 +17 -17
- package/dist/schemas/link.js +13 -0
- package/dist/types/database.d.ts +46 -0
- package/dist/types/database.d.ts.map +1 -1
- package/package.json +14 -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/primitives/index.ts +11 -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/shell/BugReportFAB.tsx +13 -9
- package/src/hooks/useEditorPublish.ts +71 -49
- package/src/hooks/useMediaPipeline.ts +11 -34
- package/src/hooks/useSortableRow.ts +99 -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/dist/{chunk-ZU2MKPTG.js → chunk-3DCG4DNU.js} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drawnagency/primitives",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.61",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./package.json": "./package.json",
|
|
@@ -36,6 +36,10 @@
|
|
|
36
36
|
"types": "./dist/lib/env.d.ts",
|
|
37
37
|
"default": "./dist/lib/env.js"
|
|
38
38
|
},
|
|
39
|
+
"./lib/platform-broker": {
|
|
40
|
+
"types": "./dist/lib/platform-broker.d.ts",
|
|
41
|
+
"default": "./dist/lib/platform-broker.js"
|
|
42
|
+
},
|
|
39
43
|
"./lib/registry": {
|
|
40
44
|
"types": "./dist/lib/registry.d.ts",
|
|
41
45
|
"default": "./dist/lib/registry.js"
|
|
@@ -48,6 +52,14 @@
|
|
|
48
52
|
"types": "./dist/components/sections/register-schemas.d.ts",
|
|
49
53
|
"default": "./dist/components/sections/register-schemas.js"
|
|
50
54
|
},
|
|
55
|
+
"./components/editor": {
|
|
56
|
+
"types": "./dist/components/editor/index.d.ts",
|
|
57
|
+
"default": "./dist/components/editor/index.js"
|
|
58
|
+
},
|
|
59
|
+
"./components/primitives": {
|
|
60
|
+
"types": "./dist/components/primitives/index.d.ts",
|
|
61
|
+
"default": "./dist/components/primitives/index.js"
|
|
62
|
+
},
|
|
51
63
|
"./auth": {
|
|
52
64
|
"types": "./dist/auth/index.d.ts",
|
|
53
65
|
"default": "./dist/auth/index.js"
|
|
@@ -76,7 +88,7 @@
|
|
|
76
88
|
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0",
|
|
77
89
|
"@tiptap/core": "^3.20.4",
|
|
78
90
|
"@tiptap/extension-link": "^3.20.4",
|
|
79
|
-
"@tiptap/extension-placeholder": "^3.
|
|
91
|
+
"@tiptap/extension-placeholder": "^3.20.4",
|
|
80
92
|
"@tiptap/extension-underline": "^3.20.4",
|
|
81
93
|
"@tiptap/pm": "^3.20.4",
|
|
82
94
|
"@tiptap/react": "^3.20.4",
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { AuthProvider } from "./types";
|
|
2
|
+
|
|
3
|
+
export interface UiCapabilities {
|
|
4
|
+
oauth: boolean;
|
|
5
|
+
emailPassword: boolean;
|
|
6
|
+
passwordOnly: boolean;
|
|
7
|
+
userManagement: boolean;
|
|
8
|
+
audienceManagement: boolean;
|
|
9
|
+
passwordToggle: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The single mapping from an AuthProvider's structure to the booleans the editor
|
|
14
|
+
* UI gates on. Derived from sub-object / method presence so the UI and the API
|
|
15
|
+
* routes can never disagree about what a provider supports.
|
|
16
|
+
*/
|
|
17
|
+
export function deriveUiCapabilities(auth: AuthProvider): UiCapabilities {
|
|
18
|
+
return {
|
|
19
|
+
oauth: (auth.signInMethods.oauth?.length ?? 0) > 0,
|
|
20
|
+
emailPassword: auth.signInMethods.emailPassword ?? false,
|
|
21
|
+
passwordOnly: auth.signInMethods.passwordOnly ?? false,
|
|
22
|
+
userManagement: !!auth.userManagement,
|
|
23
|
+
audienceManagement: !!auth.audiences.manage,
|
|
24
|
+
passwordToggle: !!auth.passwordEnabled.set,
|
|
25
|
+
};
|
|
26
|
+
}
|
package/src/auth/index.ts
CHANGED
|
@@ -12,6 +12,9 @@ export type {
|
|
|
12
12
|
AuthContext,
|
|
13
13
|
} from "./types";
|
|
14
14
|
|
|
15
|
+
export type { UiCapabilities } from "./capabilities";
|
|
16
|
+
export { deriveUiCapabilities } from "./capabilities";
|
|
17
|
+
|
|
15
18
|
export { requireSessionSecret, isSameOriginRequest, safeNextPath } from "./security";
|
|
16
19
|
export { signSessionToken, verifySessionToken, signAudienceToken, verifyAudienceToken, setSessionCookie, SESSION_COOKIE, AUDIENCE_COOKIE, SESSION_MAX_AGE_SECONDS, AUDIENCE_MAX_AGE_SECONDS } from "./cookies";
|
|
17
20
|
export { LastOwnerError } from "./errors";
|
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
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Public authoring surface for custom-section authors: the editable primitives
|
|
2
|
+
// and the heading-section factory. Browser-safe by construction (no node:* / Buffer).
|
|
3
|
+
export { EditableRichText } from "./EditableRichText";
|
|
4
|
+
export { EditablePlainText } from "./EditablePlainText";
|
|
5
|
+
export { EditableList } from "./EditableList";
|
|
6
|
+
export { EditableGrid } from "./EditableGrid";
|
|
7
|
+
export { ImageDropZone } from "./ImageDropZone";
|
|
8
|
+
export { ResolvedMedia } from "./ResolvedMedia";
|
|
9
|
+
export { LinkPopover } from "./LinkPopover";
|
|
10
|
+
export { defineHeadingSection } from "./defineHeadingSection";
|
|
11
|
+
export { MediaBlock } from "../sections/Media/MediaBlock";
|
|
@@ -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
|
});
|