@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
|
@@ -47,6 +47,7 @@ import { formatTimestamp } from "../../lib/timestamp";
|
|
|
47
47
|
import { generateNavLinks } from "../../lib/nav";
|
|
48
48
|
import { navChangeEvent, darkModeEvent, historySelectEvent } from "../../lib/events";
|
|
49
49
|
import { cn } from "../../lib/cn";
|
|
50
|
+
import { UploadRejectionAlert, formatRejections } from "../shared/UploadRejectionAlert";
|
|
50
51
|
import { Button } from "../shared/Button";
|
|
51
52
|
import { SplitButton } from "../shared/SplitButton";
|
|
52
53
|
import { IconButton } from "../shared/IconButton";
|
|
@@ -110,6 +111,7 @@ export default function EditorShell({
|
|
|
110
111
|
const [showRestoreModal, setShowRestoreModal] = useState(false);
|
|
111
112
|
const [showOrderingModal, setShowOrderingModal] = useState(false);
|
|
112
113
|
const [isRestoring, setIsRestoring] = useState(false);
|
|
114
|
+
const [rejectedUploads, setRejectedUploads] = useState<{ name: string; reason: "size" | "type" }[]>([]);
|
|
113
115
|
|
|
114
116
|
const siteIndexRef = useRef<SiteIndex>({ siteId, order: [], sections: {} });
|
|
115
117
|
const fontLinkRef = useRef<HTMLLinkElement | null>(null);
|
|
@@ -192,6 +194,8 @@ export default function EditorShell({
|
|
|
192
194
|
root.style.setProperty("--font-heading", `${config.headingFont}, system-ui, sans-serif`);
|
|
193
195
|
root.style.setProperty("--font-body", `${config.bodyFont}, system-ui, sans-serif`);
|
|
194
196
|
root.style.setProperty("--heading-text-transform", config.uppercaseHeadings ? "uppercase" : "none");
|
|
197
|
+
root.style.setProperty("--subheading-text-transform", config.uppercaseSubheadings ? "uppercase" : "none");
|
|
198
|
+
root.style.setProperty("--nav-text-transform", config.uppercaseNavHeadings ? "uppercase" : "none");
|
|
195
199
|
|
|
196
200
|
if (config.googleFontsUrl) {
|
|
197
201
|
if (fontLinkRef.current?.href !== config.googleFontsUrl) {
|
|
@@ -610,6 +614,7 @@ export default function EditorShell({
|
|
|
610
614
|
setMediaSelectCallback(() => onSelect);
|
|
611
615
|
setShowMediaLibrary(true);
|
|
612
616
|
},
|
|
617
|
+
reportRejectedUploads: (files) => setRejectedUploads(files),
|
|
613
618
|
}}>
|
|
614
619
|
<div className="editor-shell relative">
|
|
615
620
|
<EditorToolbar
|
|
@@ -634,7 +639,16 @@ export default function EditorShell({
|
|
|
634
639
|
onOrderingClick={() => setShowOrderingModal(true)}
|
|
635
640
|
/>
|
|
636
641
|
|
|
637
|
-
<BugReportFAB
|
|
642
|
+
<BugReportFAB />
|
|
643
|
+
|
|
644
|
+
{rejectedUploads.length > 0 && (
|
|
645
|
+
<div className="sticky top-2 z-30 mx-auto w-full max-w-3xl px-4">
|
|
646
|
+
<UploadRejectionAlert
|
|
647
|
+
message={formatRejections(rejectedUploads, siteConfig?.media.maxFileSize)}
|
|
648
|
+
onDismiss={() => setRejectedUploads([])}
|
|
649
|
+
/>
|
|
650
|
+
</div>
|
|
651
|
+
)}
|
|
638
652
|
|
|
639
653
|
<HistoryOrEditorContent sections={sections}>
|
|
640
654
|
<EditorContent
|
|
@@ -885,9 +899,7 @@ function EditorContent({
|
|
|
885
899
|
getAllSections().map((def) => ({
|
|
886
900
|
type: def.type,
|
|
887
901
|
label: def.label,
|
|
888
|
-
icon: def.icon
|
|
889
|
-
? () => <>{def.icon}</>
|
|
890
|
-
: () => null,
|
|
902
|
+
icon: def.icon,
|
|
891
903
|
})),
|
|
892
904
|
[],
|
|
893
905
|
);
|
|
@@ -6,6 +6,7 @@ export interface MediaLibraryContextValue {
|
|
|
6
6
|
openSelectModal: (onSelect: (imageId: string) => void) => void;
|
|
7
7
|
uploadFile: (file: File) => void;
|
|
8
8
|
uploadFileWithCallback: (file: File, onComplete: (imageId: string) => void) => string;
|
|
9
|
+
reportRejectedUploads: (files: { name: string; reason: "size" | "type" }[]) => void;
|
|
9
10
|
manifest: MediaManifest;
|
|
10
11
|
pendingItems: MediaItem[];
|
|
11
12
|
pendingLocalUrls: Record<string, string>;
|
|
@@ -2,9 +2,10 @@ import { useState, useRef, useCallback } from "react";
|
|
|
2
2
|
import { Search, Upload, Trash2 } from "lucide-react";
|
|
3
3
|
import { cn } from "../../lib/cn";
|
|
4
4
|
import { Button } from "../shared/Button";
|
|
5
|
+
import { UploadRejectionAlert } from "../shared/UploadRejectionAlert";
|
|
5
6
|
import { Select } from "../shared/Select";
|
|
6
7
|
import type { MediaItem, MediaKind } from "../../media/types";
|
|
7
|
-
import { displayFilename, mimeToExt } from "../../media/utils";
|
|
8
|
+
import { displayFilename, formatFileSize, mimeToExt } from "../../media/utils";
|
|
8
9
|
|
|
9
10
|
export interface MediaLibraryModalProps {
|
|
10
11
|
mode: "select" | "manage";
|
|
@@ -18,12 +19,6 @@ export interface MediaLibraryModalProps {
|
|
|
18
19
|
maxFileSize?: number;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
function formatFileSize(bytes: number): string {
|
|
22
|
-
if (bytes >= 1048576) return `${Math.round(bytes / 1048576)}MB`;
|
|
23
|
-
if (bytes >= 1024) return `${Math.round(bytes / 1024)}KB`;
|
|
24
|
-
return `${bytes}B`;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
22
|
type KindFilter = "all" | MediaKind;
|
|
28
23
|
|
|
29
24
|
function thumbnailSrc(item: MediaItem, localUrls: Record<string, string>): string {
|
|
@@ -231,18 +226,14 @@ export function MediaLibraryModal({
|
|
|
231
226
|
|
|
232
227
|
{/* File size rejection alert */}
|
|
233
228
|
{rejectedFiles.length > 0 && maxFileSize && (
|
|
234
|
-
<
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
? `"${rejectedFiles[0]}" exceeds
|
|
238
|
-
: `${rejectedFiles.length} files exceed
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
Dismiss
|
|
243
|
-
</Button>
|
|
244
|
-
</div>
|
|
245
|
-
</div>
|
|
229
|
+
<UploadRejectionAlert
|
|
230
|
+
message={
|
|
231
|
+
rejectedFiles.length === 1
|
|
232
|
+
? `"${rejectedFiles[0]}" exceeds the ${formatFileSize(maxFileSize)} file size limit.`
|
|
233
|
+
: `${rejectedFiles.length} files exceed the ${formatFileSize(maxFileSize)} file size limit.`
|
|
234
|
+
}
|
|
235
|
+
onDismiss={() => setRejectedFiles([])}
|
|
236
|
+
/>
|
|
246
237
|
)}
|
|
247
238
|
</div>
|
|
248
239
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useEffect } from "react";
|
|
1
|
+
import { useState, useEffect, useRef } from "react";
|
|
2
2
|
import { SectionTypePicker, type TypeOption } from "./SectionTypePicker";
|
|
3
3
|
import { SectionLayout } from "../sections/SectionLayout";
|
|
4
4
|
import { cn } from "../../lib/cn";
|
|
@@ -11,10 +11,13 @@ interface SectionSkeletonProps {
|
|
|
11
11
|
|
|
12
12
|
export function SectionSkeleton({ types, onSelect, onDismiss }: SectionSkeletonProps) {
|
|
13
13
|
const [isVisible, setIsVisible] = useState(false);
|
|
14
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
14
15
|
|
|
15
16
|
useEffect(() => {
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
requestAnimationFrame(() => {
|
|
18
|
+
setIsVisible(true);
|
|
19
|
+
containerRef.current?.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
20
|
+
});
|
|
18
21
|
}, []);
|
|
19
22
|
|
|
20
23
|
useEffect(() => {
|
|
@@ -30,24 +33,22 @@ export function SectionSkeleton({ types, onSelect, onDismiss }: SectionSkeletonP
|
|
|
30
33
|
return (
|
|
31
34
|
<SectionLayout type="skeleton" status="draft">
|
|
32
35
|
<div
|
|
36
|
+
ref={containerRef}
|
|
33
37
|
className={cn(
|
|
34
38
|
"relative transition-all duration-200",
|
|
35
39
|
isVisible ? "opacity-100" : "opacity-0 -translate-y-2",
|
|
36
40
|
)}
|
|
37
41
|
>
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
<span className="text-sm text-base-contrast-light/50">New section</span>
|
|
42
|
+
<div className="flex h-16 items-center justify-center rounded-lg bg-base-accent">
|
|
43
|
+
<span className="text-sm text-base-contrast-light/50">Choose a section type</span>
|
|
41
44
|
</div>
|
|
42
45
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
/>
|
|
50
|
-
</div>
|
|
46
|
+
<SectionTypePicker
|
|
47
|
+
types={types}
|
|
48
|
+
onSelect={onSelect}
|
|
49
|
+
onClose={onDismiss}
|
|
50
|
+
containerRef={containerRef}
|
|
51
|
+
/>
|
|
51
52
|
</div>
|
|
52
53
|
</SectionLayout>
|
|
53
54
|
);
|
|
@@ -1,44 +1,48 @@
|
|
|
1
|
-
import { useEffect, useRef, type
|
|
1
|
+
import { useEffect, useRef, type ReactNode, type RefObject } from "react";
|
|
2
2
|
|
|
3
3
|
export interface TypeOption {
|
|
4
4
|
type: string;
|
|
5
5
|
label: string;
|
|
6
|
-
icon
|
|
6
|
+
icon?: ReactNode;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
interface SectionTypePickerProps {
|
|
10
10
|
types: TypeOption[];
|
|
11
11
|
onSelect: (type: string) => void;
|
|
12
12
|
onClose: () => void;
|
|
13
|
+
containerRef?: RefObject<HTMLElement | null>;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
export function SectionTypePicker({ types, onSelect, onClose }: SectionTypePickerProps) {
|
|
16
|
+
export function SectionTypePicker({ types, onSelect, onClose, containerRef }: SectionTypePickerProps) {
|
|
16
17
|
const panelRef = useRef<HTMLDivElement>(null);
|
|
18
|
+
const dismissRef = containerRef ?? panelRef;
|
|
17
19
|
|
|
18
20
|
useEffect(() => {
|
|
19
21
|
function handleMouseDown(e: MouseEvent) {
|
|
20
|
-
if (
|
|
22
|
+
if (dismissRef.current && !dismissRef.current.contains(e.target as Node)) {
|
|
21
23
|
onClose();
|
|
22
24
|
}
|
|
23
25
|
}
|
|
24
26
|
document.addEventListener("mousedown", handleMouseDown);
|
|
25
27
|
return () => document.removeEventListener("mousedown", handleMouseDown);
|
|
26
|
-
}, [onClose]);
|
|
28
|
+
}, [onClose, dismissRef]);
|
|
27
29
|
|
|
28
30
|
return (
|
|
29
31
|
<div
|
|
30
32
|
ref={panelRef}
|
|
31
|
-
className="
|
|
33
|
+
className="z-50 mt-3 grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3"
|
|
32
34
|
>
|
|
33
|
-
{types.map(({ type, label, icon
|
|
35
|
+
{types.map(({ type, label, icon }) => (
|
|
34
36
|
<button
|
|
35
37
|
key={type}
|
|
36
|
-
className="cursor-pointer flex
|
|
38
|
+
className="cursor-pointer flex items-center gap-3 rounded-lg border border-base-200 bg-base px-4 py-3 text-left text-sm text-base-contrast-light transition-colors hover:border-base-300 hover:bg-base-accent hover:text-base-contrast"
|
|
37
39
|
onClick={() => onSelect(type)}
|
|
38
40
|
>
|
|
39
|
-
|
|
40
|
-
<
|
|
41
|
-
|
|
41
|
+
{icon && (
|
|
42
|
+
<span className="flex h-5 w-5 shrink-0 items-center justify-center text-base-contrast-light/60">
|
|
43
|
+
{icon}
|
|
44
|
+
</span>
|
|
45
|
+
)}
|
|
42
46
|
<span>{label}</span>
|
|
43
47
|
</button>
|
|
44
48
|
))}
|
|
@@ -75,6 +75,18 @@ export function SiteSettingsDisplay({ siteConfig, onChange }: Props) {
|
|
|
75
75
|
label="Uppercase headings"
|
|
76
76
|
/>
|
|
77
77
|
|
|
78
|
+
<Checkbox
|
|
79
|
+
checked={siteConfig.uppercaseSubheadings}
|
|
80
|
+
onChange={(v) => update({ uppercaseSubheadings: v })}
|
|
81
|
+
label="Uppercase subheadings"
|
|
82
|
+
/>
|
|
83
|
+
|
|
84
|
+
<Checkbox
|
|
85
|
+
checked={siteConfig.uppercaseNavHeadings}
|
|
86
|
+
onChange={(v) => update({ uppercaseNavHeadings: v })}
|
|
87
|
+
label="Uppercase nav headings"
|
|
88
|
+
/>
|
|
89
|
+
|
|
78
90
|
<FontPicker
|
|
79
91
|
label="Body font"
|
|
80
92
|
value={siteConfig.bodyFont}
|
|
@@ -330,6 +330,9 @@ export function useMediaPipeline({
|
|
|
330
330
|
});
|
|
331
331
|
return blobUrl;
|
|
332
332
|
},
|
|
333
|
+
reportRejectedUploads: () => {
|
|
334
|
+
// Placeholder — EditorShell overrides this with banner state.
|
|
335
|
+
},
|
|
333
336
|
manifest: mediaManifest,
|
|
334
337
|
pendingItems: pendingMediaItems,
|
|
335
338
|
pendingLocalUrls: pendingLocalUrls,
|
package/src/lib/registry.ts
CHANGED
|
@@ -95,7 +95,7 @@ export interface WrapperProps {
|
|
|
95
95
|
export interface SectionDefinition<T = unknown> {
|
|
96
96
|
type: string;
|
|
97
97
|
label: string;
|
|
98
|
-
icon?:
|
|
98
|
+
icon?: ReactNode;
|
|
99
99
|
schema: ZodType<T>;
|
|
100
100
|
component: ComponentType<SectionProps<T>>;
|
|
101
101
|
defaults: () => T;
|
|
@@ -113,7 +113,7 @@ export interface SectionDefinition<T = unknown> {
|
|
|
113
113
|
type DefineSectionInput<S extends ZodType> = {
|
|
114
114
|
type: string;
|
|
115
115
|
label: string;
|
|
116
|
-
icon?:
|
|
116
|
+
icon?: ReactNode;
|
|
117
117
|
schema: S;
|
|
118
118
|
component: ComponentType<SectionProps<z.infer<S>>>;
|
|
119
119
|
defaults: () => z.infer<S>;
|
package/src/media/utils.ts
CHANGED
|
@@ -54,3 +54,9 @@ export function displayFilename(originalName: string, mimeType: string): string
|
|
|
54
54
|
if (originalName.endsWith(suffix)) return originalName;
|
|
55
55
|
return `${originalName}${suffix}`;
|
|
56
56
|
}
|
|
57
|
+
|
|
58
|
+
export function formatFileSize(bytes: number): string {
|
|
59
|
+
if (bytes >= 1048576) return `${Math.round(bytes / 1048576)}MB`;
|
|
60
|
+
if (bytes >= 1024) return `${Math.round(bytes / 1024)}KB`;
|
|
61
|
+
return `${bytes}B`;
|
|
62
|
+
}
|
|
@@ -38,6 +38,8 @@ export const SiteConfigSchema = z.object({
|
|
|
38
38
|
headingFont: z.string().default("system-ui"),
|
|
39
39
|
bodyFont: z.string().default("system-ui"),
|
|
40
40
|
uppercaseHeadings: z.boolean().default(true),
|
|
41
|
+
uppercaseSubheadings: z.boolean().default(true),
|
|
42
|
+
uppercaseNavHeadings: z.boolean().default(true),
|
|
41
43
|
googleFontsUrl: z.string()
|
|
42
44
|
.refine(url => url.startsWith("https://fonts.googleapis.com/"), "Must be a Google Fonts URL")
|
|
43
45
|
.nullable()
|