@drawnagency/primitives 0.1.56 → 0.1.57
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-KGYWQDBB.js → chunk-ICLXLWQ5.js} +9 -72
- package/dist/chunk-NSCT3AMV.js +32 -0
- package/dist/{chunk-EU6NZ4GS.js → chunk-PRKUXM7E.js} +23 -9
- package/dist/{chunk-7IAWF7LE.js → chunk-PYWS3MOJ.js} +12 -2
- package/dist/chunk-TG43X7JO.js +123 -0
- package/dist/chunk-VKAGMEKE.js +90 -0
- package/dist/components/editor/ChildBlockWrapper.d.ts +19 -0
- package/dist/components/editor/ChildBlockWrapper.d.ts.map +1 -0
- package/dist/components/editor/ColSpanControl.d.ts +9 -0
- package/dist/components/editor/ColSpanControl.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 +5 -1
- package/dist/components/editor/SettingsForm.d.ts.map +1 -1
- package/dist/components/primitives/EditableGrid.d.ts.map +1 -1
- package/dist/components/primitives/IconPicker.d.ts +7 -1
- package/dist/components/primitives/IconPicker.d.ts.map +1 -1
- package/dist/components/sections/Container/Container.d.ts +20 -0
- package/dist/components/sections/Container/Container.d.ts.map +1 -0
- package/dist/components/sections/Container/ContainerSettingsForm.d.ts +17 -0
- package/dist/components/sections/Container/ContainerSettingsForm.d.ts.map +1 -0
- package/dist/components/sections/Container/index.d.ts +11 -0
- package/dist/components/sections/Container/index.d.ts.map +1 -0
- package/dist/components/sections/IconList/IconList.d.ts +1 -0
- package/dist/components/sections/IconList/IconList.d.ts.map +1 -1
- package/dist/components/sections/IconList/IconListSettings.d.ts +3 -4
- package/dist/components/sections/IconList/IconListSettings.d.ts.map +1 -1
- package/dist/components/sections/IconList/index.d.ts +1 -0
- package/dist/components/sections/IconList/index.d.ts.map +1 -1
- package/dist/components/sections/Media/MediaBlock.d.ts +19 -0
- package/dist/components/sections/Media/MediaBlock.d.ts.map +1 -0
- package/dist/components/sections/{MediaGrid → Media}/index.d.ts +15 -25
- package/dist/components/sections/Media/index.d.ts.map +1 -0
- package/dist/components/sections/Prose/index.d.ts.map +1 -1
- package/dist/components/sections/Spacer/Spacer.d.ts +2 -0
- package/dist/components/sections/Spacer/Spacer.d.ts.map +1 -0
- package/dist/components/sections/Spacer/index.d.ts +6 -0
- package/dist/components/sections/Spacer/index.d.ts.map +1 -0
- package/dist/components/sections/all-sections.d.ts +29 -103
- package/dist/components/sections/all-sections.d.ts.map +1 -1
- package/dist/components/sections/register-schemas.d.ts.map +1 -1
- package/dist/components/shared/Tabs.d.ts +24 -0
- package/dist/components/shared/Tabs.d.ts.map +1 -0
- package/dist/components/shell/EditorShell.d.ts.map +1 -1
- package/dist/components/shell/SiteSettingsModal.d.ts.map +1 -1
- package/dist/components/shell/blockMoveDispatch.d.ts +21 -0
- package/dist/components/shell/blockMoveDispatch.d.ts.map +1 -0
- package/dist/hooks/useBlockDnd.d.ts +48 -0
- package/dist/hooks/useBlockDnd.d.ts.map +1 -0
- package/dist/index.js +56 -48
- package/dist/lib/block-dnd.d.ts +42 -0
- package/dist/lib/block-dnd.d.ts.map +1 -0
- package/dist/lib/block-move.d.ts +31 -0
- package/dist/lib/block-move.d.ts.map +1 -0
- package/dist/lib/container-grid.d.ts +29 -0
- package/dist/lib/container-grid.d.ts.map +1 -0
- package/dist/lib/container-ops.d.ts +44 -0
- package/dist/lib/container-ops.d.ts.map +1 -0
- package/dist/lib/dexie.d.ts.map +1 -1
- package/dist/lib/dexie.js +13 -0
- package/dist/lib/index.js +10 -7
- package/dist/lib/loader.d.ts.map +1 -1
- package/dist/lib/migrate-sections-transform.d.ts +12 -0
- package/dist/lib/migrate-sections-transform.d.ts.map +1 -0
- package/dist/lib/migrate-sections-transform.js +6 -0
- package/dist/lib/registry.d.ts +39 -0
- package/dist/lib/registry.d.ts.map +1 -1
- package/dist/lib/registry.js +26 -0
- package/dist/schemas/block.d.ts +20 -0
- package/dist/schemas/block.d.ts.map +1 -0
- package/dist/schemas/block.js +14 -0
- package/dist/schemas/index.js +8 -2
- package/dist/schemas/link.d.ts +7 -0
- package/dist/schemas/link.d.ts.map +1 -1
- package/dist/schemas/rich-text.d.ts +9 -0
- package/dist/schemas/rich-text.d.ts.map +1 -0
- package/dist/schemas/sections.d.ts +2 -0
- package/dist/schemas/sections.d.ts.map +1 -1
- package/dist/schemas/shared.d.ts +30 -0
- package/dist/schemas/shared.d.ts.map +1 -1
- package/package.json +13 -1
- package/src/components/brandguide/Colors.tsx +35 -33
- package/src/components/editor/ChildBlockWrapper.tsx +108 -0
- package/src/components/editor/ColSpanControl.tsx +56 -0
- package/src/components/editor/SectionWrapper.tsx +44 -20
- package/src/components/editor/SettingsForm.tsx +100 -73
- package/src/components/primitives/EditableGrid.tsx +40 -36
- package/src/components/primitives/IconPicker.tsx +116 -26
- package/src/components/sections/Container/Container.tsx +354 -0
- package/src/components/sections/Container/ContainerSettingsForm.tsx +113 -0
- package/src/components/sections/Container/index.tsx +51 -0
- package/src/components/sections/IconList/IconList.tsx +113 -43
- package/src/components/sections/IconList/IconListSettings.tsx +2 -2
- package/src/components/sections/IconList/index.tsx +1 -1
- package/src/components/sections/Media/MediaBlock.tsx +103 -0
- package/src/components/sections/Media/index.tsx +85 -0
- package/src/components/sections/Prose/index.tsx +1 -0
- package/src/components/sections/Spacer/Spacer.tsx +6 -0
- package/src/components/sections/Spacer/index.tsx +18 -0
- package/src/components/sections/all-sections.ts +10 -8
- package/src/components/sections/register-schemas.ts +5 -2
- package/src/components/shared/Tabs.tsx +63 -0
- package/src/components/shell/EditorShell.tsx +105 -13
- package/src/components/shell/SiteSettingsModal.tsx +41 -51
- package/src/components/shell/blockMoveDispatch.ts +40 -0
- package/src/hooks/useBlockDnd.ts +144 -0
- package/src/lib/block-dnd.ts +58 -0
- package/src/lib/block-move.ts +236 -0
- package/src/lib/container-grid.ts +58 -0
- package/src/lib/container-ops.ts +159 -0
- package/src/lib/dexie.ts +22 -0
- package/src/lib/loader.ts +16 -4
- package/src/lib/migrate-sections-transform.ts +147 -0
- package/src/lib/registry.ts +48 -0
- package/src/schemas/block.ts +40 -0
- package/src/schemas/link.ts +19 -1
- package/src/schemas/rich-text.ts +11 -0
- package/src/schemas/sections.ts +5 -1
- package/src/schemas/shared.ts +6 -0
- package/dist/components/brandguide/DoDontList.d.ts +0 -16
- package/dist/components/brandguide/DoDontList.d.ts.map +0 -1
- package/dist/components/brandguide/DoDontMediaGrid.d.ts +0 -16
- package/dist/components/brandguide/DoDontMediaGrid.d.ts.map +0 -1
- package/dist/components/primitives/MediaSettingsForms.d.ts +0 -23
- package/dist/components/primitives/MediaSettingsForms.d.ts.map +0 -1
- package/dist/components/sections/DoDontList/index.d.ts +0 -21
- package/dist/components/sections/DoDontList/index.d.ts.map +0 -1
- package/dist/components/sections/DoDontMediaGrid/index.d.ts +0 -55
- package/dist/components/sections/DoDontMediaGrid/index.d.ts.map +0 -1
- package/dist/components/sections/MediaGrid/MediaGrid.d.ts +0 -17
- package/dist/components/sections/MediaGrid/MediaGrid.d.ts.map +0 -1
- package/dist/components/sections/MediaGrid/index.d.ts.map +0 -1
- package/dist/components/sections/SplitContent/SplitContent.d.ts +0 -14
- package/dist/components/sections/SplitContent/SplitContent.d.ts.map +0 -1
- package/dist/components/sections/SplitContent/index.d.ts +0 -13
- package/dist/components/sections/SplitContent/index.d.ts.map +0 -1
- package/src/components/brandguide/DoDontList.d.ts.map +0 -1
- package/src/components/brandguide/DoDontList.tsx +0 -67
- package/src/components/brandguide/DoDontMediaGrid.d.ts.map +0 -1
- package/src/components/brandguide/DoDontMediaGrid.tsx +0 -19
- package/src/components/primitives/MediaSettingsForms.tsx +0 -128
- package/src/components/sections/DoDontList/index.d.ts.map +0 -1
- package/src/components/sections/DoDontList/index.tsx +0 -45
- package/src/components/sections/DoDontMediaGrid/index.d.ts.map +0 -1
- package/src/components/sections/DoDontMediaGrid/index.tsx +0 -63
- package/src/components/sections/MediaGrid/MediaGrid.d.ts.map +0 -1
- package/src/components/sections/MediaGrid/MediaGrid.tsx +0 -239
- package/src/components/sections/MediaGrid/index.d.ts.map +0 -1
- package/src/components/sections/MediaGrid/index.tsx +0 -57
- package/src/components/sections/SplitContent/SplitContent.d.ts.map +0 -1
- package/src/components/sections/SplitContent/SplitContent.tsx +0 -84
- package/src/components/sections/SplitContent/index.d.ts.map +0 -1
- package/src/components/sections/SplitContent/index.tsx +0 -55
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useState, useEffect } from "react";
|
|
2
2
|
import { Eye, EyeOff } from "lucide-react";
|
|
3
3
|
import { cn } from "../../lib/cn";
|
|
4
|
-
import {
|
|
4
|
+
import { containerGridClass } from "../../lib/container-grid";
|
|
5
5
|
import { EditableGrid } from "../primitives/EditableGrid";
|
|
6
6
|
import { EditablePlainText } from "../primitives/EditablePlainText";
|
|
7
7
|
import type { ReactNode } from "react";
|
|
@@ -119,40 +119,42 @@ function ColorsView({
|
|
|
119
119
|
</div>
|
|
120
120
|
)}
|
|
121
121
|
|
|
122
|
-
<div className=
|
|
123
|
-
{
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
<div className=
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
</div>
|
|
139
|
-
{expanded && (
|
|
140
|
-
<div className="space-y-1 bg-base-accent p-3 text-sm">
|
|
141
|
-
{color.spaces.map((space, j) =>
|
|
142
|
-
Object.entries(space).map(([key, value]) =>
|
|
143
|
-
value ? (
|
|
144
|
-
<button key={`${j}-${key}`} onClick={() => handleCopy(value, i)} className="cursor-pointer flex w-full justify-between hover:text-primary">
|
|
145
|
-
<span className="font-medium uppercase">{key}</span>
|
|
146
|
-
<span>{value}</span>
|
|
147
|
-
</button>
|
|
148
|
-
) : null
|
|
149
|
-
)
|
|
122
|
+
<div className="@container">
|
|
123
|
+
<div className={cn("grid gap-4", containerGridClass[columns] || containerGridClass[3])}>
|
|
124
|
+
{colors.map((color, i) => {
|
|
125
|
+
const hex = color.spaces[0]?.hex;
|
|
126
|
+
const contrast = getContrastClass(hex);
|
|
127
|
+
return (
|
|
128
|
+
<div key={i} className="overflow-hidden rounded-md border border-base-200">
|
|
129
|
+
<div className={cn("relative flex min-h-[80px] items-end p-3", contrast)} style={{ backgroundColor: hex || "#ccc" }}>
|
|
130
|
+
{color.name && <span className="text-sm font-bold">{color.name}</span>}
|
|
131
|
+
{copiedIndex === i && (
|
|
132
|
+
<span className={cn(
|
|
133
|
+
"absolute top-2 right-2 rounded-full px-2.5 py-0.5 text-xs font-medium",
|
|
134
|
+
getContrastClass(hex) === "text-black" ? "bg-black/15 text-black" : "bg-white/25 text-white",
|
|
135
|
+
)}>
|
|
136
|
+
Copied!
|
|
137
|
+
</span>
|
|
150
138
|
)}
|
|
151
139
|
</div>
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
140
|
+
{expanded && (
|
|
141
|
+
<div className="space-y-1 bg-base-accent p-3 text-sm">
|
|
142
|
+
{color.spaces.map((space, j) =>
|
|
143
|
+
Object.entries(space).map(([key, value]) =>
|
|
144
|
+
value ? (
|
|
145
|
+
<button key={`${j}-${key}`} onClick={() => handleCopy(value, i)} className="cursor-pointer flex w-full justify-between hover:text-primary">
|
|
146
|
+
<span className="font-medium uppercase">{key}</span>
|
|
147
|
+
<span>{value}</span>
|
|
148
|
+
</button>
|
|
149
|
+
) : null
|
|
150
|
+
)
|
|
151
|
+
)}
|
|
152
|
+
</div>
|
|
153
|
+
)}
|
|
154
|
+
</div>
|
|
155
|
+
);
|
|
156
|
+
})}
|
|
157
|
+
</div>
|
|
156
158
|
</div>
|
|
157
159
|
</div>
|
|
158
160
|
);
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { forwardRef, type ReactNode, type Ref } from "react";
|
|
2
|
+
import type { Edge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge";
|
|
3
|
+
import { CopyPlus } from "lucide-react";
|
|
4
|
+
import { DragHandle } from "./DragHandle";
|
|
5
|
+
import { DeleteButton } from "./DeleteButton";
|
|
6
|
+
import { SettingsButton } from "./SettingsButton";
|
|
7
|
+
import { ColSpanControl } from "./ColSpanControl";
|
|
8
|
+
import { cn } from "../../lib/cn";
|
|
9
|
+
|
|
10
|
+
export interface ChildBlockWrapperProps {
|
|
11
|
+
label: string;
|
|
12
|
+
columns: number;
|
|
13
|
+
colSpan: number;
|
|
14
|
+
closestEdge: Edge | null;
|
|
15
|
+
isDragging: boolean;
|
|
16
|
+
hasSettings: boolean;
|
|
17
|
+
/** Forwarded to the drag handle button (pragmatic-dnd dragHandle target). */
|
|
18
|
+
dragHandleRef?: Ref<HTMLButtonElement>;
|
|
19
|
+
onDelete: () => void;
|
|
20
|
+
onDuplicate: () => void;
|
|
21
|
+
onColSpanChange: (span: number) => void;
|
|
22
|
+
onOpenSettings?: () => void;
|
|
23
|
+
children: ReactNode;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const ChildBlockWrapper = forwardRef<HTMLDivElement, ChildBlockWrapperProps>(
|
|
27
|
+
function ChildBlockWrapper(
|
|
28
|
+
{
|
|
29
|
+
label,
|
|
30
|
+
columns,
|
|
31
|
+
colSpan,
|
|
32
|
+
closestEdge,
|
|
33
|
+
isDragging,
|
|
34
|
+
hasSettings,
|
|
35
|
+
dragHandleRef,
|
|
36
|
+
onDelete,
|
|
37
|
+
onDuplicate,
|
|
38
|
+
onColSpanChange,
|
|
39
|
+
onOpenSettings,
|
|
40
|
+
children,
|
|
41
|
+
},
|
|
42
|
+
ref,
|
|
43
|
+
) {
|
|
44
|
+
return (
|
|
45
|
+
<div
|
|
46
|
+
ref={ref}
|
|
47
|
+
className={cn(
|
|
48
|
+
"group/childblock relative h-full rounded-sm",
|
|
49
|
+
isDragging && "opacity-50",
|
|
50
|
+
"hover:outline hover:outline-2 hover:outline-primary/30",
|
|
51
|
+
)}
|
|
52
|
+
>
|
|
53
|
+
{/* Controls — vertical rail in the cell's LEFT gutter. Revealed on hover OR
|
|
54
|
+
focus-within, so an open col-span popover (which traps focus) keeps the rail
|
|
55
|
+
visible even when the pointer moves off the block to reach an option. */}
|
|
56
|
+
<div className="absolute top-0 left-0 -translate-x-full z-20 flex flex-col items-center gap-1 opacity-0 transition-opacity group-hover/childblock:opacity-100 group-focus-within/childblock:opacity-100 pointer-events-none">
|
|
57
|
+
<span className="sr-only">{label}</span>
|
|
58
|
+
<div className="pointer-events-auto">
|
|
59
|
+
<DragHandle ref={dragHandleRef} />
|
|
60
|
+
</div>
|
|
61
|
+
<div className="pointer-events-auto">
|
|
62
|
+
<ColSpanControl colSpan={colSpan} columns={columns} onChange={onColSpanChange} />
|
|
63
|
+
</div>
|
|
64
|
+
{hasSettings && onOpenSettings && (
|
|
65
|
+
<div className="pointer-events-auto">
|
|
66
|
+
<SettingsButton onClick={onOpenSettings} />
|
|
67
|
+
</div>
|
|
68
|
+
)}
|
|
69
|
+
<div className="pointer-events-auto">
|
|
70
|
+
<button
|
|
71
|
+
type="button"
|
|
72
|
+
aria-label="Duplicate block"
|
|
73
|
+
onClick={onDuplicate}
|
|
74
|
+
className="cursor-pointer rounded p-1 text-base-contrast-light hover:text-primary"
|
|
75
|
+
>
|
|
76
|
+
<CopyPlus size={16} />
|
|
77
|
+
</button>
|
|
78
|
+
</div>
|
|
79
|
+
<div className="pointer-events-auto">
|
|
80
|
+
<DeleteButton onDelete={onDelete} />
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
{/* Drop-edge indicators */}
|
|
85
|
+
{closestEdge === "left" && (
|
|
86
|
+
<div
|
|
87
|
+
data-drop-edge="left"
|
|
88
|
+
className="absolute bottom-0 left-0 top-0 z-10 w-0.5 -translate-x-1 bg-primary"
|
|
89
|
+
/>
|
|
90
|
+
)}
|
|
91
|
+
{closestEdge === "right" && (
|
|
92
|
+
<div
|
|
93
|
+
data-drop-edge="right"
|
|
94
|
+
className="absolute bottom-0 right-0 top-0 z-10 w-0.5 translate-x-1 bg-primary"
|
|
95
|
+
/>
|
|
96
|
+
)}
|
|
97
|
+
{closestEdge === "top" && (
|
|
98
|
+
<div data-drop-edge="top" className="absolute left-0 right-0 top-0 z-10 h-0.5 -translate-y-1 bg-primary" />
|
|
99
|
+
)}
|
|
100
|
+
{closestEdge === "bottom" && (
|
|
101
|
+
<div data-drop-edge="bottom" className="absolute left-0 right-0 bottom-0 z-10 h-0.5 translate-y-1 bg-primary" />
|
|
102
|
+
)}
|
|
103
|
+
|
|
104
|
+
{children}
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
},
|
|
108
|
+
);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { useRef, useState } from "react";
|
|
2
|
+
import { Columns3 } from "lucide-react";
|
|
3
|
+
import { Popover } from "../shared/Popover";
|
|
4
|
+
import { PopoverItem } from "../shared/PopoverItem";
|
|
5
|
+
import { cn } from "../../lib/cn";
|
|
6
|
+
|
|
7
|
+
interface ColSpanControlProps {
|
|
8
|
+
colSpan: number;
|
|
9
|
+
columns: number;
|
|
10
|
+
onChange: (span: number) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Contextual col-span picker — only meaningful inside a multi-column container. */
|
|
14
|
+
export function ColSpanControl({ colSpan, columns, onChange }: ColSpanControlProps) {
|
|
15
|
+
const buttonRef = useRef<HTMLButtonElement>(null);
|
|
16
|
+
const [open, setOpen] = useState(false);
|
|
17
|
+
|
|
18
|
+
if (columns <= 1) return null;
|
|
19
|
+
|
|
20
|
+
const current = Math.min(Math.max(colSpan, 1), columns);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="relative">
|
|
24
|
+
<button
|
|
25
|
+
ref={buttonRef}
|
|
26
|
+
type="button"
|
|
27
|
+
aria-label="Column span"
|
|
28
|
+
aria-haspopup="true"
|
|
29
|
+
aria-expanded={open}
|
|
30
|
+
onClick={() => setOpen((v) => !v)}
|
|
31
|
+
className="pointer-events-auto inline-flex cursor-pointer items-center gap-1 rounded p-1 text-xs text-base-contrast-light hover:text-primary"
|
|
32
|
+
>
|
|
33
|
+
<Columns3 size={14} />
|
|
34
|
+
</button>
|
|
35
|
+
|
|
36
|
+
<Popover isOpen={open} onClose={() => setOpen(false)} anchorRef={buttonRef} className="min-w-28">
|
|
37
|
+
<ul role="list" className="py-1">
|
|
38
|
+
{Array.from({ length: columns }, (_, i) => i + 1).map((span) => (
|
|
39
|
+
<li key={span}>
|
|
40
|
+
<PopoverItem
|
|
41
|
+
aria-current={span === current}
|
|
42
|
+
onClick={() => {
|
|
43
|
+
onChange(span);
|
|
44
|
+
setOpen(false);
|
|
45
|
+
}}
|
|
46
|
+
className={cn(span === current && "font-semibold text-primary")}
|
|
47
|
+
>
|
|
48
|
+
Span {span}
|
|
49
|
+
</PopoverItem>
|
|
50
|
+
</li>
|
|
51
|
+
))}
|
|
52
|
+
</ul>
|
|
53
|
+
</Popover>
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState, useRef, useEffect } from "react";
|
|
2
|
-
import { ArrowRightLeft } from "lucide-react";
|
|
2
|
+
import { ArrowRightLeft, Plus } from "lucide-react";
|
|
3
3
|
import type { Edge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge";
|
|
4
4
|
import { DragHandle } from "./DragHandle";
|
|
5
5
|
import { InsertButton } from "./InsertButton";
|
|
@@ -11,6 +11,7 @@ import { SettingsForm } from "./SettingsForm";
|
|
|
11
11
|
import { useEditorContext } from "../shell/EditorContext";
|
|
12
12
|
import { useEditorModal } from "../shell/EditorModalContext";
|
|
13
13
|
import type { WrapperProps } from "../../lib/registry";
|
|
14
|
+
import { buildBlockDragData, buildBlockDropData, isBlockDragData, canDropBlock } from "../../lib/block-dnd";
|
|
14
15
|
import { cn } from "../../lib/cn";
|
|
15
16
|
|
|
16
17
|
export function SectionWrapper({
|
|
@@ -20,6 +21,8 @@ export function SectionWrapper({
|
|
|
20
21
|
dirty,
|
|
21
22
|
index,
|
|
22
23
|
isLast,
|
|
24
|
+
containerId,
|
|
25
|
+
isContainerBlock,
|
|
23
26
|
definition,
|
|
24
27
|
options,
|
|
25
28
|
audiences,
|
|
@@ -31,6 +34,7 @@ export function SectionWrapper({
|
|
|
31
34
|
onRequestInsert,
|
|
32
35
|
onDelete,
|
|
33
36
|
onMoveSection,
|
|
37
|
+
onAddChild,
|
|
34
38
|
mainStatus,
|
|
35
39
|
contentDiffersFromMain,
|
|
36
40
|
isLocalOnly,
|
|
@@ -77,7 +81,7 @@ export function SectionWrapper({
|
|
|
77
81
|
useEffect(() => {
|
|
78
82
|
const block = blockRef.current;
|
|
79
83
|
const handle = handleRef.current;
|
|
80
|
-
if (!block || !handle || !
|
|
84
|
+
if (!block || !handle || !isEditMode) return;
|
|
81
85
|
|
|
82
86
|
let cleanup: (() => void) | undefined;
|
|
83
87
|
let cancelled = false;
|
|
@@ -96,7 +100,13 @@ export function SectionWrapper({
|
|
|
96
100
|
const cleanupDraggable = draggable({
|
|
97
101
|
element: dropElement,
|
|
98
102
|
dragHandle: handle,
|
|
99
|
-
getInitialData: () =>
|
|
103
|
+
getInitialData: () =>
|
|
104
|
+
buildBlockDragData({
|
|
105
|
+
blockId: sectionId,
|
|
106
|
+
containerId,
|
|
107
|
+
index,
|
|
108
|
+
isContainer: isContainerBlock,
|
|
109
|
+
}),
|
|
100
110
|
onGenerateDragPreview: () => {
|
|
101
111
|
dropElement.style.opacity = "0.4";
|
|
102
112
|
requestAnimationFrame(() => {
|
|
@@ -115,31 +125,32 @@ export function SectionWrapper({
|
|
|
115
125
|
|
|
116
126
|
const cleanupDropTarget = dropTargetForElements({
|
|
117
127
|
element: dropElement,
|
|
118
|
-
canDrop: ({ source }) =>
|
|
128
|
+
canDrop: ({ source }) =>
|
|
129
|
+
isBlockDragData(source.data) && canDropBlock(source.data, containerId),
|
|
119
130
|
getData: ({ input, element }) =>
|
|
120
131
|
attachClosestEdge(
|
|
121
|
-
{
|
|
132
|
+
buildBlockDropData({ dropContainerId: containerId, index }),
|
|
122
133
|
{ input, element, allowedEdges: ["top", "bottom"] },
|
|
123
134
|
),
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
135
|
+
onDrag: ({ self, source, location }) => {
|
|
136
|
+
const innermost = location.current.dropTargets[0]?.element === self.element;
|
|
137
|
+
if (
|
|
138
|
+
!innermost ||
|
|
139
|
+
!isBlockDragData(source.data) ||
|
|
140
|
+
source.data.blockId === sectionId
|
|
141
|
+
) {
|
|
142
|
+
setClosestEdge(null);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
128
145
|
setClosestEdge(extractClosestEdge(self.data));
|
|
129
146
|
},
|
|
130
147
|
onDragLeave: () => {
|
|
131
148
|
setClosestEdge(null);
|
|
132
149
|
},
|
|
133
|
-
onDrop: (
|
|
150
|
+
onDrop: () => {
|
|
151
|
+
// Central monitor (EditorContent) dispatches the move; we only
|
|
152
|
+
// clear local hover state here.
|
|
134
153
|
setClosestEdge(null);
|
|
135
|
-
const fromIndex = source.data.index as number;
|
|
136
|
-
const edge = extractClosestEdge(self.data);
|
|
137
|
-
let toIndex = index;
|
|
138
|
-
if (edge === "bottom") toIndex = index + 1;
|
|
139
|
-
if (fromIndex < toIndex) toIndex--;
|
|
140
|
-
if (fromIndex !== toIndex) {
|
|
141
|
-
onReorder(fromIndex, toIndex);
|
|
142
|
-
}
|
|
143
154
|
},
|
|
144
155
|
});
|
|
145
156
|
|
|
@@ -153,7 +164,7 @@ export function SectionWrapper({
|
|
|
153
164
|
cancelled = true;
|
|
154
165
|
cleanup?.();
|
|
155
166
|
};
|
|
156
|
-
}, [sectionId, index,
|
|
167
|
+
}, [sectionId, index, containerId, isContainerBlock, isEditMode]);
|
|
157
168
|
|
|
158
169
|
function handleSettingsClick() {
|
|
159
170
|
if (!onSectionChange) return;
|
|
@@ -164,7 +175,8 @@ export function SectionWrapper({
|
|
|
164
175
|
`${definition.label} Settings`,
|
|
165
176
|
<CustomForm
|
|
166
177
|
{...(options ?? {})}
|
|
167
|
-
onChange={(
|
|
178
|
+
onChange={(result: { content: Record<string, unknown>; options: Record<string, unknown> }) =>
|
|
179
|
+
onSectionChange(result as unknown as Record<string, unknown>)}
|
|
168
180
|
/>,
|
|
169
181
|
);
|
|
170
182
|
} else if (definition.settings) {
|
|
@@ -174,6 +186,7 @@ export function SectionWrapper({
|
|
|
174
186
|
schema={definition.settings}
|
|
175
187
|
values={options ?? {}}
|
|
176
188
|
onChange={(result) => onSectionChange(result as unknown as Record<string, unknown>)}
|
|
189
|
+
tabs={definition.settingsTabs}
|
|
177
190
|
/>,
|
|
178
191
|
);
|
|
179
192
|
}
|
|
@@ -284,6 +297,17 @@ export function SectionWrapper({
|
|
|
284
297
|
/>
|
|
285
298
|
</div>
|
|
286
299
|
|
|
300
|
+
{onAddChild && (
|
|
301
|
+
<button
|
|
302
|
+
type="button"
|
|
303
|
+
onClick={onAddChild}
|
|
304
|
+
aria-label="Add block"
|
|
305
|
+
className="pointer-events-auto cursor-pointer rounded p-1 text-base-contrast-light hover:text-primary"
|
|
306
|
+
>
|
|
307
|
+
<Plus size={16} />
|
|
308
|
+
</button>
|
|
309
|
+
)}
|
|
310
|
+
|
|
287
311
|
{hasSettings && (
|
|
288
312
|
<SettingsButton onClick={handleSettingsClick} />
|
|
289
313
|
)}
|
|
@@ -5,6 +5,7 @@ import { Select } from "../shared/Select";
|
|
|
5
5
|
import { Checkbox } from "../shared/Checkbox";
|
|
6
6
|
import { FormLabel } from "../shared/FormLabel";
|
|
7
7
|
import { LinkField } from "../shared/LinkField";
|
|
8
|
+
import { Tabs } from "../shared/Tabs";
|
|
8
9
|
import type { LinkValue } from "../../schemas/link";
|
|
9
10
|
import { cn } from "../../lib/cn";
|
|
10
11
|
|
|
@@ -17,6 +18,7 @@ interface SettingsFormProps {
|
|
|
17
18
|
schema: SettingsSchema;
|
|
18
19
|
values: Record<string, unknown>;
|
|
19
20
|
onChange: (result: SettingsFormResult) => void;
|
|
21
|
+
tabs?: { label: string; fields: string[] }[];
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
function resolveValues(
|
|
@@ -37,6 +39,13 @@ function coerceValue(field: SettingsFieldDef, value: unknown): unknown {
|
|
|
37
39
|
return value;
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
function normalizeEmpty(field: SettingsFieldDef, value: unknown): unknown {
|
|
43
|
+
if (field.type === "select" && "emptyIsUndefined" in field && field.emptyIsUndefined && value === "") {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
|
|
40
49
|
function splitByTarget(
|
|
41
50
|
schema: SettingsSchema,
|
|
42
51
|
allValues: Record<string, unknown>,
|
|
@@ -44,7 +53,7 @@ function splitByTarget(
|
|
|
44
53
|
const content: Record<string, unknown> = {};
|
|
45
54
|
const options: Record<string, unknown> = {};
|
|
46
55
|
for (const [key, field] of Object.entries(schema)) {
|
|
47
|
-
const value = coerceValue(field, allValues[key]);
|
|
56
|
+
const value = normalizeEmpty(field, coerceValue(field, allValues[key]));
|
|
48
57
|
const target = field.target ?? "options";
|
|
49
58
|
if (target === "content") {
|
|
50
59
|
content[key] = value;
|
|
@@ -84,7 +93,82 @@ function RangeField({
|
|
|
84
93
|
);
|
|
85
94
|
}
|
|
86
95
|
|
|
87
|
-
|
|
96
|
+
function renderField(
|
|
97
|
+
key: string,
|
|
98
|
+
field: SettingsFieldDef,
|
|
99
|
+
value: unknown,
|
|
100
|
+
handleFieldChange: (key: string, newValue: unknown) => void,
|
|
101
|
+
) {
|
|
102
|
+
switch (field.type) {
|
|
103
|
+
case "text":
|
|
104
|
+
return (
|
|
105
|
+
<Input
|
|
106
|
+
key={key}
|
|
107
|
+
label={field.label}
|
|
108
|
+
value={String(value ?? "")}
|
|
109
|
+
placeholder={field.placeholder}
|
|
110
|
+
onChange={(v) => handleFieldChange(key, v)}
|
|
111
|
+
/>
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
case "number":
|
|
115
|
+
return (
|
|
116
|
+
<Input
|
|
117
|
+
key={key}
|
|
118
|
+
label={field.label}
|
|
119
|
+
type="number"
|
|
120
|
+
value={String(value ?? "")}
|
|
121
|
+
onChange={(v) => handleFieldChange(key, v === "" ? "" : Number(v))}
|
|
122
|
+
/>
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
case "checkbox":
|
|
126
|
+
return (
|
|
127
|
+
<Checkbox
|
|
128
|
+
key={key}
|
|
129
|
+
label={field.label}
|
|
130
|
+
checked={Boolean(value)}
|
|
131
|
+
onChange={(checked) => handleFieldChange(key, checked)}
|
|
132
|
+
/>
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
case "select":
|
|
136
|
+
return (
|
|
137
|
+
<Select
|
|
138
|
+
key={key}
|
|
139
|
+
label={field.label}
|
|
140
|
+
value={String(value ?? "")}
|
|
141
|
+
options={field.options}
|
|
142
|
+
onChange={(v) => handleFieldChange(key, v)}
|
|
143
|
+
/>
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
case "range":
|
|
147
|
+
return (
|
|
148
|
+
<RangeField
|
|
149
|
+
key={key}
|
|
150
|
+
field={field}
|
|
151
|
+
value={Number(value ?? field.min)}
|
|
152
|
+
onChange={(v) => handleFieldChange(key, v)}
|
|
153
|
+
/>
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
case "link":
|
|
157
|
+
return (
|
|
158
|
+
<LinkField
|
|
159
|
+
key={key}
|
|
160
|
+
label={field.label}
|
|
161
|
+
value={(value as LinkValue) ?? field.default}
|
|
162
|
+
onChange={(v) => handleFieldChange(key, v)}
|
|
163
|
+
/>
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
default:
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function SettingsForm({ schema, values, onChange, tabs }: SettingsFormProps) {
|
|
88
172
|
const [local, setLocal] = useState(() => resolveValues(schema, values));
|
|
89
173
|
|
|
90
174
|
function handleFieldChange(key: string, newValue: unknown) {
|
|
@@ -93,79 +177,22 @@ export function SettingsForm({ schema, values, onChange }: SettingsFormProps) {
|
|
|
93
177
|
onChange(splitByTarget(schema, next));
|
|
94
178
|
}
|
|
95
179
|
|
|
180
|
+
if (tabs) {
|
|
181
|
+
const fields = (keys: string[]) =>
|
|
182
|
+
keys.filter((key) => key in schema).map((key) => renderField(key, schema[key], local[key], handleFieldChange));
|
|
183
|
+
|
|
184
|
+
const listed = new Set(tabs.flatMap((t) => t.fields));
|
|
185
|
+
const otherKeys = Object.keys(schema).filter((key) => !listed.has(key));
|
|
186
|
+
const tabItems = [
|
|
187
|
+
...tabs.map((t) => ({ id: t.label, label: t.label, content: <div className="flex flex-col gap-4">{fields(t.fields)}</div> })),
|
|
188
|
+
...(otherKeys.length > 0 ? [{ id: "__other__", label: "Other", content: <div className="flex flex-col gap-4">{fields(otherKeys)}</div> }] : []),
|
|
189
|
+
];
|
|
190
|
+
return <Tabs tabs={tabItems} fullBleedTabBar />;
|
|
191
|
+
}
|
|
192
|
+
|
|
96
193
|
return (
|
|
97
194
|
<div className="flex flex-col gap-4">
|
|
98
|
-
{Object.entries(schema).map(([key, field]) =>
|
|
99
|
-
const value = local[key];
|
|
100
|
-
|
|
101
|
-
switch (field.type) {
|
|
102
|
-
case "text":
|
|
103
|
-
return (
|
|
104
|
-
<Input
|
|
105
|
-
key={key}
|
|
106
|
-
label={field.label}
|
|
107
|
-
value={String(value ?? "")}
|
|
108
|
-
placeholder={field.placeholder}
|
|
109
|
-
onChange={(v) => handleFieldChange(key, v)}
|
|
110
|
-
/>
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
case "number":
|
|
114
|
-
return (
|
|
115
|
-
<Input
|
|
116
|
-
key={key}
|
|
117
|
-
label={field.label}
|
|
118
|
-
type="number"
|
|
119
|
-
value={String(value ?? "")}
|
|
120
|
-
onChange={(v) => handleFieldChange(key, v === "" ? "" : Number(v))}
|
|
121
|
-
/>
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
case "checkbox":
|
|
125
|
-
return (
|
|
126
|
-
<Checkbox
|
|
127
|
-
key={key}
|
|
128
|
-
label={field.label}
|
|
129
|
-
checked={Boolean(value)}
|
|
130
|
-
onChange={(checked) => handleFieldChange(key, checked)}
|
|
131
|
-
/>
|
|
132
|
-
);
|
|
133
|
-
|
|
134
|
-
case "select":
|
|
135
|
-
return (
|
|
136
|
-
<Select
|
|
137
|
-
key={key}
|
|
138
|
-
label={field.label}
|
|
139
|
-
value={String(value ?? "")}
|
|
140
|
-
options={field.options}
|
|
141
|
-
onChange={(v) => handleFieldChange(key, v)}
|
|
142
|
-
/>
|
|
143
|
-
);
|
|
144
|
-
|
|
145
|
-
case "range":
|
|
146
|
-
return (
|
|
147
|
-
<RangeField
|
|
148
|
-
key={key}
|
|
149
|
-
field={field}
|
|
150
|
-
value={Number(value ?? field.min)}
|
|
151
|
-
onChange={(v) => handleFieldChange(key, v)}
|
|
152
|
-
/>
|
|
153
|
-
);
|
|
154
|
-
|
|
155
|
-
case "link":
|
|
156
|
-
return (
|
|
157
|
-
<LinkField
|
|
158
|
-
key={key}
|
|
159
|
-
label={field.label}
|
|
160
|
-
value={(value as LinkValue) ?? field.default}
|
|
161
|
-
onChange={(v) => handleFieldChange(key, v)}
|
|
162
|
-
/>
|
|
163
|
-
);
|
|
164
|
-
|
|
165
|
-
default:
|
|
166
|
-
return null;
|
|
167
|
-
}
|
|
168
|
-
})}
|
|
195
|
+
{Object.entries(schema).map(([key, field]) => renderField(key, field, local[key], handleFieldChange))}
|
|
169
196
|
</div>
|
|
170
197
|
);
|
|
171
198
|
}
|