@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.
- package/dist/{chunk-BJ6FYGYP.js → chunk-24SUF2BC.js} +98 -9
- package/dist/{chunk-P3HO76OS.js → chunk-KDGYHU36.js} +6 -3
- package/dist/{chunk-5XYUO4HP.js → chunk-PUNXQK4M.js} +19 -2
- package/dist/components/editor/MoveSectionModal.d.ts +12 -0
- package/dist/components/editor/MoveSectionModal.d.ts.map +1 -0
- package/dist/components/editor/PagesModal.d.ts +18 -0
- package/dist/components/editor/PagesModal.d.ts.map +1 -0
- package/dist/components/editor/SectionWrapper.d.ts +1 -1
- package/dist/components/editor/SectionWrapper.d.ts.map +1 -1
- package/dist/components/editor/SettingsForm.d.ts.map +1 -1
- package/dist/components/primitives/LinkPopover.d.ts.map +1 -1
- package/dist/components/primitives/tiptap-presets.d.ts.map +1 -1
- package/dist/components/sections/Button/CTAButton.d.ts +3 -3
- package/dist/components/sections/Button/CTAButton.d.ts.map +1 -1
- package/dist/components/sections/Button/index.d.ts +10 -2
- package/dist/components/sections/Button/index.d.ts.map +1 -1
- package/dist/components/shared/Input.d.ts +1 -0
- package/dist/components/shared/Input.d.ts.map +1 -1
- package/dist/components/shared/LinkField.d.ts +9 -0
- package/dist/components/shared/LinkField.d.ts.map +1 -0
- package/dist/components/shared/Navigation.d.ts +4 -3
- package/dist/components/shared/Navigation.d.ts.map +1 -1
- package/dist/components/shared/PagesContext.d.ts +13 -0
- package/dist/components/shared/PagesContext.d.ts.map +1 -0
- package/dist/components/shared/RadioGroup.d.ts +15 -0
- package/dist/components/shared/RadioGroup.d.ts.map +1 -0
- package/dist/components/shell/EditorShell.d.ts.map +1 -1
- package/dist/components/shell/SiteSettingsDisplay.d.ts.map +1 -1
- package/dist/hooks/useBuildStatus.d.ts.map +1 -1
- package/dist/index.js +17 -3
- package/dist/lib/dexie.d.ts.map +1 -1
- package/dist/lib/dexie.js +16 -3
- package/dist/lib/events.d.ts +3 -1
- package/dist/lib/events.d.ts.map +1 -1
- package/dist/lib/index.js +2 -2
- package/dist/lib/links.d.ts +25 -0
- package/dist/lib/links.d.ts.map +1 -0
- package/dist/lib/loader.d.ts +2 -2
- package/dist/lib/loader.d.ts.map +1 -1
- package/dist/lib/nav.d.ts +23 -0
- package/dist/lib/nav.d.ts.map +1 -1
- package/dist/lib/pages.d.ts +31 -0
- package/dist/lib/pages.d.ts.map +1 -0
- package/dist/lib/registry.d.ts +7 -0
- package/dist/lib/registry.d.ts.map +1 -1
- package/dist/schemas/index.d.ts +1 -0
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +17 -3
- package/dist/schemas/link.d.ts +24 -0
- package/dist/schemas/link.d.ts.map +1 -0
- package/dist/schemas/site-config.d.ts +129 -3
- package/dist/schemas/site-config.d.ts.map +1 -1
- package/package.json +5 -1
- package/src/components/editor/MoveSectionModal.tsx +36 -0
- package/src/components/editor/PagesModal.tsx +400 -0
- package/src/components/editor/SectionWrapper.tsx +13 -0
- package/src/components/editor/SettingsForm.tsx +12 -0
- package/src/components/primitives/LinkPopover.tsx +99 -27
- package/src/components/primitives/tiptap-presets.ts +3 -1
- package/src/components/sections/Button/CTAButton.tsx +10 -11
- package/src/components/sections/Button/index.tsx +4 -9
- package/src/components/shared/Input.tsx +14 -3
- package/src/components/shared/LinkField.tsx +90 -0
- package/src/components/shared/Navigation.tsx +147 -137
- package/src/components/shared/PagesContext.tsx +12 -0
- package/src/components/shared/RadioGroup.tsx +71 -0
- package/src/components/shell/EditorShell.tsx +273 -78
- package/src/components/shell/SiteSettingsDisplay.tsx +65 -0
- package/src/hooks/useBuildStatus.ts +19 -4
- package/src/hooks/useEditorPublish.ts +1 -1
- package/src/lib/dexie.ts +18 -5
- package/src/lib/events.ts +3 -1
- package/src/lib/links.ts +108 -0
- package/src/lib/loader.ts +5 -4
- package/src/lib/nav.ts +59 -0
- package/src/lib/pages.ts +209 -0
- package/src/lib/registry.ts +8 -0
- package/src/schemas/index.ts +1 -0
- package/src/schemas/link.ts +17 -0
- 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
|
-
|
|
18
|
-
|
|
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
|
-
})
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
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
|
|