@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.
Files changed (60) hide show
  1. package/dist/chunk-4VMAZZXT.js +968 -0
  2. package/dist/{chunk-VEIDANEU.js → chunk-KM6HOW4H.js} +126 -39
  3. package/dist/{chunk-DQTKWKSK.js → chunk-WJ7RZ6CC.js} +11 -2
  4. package/dist/components/primitives/defineHeadingSection.d.ts +2 -0
  5. package/dist/components/primitives/defineHeadingSection.d.ts.map +1 -1
  6. package/dist/components/primitives/index.js +1 -1
  7. package/dist/components/sections/Button/index.d.ts.map +1 -1
  8. package/dist/components/sections/Colors/index.d.ts.map +1 -1
  9. package/dist/components/sections/Colors/schema.d.ts.map +1 -1
  10. package/dist/components/sections/Container/index.d.ts.map +1 -1
  11. package/dist/components/sections/DoDontMedia/index.d.ts.map +1 -1
  12. package/dist/components/sections/IconList/IconList.d.ts +2 -1
  13. package/dist/components/sections/IconList/IconList.d.ts.map +1 -1
  14. package/dist/components/sections/IconList/IconListSettings.d.ts +2 -1
  15. package/dist/components/sections/IconList/IconListSettings.d.ts.map +1 -1
  16. package/dist/components/sections/IconList/index.d.ts +1 -0
  17. package/dist/components/sections/IconList/index.d.ts.map +1 -1
  18. package/dist/components/sections/LinkHeading/index.d.ts.map +1 -1
  19. package/dist/components/sections/Media/index.d.ts.map +1 -1
  20. package/dist/components/sections/Prose/index.d.ts.map +1 -1
  21. package/dist/components/sections/Spacer/index.d.ts.map +1 -1
  22. package/dist/components/sections/SubHeading/index.d.ts.map +1 -1
  23. package/dist/components/sections/SubSubHeading/index.d.ts.map +1 -1
  24. package/dist/components/sections/all-sections.d.ts +1 -0
  25. package/dist/components/sections/all-sections.d.ts.map +1 -1
  26. package/dist/components/sections/brand-guide.d.ts +1 -0
  27. package/dist/components/sections/brand-guide.d.ts.map +1 -1
  28. package/dist/components/sections/brand-guide.js +1 -1
  29. package/dist/components/sections/register-schemas.js +11 -941
  30. package/dist/components/sections/register.js +36 -0
  31. package/dist/components/shell/EditorShell.d.ts.map +1 -1
  32. package/dist/index.js +20 -20
  33. package/dist/lib/index.js +2 -2
  34. package/dist/lib/insert-menu.d.ts +19 -0
  35. package/dist/lib/insert-menu.d.ts.map +1 -0
  36. package/dist/lib/registry.d.ts +23 -0
  37. package/dist/lib/registry.d.ts.map +1 -1
  38. package/dist/types/database.d.ts +248 -34
  39. package/dist/types/database.d.ts.map +1 -1
  40. package/package.json +5 -1
  41. package/src/components/primitives/defineHeadingSection.tsx +18 -2
  42. package/src/components/sections/Button/index.tsx +10 -3
  43. package/src/components/sections/Colors/index.tsx +15 -7
  44. package/src/components/sections/Colors/schema.ts +5 -2
  45. package/src/components/sections/Container/index.tsx +19 -3
  46. package/src/components/sections/DoDontMedia/index.tsx +18 -7
  47. package/src/components/sections/IconList/IconList.tsx +43 -19
  48. package/src/components/sections/IconList/IconListSettings.tsx +58 -11
  49. package/src/components/sections/IconList/index.tsx +51 -7
  50. package/src/components/sections/LinkHeading/index.tsx +2 -0
  51. package/src/components/sections/Media/index.tsx +15 -6
  52. package/src/components/sections/Prose/index.tsx +9 -1
  53. package/src/components/sections/Spacer/index.tsx +2 -0
  54. package/src/components/sections/SubHeading/index.tsx +2 -0
  55. package/src/components/sections/SubSubHeading/index.tsx +2 -0
  56. package/src/components/shell/EditorShell.tsx +13 -19
  57. package/src/lib/insert-menu.ts +42 -0
  58. package/src/lib/registry.ts +24 -0
  59. package/src/types/database.ts +236 -36
  60. 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.array(IconListItemSchema),
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.object({
22
- icon: z.string().nullable().optional(),
23
- showLabel: z.boolean().optional(),
24
- stackText: z.boolean().optional(),
25
- }).optional(),
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
- link: LinkValueSchema.optional(),
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.enum(["cover", "contain"]).optional(),
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({ body: z.string() }),
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, type: string) => {
595
- const definition = getSection(type);
596
- if (!definition || !resolvedActivePage) return;
597
- const id = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
598
- ? crypto.randomUUID() : `_id_${Date.now()}_${Math.random().toString(36).slice(2)}`;
599
- const content = definition.defaults() as object;
600
- const meta = { type, status: "draft" as const, access: [] as string[] };
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, type: string) => void;
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
+ }
@@ -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>(