@agntcms/next 0.2.0

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/index.mjs ADDED
@@ -0,0 +1,319 @@
1
+ // src/domain/fields.ts
2
+ var TextField = { kind: "text" };
3
+ var RichTextField = { kind: "richText" };
4
+ var ImageField = { kind: "image" };
5
+ var VideoField = { kind: "video" };
6
+ var ReferenceField = { kind: "reference" };
7
+ var LinkField = { kind: "link" };
8
+ var NumberField = { kind: "number" };
9
+ var BooleanField = { kind: "boolean" };
10
+ var SelectField = (options, opts) => ({
11
+ kind: "select",
12
+ options,
13
+ ...opts?.default !== void 0 ? { default: opts.default } : {}
14
+ });
15
+ var ButtonField = (variants, opts) => ({
16
+ kind: "button",
17
+ variants,
18
+ ...opts?.default !== void 0 ? { default: opts.default } : {}
19
+ });
20
+ var ListField = (itemSchema, opts) => ({
21
+ kind: "list",
22
+ itemSchema,
23
+ ...opts?.min !== void 0 ? { min: opts.min } : {},
24
+ ...opts?.max !== void 0 ? { max: opts.max } : {},
25
+ ...opts?.default !== void 0 ? { default: opts.default } : {}
26
+ });
27
+
28
+ // src/domain/invariants.ts
29
+ function hasUniqueSectionIds(page) {
30
+ const seen = /* @__PURE__ */ new Set();
31
+ for (const section of page.sections) {
32
+ if (seen.has(section.id)) return false;
33
+ seen.add(section.id);
34
+ }
35
+ return true;
36
+ }
37
+
38
+ // src/domain/linkValidation.ts
39
+ var SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-/]*[a-z0-9])?$/;
40
+ var validateInternalSlug = (slug) => {
41
+ if (slug === "") return null;
42
+ if (/^https?:\/\//i.test(slug)) return "slug must not include a protocol";
43
+ if (slug.startsWith("//")) return "slug must not start with //";
44
+ if (slug.startsWith("/")) return "slug must not start with /";
45
+ if (slug.endsWith("/")) return "slug must not end with /";
46
+ if (slug.includes("//")) return "slug must not contain //";
47
+ if (slug.includes("..")) return "slug must not contain ..";
48
+ if (!SLUG_PATTERN.test(slug)) {
49
+ return "slug must use lowercase letters, digits, hyphens, and slashes only";
50
+ }
51
+ return null;
52
+ };
53
+ var validateExternalUrl = (url) => {
54
+ if (url === "") return null;
55
+ if (typeof url !== "string") return "URL must be a string";
56
+ if (!/^https?:\/\//i.test(url)) {
57
+ return "External link must start with http:// or https://";
58
+ }
59
+ try {
60
+ const parsed = new globalThis.URL(url);
61
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
62
+ return "External link must use http or https";
63
+ }
64
+ } catch {
65
+ return "External link is not a valid URL";
66
+ }
67
+ return null;
68
+ };
69
+ var EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
70
+ var validateEmail = (email) => {
71
+ if (email === "") return null;
72
+ if (typeof email !== "string") return "Invalid email address";
73
+ if (!EMAIL_PATTERN.test(email)) return "Invalid email address";
74
+ return null;
75
+ };
76
+ var validatePhone = (phone) => {
77
+ if (phone === "") return null;
78
+ if (typeof phone !== "string") return "Phone number is too short";
79
+ const digits = phone.replace(/[^\d+]/g, "").replace(/\+/g, "");
80
+ if (digits.length < 7) return "Phone number is too short";
81
+ return null;
82
+ };
83
+
84
+ // src/domain/link.ts
85
+ function hrefOf(link) {
86
+ if (link.type === "external") return link.url;
87
+ if (link.type === "email") {
88
+ if (link.email === "") return "";
89
+ return "mailto:" + link.email;
90
+ }
91
+ if (link.type === "phone") {
92
+ if (link.phone === "") return "";
93
+ return "tel:" + link.phone.replace(/[^\d+]/g, "");
94
+ }
95
+ const slug = link.slug.startsWith("/") ? link.slug.slice(1) : link.slug;
96
+ if (slug === "" || slug === "home") return "/";
97
+ return "/" + slug;
98
+ }
99
+ function isExternalLink(link) {
100
+ return link.type === "external";
101
+ }
102
+ function linkAnchorAttrs(link) {
103
+ const href = hrefOf(link);
104
+ if (link.type === "external") {
105
+ return { href, target: "_blank", rel: "noreferrer" };
106
+ }
107
+ return { href };
108
+ }
109
+ function normalizeLinkValue(raw) {
110
+ if (raw === null || typeof raw !== "object") {
111
+ return { type: "internal", slug: "", label: "" };
112
+ }
113
+ const obj = raw;
114
+ const label = typeof obj["label"] === "string" ? obj["label"] : "";
115
+ if (obj["type"] === "internal") {
116
+ const slug = typeof obj["slug"] === "string" ? obj["slug"] : "";
117
+ return { type: "internal", slug, label };
118
+ }
119
+ if (obj["type"] === "external") {
120
+ const url = typeof obj["url"] === "string" ? obj["url"] : "";
121
+ return { type: "external", url, label };
122
+ }
123
+ if (obj["type"] === "email") {
124
+ const email = typeof obj["email"] === "string" ? obj["email"] : "";
125
+ return { type: "email", email, label };
126
+ }
127
+ if (obj["type"] === "phone") {
128
+ const phone = typeof obj["phone"] === "string" ? obj["phone"] : "";
129
+ return { type: "phone", phone, label };
130
+ }
131
+ if (typeof obj["href"] === "string") {
132
+ const href = obj["href"];
133
+ if (/^mailto:/i.test(href)) {
134
+ return { type: "email", email: href.slice("mailto:".length), label };
135
+ }
136
+ if (/^tel:/i.test(href)) {
137
+ return { type: "phone", phone: href.slice("tel:".length), label };
138
+ }
139
+ if (/^https?:\/\//i.test(href)) {
140
+ return { type: "external", url: href, label };
141
+ }
142
+ const slug = href.startsWith("/") ? href.slice(1) : href;
143
+ return { type: "internal", slug, label };
144
+ }
145
+ return { type: "internal", slug: "", label: "" };
146
+ }
147
+
148
+ // src/domain/form.ts
149
+ var FORM_FORBIDDEN_KINDS = /* @__PURE__ */ new Set([
150
+ "image",
151
+ "video",
152
+ "reference",
153
+ "list",
154
+ // `formOverrides` is a section-only descriptor (it overrides another
155
+ // form schema instance). Putting it inside a form would mean a form's
156
+ // payload could carry overrides for itself or another form — a
157
+ // recursive shape with no ergonomic editor UI. Section-only by design.
158
+ "formOverrides",
159
+ // `button` is a section-only descriptor: a styled CTA with an
160
+ // optional link. Public-form payloads collect user input — a button
161
+ // value is authored content, not a submitted answer. Section-only
162
+ // by design (mirrors `formOverrides`).
163
+ "button"
164
+ ]);
165
+ var SubmissionsNotReadableError = class extends Error {
166
+ constructor(message = "submission adapter does not support reading") {
167
+ super(message);
168
+ this.name = "SubmissionsNotReadableError";
169
+ }
170
+ };
171
+
172
+ // src/domain/formOverrides.ts
173
+ var FormOverridesField = (formName, opts) => ({
174
+ kind: "formOverrides",
175
+ formName,
176
+ ...opts?.default !== void 0 ? { default: opts.default } : {}
177
+ });
178
+
179
+ // src/sections/defineSection.ts
180
+ function builtInDefault(fieldName, descriptor) {
181
+ switch (descriptor.kind) {
182
+ case "image":
183
+ return { filename: "placeholder.png", alt: "Placeholder image" };
184
+ case "video":
185
+ return { url: "" };
186
+ case "richText":
187
+ return "Start writing here...";
188
+ case "text":
189
+ return fieldName.charAt(0).toUpperCase() + fieldName.slice(1).replace(/([A-Z])/g, " $1");
190
+ case "reference":
191
+ return { slug: "" };
192
+ case "link":
193
+ return { type: "internal", slug: "", label: "Learn more" };
194
+ case "button": {
195
+ const firstVariant = descriptor.variants[0]?.value ?? "";
196
+ return { label: "Get started", variant: firstVariant };
197
+ }
198
+ case "number":
199
+ return 0;
200
+ case "boolean":
201
+ return false;
202
+ case "select":
203
+ return descriptor.options[0]?.value ?? "";
204
+ case "list":
205
+ return [];
206
+ case "formOverrides":
207
+ return {};
208
+ default: {
209
+ const _exhaustive = descriptor;
210
+ void _exhaustive;
211
+ return "";
212
+ }
213
+ }
214
+ }
215
+ function defineSection(input) {
216
+ const defaults = {};
217
+ for (const [key, descriptor] of Object.entries(input.schema)) {
218
+ defaults[key] = descriptor.default ?? builtInDefault(key, descriptor);
219
+ }
220
+ return {
221
+ name: input.name,
222
+ ...input.category !== void 0 ? { category: input.category } : {},
223
+ schema: input.schema,
224
+ component: input.component,
225
+ defaults,
226
+ // Conditional spread mirrors the `category` pattern — required so
227
+ // `exactOptionalPropertyTypes` does not see `layouts: undefined` being
228
+ // assigned to an optional-only property.
229
+ ...input.layouts !== void 0 ? { layouts: input.layouts } : {}
230
+ };
231
+ }
232
+
233
+ // src/forms/defineForm.ts
234
+ var InvalidFormFieldError = class extends Error {
235
+ formName;
236
+ fieldName;
237
+ fieldKind;
238
+ constructor(formName, fieldName, fieldKind) {
239
+ super(
240
+ `Form "${formName}": field "${fieldName}" uses kind "${fieldKind}", which is not allowed in a form schema in v1. Allowed kinds: text, richText, number, boolean, select, link. Forbidden kinds: image, reference, list, formOverrides. See ARCHITECTURE.md \xA76.5.`
241
+ );
242
+ this.name = "InvalidFormFieldError";
243
+ this.formName = formName;
244
+ this.fieldName = fieldName;
245
+ this.fieldKind = fieldKind;
246
+ }
247
+ };
248
+ var HoneypotCollisionError = class extends Error {
249
+ formName;
250
+ fieldName;
251
+ reason;
252
+ constructor(formName, fieldName, reason = "collision") {
253
+ super(
254
+ reason === "empty" ? `Form "${formName}": honeypot name must be a non-empty string.` : `Form "${formName}": honeypot name "${fieldName}" collides with a real field. Choose a honeypot name that does not appear in the schema.`
255
+ );
256
+ this.name = "HoneypotCollisionError";
257
+ this.formName = formName;
258
+ this.fieldName = fieldName;
259
+ this.reason = reason;
260
+ }
261
+ };
262
+ var InvalidFormNameError = class extends Error {
263
+ constructor(name) {
264
+ super(
265
+ `Invalid form name ${JSON.stringify(name)}. Use letters, digits, hyphen, and underscore only (no path separators).`
266
+ );
267
+ this.name = "InvalidFormNameError";
268
+ }
269
+ };
270
+ var FORM_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
271
+ function defineForm(input) {
272
+ if (typeof input.name !== "string" || !FORM_NAME_PATTERN.test(input.name)) {
273
+ throw new InvalidFormNameError(input.name);
274
+ }
275
+ for (const [fieldName, descriptor] of Object.entries(input.schema)) {
276
+ const kind = descriptor.kind;
277
+ if (FORM_FORBIDDEN_KINDS.has(kind)) {
278
+ throw new InvalidFormFieldError(input.name, fieldName, kind);
279
+ }
280
+ }
281
+ if (input.honeypot !== void 0) {
282
+ if (typeof input.honeypot !== "string" || input.honeypot === "") {
283
+ throw new HoneypotCollisionError(input.name, input.honeypot, "empty");
284
+ }
285
+ if (Object.prototype.hasOwnProperty.call(input.schema, input.honeypot)) {
286
+ throw new HoneypotCollisionError(input.name, input.honeypot, "collision");
287
+ }
288
+ }
289
+ return input.honeypot !== void 0 ? { name: input.name, schema: input.schema, honeypot: input.honeypot } : { name: input.name, schema: input.schema };
290
+ }
291
+ export {
292
+ BooleanField,
293
+ ButtonField,
294
+ FormOverridesField,
295
+ HoneypotCollisionError,
296
+ ImageField,
297
+ InvalidFormFieldError,
298
+ InvalidFormNameError,
299
+ LinkField,
300
+ ListField,
301
+ NumberField,
302
+ ReferenceField,
303
+ RichTextField,
304
+ SelectField,
305
+ SubmissionsNotReadableError,
306
+ TextField,
307
+ VideoField,
308
+ defineForm,
309
+ defineSection,
310
+ hasUniqueSectionIds,
311
+ hrefOf,
312
+ isExternalLink,
313
+ linkAnchorAttrs,
314
+ normalizeLinkValue,
315
+ validateEmail,
316
+ validateExternalUrl,
317
+ validateInternalSlug,
318
+ validatePhone
319
+ };