@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.
Files changed (80) hide show
  1. package/dist/{chunk-62OWSJ7V.js → chunk-5XYUO4HP.js} +1 -1
  2. package/dist/{chunk-A4RARGF2.js → chunk-BJ6FYGYP.js} +2 -0
  3. package/dist/{chunk-VY67DS3O.js → chunk-L2JJFOXD.js} +6 -0
  4. package/dist/{chunk-BU52OBPW.js → chunk-P3HO76OS.js} +1 -1
  5. package/dist/components/editor/SectionWrapper.d.ts.map +1 -1
  6. package/dist/components/primitives/EditableRichText.d.ts.map +1 -1
  7. package/dist/components/primitives/ImageDropZone.d.ts +15 -0
  8. package/dist/components/primitives/ImageDropZone.d.ts.map +1 -0
  9. package/dist/components/primitives/RichTextToolbar.d.ts.map +1 -1
  10. package/dist/components/sections/Button/index.d.ts.map +1 -1
  11. package/dist/components/sections/Colors/index.d.ts.map +1 -1
  12. package/dist/components/sections/DoDontList/index.d.ts.map +1 -1
  13. package/dist/components/sections/DoDontMediaGrid/index.d.ts.map +1 -1
  14. package/dist/components/sections/IconList/index.d.ts.map +1 -1
  15. package/dist/components/sections/LinkHeading/index.d.ts.map +1 -1
  16. package/dist/components/sections/MediaGrid/MediaGrid.d.ts.map +1 -1
  17. package/dist/components/sections/MediaGrid/index.d.ts.map +1 -1
  18. package/dist/components/sections/Prose/index.d.ts.map +1 -1
  19. package/dist/components/sections/SplitContent/SplitContent.d.ts.map +1 -1
  20. package/dist/components/sections/SplitContent/index.d.ts.map +1 -1
  21. package/dist/components/sections/SubHeading/index.d.ts.map +1 -1
  22. package/dist/components/sections/SubSubHeading/index.d.ts.map +1 -1
  23. package/dist/components/shared/UploadRejectionAlert.d.ts +15 -0
  24. package/dist/components/shared/UploadRejectionAlert.d.ts.map +1 -0
  25. package/dist/components/shell/BugReportFAB.d.ts +1 -5
  26. package/dist/components/shell/BugReportFAB.d.ts.map +1 -1
  27. package/dist/components/shell/EditorShell.d.ts.map +1 -1
  28. package/dist/components/shell/MediaLibraryContext.d.ts +4 -0
  29. package/dist/components/shell/MediaLibraryContext.d.ts.map +1 -1
  30. package/dist/components/shell/MediaLibraryModal.d.ts.map +1 -1
  31. package/dist/components/shell/SectionSkeleton.d.ts.map +1 -1
  32. package/dist/components/shell/SectionTypePicker.d.ts +4 -3
  33. package/dist/components/shell/SectionTypePicker.d.ts.map +1 -1
  34. package/dist/components/shell/SiteSettingsDisplay.d.ts.map +1 -1
  35. package/dist/hooks/useMediaPipeline.d.ts.map +1 -1
  36. package/dist/index.js +6 -4
  37. package/dist/lib/index.js +2 -2
  38. package/dist/lib/registry.d.ts +2 -2
  39. package/dist/lib/registry.d.ts.map +1 -1
  40. package/dist/media/index.js +3 -1
  41. package/dist/media/utils.d.ts +1 -0
  42. package/dist/media/utils.d.ts.map +1 -1
  43. package/dist/schemas/index.js +2 -2
  44. package/dist/schemas/site-config.d.ts +2 -0
  45. package/dist/schemas/site-config.d.ts.map +1 -1
  46. package/dist/types/database.d.ts +512 -0
  47. package/dist/types/database.d.ts.map +1 -0
  48. package/dist/types/database.js +12 -0
  49. package/package.json +5 -1
  50. package/src/components/editor/SectionWrapper.tsx +9 -4
  51. package/src/components/primitives/EditableRichText.tsx +1 -0
  52. package/src/components/primitives/ImageDropZone.tsx +74 -0
  53. package/src/components/primitives/RichTextToolbar.tsx +42 -31
  54. package/src/components/primitives/tiptap-presets.ts +1 -1
  55. package/src/components/sections/Button/index.tsx +2 -0
  56. package/src/components/sections/Colors/index.tsx +2 -0
  57. package/src/components/sections/DoDontList/index.tsx +2 -0
  58. package/src/components/sections/DoDontMediaGrid/index.tsx +2 -0
  59. package/src/components/sections/IconList/index.tsx +2 -0
  60. package/src/components/sections/LinkHeading/index.tsx +2 -1
  61. package/src/components/sections/MediaGrid/MediaGrid.tsx +18 -5
  62. package/src/components/sections/MediaGrid/index.tsx +2 -0
  63. package/src/components/sections/Prose/index.tsx +2 -0
  64. package/src/components/sections/SplitContent/SplitContent.tsx +8 -7
  65. package/src/components/sections/SplitContent/index.tsx +2 -0
  66. package/src/components/sections/SubHeading/index.tsx +2 -1
  67. package/src/components/sections/SubSubHeading/index.tsx +2 -1
  68. package/src/components/shared/UploadRejectionAlert.tsx +52 -0
  69. package/src/components/shell/BugReportFAB.tsx +21 -26
  70. package/src/components/shell/EditorShell.tsx +16 -4
  71. package/src/components/shell/MediaLibraryContext.tsx +1 -0
  72. package/src/components/shell/MediaLibraryModal.tsx +10 -19
  73. package/src/components/shell/SectionSkeleton.tsx +15 -14
  74. package/src/components/shell/SectionTypePicker.tsx +15 -11
  75. package/src/components/shell/SiteSettingsDisplay.tsx +12 -0
  76. package/src/hooks/useMediaPipeline.ts +3 -0
  77. package/src/lib/registry.ts +2 -2
  78. package/src/media/utils.ts +6 -0
  79. package/src/schemas/site-config.ts +2 -0
  80. 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 siteId={siteId} />
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
- <div 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">
235
- <p className="mb-2 text-center text-sm text-amber-800 lg:mb-0 lg:text-left dark:text-amber-200">
236
- {rejectedFiles.length === 1
237
- ? `"${rejectedFiles[0]}" exceeds`
238
- : `${rejectedFiles.length} files exceed`} the {formatFileSize(maxFileSize)} file size limit.
239
- </p>
240
- <div className="flex shrink-0 items-center">
241
- <Button variant="secondary" size="sm" onClick={() => setRejectedFiles([])}>
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
- // Trigger animation on next frame
17
- requestAnimationFrame(() => setIsVisible(true));
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
- {/* Placeholder block */}
39
- <div className="flex h-20 items-center justify-center rounded-lg bg-base-accent">
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
- {/* Type picker — centered below skeleton */}
44
- <div className="relative flex justify-center">
45
- <SectionTypePicker
46
- types={types}
47
- onSelect={onSelect}
48
- onClose={onDismiss}
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 ComponentType } from "react";
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: ComponentType;
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 (panelRef.current && !panelRef.current.contains(e.target as Node)) {
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="absolute z-50 mt-1 w-64 rounded-lg border border-base-200 bg-base p-2 shadow-lg"
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: Icon }) => (
35
+ {types.map(({ type, label, icon }) => (
34
36
  <button
35
37
  key={type}
36
- className="cursor-pointer flex w-full items-center gap-3 rounded-md px-3 py-2 text-left text-sm text-base-contrast-light hover:bg-base-accent hover:text-base-contrast"
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
- <span className="flex h-6 w-6 items-center justify-center text-base">
40
- <Icon />
41
- </span>
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,
@@ -95,7 +95,7 @@ export interface WrapperProps {
95
95
  export interface SectionDefinition<T = unknown> {
96
96
  type: string;
97
97
  label: string;
98
- icon?: string;
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?: string;
116
+ icon?: ReactNode;
117
117
  schema: S;
118
118
  component: ComponentType<SectionProps<z.infer<S>>>;
119
119
  defaults: () => z.infer<S>;
@@ -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()