@drawnagency/primitives 0.1.49 → 0.1.51

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-BJ6FYGYP.js → chunk-24SUF2BC.js} +98 -9
  2. package/dist/{chunk-P3HO76OS.js → chunk-KDGYHU36.js} +6 -3
  3. package/dist/{chunk-5XYUO4HP.js → chunk-PUNXQK4M.js} +19 -2
  4. package/dist/components/editor/MoveSectionModal.d.ts +12 -0
  5. package/dist/components/editor/MoveSectionModal.d.ts.map +1 -0
  6. package/dist/components/editor/PagesModal.d.ts +18 -0
  7. package/dist/components/editor/PagesModal.d.ts.map +1 -0
  8. package/dist/components/editor/SectionWrapper.d.ts +1 -1
  9. package/dist/components/editor/SectionWrapper.d.ts.map +1 -1
  10. package/dist/components/editor/SettingsForm.d.ts.map +1 -1
  11. package/dist/components/primitives/LinkPopover.d.ts.map +1 -1
  12. package/dist/components/primitives/tiptap-presets.d.ts.map +1 -1
  13. package/dist/components/sections/Button/CTAButton.d.ts +3 -3
  14. package/dist/components/sections/Button/CTAButton.d.ts.map +1 -1
  15. package/dist/components/sections/Button/index.d.ts +10 -2
  16. package/dist/components/sections/Button/index.d.ts.map +1 -1
  17. package/dist/components/shared/Input.d.ts +1 -0
  18. package/dist/components/shared/Input.d.ts.map +1 -1
  19. package/dist/components/shared/LinkField.d.ts +9 -0
  20. package/dist/components/shared/LinkField.d.ts.map +1 -0
  21. package/dist/components/shared/Navigation.d.ts +4 -3
  22. package/dist/components/shared/Navigation.d.ts.map +1 -1
  23. package/dist/components/shared/PagesContext.d.ts +13 -0
  24. package/dist/components/shared/PagesContext.d.ts.map +1 -0
  25. package/dist/components/shared/RadioGroup.d.ts +15 -0
  26. package/dist/components/shared/RadioGroup.d.ts.map +1 -0
  27. package/dist/components/shell/EditorShell.d.ts.map +1 -1
  28. package/dist/components/shell/SiteSettingsDisplay.d.ts.map +1 -1
  29. package/dist/hooks/useBuildStatus.d.ts.map +1 -1
  30. package/dist/index.js +17 -3
  31. package/dist/lib/dexie.d.ts.map +1 -1
  32. package/dist/lib/dexie.js +16 -3
  33. package/dist/lib/events.d.ts +3 -1
  34. package/dist/lib/events.d.ts.map +1 -1
  35. package/dist/lib/index.js +2 -2
  36. package/dist/lib/links.d.ts +25 -0
  37. package/dist/lib/links.d.ts.map +1 -0
  38. package/dist/lib/loader.d.ts +2 -2
  39. package/dist/lib/loader.d.ts.map +1 -1
  40. package/dist/lib/nav.d.ts +23 -0
  41. package/dist/lib/nav.d.ts.map +1 -1
  42. package/dist/lib/pages.d.ts +31 -0
  43. package/dist/lib/pages.d.ts.map +1 -0
  44. package/dist/lib/registry.d.ts +7 -0
  45. package/dist/lib/registry.d.ts.map +1 -1
  46. package/dist/schemas/index.d.ts +1 -0
  47. package/dist/schemas/index.d.ts.map +1 -1
  48. package/dist/schemas/index.js +17 -3
  49. package/dist/schemas/link.d.ts +24 -0
  50. package/dist/schemas/link.d.ts.map +1 -0
  51. package/dist/schemas/site-config.d.ts +129 -3
  52. package/dist/schemas/site-config.d.ts.map +1 -1
  53. package/package.json +5 -1
  54. package/src/components/editor/MoveSectionModal.tsx +36 -0
  55. package/src/components/editor/PagesModal.tsx +400 -0
  56. package/src/components/editor/SectionWrapper.tsx +13 -0
  57. package/src/components/editor/SettingsForm.tsx +12 -0
  58. package/src/components/primitives/LinkPopover.tsx +99 -27
  59. package/src/components/primitives/tiptap-presets.ts +3 -1
  60. package/src/components/sections/Button/CTAButton.tsx +10 -11
  61. package/src/components/sections/Button/index.tsx +4 -9
  62. package/src/components/shared/Input.tsx +14 -3
  63. package/src/components/shared/LinkField.tsx +90 -0
  64. package/src/components/shared/Navigation.tsx +147 -137
  65. package/src/components/shared/PagesContext.tsx +12 -0
  66. package/src/components/shared/RadioGroup.tsx +71 -0
  67. package/src/components/shell/EditorShell.tsx +273 -78
  68. package/src/components/shell/SiteSettingsDisplay.tsx +65 -0
  69. package/src/hooks/useBuildStatus.ts +19 -4
  70. package/src/hooks/useEditorPublish.ts +1 -1
  71. package/src/lib/dexie.ts +18 -5
  72. package/src/lib/events.ts +3 -1
  73. package/src/lib/links.ts +108 -0
  74. package/src/lib/loader.ts +5 -4
  75. package/src/lib/nav.ts +59 -0
  76. package/src/lib/pages.ts +209 -0
  77. package/src/lib/registry.ts +8 -0
  78. package/src/schemas/index.ts +1 -0
  79. package/src/schemas/link.ts +17 -0
  80. package/src/schemas/site-config.ts +119 -11
@@ -0,0 +1,17 @@
1
+ import { z } from "zod";
2
+
3
+ export const LinkTargetSchema = z.enum(["_self", "_blank"]);
4
+
5
+ export const LinkValueSchema = z.discriminatedUnion("kind", [
6
+ z.object({ kind: z.literal("external"), href: z.string(), target: LinkTargetSchema }),
7
+ z.object({
8
+ kind: z.literal("internal"),
9
+ pageId: z.string(),
10
+ anchorSectionId: z.string().nullable().optional(),
11
+ target: LinkTargetSchema,
12
+ }),
13
+ ]);
14
+
15
+ export type LinkValue = z.infer<typeof LinkValueSchema>;
16
+
17
+ export const DEFAULT_LINK: LinkValue = { kind: "external", href: "", target: "_self" };
@@ -14,21 +14,123 @@ export const SectionMetaSchema = z.object({
14
14
 
15
15
  export type SectionMeta = z.infer<typeof SectionMetaSchema>;
16
16
 
17
- export const IndexSchema = z.object({
18
- siteId: z.string(),
17
+ // Page slugs that would shadow a platform route. KEEP IN SYNC with the root and
18
+ // /edit/* route patterns injected in packages/core/src/integration.ts — if a new
19
+ // root-level route is added there, add its first segment here (the test in Step 1
20
+ // guards the known set). This is the single source of truth for reserved slugs;
21
+ // the schema, uniquePageSlug, and /api/save all import it.
22
+ export const RESERVED_SLUGS = ["edit", "api", "login", "set-password", "404"] as const;
23
+
24
+ export const PageStatusSchema = z.enum(["live", "archived"]);
25
+
26
+ export const PageSchema = z.object({
27
+ id: z.string().min(1),
28
+ title: z.string(),
29
+ slug: z.string(),
30
+ isHome: z.boolean(),
31
+ showInNav: z.boolean(),
32
+ status: PageStatusSchema,
33
+ access: z.array(z.string()),
19
34
  order: z.array(z.string()),
35
+ });
36
+
37
+ export type Page = z.infer<typeof PageSchema>;
38
+
39
+ const SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
40
+
41
+ // Raw input: accepts legacy (order[]) OR canonical (pages[]).
42
+ const RawIndexSchema = z.object({
43
+ siteId: z.string(),
44
+ order: z.array(z.string()).optional(),
45
+ pages: z.array(PageSchema).optional(),
20
46
  sections: z.record(z.string(), SectionMetaSchema),
21
47
  lastModified: z.string().nullable().optional(),
22
- }).refine(
23
- (data) => {
24
- const orderSet = new Set(data.order);
25
- return data.order.every((id) => id in data.sections) &&
26
- Object.keys(data.sections).every((key) => orderSet.has(key));
27
- },
28
- { message: "Every id in order must have a sections entry and vice versa" }
29
- );
48
+ });
49
+
50
+ function normalizeRawIndex(data: z.infer<typeof RawIndexSchema>): z.input<typeof CanonicalIndexSchema> {
51
+ if (data.pages && data.pages.length > 0) {
52
+ return {
53
+ siteId: data.siteId,
54
+ pages: data.pages,
55
+ sections: data.sections,
56
+ lastModified: data.lastModified ?? null,
57
+ };
58
+ }
59
+ const order = data.order ?? Object.keys(data.sections);
60
+ return {
61
+ siteId: data.siteId,
62
+ pages: [
63
+ {
64
+ id: "home",
65
+ title: "Home",
66
+ slug: "",
67
+ isHome: true,
68
+ showInNav: true,
69
+ status: "live" as const,
70
+ access: [],
71
+ order,
72
+ },
73
+ ],
74
+ sections: data.sections,
75
+ lastModified: data.lastModified ?? null,
76
+ };
77
+ }
78
+
79
+ const CanonicalIndexSchema = z
80
+ .object({
81
+ siteId: z.string(),
82
+ pages: z.array(PageSchema),
83
+ sections: z.record(z.string(), SectionMetaSchema),
84
+ lastModified: z.string().nullable().optional(),
85
+ })
86
+ .superRefine((data, ctx) => {
87
+ const homes = data.pages.filter((p) => p.isHome);
88
+ if (homes.length !== 1) {
89
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Exactly one page must be the home page" });
90
+ }
91
+ if (homes[0]?.status === "archived") {
92
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "The home page cannot be archived" });
93
+ }
94
+ if (homes[0] && homes[0].slug !== "") {
95
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "The home page slug must be empty" });
96
+ }
97
+
98
+ const seenSlugs = new Set<string>();
99
+ for (const p of data.pages) {
100
+ if (!p.isHome) {
101
+ if (!SLUG_RE.test(p.slug)) {
102
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Invalid slug "${p.slug}"` });
103
+ }
104
+ if ((RESERVED_SLUGS as readonly string[]).includes(p.slug)) {
105
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Slug "${p.slug}" is reserved` });
106
+ }
107
+ }
108
+ if (seenSlugs.has(p.slug)) {
109
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Duplicate slug "${p.slug}"` });
110
+ }
111
+ seenSlugs.add(p.slug);
112
+ }
113
+
114
+ const fromPages: string[] = [];
115
+ for (const p of data.pages) fromPages.push(...p.order);
116
+ const fromPagesSet = new Set(fromPages);
117
+ if (fromPages.length !== fromPagesSet.size) {
118
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "A section id appears in more than one page" });
119
+ }
120
+ const sectionKeys = Object.keys(data.sections);
121
+ if (fromPagesSet.size !== sectionKeys.length || !sectionKeys.every((k) => fromPagesSet.has(k))) {
122
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Page section orders must partition sections{} exactly" });
123
+ }
124
+ });
30
125
 
31
- export type SiteIndex = z.infer<typeof IndexSchema>;
126
+ export const IndexSchema = RawIndexSchema.transform(normalizeRawIndex).pipe(CanonicalIndexSchema);
127
+
128
+ export type SiteIndex = z.infer<typeof CanonicalIndexSchema>;
129
+
130
+ /** Parse + normalize any raw index (legacy or canonical) to the canonical shape. */
131
+ export function normalizeSiteIndex(raw: unknown): SiteIndex {
132
+ return IndexSchema.parse(raw);
133
+ }
32
134
 
33
135
  export const SiteConfigSchema = z.object({
34
136
  siteName: z.string().default("Brand Portal"),
@@ -44,6 +146,12 @@ export const SiteConfigSchema = z.object({
44
146
  .refine(url => url.startsWith("https://fonts.googleapis.com/"), "Must be a Google Fonts URL")
45
147
  .nullable()
46
148
  .default(null),
149
+ // Favicon stored as a data URL (SVG/PNG, small) so it travels with
150
+ // site-config.json — no media manifest or CDN round-trip needed.
151
+ favicon: z.string()
152
+ .refine((v) => v.startsWith("data:image/"), "Favicon must be an image data URL")
153
+ .nullable()
154
+ .default(null),
47
155
  media: MediaConfigSchema.default(MediaConfigSchema.parse({})),
48
156
  });
49
157