@drawnagency/primitives 0.1.42 → 0.1.44
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-62OWSJ7V.js → chunk-5XYUO4HP.js} +1 -1
- package/dist/{chunk-A4RARGF2.js → chunk-BJ6FYGYP.js} +2 -0
- package/dist/{chunk-VY67DS3O.js → chunk-L2JJFOXD.js} +6 -0
- package/dist/{chunk-BU52OBPW.js → chunk-P3HO76OS.js} +1 -1
- package/dist/components/editor/SectionWrapper.d.ts.map +1 -1
- package/dist/components/primitives/EditableRichText.d.ts.map +1 -1
- package/dist/components/primitives/ImageDropZone.d.ts +15 -0
- package/dist/components/primitives/ImageDropZone.d.ts.map +1 -0
- package/dist/components/primitives/RichTextToolbar.d.ts.map +1 -1
- package/dist/components/sections/Button/index.d.ts.map +1 -1
- package/dist/components/sections/Colors/index.d.ts.map +1 -1
- package/dist/components/sections/DoDontList/index.d.ts.map +1 -1
- package/dist/components/sections/DoDontMediaGrid/index.d.ts.map +1 -1
- 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/MediaGrid/MediaGrid.d.ts.map +1 -1
- package/dist/components/sections/MediaGrid/index.d.ts.map +1 -1
- package/dist/components/sections/Prose/index.d.ts.map +1 -1
- package/dist/components/sections/SplitContent/SplitContent.d.ts.map +1 -1
- package/dist/components/sections/SplitContent/index.d.ts.map +1 -1
- package/dist/components/sections/SubHeading/index.d.ts.map +1 -1
- package/dist/components/sections/SubSubHeading/index.d.ts.map +1 -1
- package/dist/components/shared/UploadRejectionAlert.d.ts +15 -0
- package/dist/components/shared/UploadRejectionAlert.d.ts.map +1 -0
- package/dist/components/shell/BugReportFAB.d.ts +1 -5
- package/dist/components/shell/BugReportFAB.d.ts.map +1 -1
- package/dist/components/shell/EditorShell.d.ts.map +1 -1
- package/dist/components/shell/MediaLibraryContext.d.ts +4 -0
- package/dist/components/shell/MediaLibraryContext.d.ts.map +1 -1
- package/dist/components/shell/MediaLibraryModal.d.ts.map +1 -1
- package/dist/components/shell/SectionSkeleton.d.ts.map +1 -1
- package/dist/components/shell/SectionTypePicker.d.ts +4 -3
- package/dist/components/shell/SectionTypePicker.d.ts.map +1 -1
- package/dist/components/shell/SiteSettingsDisplay.d.ts.map +1 -1
- package/dist/hooks/useMediaPipeline.d.ts.map +1 -1
- package/dist/index.js +6 -4
- package/dist/lib/index.js +2 -2
- package/dist/lib/registry.d.ts +2 -2
- package/dist/lib/registry.d.ts.map +1 -1
- package/dist/media/index.js +3 -1
- package/dist/media/utils.d.ts +1 -0
- package/dist/media/utils.d.ts.map +1 -1
- package/dist/schemas/index.js +2 -2
- package/dist/schemas/site-config.d.ts +2 -0
- package/dist/schemas/site-config.d.ts.map +1 -1
- package/dist/types/database.d.ts +512 -0
- package/dist/types/database.d.ts.map +1 -0
- package/dist/types/database.js +12 -0
- package/package.json +5 -1
- package/src/components/editor/SectionWrapper.tsx +9 -4
- package/src/components/primitives/EditableRichText.tsx +1 -0
- package/src/components/primitives/ImageDropZone.tsx +74 -0
- package/src/components/primitives/RichTextToolbar.tsx +42 -31
- package/src/components/primitives/tiptap-presets.ts +1 -1
- package/src/components/sections/Button/index.tsx +2 -0
- package/src/components/sections/Colors/index.tsx +2 -0
- package/src/components/sections/DoDontList/index.tsx +2 -0
- package/src/components/sections/DoDontMediaGrid/index.tsx +2 -0
- package/src/components/sections/IconList/index.tsx +2 -0
- package/src/components/sections/LinkHeading/index.tsx +2 -1
- package/src/components/sections/MediaGrid/MediaGrid.tsx +18 -5
- package/src/components/sections/MediaGrid/index.tsx +2 -0
- package/src/components/sections/Prose/index.tsx +2 -0
- package/src/components/sections/SplitContent/SplitContent.tsx +8 -7
- package/src/components/sections/SplitContent/index.tsx +2 -0
- package/src/components/sections/SubHeading/index.tsx +2 -1
- package/src/components/sections/SubSubHeading/index.tsx +2 -1
- package/src/components/shared/UploadRejectionAlert.tsx +52 -0
- package/src/components/shell/BugReportFAB.tsx +21 -26
- package/src/components/shell/EditorShell.tsx +16 -4
- package/src/components/shell/MediaLibraryContext.tsx +1 -0
- package/src/components/shell/MediaLibraryModal.tsx +10 -19
- package/src/components/shell/SectionSkeleton.tsx +15 -14
- package/src/components/shell/SectionTypePicker.tsx +15 -11
- package/src/components/shell/SiteSettingsDisplay.tsx +12 -0
- package/src/hooks/useMediaPipeline.ts +3 -0
- package/src/lib/registry.ts +2 -2
- package/src/media/utils.ts +6 -0
- package/src/schemas/site-config.ts +2 -0
- package/src/types/database.ts +568 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { useState, type DragEvent, type ReactNode } from "react";
|
|
2
|
+
import { cn } from "../../lib/cn";
|
|
3
|
+
import { useMediaLibrary } from "../shell/MediaLibraryContext";
|
|
4
|
+
|
|
5
|
+
interface ImageDropZoneProps {
|
|
6
|
+
onImageSelected: (imageId: string) => void;
|
|
7
|
+
className?: string;
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Equivalent to the media library modal's accept="image/*,video/*".
|
|
12
|
+
const ACCEPTED = /^(image|video)\//;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Wraps an image slot and turns it into a native-HTML file drop target.
|
|
16
|
+
* Only reacts to drags carrying files, so it never intercepts the grid's
|
|
17
|
+
* pragmatic drag-and-drop reorder (which carries no `Files`). No media
|
|
18
|
+
* library context (viewer / not edit mode) → renders children unchanged.
|
|
19
|
+
*/
|
|
20
|
+
export function ImageDropZone({ onImageSelected, className, children }: ImageDropZoneProps) {
|
|
21
|
+
const mediaLibrary = useMediaLibrary();
|
|
22
|
+
const [dragging, setDragging] = useState(false);
|
|
23
|
+
|
|
24
|
+
if (!mediaLibrary) return <>{children}</>;
|
|
25
|
+
|
|
26
|
+
const maxFileSize = mediaLibrary.siteConfig?.media.maxFileSize;
|
|
27
|
+
const hasFiles = (e: DragEvent) => e.dataTransfer.types.includes("Files");
|
|
28
|
+
|
|
29
|
+
const handleDragOver = (e: DragEvent) => {
|
|
30
|
+
if (!hasFiles(e)) return;
|
|
31
|
+
e.preventDefault();
|
|
32
|
+
e.dataTransfer.dropEffect = "copy";
|
|
33
|
+
setDragging(true);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const handleDragLeave = (e: DragEvent) => {
|
|
37
|
+
if (!hasFiles(e)) return;
|
|
38
|
+
const related = e.relatedTarget as Node | null;
|
|
39
|
+
if (related && e.currentTarget.contains(related)) return;
|
|
40
|
+
setDragging(false);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const handleDrop = (e: DragEvent) => {
|
|
44
|
+
if (!hasFiles(e)) return;
|
|
45
|
+
e.preventDefault();
|
|
46
|
+
e.stopPropagation();
|
|
47
|
+
setDragging(false);
|
|
48
|
+
const file = e.dataTransfer.files[0];
|
|
49
|
+
if (!file) return;
|
|
50
|
+
if (!ACCEPTED.test(file.type)) {
|
|
51
|
+
mediaLibrary.reportRejectedUploads([{ name: file.name, reason: "type" }]);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (maxFileSize && file.size > maxFileSize) {
|
|
55
|
+
mediaLibrary.reportRejectedUploads([{ name: file.name, reason: "size" }]);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
mediaLibrary.uploadFileWithCallback(file, onImageSelected);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div
|
|
63
|
+
className={cn("relative", className)}
|
|
64
|
+
onDragOver={handleDragOver}
|
|
65
|
+
onDragLeave={handleDragLeave}
|
|
66
|
+
onDrop={handleDrop}
|
|
67
|
+
>
|
|
68
|
+
{children}
|
|
69
|
+
{dragging && (
|
|
70
|
+
<div className="pointer-events-none absolute inset-0 z-10 rounded-md border-2 border-primary bg-primary/5" />
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
2
|
import type { Editor } from "@tiptap/core";
|
|
3
|
+
import { useEditorState } from "@tiptap/react";
|
|
3
4
|
import { cn } from "../../lib/cn";
|
|
4
5
|
import { LinkPopover } from "./LinkPopover";
|
|
5
6
|
import type { PresetName } from "./tiptap-presets";
|
|
@@ -46,14 +47,28 @@ function Separator() {
|
|
|
46
47
|
export function RichTextToolbar({ editor, preset }: RichTextToolbarProps) {
|
|
47
48
|
const [showLinkPopover, setShowLinkPopover] = useState(false);
|
|
48
49
|
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
50
|
+
const state = useEditorState({
|
|
51
|
+
editor,
|
|
52
|
+
selector: ({ editor: ed }) => ({
|
|
53
|
+
isBold: ed.isActive("bold"),
|
|
54
|
+
isItalic: ed.isActive("italic"),
|
|
55
|
+
isUnderline: ed.isActive("underline"),
|
|
56
|
+
isLink: ed.isActive("link"),
|
|
57
|
+
isBulletList: ed.isActive("bulletList"),
|
|
58
|
+
isOrderedList: ed.isActive("orderedList"),
|
|
59
|
+
isH3: ed.isActive("heading", { level: 3 }),
|
|
60
|
+
isH4: ed.isActive("heading", { level: 4 }),
|
|
61
|
+
isLarge: ed.isActive("paragraph", { class: "large" }),
|
|
62
|
+
isLeadIn: ed.isActive("paragraph", { class: "lead-in" }),
|
|
63
|
+
}),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const toggleParagraphVariant = (cls: string, isCurrentlyActive: boolean) => {
|
|
67
|
+
if (isCurrentlyActive) {
|
|
68
|
+
editor.chain().focus().setNode("paragraph").run();
|
|
69
|
+
} else {
|
|
70
|
+
editor.chain().focus().setNode("paragraph", { class: cls }).run();
|
|
71
|
+
}
|
|
57
72
|
};
|
|
58
73
|
|
|
59
74
|
return (
|
|
@@ -63,21 +78,21 @@ export function RichTextToolbar({ editor, preset }: RichTextToolbarProps) {
|
|
|
63
78
|
>
|
|
64
79
|
{/* Inline formatting group */}
|
|
65
80
|
<ToolbarButton
|
|
66
|
-
isActive={
|
|
81
|
+
isActive={state.isBold}
|
|
67
82
|
onClick={() => editor.chain().focus().toggleBold().run()}
|
|
68
83
|
title="Bold"
|
|
69
84
|
>
|
|
70
85
|
<strong>B</strong>
|
|
71
86
|
</ToolbarButton>
|
|
72
87
|
<ToolbarButton
|
|
73
|
-
isActive={
|
|
88
|
+
isActive={state.isItalic}
|
|
74
89
|
onClick={() => editor.chain().focus().toggleItalic().run()}
|
|
75
90
|
title="Italic"
|
|
76
91
|
>
|
|
77
92
|
<em>I</em>
|
|
78
93
|
</ToolbarButton>
|
|
79
94
|
<ToolbarButton
|
|
80
|
-
isActive={
|
|
95
|
+
isActive={state.isUnderline}
|
|
81
96
|
onClick={() => editor.chain().focus().toggleUnderline().run()}
|
|
82
97
|
title="Underline"
|
|
83
98
|
>
|
|
@@ -91,7 +106,7 @@ export function RichTextToolbar({ editor, preset }: RichTextToolbarProps) {
|
|
|
91
106
|
{/* Link */}
|
|
92
107
|
<div className="relative">
|
|
93
108
|
<ToolbarButton
|
|
94
|
-
isActive={
|
|
109
|
+
isActive={state.isLink || showLinkPopover}
|
|
95
110
|
onClick={() => setShowLinkPopover((prev) => !prev)}
|
|
96
111
|
title="Link"
|
|
97
112
|
>
|
|
@@ -111,14 +126,14 @@ export function RichTextToolbar({ editor, preset }: RichTextToolbarProps) {
|
|
|
111
126
|
|
|
112
127
|
{/* Lists */}
|
|
113
128
|
<ToolbarButton
|
|
114
|
-
isActive={
|
|
129
|
+
isActive={state.isBulletList}
|
|
115
130
|
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
|
116
131
|
title="Bullet List"
|
|
117
132
|
>
|
|
118
133
|
•≡
|
|
119
134
|
</ToolbarButton>
|
|
120
135
|
<ToolbarButton
|
|
121
|
-
isActive={
|
|
136
|
+
isActive={state.isOrderedList}
|
|
122
137
|
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
|
123
138
|
title="Ordered List"
|
|
124
139
|
>
|
|
@@ -127,35 +142,31 @@ export function RichTextToolbar({ editor, preset }: RichTextToolbarProps) {
|
|
|
127
142
|
|
|
128
143
|
<Separator />
|
|
129
144
|
|
|
130
|
-
{/*
|
|
131
|
-
<ToolbarButton
|
|
132
|
-
isActive={editor.isActive("heading", { level: 2 })}
|
|
133
|
-
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
|
|
134
|
-
title="Heading 2"
|
|
135
|
-
>
|
|
136
|
-
H2
|
|
137
|
-
</ToolbarButton>
|
|
145
|
+
{/* Block styles — mutually exclusive */}
|
|
138
146
|
<ToolbarButton
|
|
139
|
-
isActive={
|
|
147
|
+
isActive={state.isH3}
|
|
140
148
|
onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}
|
|
141
149
|
title="Heading 3"
|
|
142
150
|
>
|
|
143
151
|
H3
|
|
144
152
|
</ToolbarButton>
|
|
145
|
-
|
|
146
|
-
<Separator />
|
|
147
|
-
|
|
148
|
-
{/* Paragraph variants */}
|
|
149
153
|
<ToolbarButton
|
|
150
|
-
isActive={
|
|
151
|
-
onClick={() =>
|
|
154
|
+
isActive={state.isH4}
|
|
155
|
+
onClick={() => editor.chain().focus().toggleHeading({ level: 4 }).run()}
|
|
156
|
+
title="Heading 4"
|
|
157
|
+
>
|
|
158
|
+
H4
|
|
159
|
+
</ToolbarButton>
|
|
160
|
+
<ToolbarButton
|
|
161
|
+
isActive={state.isLarge}
|
|
162
|
+
onClick={() => toggleParagraphVariant("large", state.isLarge)}
|
|
152
163
|
title="Large Paragraph"
|
|
153
164
|
>
|
|
154
165
|
Lg
|
|
155
166
|
</ToolbarButton>
|
|
156
167
|
<ToolbarButton
|
|
157
|
-
isActive={
|
|
158
|
-
onClick={() =>
|
|
168
|
+
isActive={state.isLeadIn}
|
|
169
|
+
onClick={() => toggleParagraphVariant("lead-in", state.isLeadIn)}
|
|
159
170
|
title="Lead-In"
|
|
160
171
|
>
|
|
161
172
|
Li
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineSection } from "../../../lib/registry";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { RectangleHorizontal } from "lucide-react";
|
|
3
4
|
import CTAButton from "./CTAButton";
|
|
4
5
|
|
|
5
6
|
const schema = z.object({
|
|
@@ -15,6 +16,7 @@ const schema = z.object({
|
|
|
15
16
|
export default defineSection({
|
|
16
17
|
type: "button",
|
|
17
18
|
label: "Button",
|
|
19
|
+
icon: <RectangleHorizontal size={18} />,
|
|
18
20
|
schema,
|
|
19
21
|
component: ({ content, onChange }) => (
|
|
20
22
|
<CTAButton
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineSection } from "../../../lib/registry";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { Palette } from "lucide-react";
|
|
3
4
|
import { ColorItemSchema } from "../../../schemas/shared";
|
|
4
5
|
import Colors from "../../brandguide/Colors";
|
|
5
6
|
|
|
@@ -17,6 +18,7 @@ const schema = z.object({
|
|
|
17
18
|
export default defineSection({
|
|
18
19
|
type: "colors",
|
|
19
20
|
label: "Colors",
|
|
21
|
+
icon: <Palette size={18} />,
|
|
20
22
|
schema,
|
|
21
23
|
component: ({ content, options, onChange, openModal }) => (
|
|
22
24
|
<Colors
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineSection } from "../../../lib/registry";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { ListChecks } from "lucide-react";
|
|
3
4
|
import DoDontList from "../../brandguide/DoDontList";
|
|
4
5
|
|
|
5
6
|
const DoDontItemSchema = z.object({ label: z.string(), text: z.string(), icon: z.string().optional() });
|
|
@@ -19,6 +20,7 @@ const schema = z.object({
|
|
|
19
20
|
export default defineSection({
|
|
20
21
|
type: "do_dont",
|
|
21
22
|
label: "Do / Don't",
|
|
23
|
+
icon: <ListChecks size={18} />,
|
|
22
24
|
schema,
|
|
23
25
|
component: ({ content, options, onChange }) => (
|
|
24
26
|
<DoDontList
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineSection } from "../../../lib/registry";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { GalleryVerticalEnd } from "lucide-react";
|
|
3
4
|
import { MediaReferenceSchema } from "../../../schemas/shared";
|
|
4
5
|
import { MediaGridOptionsSchema } from "../../../schemas/media-grid-options";
|
|
5
6
|
import DoDontMediaGrid from "../../brandguide/DoDontMediaGrid";
|
|
@@ -16,6 +17,7 @@ const schema = z.object({
|
|
|
16
17
|
export default defineSection({
|
|
17
18
|
type: "do_dont_grid",
|
|
18
19
|
label: "Do / Don't Grid",
|
|
20
|
+
icon: <GalleryVerticalEnd size={18} />,
|
|
19
21
|
schema,
|
|
20
22
|
component: ({ content, options, onChange, openModal }) => (
|
|
21
23
|
<DoDontMediaGrid
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineSection } from "../../../lib/registry";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { List } from "lucide-react";
|
|
3
4
|
import IconList from "./IconList";
|
|
4
5
|
import { IconListSettings } from "./IconListSettings";
|
|
5
6
|
|
|
@@ -18,6 +19,7 @@ const schema = z.object({
|
|
|
18
19
|
export default defineSection({
|
|
19
20
|
type: "icon_list",
|
|
20
21
|
label: "Icon List",
|
|
22
|
+
icon: <List size={18} />,
|
|
21
23
|
schema,
|
|
22
24
|
component: ({ content, options, onChange }) => (
|
|
23
25
|
<IconList
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineSection } from "../../../lib/registry";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { Heading1 } from "lucide-react";
|
|
3
4
|
import { HeadingSection } from "../../primitives/HeadingSection";
|
|
4
5
|
|
|
5
6
|
const schema = z.object({
|
|
@@ -10,6 +11,7 @@ const schema = z.object({
|
|
|
10
11
|
export default defineSection({
|
|
11
12
|
type: "link_heading",
|
|
12
13
|
label: "Link Heading",
|
|
14
|
+
icon: <Heading1 size={18} />,
|
|
13
15
|
schema,
|
|
14
16
|
navRole: "h1",
|
|
15
17
|
component: ({ content, onChange }) => (
|
|
@@ -17,7 +19,6 @@ export default defineSection({
|
|
|
17
19
|
heading={content.content.heading}
|
|
18
20
|
tag="h2"
|
|
19
21
|
placeholder="Section heading"
|
|
20
|
-
className="text-primary"
|
|
21
22
|
onChange={onChange ? (heading: string) => onChange({ type: "link_heading", content: { heading } }) : undefined}
|
|
22
23
|
/>
|
|
23
24
|
),
|
|
@@ -9,6 +9,7 @@ import { EditablePlainText } from "../../primitives/EditablePlainText";
|
|
|
9
9
|
import type { ReactNode } from "react";
|
|
10
10
|
import { Check, X } from "lucide-react";
|
|
11
11
|
import { useMediaLibrary } from "../../shell/MediaLibraryContext";
|
|
12
|
+
import { ImageDropZone } from "../../primitives/ImageDropZone";
|
|
12
13
|
|
|
13
14
|
interface Props {
|
|
14
15
|
media: MediaReference[];
|
|
@@ -101,11 +102,13 @@ function MediaGridEditable({ media, columns, square, border, crop, showCaptions,
|
|
|
101
102
|
}
|
|
102
103
|
};
|
|
103
104
|
|
|
105
|
+
const setItemImage = (index: number, imageId: string) => {
|
|
106
|
+
const newMedia = media.map((m, i) => i === index ? { ...m, imageId } : m);
|
|
107
|
+
onChange({ type: sectionType, content: { columns, media: newMedia }, ...opts } as SectionContent);
|
|
108
|
+
};
|
|
109
|
+
|
|
104
110
|
const handleItemImageClick = (index: number) => {
|
|
105
|
-
mediaLibrary?.openSelectModal((imageId) =>
|
|
106
|
-
const newMedia = media.map((m, i) => i === index ? { ...m, imageId } : m);
|
|
107
|
-
onChange({ type: sectionType, content: { columns, media: newMedia }, ...opts } as SectionContent);
|
|
108
|
-
});
|
|
111
|
+
mediaLibrary?.openSelectModal((imageId) => setItemImage(index, imageId));
|
|
109
112
|
};
|
|
110
113
|
|
|
111
114
|
return (
|
|
@@ -128,6 +131,7 @@ function MediaGridEditable({ media, columns, square, border, crop, showCaptions,
|
|
|
128
131
|
border={border}
|
|
129
132
|
crop={crop}
|
|
130
133
|
showCaptions={showCaptions}
|
|
134
|
+
onImageReplace={mediaLibrary ? (imageId) => setItemImage(index, imageId) : undefined}
|
|
131
135
|
onCaptionChange={(caption) => {
|
|
132
136
|
const newMedia = media.map((m, i) => i === index ? { ...m, caption: caption || undefined } : m);
|
|
133
137
|
onChange({ type: sectionType, content: { columns, media: newMedia }, ...opts } as SectionContent);
|
|
@@ -146,6 +150,7 @@ function MediaGridItem({
|
|
|
146
150
|
crop,
|
|
147
151
|
showCaptions,
|
|
148
152
|
onCaptionChange,
|
|
153
|
+
onImageReplace,
|
|
149
154
|
}: {
|
|
150
155
|
item: MediaReference;
|
|
151
156
|
isEditMode: boolean;
|
|
@@ -154,6 +159,7 @@ function MediaGridItem({
|
|
|
154
159
|
crop?: boolean;
|
|
155
160
|
showCaptions?: boolean;
|
|
156
161
|
onCaptionChange?: (caption: string) => void;
|
|
162
|
+
onImageReplace?: (imageId: string) => void;
|
|
157
163
|
}) {
|
|
158
164
|
const isDoDont = item.type === "doDontImage";
|
|
159
165
|
|
|
@@ -167,7 +173,7 @@ function MediaGridItem({
|
|
|
167
173
|
: "";
|
|
168
174
|
|
|
169
175
|
const itemAny = item as Record<string, unknown>;
|
|
170
|
-
const
|
|
176
|
+
const resolvedMedia = (
|
|
171
177
|
<ResolvedMedia
|
|
172
178
|
imageId={item.imageId || undefined}
|
|
173
179
|
src={itemAny.src as string | undefined}
|
|
@@ -178,6 +184,13 @@ function MediaGridItem({
|
|
|
178
184
|
invertFrom={item.invertFrom}
|
|
179
185
|
/>
|
|
180
186
|
);
|
|
187
|
+
const media = onImageReplace ? (
|
|
188
|
+
<ImageDropZone onImageSelected={onImageReplace} className="h-full w-full">
|
|
189
|
+
{resolvedMedia}
|
|
190
|
+
</ImageDropZone>
|
|
191
|
+
) : (
|
|
192
|
+
resolvedMedia
|
|
193
|
+
);
|
|
181
194
|
|
|
182
195
|
return (
|
|
183
196
|
<figure>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineSection } from "../../../lib/registry";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { LayoutGrid } from "lucide-react";
|
|
3
4
|
import { MediaReferenceSchema } from "../../../schemas/shared";
|
|
4
5
|
import { MediaGridOptionsSchema } from "../../../schemas/media-grid-options";
|
|
5
6
|
import MediaGrid from "./MediaGrid";
|
|
@@ -16,6 +17,7 @@ const schema = z.object({
|
|
|
16
17
|
export default defineSection({
|
|
17
18
|
type: "media_grid",
|
|
18
19
|
label: "Media Grid",
|
|
20
|
+
icon: <LayoutGrid size={18} />,
|
|
19
21
|
schema,
|
|
20
22
|
component: ({ content, options, onChange, openModal }) => (
|
|
21
23
|
<MediaGrid
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineSection } from "../../../lib/registry";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { AlignLeft } from "lucide-react";
|
|
3
4
|
import Prose from "./Prose";
|
|
4
5
|
import { stripHtmlToPlainText, truncate } from "../../../lib/text";
|
|
5
6
|
|
|
@@ -11,6 +12,7 @@ const schema = z.object({
|
|
|
11
12
|
export default defineSection({
|
|
12
13
|
type: "prose",
|
|
13
14
|
label: "Prose",
|
|
15
|
+
icon: <AlignLeft size={18} />,
|
|
14
16
|
schema,
|
|
15
17
|
component: ({ content, onChange }) => (
|
|
16
18
|
<Prose body={content.content.body} onChange={onChange ? (c) => onChange(c as typeof content) : undefined} />
|
|
@@ -3,6 +3,7 @@ import { sanitizeHtml } from "../../../lib/sanitize";
|
|
|
3
3
|
import { ResolvedMedia } from "../../primitives/ResolvedMedia";
|
|
4
4
|
import { EditableRichText } from "../../primitives/EditableRichText";
|
|
5
5
|
import { IconButton } from "../../shared/IconButton";
|
|
6
|
+
import { ImageDropZone } from "../../primitives/ImageDropZone";
|
|
6
7
|
import type { SectionContent } from "../../../schemas/sections";
|
|
7
8
|
import { useMediaLibrary } from "../../shell/MediaLibraryContext";
|
|
8
9
|
import { ImageIcon } from "lucide-react";
|
|
@@ -47,13 +48,13 @@ export default function SplitContent({ imageId, src, srcset, alt, body, border,
|
|
|
47
48
|
imagePosition === "right" && "lg:flex-row-reverse",
|
|
48
49
|
)}>
|
|
49
50
|
<div className={cn("group/img relative flex-shrink-0 lg:w-1/2", border && "overflow-hidden rounded-md border border-base-200")}>
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
className="w-full"
|
|
56
|
-
|
|
51
|
+
{onChange && mediaLibrary ? (
|
|
52
|
+
<ImageDropZone onImageSelected={handleImageChange}>
|
|
53
|
+
<ResolvedMedia imageId={imageId} src={src} srcset={srcset} alt={alt} className="w-full" />
|
|
54
|
+
</ImageDropZone>
|
|
55
|
+
) : (
|
|
56
|
+
<ResolvedMedia imageId={imageId} src={src} srcset={srcset} alt={alt} className="w-full" />
|
|
57
|
+
)}
|
|
57
58
|
{onChange && mediaLibrary && (
|
|
58
59
|
<div className="absolute top-1.5 right-1.5 z-10 opacity-0 transition-opacity group-hover/img:opacity-100">
|
|
59
60
|
<IconButton
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineSection } from "../../../lib/registry";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { Columns2 } from "lucide-react";
|
|
3
4
|
import SplitContent from "./SplitContent";
|
|
4
5
|
import { stripHtmlToPlainText, truncate } from "../../../lib/text";
|
|
5
6
|
|
|
@@ -18,6 +19,7 @@ const schema = z.object({
|
|
|
18
19
|
export default defineSection({
|
|
19
20
|
type: "split_content",
|
|
20
21
|
label: "Split Content",
|
|
22
|
+
icon: <Columns2 size={18} />,
|
|
21
23
|
schema,
|
|
22
24
|
component: ({ content, options, onChange }) => (
|
|
23
25
|
<SplitContent
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineSection } from "../../../lib/registry";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { Heading2 } from "lucide-react";
|
|
3
4
|
import { HeadingSection } from "../../primitives/HeadingSection";
|
|
4
5
|
|
|
5
6
|
const schema = z.object({
|
|
@@ -10,6 +11,7 @@ const schema = z.object({
|
|
|
10
11
|
export default defineSection({
|
|
11
12
|
type: "sub_heading",
|
|
12
13
|
label: "Sub Heading",
|
|
14
|
+
icon: <Heading2 size={18} />,
|
|
13
15
|
schema,
|
|
14
16
|
navRole: "h2",
|
|
15
17
|
component: ({ content, onChange }) => (
|
|
@@ -17,7 +19,6 @@ export default defineSection({
|
|
|
17
19
|
heading={content.content.heading}
|
|
18
20
|
tag="h3"
|
|
19
21
|
placeholder="Sub heading"
|
|
20
|
-
className="text-base-contrast"
|
|
21
22
|
onChange={onChange ? (heading: string) => onChange({ ...content, content: { ...content.content, heading } }) : undefined}
|
|
22
23
|
/>
|
|
23
24
|
),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineSection } from "../../../lib/registry";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { Heading3 } from "lucide-react";
|
|
3
4
|
import { HeadingSection } from "../../primitives/HeadingSection";
|
|
4
5
|
|
|
5
6
|
const schema = z.object({
|
|
@@ -10,6 +11,7 @@ const schema = z.object({
|
|
|
10
11
|
export default defineSection({
|
|
11
12
|
type: "sub_sub_heading",
|
|
12
13
|
label: "Sub Sub Heading",
|
|
14
|
+
icon: <Heading3 size={18} />,
|
|
13
15
|
schema,
|
|
14
16
|
navRole: "h3",
|
|
15
17
|
component: ({ content, onChange }) => (
|
|
@@ -17,7 +19,6 @@ export default defineSection({
|
|
|
17
19
|
heading={content.content.heading}
|
|
18
20
|
tag="h4"
|
|
19
21
|
placeholder="Sub sub heading"
|
|
20
|
-
className="text-lg font-bold text-base-contrast"
|
|
21
22
|
onChange={onChange ? (heading: string) => onChange({ ...content, content: { ...content.content, heading } }) : undefined}
|
|
22
23
|
/>
|
|
23
24
|
),
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Button } from "./Button";
|
|
2
|
+
import { formatFileSize } from "../../media/utils";
|
|
3
|
+
|
|
4
|
+
export interface UploadRejection {
|
|
5
|
+
name: string;
|
|
6
|
+
reason: "size" | "type";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Compose a human-readable message from a list of rejected uploads.
|
|
11
|
+
* Inline drops only ever send one file, so the single-file branches are the
|
|
12
|
+
* common path; the multi-file branch keeps the helper reusable by the modal.
|
|
13
|
+
*/
|
|
14
|
+
export function formatRejections(files: UploadRejection[], maxFileSize?: number): string {
|
|
15
|
+
if (files.length === 0) return "";
|
|
16
|
+
if (files.length === 1) {
|
|
17
|
+
const f = files[0];
|
|
18
|
+
if (f.reason === "size") {
|
|
19
|
+
return maxFileSize
|
|
20
|
+
? `"${f.name}" exceeds the ${formatFileSize(maxFileSize)} file size limit.`
|
|
21
|
+
: `"${f.name}" exceeds the file size limit.`;
|
|
22
|
+
}
|
|
23
|
+
return `"${f.name}" — images and videos only.`;
|
|
24
|
+
}
|
|
25
|
+
const sizeCount = files.filter((f) => f.reason === "size").length;
|
|
26
|
+
const typeCount = files.length - sizeCount;
|
|
27
|
+
const parts: string[] = [];
|
|
28
|
+
if (sizeCount > 0) parts.push(`${sizeCount} too large`);
|
|
29
|
+
if (typeCount > 0) parts.push(`${typeCount} not an image or video`);
|
|
30
|
+
return `${files.length} files rejected (${parts.join(", ")}).`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function UploadRejectionAlert({
|
|
34
|
+
message,
|
|
35
|
+
onDismiss,
|
|
36
|
+
}: {
|
|
37
|
+
message: string;
|
|
38
|
+
onDismiss: () => void;
|
|
39
|
+
}) {
|
|
40
|
+
return (
|
|
41
|
+
<div role="alert" className="flex flex-col items-center rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 lg:flex-row lg:justify-between dark:border-amber-900/50 dark:bg-amber-950/30">
|
|
42
|
+
<p className="mb-2 text-center text-sm text-amber-800 lg:mb-0 lg:text-left dark:text-amber-200">
|
|
43
|
+
{message}
|
|
44
|
+
</p>
|
|
45
|
+
<div className="flex shrink-0 items-center">
|
|
46
|
+
<Button variant="secondary" size="sm" type="button" onClick={onDismiss}>
|
|
47
|
+
Dismiss
|
|
48
|
+
</Button>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
import { useState, useCallback, useRef, useEffect } from "react";
|
|
2
|
-
import { createClient } from "@supabase/supabase-js";
|
|
3
2
|
import { EditorModal } from "./EditorModal";
|
|
4
3
|
import { useEditorContext } from "./EditorContext";
|
|
5
|
-
import { env } from "../../lib/env";
|
|
6
|
-
|
|
7
|
-
interface Props {
|
|
8
|
-
siteId: string;
|
|
9
|
-
}
|
|
10
4
|
|
|
11
5
|
type Category = "Visual" | "Unexpected Behavior" | "Media / Media Library" | "Save / Publish" | "Feature Request" | "Other";
|
|
12
6
|
|
|
@@ -32,7 +26,7 @@ interface ImagePreview {
|
|
|
32
26
|
name: string;
|
|
33
27
|
}
|
|
34
28
|
|
|
35
|
-
export function BugReportFAB(
|
|
29
|
+
export function BugReportFAB() {
|
|
36
30
|
const { isEditMode, historyState } = useEditorContext();
|
|
37
31
|
const [isOpen, setIsOpen] = useState(false);
|
|
38
32
|
const [category, setCategory] = useState<Category>("Visual");
|
|
@@ -149,26 +143,27 @@ export function BugReportFAB({ siteId }: Props) {
|
|
|
149
143
|
setIsSubmitting(true);
|
|
150
144
|
|
|
151
145
|
try {
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
user_id: userId,
|
|
163
|
-
category,
|
|
164
|
-
is_critical: isCritical,
|
|
165
|
-
description: description.trim(),
|
|
166
|
-
images: images.map((img) => img.dataUri),
|
|
167
|
-
context: capturedContextRef.current,
|
|
146
|
+
const response = await fetch("/api/bug-report", {
|
|
147
|
+
method: "POST",
|
|
148
|
+
headers: { "Content-Type": "application/json" },
|
|
149
|
+
body: JSON.stringify({
|
|
150
|
+
category,
|
|
151
|
+
is_critical: isCritical,
|
|
152
|
+
description: description.trim(),
|
|
153
|
+
images: images.map((img) => img.dataUri),
|
|
154
|
+
context: capturedContextRef.current,
|
|
155
|
+
}),
|
|
168
156
|
});
|
|
169
157
|
|
|
170
|
-
if (
|
|
171
|
-
|
|
158
|
+
if (!response.ok) {
|
|
159
|
+
let message = "Submission failed";
|
|
160
|
+
try {
|
|
161
|
+
const data = await response.json();
|
|
162
|
+
if (data?.error) message = data.error;
|
|
163
|
+
} catch {
|
|
164
|
+
// No JSON body on the error response; keep the default message.
|
|
165
|
+
}
|
|
166
|
+
setSubmitError(message);
|
|
172
167
|
return;
|
|
173
168
|
}
|
|
174
169
|
|
|
@@ -180,7 +175,7 @@ export function BugReportFAB({ siteId }: Props) {
|
|
|
180
175
|
} finally {
|
|
181
176
|
setIsSubmitting(false);
|
|
182
177
|
}
|
|
183
|
-
}, [
|
|
178
|
+
}, [category, isCritical, description, images, handleClose]);
|
|
184
179
|
|
|
185
180
|
const visible = isEditMode && historyState === null;
|
|
186
181
|
|