@drawnagency/primitives 0.1.61 → 0.1.63
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-UQ4OYRW7.js → chunk-2BHPWO7M.js} +54 -166
- package/dist/chunk-4E66N6UV.js +10 -0
- package/dist/chunk-5THS4K5D.js +123 -0
- package/dist/{chunk-TG43X7JO.js → chunk-75P6HWPN.js} +29 -5
- package/dist/{chunk-ODCJQOVO.js → chunk-7IMQVDDJ.js} +1 -1
- package/dist/{chunk-4OX3Z7SB.js → chunk-FECCYOPJ.js} +32 -671
- package/dist/{chunk-WLMJCNU7.js → chunk-I5737FJY.js} +0 -8
- package/dist/chunk-QYQ3DQHI.js +1201 -0
- package/dist/{chunk-BT7STGDW.js → chunk-TEYTLQVH.js} +0 -15
- package/dist/chunk-VMDKWUMT.js +652 -0
- package/dist/{chunk-AM5LZD6Q.js → chunk-ZYXLNJ5M.js} +6 -5
- package/dist/components/brandguide/ColorSwatchSettings.d.ts +1 -1
- package/dist/components/brandguide/ColorSwatchSettings.d.ts.map +1 -1
- package/dist/components/brandguide/Colors.d.ts +1 -1
- package/dist/components/brandguide/Colors.d.ts.map +1 -1
- package/dist/components/editor/index.js +4 -2
- package/dist/components/primitives/IconPicker.d.ts +1 -7
- package/dist/components/primitives/IconPicker.d.ts.map +1 -1
- package/dist/components/primitives/index.js +10 -7
- package/dist/components/sections/Colors/index.d.ts.map +1 -1
- package/dist/components/sections/Colors/schema.d.ts +19 -0
- package/dist/components/sections/Colors/schema.d.ts.map +1 -0
- package/dist/components/sections/DoDontMedia/DoDontMediaBlock.d.ts +20 -0
- package/dist/components/sections/DoDontMedia/DoDontMediaBlock.d.ts.map +1 -0
- package/dist/components/sections/DoDontMedia/index.d.ts +45 -0
- package/dist/components/sections/DoDontMedia/index.d.ts.map +1 -0
- package/dist/components/sections/IconList/DoDontToggle.d.ts +14 -0
- package/dist/components/sections/IconList/DoDontToggle.d.ts.map +1 -0
- package/dist/components/sections/IconList/IconList.d.ts.map +1 -1
- package/dist/components/sections/IconList/index.d.ts.map +1 -1
- package/dist/components/sections/Media/MediaBlock.d.ts +1 -3
- package/dist/components/sections/Media/MediaBlock.d.ts.map +1 -1
- package/dist/components/sections/Media/index.d.ts +0 -1
- package/dist/components/sections/Media/index.d.ts.map +1 -1
- package/dist/components/sections/all-sections.d.ts +54 -18
- package/dist/components/sections/all-sections.d.ts.map +1 -1
- package/dist/components/sections/brand-guide.d.ts +84 -0
- package/dist/components/sections/brand-guide.d.ts.map +1 -0
- package/dist/components/sections/brand-guide.js +21 -0
- package/dist/components/sections/core-sections.d.ts +93 -0
- package/dist/components/sections/core-sections.d.ts.map +1 -0
- package/dist/components/sections/register-schemas.js +108 -1155
- package/dist/components/shell/EditorShell.d.ts.map +1 -1
- package/dist/components/shell/view-branch.d.ts +12 -0
- package/dist/components/shell/view-branch.d.ts.map +1 -0
- package/dist/index.js +29 -33
- package/dist/lib/index.js +4 -4
- package/dist/lib/loader.d.ts.map +1 -1
- package/dist/lib/migrate-sections-transform.d.ts.map +1 -1
- package/dist/lib/migrate-sections-transform.js +1 -1
- package/dist/lib/registry.d.ts +6 -1
- package/dist/lib/registry.d.ts.map +1 -1
- package/dist/media/index.js +1 -1
- package/dist/schemas/index.js +3 -7
- package/dist/schemas/shared.d.ts +0 -17
- package/dist/schemas/shared.d.ts.map +1 -1
- package/package.json +5 -1
- package/src/components/brandguide/ColorSwatchSettings.tsx +1 -1
- package/src/components/brandguide/Colors.tsx +1 -1
- package/src/components/primitives/IconPicker.tsx +27 -119
- package/src/components/sections/Colors/index.tsx +2 -1
- package/src/components/sections/Colors/schema.ts +24 -0
- package/src/components/sections/DoDontMedia/DoDontMediaBlock.tsx +35 -0
- package/src/components/sections/DoDontMedia/index.tsx +84 -0
- package/src/components/sections/IconList/DoDontToggle.tsx +76 -0
- package/src/components/sections/IconList/IconList.tsx +24 -14
- package/src/components/sections/IconList/index.tsx +1 -0
- package/src/components/sections/Media/MediaBlock.tsx +10 -11
- package/src/components/sections/Media/index.tsx +0 -15
- package/src/components/sections/all-sections.ts +7 -35
- package/src/components/sections/brand-guide.ts +13 -0
- package/src/components/sections/core-sections.ts +26 -0
- package/src/components/shell/EditorShell.tsx +7 -5
- package/src/components/shell/view-branch.ts +14 -0
- package/src/lib/loader.ts +10 -4
- package/src/lib/migrate-sections-transform.ts +33 -5
- package/src/lib/registry.ts +6 -1
- package/src/schemas/shared.ts +0 -18
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { defineSection } from "../../../lib/registry";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { Image as ImageIcon } from "lucide-react";
|
|
4
|
+
import { SingleMediaReferenceSchema } from "../../../schemas/shared";
|
|
5
|
+
import { LinkValueSchema, DEFAULT_LINK } from "../../../schemas/link";
|
|
6
|
+
import { DoDontMediaBlock } from "./DoDontMediaBlock";
|
|
7
|
+
|
|
8
|
+
const schema = z.object({
|
|
9
|
+
type: z.literal("dodont_media"),
|
|
10
|
+
content: z.object({
|
|
11
|
+
ref: SingleMediaReferenceSchema,
|
|
12
|
+
link: LinkValueSchema.optional(),
|
|
13
|
+
dodont: z.enum(["do", "dont"]),
|
|
14
|
+
}),
|
|
15
|
+
options: z
|
|
16
|
+
.object({
|
|
17
|
+
square: z.boolean().optional(),
|
|
18
|
+
showCaption: z.boolean().optional(),
|
|
19
|
+
border: z.boolean().optional(),
|
|
20
|
+
objectFit: z.enum(["cover", "contain"]).optional(),
|
|
21
|
+
})
|
|
22
|
+
.default({}),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export default defineSection({
|
|
26
|
+
type: "dodont_media",
|
|
27
|
+
label: "Do / Don't Media",
|
|
28
|
+
category: "brand-guide",
|
|
29
|
+
icon: <ImageIcon size={18} />,
|
|
30
|
+
schema,
|
|
31
|
+
component: ({ content, options, onChange }) => (
|
|
32
|
+
<DoDontMediaBlock
|
|
33
|
+
refValue={content.content.ref}
|
|
34
|
+
options={(options ?? {}) as { square?: boolean; showCaption?: boolean; border?: boolean; objectFit?: "cover" | "contain" }}
|
|
35
|
+
link={content.content.link}
|
|
36
|
+
dodont={content.content.dodont}
|
|
37
|
+
isEditMode={!!onChange}
|
|
38
|
+
onRefChange={
|
|
39
|
+
onChange ? (ref) => onChange({ ...content, content: { ...content.content, ref } }) : undefined
|
|
40
|
+
}
|
|
41
|
+
/>
|
|
42
|
+
),
|
|
43
|
+
defaults: () => ({
|
|
44
|
+
type: "dodont_media" as const,
|
|
45
|
+
content: { ref: { type: "image" as const, imageId: "" }, dodont: "do" as const },
|
|
46
|
+
options: {},
|
|
47
|
+
}),
|
|
48
|
+
getLabel: () => "Do / Don't Media",
|
|
49
|
+
getThumbnails: (content) =>
|
|
50
|
+
content.content.ref.imageId
|
|
51
|
+
? [{ type: "image" as const, src: content.content.ref.imageId }]
|
|
52
|
+
: [],
|
|
53
|
+
settings: {
|
|
54
|
+
square: { type: "checkbox", label: "Square aspect ratio", default: false },
|
|
55
|
+
showCaption: { type: "checkbox", label: "Show caption", default: false },
|
|
56
|
+
border: { type: "checkbox", label: "Border", default: false },
|
|
57
|
+
objectFit: {
|
|
58
|
+
type: "select",
|
|
59
|
+
label: "Image fit",
|
|
60
|
+
default: "contain",
|
|
61
|
+
options: [
|
|
62
|
+
{ label: "Contain", value: "contain" },
|
|
63
|
+
{ label: "Cover", value: "cover" },
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
link: { type: "link", label: "Link", default: DEFAULT_LINK, target: "content" },
|
|
67
|
+
dodont: {
|
|
68
|
+
type: "select",
|
|
69
|
+
label: "Do / Don't",
|
|
70
|
+
target: "content",
|
|
71
|
+
default: "do",
|
|
72
|
+
options: [
|
|
73
|
+
{ label: "Do", value: "do" },
|
|
74
|
+
{ label: "Don't", value: "dont" },
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
inheritableSettings: ["square", "showCaption", "border", "objectFit"],
|
|
79
|
+
settingsTabs: [
|
|
80
|
+
{ label: "Display", fields: ["square", "showCaption", "border", "objectFit"] },
|
|
81
|
+
{ label: "Link", fields: ["link"] },
|
|
82
|
+
{ label: "Do / Don't", fields: ["dodont"] },
|
|
83
|
+
],
|
|
84
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import { Check, X } from "lucide-react";
|
|
3
|
+
import { cn } from "../../../lib/cn";
|
|
4
|
+
|
|
5
|
+
interface DoDontToggleProps {
|
|
6
|
+
value?: "do" | "dont";
|
|
7
|
+
onSelect: (v: "do" | "dont" | undefined) => void;
|
|
8
|
+
onClose: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Brand-guide do/dont tagging control. Kept OUT of the generic IconPicker so
|
|
13
|
+
* that primitive stays brand-agnostic — do/dont semantics (do = check/green,
|
|
14
|
+
* dont = x/red) live here, in the brand-guide layer. This is the reference
|
|
15
|
+
* example of extending a true primitive with a branded composition.
|
|
16
|
+
*/
|
|
17
|
+
export function DoDontToggle({ value, onSelect, onClose }: DoDontToggleProps) {
|
|
18
|
+
const panelRef = useRef<HTMLDivElement>(null);
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
function handleMouseDown(e: MouseEvent) {
|
|
22
|
+
if (panelRef.current && !panelRef.current.contains(e.target as Node)) onClose();
|
|
23
|
+
}
|
|
24
|
+
document.addEventListener("mousedown", handleMouseDown);
|
|
25
|
+
return () => document.removeEventListener("mousedown", handleMouseDown);
|
|
26
|
+
}, [onClose]);
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div
|
|
30
|
+
ref={panelRef}
|
|
31
|
+
className="absolute z-50 w-44 rounded-lg border border-base-200 bg-base p-2 shadow-lg"
|
|
32
|
+
>
|
|
33
|
+
<div className="grid grid-cols-3 gap-2">
|
|
34
|
+
<button
|
|
35
|
+
type="button"
|
|
36
|
+
aria-label="No do/don't tag"
|
|
37
|
+
className={cn(
|
|
38
|
+
"cursor-pointer flex flex-col items-center justify-center gap-1 rounded py-2 transition-colors",
|
|
39
|
+
"text-base-contrast-light hover:bg-base-accent",
|
|
40
|
+
value === undefined && "ring-2 ring-primary bg-base-accent",
|
|
41
|
+
)}
|
|
42
|
+
onClick={() => onSelect(undefined)}
|
|
43
|
+
>
|
|
44
|
+
<span className="flex h-5 w-5 items-center justify-center rounded-sm border border-dashed border-base-contrast-light/40" />
|
|
45
|
+
<span className="text-xs font-medium">None</span>
|
|
46
|
+
</button>
|
|
47
|
+
<button
|
|
48
|
+
type="button"
|
|
49
|
+
aria-label="Do"
|
|
50
|
+
className={cn(
|
|
51
|
+
"cursor-pointer flex flex-col items-center justify-center gap-1 rounded py-2 transition-colors",
|
|
52
|
+
"text-green-600 hover:bg-base-accent",
|
|
53
|
+
value === "do" && "ring-2 ring-primary bg-base-accent",
|
|
54
|
+
)}
|
|
55
|
+
onClick={() => onSelect("do")}
|
|
56
|
+
>
|
|
57
|
+
<Check size={20} />
|
|
58
|
+
<span className="text-xs font-medium">Do</span>
|
|
59
|
+
</button>
|
|
60
|
+
<button
|
|
61
|
+
type="button"
|
|
62
|
+
aria-label="Don't"
|
|
63
|
+
className={cn(
|
|
64
|
+
"cursor-pointer flex flex-col items-center justify-center gap-1 rounded py-2 transition-colors",
|
|
65
|
+
"text-red-600 hover:bg-base-accent",
|
|
66
|
+
value === "dont" && "ring-2 ring-primary bg-base-accent",
|
|
67
|
+
)}
|
|
68
|
+
onClick={() => onSelect("dont")}
|
|
69
|
+
>
|
|
70
|
+
<X size={20} />
|
|
71
|
+
<span className="text-xs font-medium">Don't</span>
|
|
72
|
+
</button>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -5,9 +5,10 @@ import { getIcon } from "../../../lib/icons";
|
|
|
5
5
|
import { useEditableCollection } from "../../primitives/useEditableCollection";
|
|
6
6
|
import { EditablePlainText } from "../../primitives/EditablePlainText";
|
|
7
7
|
import { DragHandle, DeleteIcon, AddIcon } from "../../shared/icons";
|
|
8
|
-
import { CopyPlus, Pencil } from "lucide-react";
|
|
8
|
+
import { Check, CopyPlus, Pencil, X } from "lucide-react";
|
|
9
9
|
import { IconButton } from "../../shared/IconButton";
|
|
10
10
|
import { IconPicker } from "../../primitives/IconPicker";
|
|
11
|
+
import { DoDontToggle } from "./DoDontToggle";
|
|
11
12
|
import type { IconListItem } from "./index";
|
|
12
13
|
|
|
13
14
|
interface Props {
|
|
@@ -298,6 +299,7 @@ function EditableRow({
|
|
|
298
299
|
const handleRef = useRef<HTMLButtonElement>(null);
|
|
299
300
|
const [iconHover, setIconHover] = useState(false);
|
|
300
301
|
const [pickerOpen, setPickerOpen] = useState(false);
|
|
302
|
+
const [dodontOpen, setDodontOpen] = useState(false);
|
|
301
303
|
const [editingSuppressed, setEditingSuppressed] = useState(false);
|
|
302
304
|
|
|
303
305
|
const iconEntry = resolveIcon(item, defaultIcon);
|
|
@@ -387,7 +389,7 @@ function EditableRow({
|
|
|
387
389
|
<button
|
|
388
390
|
className="cursor-pointer transition-opacity hover:opacity-70"
|
|
389
391
|
aria-label="Change icon"
|
|
390
|
-
onClick={() => setPickerOpen((prev) => !prev)}
|
|
392
|
+
onClick={() => { setPickerOpen((prev) => !prev); setDodontOpen(false); }}
|
|
391
393
|
>
|
|
392
394
|
{iconHover && !pickerOpen ? (
|
|
393
395
|
<Pencil size={18} className="text-base-contrast-light" />
|
|
@@ -403,23 +405,11 @@ function EditableRow({
|
|
|
403
405
|
<IconPicker
|
|
404
406
|
selected={effectiveIconId}
|
|
405
407
|
showRemove={true}
|
|
406
|
-
allowDoDont={true}
|
|
407
|
-
dodont={item.dodont}
|
|
408
408
|
onSelect={(newIcon) => {
|
|
409
409
|
onUpdateItem({ icon: newIcon ?? undefined, dodont: undefined });
|
|
410
410
|
setPickerOpen(false);
|
|
411
411
|
setIconHover(false);
|
|
412
412
|
}}
|
|
413
|
-
onSelectDoDont={(v) => {
|
|
414
|
-
onUpdateItem({ dodont: v });
|
|
415
|
-
// Switching to None mode (v === undefined) clears the tag but
|
|
416
|
-
// keeps the picker open so the grid is reachable; choosing a
|
|
417
|
-
// Do/Don't tile commits and closes.
|
|
418
|
-
if (v !== undefined) {
|
|
419
|
-
setPickerOpen(false);
|
|
420
|
-
setIconHover(false);
|
|
421
|
-
}
|
|
422
|
-
}}
|
|
423
413
|
onClose={() => {
|
|
424
414
|
setPickerOpen(false);
|
|
425
415
|
setIconHover(false);
|
|
@@ -462,6 +452,26 @@ function EditableRow({
|
|
|
462
452
|
: "opacity-0 group-hover/row:opacity-100 no-hover:opacity-100",
|
|
463
453
|
)}
|
|
464
454
|
>
|
|
455
|
+
<div className="relative">
|
|
456
|
+
<button
|
|
457
|
+
aria-label="Set do/don't"
|
|
458
|
+
onClick={() => { setDodontOpen((p) => !p); setPickerOpen(false); }}
|
|
459
|
+
className="cursor-pointer rounded bg-base/80 p-1 shadow-sm text-base-contrast-light hover:text-primary"
|
|
460
|
+
>
|
|
461
|
+
{item.dodont === "do" ? <Check size={14} className="text-green-600" />
|
|
462
|
+
: item.dodont === "dont" ? <X size={14} className="text-red-600" />
|
|
463
|
+
: <Pencil size={14} />}
|
|
464
|
+
</button>
|
|
465
|
+
{dodontOpen && (
|
|
466
|
+
<div className="absolute top-full right-0 z-50 mt-1">
|
|
467
|
+
<DoDontToggle
|
|
468
|
+
value={item.dodont}
|
|
469
|
+
onSelect={(v) => { onUpdateItem({ dodont: v }); setDodontOpen(false); }}
|
|
470
|
+
onClose={() => setDodontOpen(false)}
|
|
471
|
+
/>
|
|
472
|
+
</div>
|
|
473
|
+
)}
|
|
474
|
+
</div>
|
|
465
475
|
<IconButton
|
|
466
476
|
ref={handleRef}
|
|
467
477
|
icon={<DragHandle size={14} />}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { SingleMediaReference } from "../../../schemas/shared";
|
|
2
2
|
import type { LinkValue } from "../../../schemas/link";
|
|
3
|
-
import { Check, X } from "lucide-react";
|
|
4
3
|
import { cn } from "../../../lib/cn";
|
|
5
4
|
import { ResolvedMedia } from "../../primitives/ResolvedMedia";
|
|
6
5
|
import { ImageDropZone } from "../../primitives/ImageDropZone";
|
|
@@ -12,13 +11,11 @@ export interface MediaBlockProps {
|
|
|
12
11
|
options: { square?: boolean; showCaption?: boolean; border?: boolean; objectFit?: "cover" | "contain" };
|
|
13
12
|
/** Per-item link (viewer: already resolved to kind:"external" upstream). */
|
|
14
13
|
link?: LinkValue;
|
|
15
|
-
/** Per-item do/dont badge. */
|
|
16
|
-
dodont?: "do" | "dont";
|
|
17
14
|
isEditMode: boolean;
|
|
18
15
|
onRefChange?: (ref: SingleMediaReference) => void;
|
|
19
16
|
}
|
|
20
17
|
|
|
21
|
-
export function MediaBlock({ refValue, options, link,
|
|
18
|
+
export function MediaBlock({ refValue, options, link, isEditMode, onRefChange }: MediaBlockProps) {
|
|
22
19
|
const mediaLibrary = useMediaLibrary();
|
|
23
20
|
// Per-item ref value overrides the inherited option (childDefaults); default contain.
|
|
24
21
|
const fit = refValue.objectFit ?? options.objectFit;
|
|
@@ -30,9 +27,18 @@ export function MediaBlock({ refValue, options, link, dodont, isEditMode, onRefC
|
|
|
30
27
|
|
|
31
28
|
const replace = onRefChange ? (imageId: string) => onRefChange({ ...refValue, imageId }) : undefined;
|
|
32
29
|
|
|
30
|
+
// The zero-JS viewer has no MediaLibraryContext, so useResolvedMedia() can't
|
|
31
|
+
// resolve the URL. The SSR pass (resolveManifestReferences) bakes src/srcset/alt
|
|
32
|
+
// onto the ref instead — forward them so a real <img> renders rather than the
|
|
33
|
+
// placeholder. In edit mode these are absent and ResolvedMedia falls back to the
|
|
34
|
+
// live manifest via context. (Mirrors a3e8913, which did this for MediaGrid/SplitContent.)
|
|
35
|
+
const baked = refValue as Record<string, unknown>;
|
|
33
36
|
const resolved = (
|
|
34
37
|
<ResolvedMedia
|
|
35
38
|
imageId={refValue.imageId || undefined}
|
|
39
|
+
src={baked.src as string | undefined}
|
|
40
|
+
srcset={baked.srcset as string | undefined}
|
|
41
|
+
alt={baked.alt as string | undefined}
|
|
36
42
|
className="h-full w-full"
|
|
37
43
|
imgClassName={fitClass}
|
|
38
44
|
invertFrom={refValue.invertFrom}
|
|
@@ -61,13 +67,6 @@ export function MediaBlock({ refValue, options, link, dodont, isEditMode, onRefC
|
|
|
61
67
|
|
|
62
68
|
return (
|
|
63
69
|
<figure>
|
|
64
|
-
{dodont && (
|
|
65
|
-
<div className="flex h-10 items-center justify-center pb-1">
|
|
66
|
-
{dodont === "do"
|
|
67
|
-
? <Check size={28} className="text-green-600" />
|
|
68
|
-
: <X size={28} className="text-red-600" />}
|
|
69
|
-
</div>
|
|
70
|
-
)}
|
|
71
70
|
<div
|
|
72
71
|
data-testid="media-open-surface"
|
|
73
72
|
className={cn(
|
|
@@ -10,7 +10,6 @@ const schema = z.object({
|
|
|
10
10
|
content: z.object({
|
|
11
11
|
ref: SingleMediaReferenceSchema,
|
|
12
12
|
link: LinkValueSchema.optional(),
|
|
13
|
-
dodont: z.enum(["do", "dont"]).optional(),
|
|
14
13
|
}),
|
|
15
14
|
options: z
|
|
16
15
|
.object({
|
|
@@ -32,7 +31,6 @@ export default defineSection({
|
|
|
32
31
|
refValue={content.content.ref}
|
|
33
32
|
options={(options ?? {}) as { square?: boolean; showCaption?: boolean; border?: boolean; objectFit?: "cover" | "contain" }}
|
|
34
33
|
link={content.content.link}
|
|
35
|
-
dodont={content.content.dodont}
|
|
36
34
|
isEditMode={!!onChange}
|
|
37
35
|
onRefChange={
|
|
38
36
|
onChange ? (ref) => onChange({ ...content, content: { ...content.content, ref } }) : undefined
|
|
@@ -63,23 +61,10 @@ export default defineSection({
|
|
|
63
61
|
],
|
|
64
62
|
},
|
|
65
63
|
link: { type: "link", label: "Link", default: DEFAULT_LINK, target: "content" },
|
|
66
|
-
dodont: {
|
|
67
|
-
type: "select",
|
|
68
|
-
label: "Do / Don't",
|
|
69
|
-
target: "content",
|
|
70
|
-
default: "",
|
|
71
|
-
emptyIsUndefined: true,
|
|
72
|
-
options: [
|
|
73
|
-
{ label: "None", value: "" },
|
|
74
|
-
{ label: "Do", value: "do" },
|
|
75
|
-
{ label: "Don't", value: "dont" },
|
|
76
|
-
],
|
|
77
|
-
},
|
|
78
64
|
},
|
|
79
65
|
inheritableSettings: ["square", "showCaption", "border", "objectFit"],
|
|
80
66
|
settingsTabs: [
|
|
81
67
|
{ label: "Display", fields: ["square", "showCaption", "border", "objectFit"] },
|
|
82
68
|
{ label: "Link", fields: ["link"] },
|
|
83
|
-
{ label: "Do / Don't", fields: ["dodont"] },
|
|
84
69
|
],
|
|
85
70
|
});
|
|
@@ -1,40 +1,12 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import subSubHeading from "./SubSubHeading";
|
|
4
|
-
import prose from "./Prose";
|
|
5
|
-
import media from "./Media";
|
|
6
|
-
import button from "./Button";
|
|
7
|
-
import colors from "./Colors";
|
|
8
|
-
import iconList from "./IconList";
|
|
9
|
-
import container from "./Container";
|
|
10
|
-
import spacer from "./Spacer";
|
|
1
|
+
import { coreSectionDefs } from "./core-sections";
|
|
2
|
+
import { brandGuideSectionDefs } from "./brand-guide";
|
|
11
3
|
|
|
12
4
|
/**
|
|
13
|
-
* The single ordered list of built-in section definitions
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* (schema-only registration for /api/save validation) derive from this one
|
|
17
|
-
* array, so the two can no longer drift — previously they hand-maintained
|
|
18
|
-
* parallel 11-entry lists, and a section added to one but not the other would
|
|
19
|
-
* make the editor render a type that save rejects with a 400 (or vice versa).
|
|
5
|
+
* The single ordered list of built-in section definitions, composed from the
|
|
6
|
+
* core (generic) and brand-guide groups. Both `register.ts` and
|
|
7
|
+
* `register-schemas.ts` derive from this one array, so they cannot drift.
|
|
20
8
|
*
|
|
21
9
|
* Retired: split_content, media_grid, do_dont_grid, do_dont — fully decomposable
|
|
22
|
-
* into container sections
|
|
23
|
-
* legacy-typed JSON to container before validation.
|
|
10
|
+
* into container sections by the loader fallback (mergeSiteContent).
|
|
24
11
|
*/
|
|
25
|
-
|
|
26
|
-
// and a wider `SectionDefinition<unknown>[]` annotation isn't assignable from
|
|
27
|
-
// them (generic param variance). The inferred union is what registerSection /
|
|
28
|
-
// registerSchema already accept.
|
|
29
|
-
export const allSectionDefs = [
|
|
30
|
-
linkHeading,
|
|
31
|
-
subHeading,
|
|
32
|
-
subSubHeading,
|
|
33
|
-
prose,
|
|
34
|
-
media,
|
|
35
|
-
button,
|
|
36
|
-
colors,
|
|
37
|
-
iconList,
|
|
38
|
-
container,
|
|
39
|
-
spacer,
|
|
40
|
-
];
|
|
12
|
+
export const allSectionDefs = [...coreSectionDefs, ...brandGuideSectionDefs];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import colors from "./Colors";
|
|
2
|
+
import iconList from "./IconList";
|
|
3
|
+
import doDontMedia from "./DoDontMedia";
|
|
4
|
+
|
|
5
|
+
/** Opinionated brand-guide sections. Self-contained subtree: a build that does
|
|
6
|
+
* not import this module drops these sections (and their cmyk/pantone schemas
|
|
7
|
+
* and do/dont compositions) entirely. Exposed at
|
|
8
|
+
* `@drawnagency/primitives/components/sections/brand-guide`. */
|
|
9
|
+
// Un-annotated for the same generic-variance reason as coreSectionDefs.
|
|
10
|
+
export const brandGuideSectionDefs = [colors, iconList, doDontMedia];
|
|
11
|
+
|
|
12
|
+
export { ColorSpaceSchema, ColorItemSchema } from "./Colors/schema";
|
|
13
|
+
export type { ColorSpace, ColorItem } from "./Colors/schema";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import linkHeading from "./LinkHeading";
|
|
2
|
+
import subHeading from "./SubHeading";
|
|
3
|
+
import subSubHeading from "./SubSubHeading";
|
|
4
|
+
import prose from "./Prose";
|
|
5
|
+
import media from "./Media";
|
|
6
|
+
import button from "./Button";
|
|
7
|
+
import container from "./Container";
|
|
8
|
+
import spacer from "./Spacer";
|
|
9
|
+
|
|
10
|
+
/** Generic, brand-agnostic built-in sections. A non-brand site can register
|
|
11
|
+
* exactly these (plus its own custom sections) and tree-shake the brand-guide
|
|
12
|
+
* group out of its bundle. */
|
|
13
|
+
// Intentionally un-annotated: each section is SectionDefinition<its-own-shape>,
|
|
14
|
+
// and a wider `SectionDefinition<unknown>[]` annotation isn't assignable from
|
|
15
|
+
// them (generic param variance). The inferred union is what registerSection /
|
|
16
|
+
// registerSchema already accept.
|
|
17
|
+
export const coreSectionDefs = [
|
|
18
|
+
linkHeading,
|
|
19
|
+
subHeading,
|
|
20
|
+
subSubHeading,
|
|
21
|
+
prose,
|
|
22
|
+
media,
|
|
23
|
+
button,
|
|
24
|
+
container,
|
|
25
|
+
spacer,
|
|
26
|
+
];
|
|
@@ -69,6 +69,7 @@ import { Button } from "../shared/Button";
|
|
|
69
69
|
import { SplitButton } from "../shared/SplitButton";
|
|
70
70
|
import { IconButton } from "../shared/IconButton";
|
|
71
71
|
import { SegmentedControl } from "../shared/SegmentedControl";
|
|
72
|
+
import { isSectionVisibleInViewBranch } from "./view-branch";
|
|
72
73
|
import { SettingsIcon } from "../shared/icons";
|
|
73
74
|
import { ImageIcon, ListOrderedIcon, FilesIcon, X } from "lucide-react";
|
|
74
75
|
import { ErrorBoundary } from "../shared/ErrorBoundary";
|
|
@@ -1216,12 +1217,13 @@ function EditorContent({
|
|
|
1216
1217
|
const editingEnabled = isEditMode && !isPublishing;
|
|
1217
1218
|
|
|
1218
1219
|
// View mode: render the active page's sections from the fetched branch, in page
|
|
1219
|
-
// order (
|
|
1220
|
-
//
|
|
1220
|
+
// order. "Published" (live) shows only status === "live"; "Unpublished" (saved)
|
|
1221
|
+
// shows all in-progress edits except archived (see view-branch.ts). Edit mode:
|
|
1222
|
+
// the active page's in-memory sections (the `sections` prop is already `activeSections`).
|
|
1221
1223
|
const displaySections = !isEditMode && viewSections !== null
|
|
1222
1224
|
? activePageSectionIds
|
|
1223
1225
|
.map((id) => viewSections.find((s) => s.section.id === id))
|
|
1224
|
-
.filter((s): s is LoadedSection => !!s && (
|
|
1226
|
+
.filter((s): s is LoadedSection => !!s && isSectionVisibleInViewBranch(s.meta.status, viewBranch))
|
|
1225
1227
|
: sections;
|
|
1226
1228
|
|
|
1227
1229
|
useEffect(() => {
|
|
@@ -1642,8 +1644,8 @@ function EditorToolbar({
|
|
|
1642
1644
|
<div className="mx-auto max-w-screen-xl flex items-center justify-center px-4 py-2">
|
|
1643
1645
|
<SegmentedControl
|
|
1644
1646
|
options={[
|
|
1645
|
-
{ value: "
|
|
1646
|
-
{ value: "
|
|
1647
|
+
{ value: "live", label: "Published" },
|
|
1648
|
+
{ value: "saved", label: "Unpublished" },
|
|
1647
1649
|
]}
|
|
1648
1650
|
value={viewBranch}
|
|
1649
1651
|
onChange={(v) => setViewBranch(v as "saved" | "live")}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type ViewBranch = "saved" | "live";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Section visibility for the editor's view-mode toggle.
|
|
5
|
+
*
|
|
6
|
+
* - `"live"` ("Published"): mirrors what an anonymous visitor sees on the
|
|
7
|
+
* deployed site — only `status === "live"` (the same predicate the real viewer
|
|
8
|
+
* applies in `viewer-page.ts`).
|
|
9
|
+
* - `"saved"` ("Unpublished"): all in-progress edits — every status EXCEPT
|
|
10
|
+
* `archived` — so edits to live content show alongside drafts.
|
|
11
|
+
*/
|
|
12
|
+
export function isSectionVisibleInViewBranch(status: string, viewBranch: ViewBranch): boolean {
|
|
13
|
+
return viewBranch === "live" ? status === "live" : status !== "archived";
|
|
14
|
+
}
|
package/src/lib/loader.ts
CHANGED
|
@@ -35,13 +35,19 @@ export function mergeSiteContent(
|
|
|
35
35
|
console.warn(`Section file missing for id: ${id}, skipping`);
|
|
36
36
|
continue;
|
|
37
37
|
}
|
|
38
|
-
// Transitional upgrade: rewrite legacy-typed JSON
|
|
39
|
-
//
|
|
40
|
-
//
|
|
38
|
+
// Transitional upgrade: rewrite legacy-typed JSON BEFORE validation so
|
|
39
|
+
// retired/legacy files render on every load path. Handles: (1) plain section
|
|
40
|
+
// types promoted to container, (2) media+dodont → dodont_media (and the same
|
|
41
|
+
// for doDontImage grid items), and (3) recursion into container children so
|
|
42
|
+
// persisted containers with legacy children are also upgraded. Returns the same
|
|
43
|
+
// reference for non-legacy input (idempotent). We normalize the emitted
|
|
41
44
|
// section's meta.type when an upgrade happens (no mutation of the caller's index).
|
|
42
45
|
const raw = upgradeLegacySection(rawFile);
|
|
43
46
|
const baseMeta = index.sections[id];
|
|
44
|
-
const
|
|
47
|
+
const upgradedType = raw && typeof raw === "object" ? (raw as Record<string, unknown>).type : undefined;
|
|
48
|
+
const meta = raw !== rawFile && typeof upgradedType === "string"
|
|
49
|
+
? { ...baseMeta, type: upgradedType }
|
|
50
|
+
: baseMeta;
|
|
45
51
|
|
|
46
52
|
if (canValidate && schema) {
|
|
47
53
|
const result = schema.safeParse(raw);
|
|
@@ -16,12 +16,36 @@ const RETIRED = new Set(["split_content", "media_grid", "do_dont_grid", "do_dont
|
|
|
16
16
|
export function upgradeLegacySection(raw: unknown): unknown {
|
|
17
17
|
if (!raw || typeof raw !== "object") return raw;
|
|
18
18
|
const section = raw as Obj;
|
|
19
|
-
|
|
20
|
-
if (
|
|
21
|
-
if (
|
|
19
|
+
const type = section.type as string;
|
|
20
|
+
if (type === "media" && hasDoDont(section)) return { ...section, type: "dodont_media" };
|
|
21
|
+
if (type === "container") return upgradeContainerChildren(section);
|
|
22
|
+
if (!RETIRED.has(type)) return raw; // same reference → idempotent
|
|
23
|
+
if (type === "split_content") return upgradeSplitContent(section);
|
|
24
|
+
if (type === "do_dont") return upgradeDoDont(section);
|
|
22
25
|
return upgradeGrid(section); // media_grid | do_dont_grid (per-item type drives the mapping)
|
|
23
26
|
}
|
|
24
27
|
|
|
28
|
+
function hasDoDont(section: Obj): boolean {
|
|
29
|
+
const content = section.content;
|
|
30
|
+
return !!content && typeof content === "object" && (content as Obj).dodont != null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Recurse into container children, upgrading media+dodont (and nested legacy)
|
|
34
|
+
* in place. Returns the SAME reference when nothing changed (idempotent). */
|
|
35
|
+
function upgradeContainerChildren(section: Obj): unknown {
|
|
36
|
+
const content = section.content as Obj | undefined;
|
|
37
|
+
const children = content && Array.isArray(content.children) ? (content.children as unknown[]) : null;
|
|
38
|
+
if (!children) return section;
|
|
39
|
+
let changed = false;
|
|
40
|
+
const mapped = children.map((child) => {
|
|
41
|
+
const up = upgradeLegacySection(child);
|
|
42
|
+
if (up !== child) changed = true;
|
|
43
|
+
return up;
|
|
44
|
+
});
|
|
45
|
+
if (!changed) return section;
|
|
46
|
+
return { ...section, content: { ...content, children: mapped } };
|
|
47
|
+
}
|
|
48
|
+
|
|
25
49
|
/** Carry only known BaseMediaRef (+ video) fields; drops render artifacts (src/srcset/alt) and any stray keys. */
|
|
26
50
|
function buildRef(item: Obj, forceImage: boolean): Obj {
|
|
27
51
|
const type = forceImage ? "image" : (item.type as string);
|
|
@@ -42,16 +66,20 @@ function buildRef(item: Obj, forceImage: boolean): Obj {
|
|
|
42
66
|
|
|
43
67
|
function mapRefItem(item: Obj, parentId: string, index: number): Obj {
|
|
44
68
|
const content: Obj = {};
|
|
69
|
+
let type = "media";
|
|
45
70
|
if (item.type === "linkedImage") {
|
|
46
71
|
content.ref = buildRef(item, true);
|
|
47
72
|
content.link = { kind: "external", href: item.href ?? "", target: item.target === "_blank" ? "_blank" : "_self" };
|
|
48
73
|
} else if (item.type === "doDontImage") {
|
|
49
74
|
content.ref = buildRef(item, true);
|
|
50
|
-
if (item.doDont !== undefined)
|
|
75
|
+
if (item.doDont !== undefined) {
|
|
76
|
+
content.dodont = item.doDont;
|
|
77
|
+
type = "dodont_media";
|
|
78
|
+
}
|
|
51
79
|
} else {
|
|
52
80
|
content.ref = buildRef(item, false); // image | video
|
|
53
81
|
}
|
|
54
|
-
return { id: `${parentId}__media_${index}`, type
|
|
82
|
+
return { id: `${parentId}__media_${index}`, type, content, options: {} };
|
|
55
83
|
}
|
|
56
84
|
|
|
57
85
|
function upgradeGrid(section: Obj): Obj {
|
package/src/lib/registry.ts
CHANGED
|
@@ -35,7 +35,7 @@ export type SettingsFieldDef =
|
|
|
35
35
|
coerce?: "number";
|
|
36
36
|
/** When set, an empty-string ("") selection is normalized to `undefined`
|
|
37
37
|
* before the content/options split, so an optional-enum field clears cleanly
|
|
38
|
-
* (e.g.
|
|
38
|
+
* (e.g. an optional-enum content field). Opt-in so other
|
|
39
39
|
* selects keep their "" value. */
|
|
40
40
|
emptyIsUndefined?: boolean;
|
|
41
41
|
options: { label: string; value: string }[];
|
|
@@ -108,6 +108,10 @@ export interface WrapperProps {
|
|
|
108
108
|
|
|
109
109
|
export interface SectionDefinition<T = unknown> {
|
|
110
110
|
type: string;
|
|
111
|
+
/** Grouping label. `"brand-guide"` marks opinionated sections separable from the
|
|
112
|
+
* generic core set; omitted/`"core"` is a generic section. Presentation/grouping
|
|
113
|
+
* only — does not affect registration. */
|
|
114
|
+
category?: "core" | "brand-guide";
|
|
111
115
|
label: string;
|
|
112
116
|
icon?: ReactNode;
|
|
113
117
|
schema: ZodType<T>;
|
|
@@ -138,6 +142,7 @@ export interface SectionDefinition<T = unknown> {
|
|
|
138
142
|
|
|
139
143
|
type DefineSectionInput<S extends ZodType> = {
|
|
140
144
|
type: string;
|
|
145
|
+
category?: "core" | "brand-guide";
|
|
141
146
|
label: string;
|
|
142
147
|
icon?: ReactNode;
|
|
143
148
|
schema: S;
|
package/src/schemas/shared.ts
CHANGED
|
@@ -92,21 +92,3 @@ export const FontNameSchema = z
|
|
|
92
92
|
.max(120)
|
|
93
93
|
.regex(/^[a-zA-Z0-9 ,'"-]+$/, "contains invalid font-name characters");
|
|
94
94
|
|
|
95
|
-
export const ColorSpaceSchema = z.object({
|
|
96
|
-
hex: HexColorSchema.optional(),
|
|
97
|
-
rgb: z.string().optional(),
|
|
98
|
-
cmyk: z.string().optional(),
|
|
99
|
-
pantone: z.string().optional(),
|
|
100
|
-
}).refine(
|
|
101
|
-
(data) => data.hex !== undefined || data.rgb !== undefined || data.cmyk !== undefined || data.pantone !== undefined,
|
|
102
|
-
{ message: "At least one color space must be defined" },
|
|
103
|
-
);
|
|
104
|
-
|
|
105
|
-
export type ColorSpace = z.infer<typeof ColorSpaceSchema>;
|
|
106
|
-
|
|
107
|
-
export const ColorItemSchema = z.object({
|
|
108
|
-
name: z.string().optional(),
|
|
109
|
-
spaces: z.array(ColorSpaceSchema).min(1),
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
export type ColorItem = z.infer<typeof ColorItemSchema>;
|