@drawnagency/primitives 0.1.66 → 0.1.67
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-4VMAZZXT.js +968 -0
- package/dist/{chunk-VEIDANEU.js → chunk-KM6HOW4H.js} +126 -39
- package/dist/{chunk-DQTKWKSK.js → chunk-WJ7RZ6CC.js} +11 -2
- package/dist/components/primitives/defineHeadingSection.d.ts +2 -0
- package/dist/components/primitives/defineHeadingSection.d.ts.map +1 -1
- package/dist/components/primitives/index.js +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/Colors/schema.d.ts.map +1 -1
- package/dist/components/sections/Container/index.d.ts.map +1 -1
- package/dist/components/sections/DoDontMedia/index.d.ts.map +1 -1
- package/dist/components/sections/IconList/IconList.d.ts +2 -1
- package/dist/components/sections/IconList/IconList.d.ts.map +1 -1
- package/dist/components/sections/IconList/IconListSettings.d.ts +2 -1
- package/dist/components/sections/IconList/IconListSettings.d.ts.map +1 -1
- package/dist/components/sections/IconList/index.d.ts +1 -0
- package/dist/components/sections/IconList/index.d.ts.map +1 -1
- package/dist/components/sections/LinkHeading/index.d.ts.map +1 -1
- package/dist/components/sections/Media/index.d.ts.map +1 -1
- package/dist/components/sections/Prose/index.d.ts.map +1 -1
- package/dist/components/sections/Spacer/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/sections/all-sections.d.ts +1 -0
- package/dist/components/sections/all-sections.d.ts.map +1 -1
- package/dist/components/sections/brand-guide.d.ts +1 -0
- package/dist/components/sections/brand-guide.d.ts.map +1 -1
- package/dist/components/sections/brand-guide.js +1 -1
- package/dist/components/sections/register-schemas.js +11 -941
- package/dist/components/sections/register.js +36 -0
- package/dist/components/shell/EditorShell.d.ts.map +1 -1
- package/dist/index.js +20 -20
- package/dist/lib/index.js +2 -2
- package/dist/lib/insert-menu.d.ts +19 -0
- package/dist/lib/insert-menu.d.ts.map +1 -0
- package/dist/lib/registry.d.ts +23 -0
- package/dist/lib/registry.d.ts.map +1 -1
- package/dist/types/database.d.ts +248 -34
- package/dist/types/database.d.ts.map +1 -1
- package/package.json +5 -1
- package/src/components/primitives/defineHeadingSection.tsx +18 -2
- package/src/components/sections/Button/index.tsx +10 -3
- package/src/components/sections/Colors/index.tsx +15 -7
- package/src/components/sections/Colors/schema.ts +5 -2
- package/src/components/sections/Container/index.tsx +19 -3
- package/src/components/sections/DoDontMedia/index.tsx +18 -7
- package/src/components/sections/IconList/IconList.tsx +43 -19
- package/src/components/sections/IconList/IconListSettings.tsx +58 -11
- package/src/components/sections/IconList/index.tsx +51 -7
- package/src/components/sections/LinkHeading/index.tsx +2 -0
- package/src/components/sections/Media/index.tsx +15 -6
- package/src/components/sections/Prose/index.tsx +9 -1
- package/src/components/sections/Spacer/index.tsx +2 -0
- package/src/components/sections/SubHeading/index.tsx +2 -0
- package/src/components/sections/SubSubHeading/index.tsx +2 -0
- package/src/components/shell/EditorShell.tsx +13 -19
- package/src/lib/insert-menu.ts +42 -0
- package/src/lib/registry.ts +24 -0
- package/src/types/database.ts +236 -36
- package/dist/{chunk-ZYXLNJ5M.js → chunk-AFK45HX2.js} +3 -3
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { defineSection } from "../../../lib/registry";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { List } from "lucide-react";
|
|
3
|
+
import { List, Columns2 } from "lucide-react";
|
|
4
|
+
import type { SectionPreset } from "../../../lib/registry";
|
|
5
|
+
import type { SectionContent } from "../../../schemas/sections";
|
|
4
6
|
import IconList from "./IconList";
|
|
5
7
|
import { IconListSettings } from "./IconListSettings";
|
|
6
8
|
import { pluralize } from "../../../lib/text";
|
|
@@ -16,27 +18,68 @@ export type IconListItem = z.infer<typeof IconListItemSchema>;
|
|
|
16
18
|
const schema = z.object({
|
|
17
19
|
type: z.literal("icon_list"),
|
|
18
20
|
content: z.object({
|
|
19
|
-
items: z
|
|
21
|
+
items: z
|
|
22
|
+
.array(IconListItemSchema)
|
|
23
|
+
.describe(
|
|
24
|
+
"The list entries. Each item has a label, descriptive text, an optional icon, and an optional do/don't marker.",
|
|
25
|
+
),
|
|
20
26
|
}),
|
|
21
|
-
options: z
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
options: z
|
|
28
|
+
.object({
|
|
29
|
+
icon: z
|
|
30
|
+
.string()
|
|
31
|
+
.nullable()
|
|
32
|
+
.describe("Default icon name applied to items that don't specify their own; null for no icon.")
|
|
33
|
+
.optional(),
|
|
34
|
+
showLabel: z.boolean().describe("When true, render each item's label.").optional(),
|
|
35
|
+
stackText: z
|
|
36
|
+
.boolean()
|
|
37
|
+
.describe("When true, stack the label and text vertically instead of inline.")
|
|
38
|
+
.optional(),
|
|
39
|
+
dodont: z
|
|
40
|
+
.enum(["do", "dont"])
|
|
41
|
+
.describe("Whole-list do/don't mode: render every item with a green check (do) or red x (don't).")
|
|
42
|
+
.optional(),
|
|
43
|
+
})
|
|
44
|
+
.optional(),
|
|
26
45
|
});
|
|
27
46
|
|
|
47
|
+
// One-click "Do / Don't list": a 2-column container holding a green-check "Do"
|
|
48
|
+
// icon_list and a red-x "Don't" icon_list, using the whole-list options.dodont
|
|
49
|
+
// default. Same shape the legacy do_dont migration produces. Lives here (brand-guide
|
|
50
|
+
// layer); it reaches the core Add-menu only via SectionDefinition.presets.
|
|
51
|
+
const doDontListPreset: SectionPreset = {
|
|
52
|
+
id: "do_dont_list",
|
|
53
|
+
label: "Do / Don't list",
|
|
54
|
+
icon: <Columns2 size={18} />,
|
|
55
|
+
build: (newId): SectionContent => ({
|
|
56
|
+
type: "container",
|
|
57
|
+
content: {
|
|
58
|
+
columns: 2,
|
|
59
|
+
flow: "row",
|
|
60
|
+
children: [
|
|
61
|
+
{ id: newId(), type: "icon_list", content: { items: [{ label: "", text: "" }] }, options: { dodont: "do" } },
|
|
62
|
+
{ id: newId(), type: "icon_list", content: { items: [{ label: "", text: "" }] }, options: { dodont: "dont" } },
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
}),
|
|
66
|
+
};
|
|
67
|
+
|
|
28
68
|
export default defineSection({
|
|
29
69
|
type: "icon_list",
|
|
30
70
|
category: "brand-guide",
|
|
31
71
|
label: "Icon List",
|
|
32
72
|
icon: <List size={18} />,
|
|
33
73
|
schema,
|
|
74
|
+
authoringNote:
|
|
75
|
+
"A list of labeled items, optionally with per-item icons and do/don't markers. Use for feature lists, guidelines, or do/don't guidance.",
|
|
34
76
|
component: ({ content, options, onChange }) => (
|
|
35
77
|
<IconList
|
|
36
78
|
items={content.content.items}
|
|
37
79
|
icon={options?.icon as string | null}
|
|
38
80
|
showLabel={options?.showLabel as boolean}
|
|
39
81
|
stackText={options?.stackText as boolean}
|
|
82
|
+
dodont={options?.dodont as "do" | "dont" | undefined}
|
|
40
83
|
onChange={onChange ? (c) => onChange(c as typeof content) : undefined}
|
|
41
84
|
/>
|
|
42
85
|
),
|
|
@@ -46,4 +89,5 @@ export default defineSection({
|
|
|
46
89
|
}),
|
|
47
90
|
getLabel: (content) => pluralize(content.content.items.length, "icon"),
|
|
48
91
|
settingsForm: IconListSettings,
|
|
92
|
+
presets: [doDontListPreset],
|
|
49
93
|
});
|
|
@@ -9,4 +9,6 @@ export default defineHeadingSection({
|
|
|
9
9
|
tag: "h2",
|
|
10
10
|
placeholder: "Section heading",
|
|
11
11
|
defaultHeading: "New Section",
|
|
12
|
+
authoringNote:
|
|
13
|
+
"A top-level section heading (renders an <h2>, reported to navigation as h1). Use it to begin a major named section of the guide; its text becomes a navigation entry.",
|
|
12
14
|
});
|
|
@@ -8,15 +8,22 @@ import { MediaBlock } from "./MediaBlock";
|
|
|
8
8
|
const schema = z.object({
|
|
9
9
|
type: z.literal("media"),
|
|
10
10
|
content: z.object({
|
|
11
|
-
ref: SingleMediaReferenceSchema
|
|
12
|
-
|
|
11
|
+
ref: SingleMediaReferenceSchema.describe(
|
|
12
|
+
"The image or video asset to display (a media reference by id). Required.",
|
|
13
|
+
),
|
|
14
|
+
link: LinkValueSchema.describe(
|
|
15
|
+
"Optional link wrapping the media — an internal page or a safe external URL.",
|
|
16
|
+
).optional(),
|
|
13
17
|
}),
|
|
14
18
|
options: z
|
|
15
19
|
.object({
|
|
16
|
-
square: z.boolean().optional(),
|
|
17
|
-
showCaption: z.boolean().optional(),
|
|
18
|
-
border: z.boolean().optional(),
|
|
19
|
-
objectFit: z
|
|
20
|
+
square: z.boolean().describe("When true, crop the media to a 1:1 square aspect ratio.").optional(),
|
|
21
|
+
showCaption: z.boolean().describe("When true, render the asset's caption beneath it.").optional(),
|
|
22
|
+
border: z.boolean().describe("When true, draw a border around the media.").optional(),
|
|
23
|
+
objectFit: z
|
|
24
|
+
.enum(["cover", "contain"])
|
|
25
|
+
.describe("How the image fills its box: 'contain' (fit whole image) or 'cover' (fill and crop).")
|
|
26
|
+
.optional(),
|
|
20
27
|
})
|
|
21
28
|
.default({}),
|
|
22
29
|
});
|
|
@@ -26,6 +33,8 @@ export default defineSection({
|
|
|
26
33
|
label: "Media",
|
|
27
34
|
icon: <ImageIcon size={18} />,
|
|
28
35
|
schema,
|
|
36
|
+
authoringNote:
|
|
37
|
+
"A single image or video with optional caption and link. Use to show one asset; for several assets side by side, place multiple Media blocks inside a Container.",
|
|
29
38
|
component: ({ content, options, onChange }) => (
|
|
30
39
|
<MediaBlock
|
|
31
40
|
refValue={content.content.ref}
|
|
@@ -6,7 +6,13 @@ import { stripHtmlToPlainText, truncate } from "../../../lib/text";
|
|
|
6
6
|
|
|
7
7
|
const schema = z.object({
|
|
8
8
|
type: z.literal("prose"),
|
|
9
|
-
content: z.object({
|
|
9
|
+
content: z.object({
|
|
10
|
+
body: z
|
|
11
|
+
.string()
|
|
12
|
+
.describe(
|
|
13
|
+
"Rich-text HTML for the paragraph block. Author with the editor; stored as sanitized HTML.",
|
|
14
|
+
),
|
|
15
|
+
}),
|
|
10
16
|
});
|
|
11
17
|
|
|
12
18
|
export default defineSection({
|
|
@@ -14,6 +20,8 @@ export default defineSection({
|
|
|
14
20
|
label: "Prose",
|
|
15
21
|
icon: <AlignLeft size={18} />,
|
|
16
22
|
schema,
|
|
23
|
+
authoringNote:
|
|
24
|
+
"A block of rich-text body copy (paragraphs, lists, inline emphasis, links). Use for any explanatory prose; author with the editor, not raw HTML.",
|
|
17
25
|
richTextFields: ["body"],
|
|
18
26
|
component: ({ content, onChange }) => (
|
|
19
27
|
<Prose body={content.content.body} onChange={onChange ? (c) => onChange(c as typeof content) : undefined} />
|
|
@@ -13,6 +13,8 @@ export default defineSection({
|
|
|
13
13
|
label: "Spacer",
|
|
14
14
|
icon: <SquareDashed size={18} />,
|
|
15
15
|
schema,
|
|
16
|
+
authoringNote:
|
|
17
|
+
"Vertical whitespace between sections. Use sparingly to add breathing room where default section spacing is insufficient.",
|
|
16
18
|
component: () => <Spacer />,
|
|
17
19
|
defaults: () => ({ type: "spacer" as const, content: {} }),
|
|
18
20
|
});
|
|
@@ -10,4 +10,6 @@ export default defineHeadingSection({
|
|
|
10
10
|
placeholder: "Sub heading",
|
|
11
11
|
defaultHeading: "New Sub Heading",
|
|
12
12
|
navExclusion: true,
|
|
13
|
+
authoringNote:
|
|
14
|
+
"A second-level heading (renders <h3>, nav level h2). Use under a Link Heading to divide a section; toggle Exclude from navigation to keep it out of the nav tree.",
|
|
13
15
|
});
|
|
@@ -10,4 +10,6 @@ export default defineHeadingSection({
|
|
|
10
10
|
placeholder: "Sub sub heading",
|
|
11
11
|
defaultHeading: "New Sub Sub Heading",
|
|
12
12
|
navExclusion: true,
|
|
13
|
+
authoringNote:
|
|
14
|
+
"A third-level heading (renders <h4>, nav level h3). Use for the finest-grained labeled subsection; toggle Exclude from navigation to omit it from the nav tree.",
|
|
13
15
|
});
|
|
@@ -27,6 +27,7 @@ import { SectionTypePicker } from "./SectionTypePicker";
|
|
|
27
27
|
import { ensureSectionsRegistered } from "../sections/register";
|
|
28
28
|
import { getSection, getAllSections } from "../../lib/registry";
|
|
29
29
|
import { childInsertableTypes, insertChildAt } from "../../lib/container-ops";
|
|
30
|
+
import { buildInsertMenuOptions, resolveInsertedContent } from "../../lib/insert-menu";
|
|
30
31
|
|
|
31
32
|
ensureSectionsRegistered();
|
|
32
33
|
import { BugReportFAB } from "./BugReportFAB";
|
|
@@ -591,13 +592,16 @@ export default function EditorShell({
|
|
|
591
592
|
);
|
|
592
593
|
|
|
593
594
|
const onAddSection = useCallback(
|
|
594
|
-
(insertIndex: number,
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
const
|
|
595
|
+
(insertIndex: number, selection: string) => {
|
|
596
|
+
if (!resolvedActivePage) return;
|
|
597
|
+
const newId = () =>
|
|
598
|
+
typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
|
|
599
|
+
? crypto.randomUUID()
|
|
600
|
+
: `_id_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
601
|
+
const content = resolveInsertedContent(selection, getAllSections(), newId);
|
|
602
|
+
if (!content) return;
|
|
603
|
+
const id = newId();
|
|
604
|
+
const meta = { type: content.type, status: "draft" as const, access: [] as string[] };
|
|
601
605
|
const newSection: LoadedSection = { section: { id, ...content } as LoadedSection["section"], meta };
|
|
602
606
|
|
|
603
607
|
setSections((prev) => [...prev, newSection]);
|
|
@@ -1189,7 +1193,7 @@ function EditorContent({
|
|
|
1189
1193
|
dirtySectionIds: Set<string>;
|
|
1190
1194
|
isPublishing: boolean;
|
|
1191
1195
|
onSectionChange: (sectionId: string, content: SectionContent) => void;
|
|
1192
|
-
onAddSection: (insertIndex: number,
|
|
1196
|
+
onAddSection: (insertIndex: number, selection: string) => void;
|
|
1193
1197
|
onDeleteSection: (sectionId: string) => void;
|
|
1194
1198
|
onReorderSections: (fromIndex: number, toIndex: number) => void;
|
|
1195
1199
|
onBlockMove: (move: BlockMove) => void;
|
|
@@ -1205,17 +1209,7 @@ function EditorContent({
|
|
|
1205
1209
|
const [pendingInsertIndex, setPendingInsertIndex] = useState<number | null>(null);
|
|
1206
1210
|
const dismissPendingInsert = useCallback(() => setPendingInsertIndex(null), []);
|
|
1207
1211
|
|
|
1208
|
-
const typeOptions = useMemo(
|
|
1209
|
-
() =>
|
|
1210
|
-
getAllSections()
|
|
1211
|
-
.filter((def) => def.type !== "spacer")
|
|
1212
|
-
.map((def) => ({
|
|
1213
|
-
type: def.type,
|
|
1214
|
-
label: def.label,
|
|
1215
|
-
icon: def.icon,
|
|
1216
|
-
})),
|
|
1217
|
-
[],
|
|
1218
|
-
);
|
|
1212
|
+
const typeOptions = useMemo(() => buildInsertMenuOptions(getAllSections()), []);
|
|
1219
1213
|
|
|
1220
1214
|
const editingEnabled = isEditMode && !isPublishing;
|
|
1221
1215
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { SectionDefinition } from "./registry";
|
|
3
|
+
import type { SectionContent } from "../schemas/sections";
|
|
4
|
+
|
|
5
|
+
/** Namespacing prefix for preset selections in the Add-section menu. */
|
|
6
|
+
export const PRESET_PREFIX = "__preset:";
|
|
7
|
+
|
|
8
|
+
/** One Add-section menu entry: a section `type`, or `__preset:<presetId>`. */
|
|
9
|
+
export interface InsertMenuOption {
|
|
10
|
+
type: string;
|
|
11
|
+
label: string;
|
|
12
|
+
icon?: ReactNode;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Every non-spacer section type, followed by presets contributed by registered
|
|
16
|
+
* sections. Presets appear only when their owning section is registered, so a
|
|
17
|
+
* core-only build (no brand-guide sections) shows no brand-guide presets. */
|
|
18
|
+
export function buildInsertMenuOptions(sections: SectionDefinition[]): InsertMenuOption[] {
|
|
19
|
+
const typeOptions = sections
|
|
20
|
+
.filter((def) => def.type !== "spacer")
|
|
21
|
+
.map((def) => ({ type: def.type, label: def.label, icon: def.icon }));
|
|
22
|
+
const presetOptions = sections
|
|
23
|
+
.flatMap((def) => def.presets ?? [])
|
|
24
|
+
.map((p) => ({ type: `${PRESET_PREFIX}${p.id}`, label: p.label, icon: p.icon }));
|
|
25
|
+
return [...typeOptions, ...presetOptions];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Resolve an Add-menu selection to ready-to-insert content. `newId` mints ids for
|
|
29
|
+
* any nested children a preset creates. Returns null for an unknown type/preset. */
|
|
30
|
+
export function resolveInsertedContent(
|
|
31
|
+
selection: string,
|
|
32
|
+
sections: SectionDefinition[],
|
|
33
|
+
newId: () => string,
|
|
34
|
+
): SectionContent | null {
|
|
35
|
+
if (selection.startsWith(PRESET_PREFIX)) {
|
|
36
|
+
const id = selection.slice(PRESET_PREFIX.length);
|
|
37
|
+
const preset = sections.flatMap((def) => def.presets ?? []).find((p) => p.id === id);
|
|
38
|
+
return preset ? preset.build(newId) : null;
|
|
39
|
+
}
|
|
40
|
+
const def = sections.find((d) => d.type === selection);
|
|
41
|
+
return def ? (def.defaults() as SectionContent) : null;
|
|
42
|
+
}
|
package/src/lib/registry.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { ZodType, z } from "zod";
|
|
|
2
2
|
import type { ComponentType, ReactNode } from "react";
|
|
3
3
|
import type { Audience } from "../schemas/auth";
|
|
4
4
|
import type { LinkValue } from "../schemas/link";
|
|
5
|
+
import type { SectionContent } from "../schemas/sections";
|
|
5
6
|
|
|
6
7
|
// --- Settings field types ---
|
|
7
8
|
|
|
@@ -106,6 +107,19 @@ export interface WrapperProps {
|
|
|
106
107
|
|
|
107
108
|
// --- Section definition ---
|
|
108
109
|
|
|
110
|
+
/**
|
|
111
|
+
* A ready-to-insert composite (e.g. a container of preconfigured child sections),
|
|
112
|
+
* surfaced in the Add-section menu. Contributed by the section definition that owns
|
|
113
|
+
* the composition, so it rides the section registry and disappears in a core-only
|
|
114
|
+
* build that never registers that section. `newId` mints unique ids for nested children.
|
|
115
|
+
*/
|
|
116
|
+
export interface SectionPreset {
|
|
117
|
+
id: string;
|
|
118
|
+
label: string;
|
|
119
|
+
icon?: ReactNode;
|
|
120
|
+
build(newId: () => string): SectionContent;
|
|
121
|
+
}
|
|
122
|
+
|
|
109
123
|
export interface SectionDefinition<T = unknown> {
|
|
110
124
|
type: string;
|
|
111
125
|
/** Grouping label. `"brand-guide"` marks opinionated sections separable from the
|
|
@@ -136,6 +150,14 @@ export interface SectionDefinition<T = unknown> {
|
|
|
136
150
|
* into a trailing 'Other' tab.
|
|
137
151
|
*/
|
|
138
152
|
settingsTabs?: { label: string; fields: string[] }[];
|
|
153
|
+
/** Composite blueprints for the Add-section menu (see SectionPreset). */
|
|
154
|
+
presets?: SectionPreset[];
|
|
155
|
+
/**
|
|
156
|
+
* Human/agent-facing prose describing when to use this section type and how to
|
|
157
|
+
* author it well. Surfaced by the get_section_schema tool as the JSON Schema
|
|
158
|
+
* top-level `description`. Presentation/metadata only — not validated.
|
|
159
|
+
*/
|
|
160
|
+
authoringNote?: string;
|
|
139
161
|
}
|
|
140
162
|
|
|
141
163
|
// --- defineSection ---
|
|
@@ -162,6 +184,8 @@ type DefineSectionInput<S extends ZodType> = {
|
|
|
162
184
|
* into a trailing 'Other' tab.
|
|
163
185
|
*/
|
|
164
186
|
settingsTabs?: { label: string; fields: string[] }[];
|
|
187
|
+
presets?: SectionPreset[];
|
|
188
|
+
authoringNote?: string;
|
|
165
189
|
};
|
|
166
190
|
|
|
167
191
|
export function defineSection<S extends ZodType>(
|