@orion-studios/cms 0.5.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.
@@ -0,0 +1,527 @@
1
+ 'use client';
2
+
3
+ // src/forms/react.tsx
4
+ import { useEffect, useMemo, useRef, useState } from "react";
5
+
6
+ // src/forms/validation.ts
7
+ var MAJOR_EMAIL_PROVIDERS = [
8
+ "gmail.com",
9
+ "yahoo.com",
10
+ "outlook.com",
11
+ "hotmail.com",
12
+ "icloud.com",
13
+ "live.com",
14
+ "protonmail.com"
15
+ ];
16
+ var KNOWN_GOOD_DOMAINS = /* @__PURE__ */ new Set([
17
+ ...MAJOR_EMAIL_PROVIDERS,
18
+ "aol.com",
19
+ "gmx.com",
20
+ "googlemail.com",
21
+ "hey.com",
22
+ "mac.com",
23
+ "mail.com",
24
+ "me.com",
25
+ "msn.com",
26
+ "pm.me",
27
+ "proton.me",
28
+ "ymail.com",
29
+ "zoho.com"
30
+ ]);
31
+ var EXPLICIT_TYPO_MAP = {
32
+ "gamil.com": "gmail.com",
33
+ "gmial.com": "gmail.com",
34
+ "gmai.com": "gmail.com",
35
+ "gmaill.com": "gmail.com",
36
+ "gmail.co": "gmail.com",
37
+ "gmail.con": "gmail.com",
38
+ "gmail.cmo": "gmail.com",
39
+ "hotmial.com": "hotmail.com",
40
+ "hotmall.com": "hotmail.com",
41
+ "hotmail.co": "hotmail.com",
42
+ "hotmail.con": "hotmail.com",
43
+ "iclod.com": "icloud.com",
44
+ "icloud.co": "icloud.com",
45
+ "icoud.com": "icloud.com",
46
+ "outlok.com": "outlook.com",
47
+ "outloook.com": "outlook.com",
48
+ "outlook.co": "outlook.com",
49
+ "outlook.con": "outlook.com",
50
+ "protonmail.co": "protonmail.com",
51
+ "yaho.com": "yahoo.com",
52
+ "yahooo.com": "yahoo.com",
53
+ "yahoo.co": "yahoo.com",
54
+ "yahoo.con": "yahoo.com"
55
+ };
56
+ var EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/;
57
+ function levenshtein(a, b) {
58
+ if (a === b) return 0;
59
+ const rows = a.length + 1;
60
+ const cols = b.length + 1;
61
+ const dist = Array.from({ length: cols }, (_, j) => j);
62
+ for (let i = 1; i < rows; i += 1) {
63
+ let prevDiagonal = dist[0];
64
+ dist[0] = i;
65
+ for (let j = 1; j < cols; j += 1) {
66
+ const temp = dist[j];
67
+ dist[j] = Math.min(
68
+ dist[j] + 1,
69
+ dist[j - 1] + 1,
70
+ prevDiagonal + (a[i - 1] === b[j - 1] ? 0 : 1)
71
+ );
72
+ prevDiagonal = temp;
73
+ }
74
+ }
75
+ return dist[cols - 1];
76
+ }
77
+ function normalizeEmail(value) {
78
+ return value.trim().toLowerCase();
79
+ }
80
+ function validateEmail(value, options = {}) {
81
+ const normalized = normalizeEmail(value);
82
+ if (normalized.length === 0) {
83
+ return { valid: false, normalized, message: "Email is required." };
84
+ }
85
+ if (!EMAIL_PATTERN.test(normalized)) {
86
+ return { valid: false, normalized, message: "Enter a valid email address." };
87
+ }
88
+ const [localPart, domain] = normalized.split("@");
89
+ const knownGood = options.knownGoodDomains ? /* @__PURE__ */ new Set([...KNOWN_GOOD_DOMAINS, ...options.knownGoodDomains.map((entry) => entry.toLowerCase())]) : KNOWN_GOOD_DOMAINS;
90
+ if (knownGood.has(domain)) {
91
+ return { valid: true, normalized };
92
+ }
93
+ const explicitCorrection = EXPLICIT_TYPO_MAP[domain];
94
+ if (explicitCorrection) {
95
+ const suggestion = `${localPart}@${explicitCorrection}`;
96
+ return {
97
+ valid: false,
98
+ normalized,
99
+ message: `Did you mean ${suggestion}?`,
100
+ suggestion
101
+ };
102
+ }
103
+ for (const provider of MAJOR_EMAIL_PROVIDERS) {
104
+ if (levenshtein(domain, provider) === 1) {
105
+ const suggestion = `${localPart}@${provider}`;
106
+ return {
107
+ valid: false,
108
+ normalized,
109
+ message: `Did you mean ${suggestion}?`,
110
+ suggestion
111
+ };
112
+ }
113
+ }
114
+ return { valid: true, normalized };
115
+ }
116
+ function normalizePhone(value) {
117
+ const digits = value.replace(/\D+/g, "");
118
+ if (digits.length === 11 && digits.startsWith("1")) {
119
+ return digits.slice(1);
120
+ }
121
+ return digits;
122
+ }
123
+ function formatPhoneUS(value) {
124
+ const digits = normalizePhone(value).slice(0, 10);
125
+ if (digits.length === 0) return "";
126
+ if (digits.length < 4) return `(${digits}`;
127
+ if (digits.length < 7) return `(${digits.slice(0, 3)}) ${digits.slice(3)}`;
128
+ return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`;
129
+ }
130
+ function validatePhoneUS(value) {
131
+ const digits = normalizePhone(value);
132
+ if (digits.length === 0) {
133
+ return { valid: false, normalized: "", message: "Phone number is required." };
134
+ }
135
+ if (digits.length !== 10) {
136
+ return { valid: false, normalized: digits, message: "Enter a 10-digit phone number." };
137
+ }
138
+ return { valid: true, normalized: digits };
139
+ }
140
+ function normalizeUrl(value) {
141
+ const trimmed = value.trim();
142
+ if (trimmed.length === 0) return "";
143
+ if (/^https?:\/\//i.test(trimmed)) return trimmed;
144
+ return `https://${trimmed}`;
145
+ }
146
+ function validateUrl(value) {
147
+ const normalized = normalizeUrl(value);
148
+ if (normalized.length === 0) {
149
+ return { valid: false, normalized, message: "URL is required." };
150
+ }
151
+ try {
152
+ const parsed = new URL(normalized);
153
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
154
+ return { valid: false, normalized, message: "Enter a valid web address." };
155
+ }
156
+ if (!parsed.hostname.includes(".")) {
157
+ return { valid: false, normalized, message: "Enter a valid web address." };
158
+ }
159
+ return { valid: true, normalized };
160
+ } catch {
161
+ return { valid: false, normalized, message: "Enter a valid web address." };
162
+ }
163
+ }
164
+ var FORM_FIELD_TYPES = [
165
+ "text",
166
+ "textarea",
167
+ "email",
168
+ "phone",
169
+ "url",
170
+ "select",
171
+ "radio",
172
+ "checkbox",
173
+ "date",
174
+ "number",
175
+ "hidden"
176
+ ];
177
+ function validateDate(value) {
178
+ const normalized = value.trim();
179
+ if (normalized.length === 0) {
180
+ return { valid: false, normalized, message: "Date is required." };
181
+ }
182
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(normalized) || Number.isNaN(Date.parse(normalized))) {
183
+ return { valid: false, normalized, message: "Enter a valid date." };
184
+ }
185
+ return { valid: true, normalized };
186
+ }
187
+ function validateNumber(value) {
188
+ const normalized = value.trim().replace(/,/g, "");
189
+ if (normalized.length === 0) {
190
+ return { valid: false, normalized, message: "A number is required." };
191
+ }
192
+ const parsed = Number(normalized);
193
+ if (!Number.isFinite(parsed)) {
194
+ return { valid: false, normalized, message: "Enter a valid number." };
195
+ }
196
+ return { valid: true, normalized: String(parsed) };
197
+ }
198
+ function inferFieldType(field) {
199
+ const type = typeof field.type === "string" ? field.type.toLowerCase() : "";
200
+ if (type === "tel") return "phone";
201
+ if (FORM_FIELD_TYPES.includes(type)) return type;
202
+ const name = typeof field.name === "string" ? field.name.toLowerCase() : "";
203
+ if (name.includes("email")) return "email";
204
+ if (name.includes("phone") || name.includes("mobile")) return "phone";
205
+ if (name.includes("website") || name.endsWith("url")) return "url";
206
+ return "text";
207
+ }
208
+ function fieldOptions(field) {
209
+ if (!Array.isArray(field.options)) return [];
210
+ return field.options.map((option) => {
211
+ if (typeof option === "string") return { label: option, value: option };
212
+ if (option && typeof option === "object") {
213
+ const record = option;
214
+ const value = typeof record.value === "string" ? record.value : "";
215
+ const label = typeof record.label === "string" && record.label ? record.label : value;
216
+ return value ? { label, value } : null;
217
+ }
218
+ return null;
219
+ }).filter((option) => option !== null);
220
+ }
221
+
222
+ // src/forms/submission.ts
223
+ var HONEYPOT_FIELD_NAME = "website_url_confirm";
224
+
225
+ // src/forms/react.tsx
226
+ import { jsx, jsxs } from "react/jsx-runtime";
227
+ var DEFAULT_CLASSES = {
228
+ form: "ocf-form",
229
+ field: "ocf-field",
230
+ fieldError: "has-error",
231
+ errorText: "ocf-error",
232
+ label: "ocf-label",
233
+ input: "ocf-input",
234
+ textarea: "ocf-input ocf-textarea",
235
+ select: "ocf-input",
236
+ checkbox: "ocf-checkbox",
237
+ radioGroup: "ocf-radio-group",
238
+ button: "ocf-button",
239
+ success: "ocf-success",
240
+ formError: "ocf-form-error",
241
+ stepTitle: "ocf-step-title"
242
+ };
243
+ var fieldKey = (field, index) => field.name || `field_${index}`;
244
+ function FormRenderer({
245
+ slug,
246
+ config,
247
+ successMessage,
248
+ basePath = "/api/cms",
249
+ classNames,
250
+ title,
251
+ intro,
252
+ submitLabel = "Send",
253
+ preview = false,
254
+ onSuccess
255
+ }) {
256
+ const cls = { ...DEFAULT_CLASSES, ...classNames };
257
+ const steps = useMemo(
258
+ () => (config.steps || []).filter((step) => (step.fields || []).length > 0),
259
+ [config]
260
+ );
261
+ const allFields = useMemo(() => steps.flatMap((step) => step.fields || []), [steps]);
262
+ const [values, setValues] = useState({});
263
+ const [errors, setErrors] = useState({});
264
+ const [stepIndex, setStepIndex] = useState(0);
265
+ const [state, setState] = useState("idle");
266
+ const [honeypot, setHoneypot] = useState("");
267
+ const renderedAt = useRef(Date.now());
268
+ const startedRef = useRef(false);
269
+ const funnel = (stage) => {
270
+ if (preview || typeof window === "undefined") return;
271
+ window.__orionTrack?.(
272
+ "form",
273
+ `${slug}:${stage}`
274
+ );
275
+ };
276
+ useEffect(() => {
277
+ const timer = setTimeout(() => funnel("view"), 100);
278
+ return () => clearTimeout(timer);
279
+ }, [slug]);
280
+ useEffect(() => {
281
+ setStepIndex((current) => Math.min(current, Math.max(steps.length - 1, 0)));
282
+ }, [steps.length]);
283
+ const setValue = (name, next, type) => {
284
+ if (!startedRef.current) {
285
+ startedRef.current = true;
286
+ funnel("start");
287
+ }
288
+ setValues((current) => ({
289
+ ...current,
290
+ [name]: type === "phone" && typeof next === "string" ? formatPhoneUS(next) : next
291
+ }));
292
+ setErrors((current) => current[name] ? { ...current, [name]: "" } : current);
293
+ };
294
+ const validateFields = (fields) => {
295
+ const nextErrors = {};
296
+ for (const [index, field] of fields.entries()) {
297
+ const name = fieldKey(field, index);
298
+ const type = inferFieldType(field);
299
+ const label = field.label || name;
300
+ const value = values[name];
301
+ const text = typeof value === "string" ? value.trim() : "";
302
+ const empty = value === void 0 || value === false || typeof value === "string" && text.length === 0 || Array.isArray(value) && value.length === 0;
303
+ if (field.required && empty) {
304
+ nextErrors[name] = `${label} is required.`;
305
+ continue;
306
+ }
307
+ if (empty) continue;
308
+ if (type === "email") {
309
+ const result = validateEmail(text);
310
+ if (!result.valid) nextErrors[name] = result.message || "Enter a valid email address.";
311
+ } else if (type === "phone") {
312
+ const result = validatePhoneUS(text);
313
+ if (!result.valid) nextErrors[name] = result.message || "Enter a valid phone number.";
314
+ } else if (type === "url") {
315
+ const result = validateUrl(text);
316
+ if (!result.valid) nextErrors[name] = result.message || "Enter a valid web address.";
317
+ } else if (type === "date") {
318
+ const result = validateDate(text);
319
+ if (!result.valid) nextErrors[name] = result.message || "Enter a valid date.";
320
+ } else if (type === "number") {
321
+ const result = validateNumber(text);
322
+ if (!result.valid) nextErrors[name] = result.message || "Enter a valid number.";
323
+ }
324
+ }
325
+ setErrors((current) => ({ ...current, ...nextErrors }));
326
+ return Object.values(nextErrors).every((message) => !message);
327
+ };
328
+ const submit = async (event) => {
329
+ event.preventDefault();
330
+ if (preview) return;
331
+ if (!validateFields(allFields)) return;
332
+ setState("submitting");
333
+ try {
334
+ const data = {
335
+ ...values,
336
+ [HONEYPOT_FIELD_NAME]: honeypot,
337
+ _renderedAt: renderedAt.current
338
+ };
339
+ const response = await fetch(`${basePath}/forms/${slug}/submit`, {
340
+ method: "POST",
341
+ headers: { "content-type": "application/json" },
342
+ body: JSON.stringify({ data })
343
+ });
344
+ if (!response.ok) {
345
+ const body = await response.json().catch(() => ({}));
346
+ if (body.fieldErrors) setErrors(body.fieldErrors);
347
+ setState("error");
348
+ return;
349
+ }
350
+ setState("success");
351
+ funnel("submit");
352
+ onSuccess?.();
353
+ } catch {
354
+ setState("error");
355
+ }
356
+ };
357
+ const nextStep = () => {
358
+ const fields = steps[stepIndex]?.fields || [];
359
+ if (!validateFields(fields)) return;
360
+ setStepIndex((current) => Math.min(current + 1, steps.length - 1));
361
+ };
362
+ if (state === "success") {
363
+ return /* @__PURE__ */ jsxs("div", { className: cls.success, role: "status", children: [
364
+ /* @__PURE__ */ jsx("strong", { children: "\u2713" }),
365
+ " ",
366
+ successMessage || "Thanks \u2014 we received your submission."
367
+ ] });
368
+ }
369
+ const isLastStep = stepIndex >= steps.length - 1;
370
+ const visibleSteps = steps.length > 0 ? [steps[stepIndex]] : [];
371
+ const renderField = (field, index) => {
372
+ const name = fieldKey(field, index);
373
+ const type = inferFieldType(field);
374
+ const label = field.label || name;
375
+ const error = errors[name];
376
+ const options = fieldOptions(field);
377
+ const id = `ocf-${slug}-${name}`;
378
+ const value = values[name];
379
+ if (type === "hidden") return null;
380
+ const wrap = (control) => /* @__PURE__ */ jsxs("div", { className: `${cls.field}${error ? ` ${cls.fieldError}` : ""}`, children: [
381
+ type !== "checkbox" || options.length > 0 ? /* @__PURE__ */ jsxs("label", { className: cls.label, htmlFor: id, children: [
382
+ label,
383
+ field.required ? " *" : ""
384
+ ] }) : null,
385
+ control,
386
+ field.help ? /* @__PURE__ */ jsx("p", { className: "ocf-help", children: field.help }) : null,
387
+ error ? /* @__PURE__ */ jsx("p", { className: cls.errorText, role: "alert", children: error }) : null
388
+ ] }, name);
389
+ if (type === "textarea") {
390
+ return wrap(
391
+ /* @__PURE__ */ jsx(
392
+ "textarea",
393
+ {
394
+ className: cls.textarea,
395
+ id,
396
+ onChange: (event) => setValue(name, event.target.value, type),
397
+ placeholder: field.placeholder,
398
+ rows: 4,
399
+ value: typeof value === "string" ? value : ""
400
+ }
401
+ )
402
+ );
403
+ }
404
+ if (type === "select") {
405
+ return wrap(
406
+ /* @__PURE__ */ jsxs(
407
+ "select",
408
+ {
409
+ className: cls.select,
410
+ id,
411
+ onChange: (event) => setValue(name, event.target.value, type),
412
+ value: typeof value === "string" ? value : "",
413
+ children: [
414
+ /* @__PURE__ */ jsx("option", { value: "", children: field.placeholder || "Choose\u2026" }),
415
+ options.map((option) => /* @__PURE__ */ jsx("option", { value: option.value, children: option.label }, option.value))
416
+ ]
417
+ }
418
+ )
419
+ );
420
+ }
421
+ if (type === "radio") {
422
+ return wrap(
423
+ /* @__PURE__ */ jsx("div", { className: cls.radioGroup, role: "radiogroup", children: options.map((option) => /* @__PURE__ */ jsxs("label", { className: cls.checkbox, children: [
424
+ /* @__PURE__ */ jsx(
425
+ "input",
426
+ {
427
+ checked: value === option.value,
428
+ name: id,
429
+ onChange: () => setValue(name, option.value, type),
430
+ type: "radio"
431
+ }
432
+ ),
433
+ option.label
434
+ ] }, option.value)) })
435
+ );
436
+ }
437
+ if (type === "checkbox" && options.length > 0) {
438
+ const selected = Array.isArray(value) ? value : [];
439
+ return wrap(
440
+ /* @__PURE__ */ jsx("div", { className: cls.radioGroup, children: options.map((option) => /* @__PURE__ */ jsxs("label", { className: cls.checkbox, children: [
441
+ /* @__PURE__ */ jsx(
442
+ "input",
443
+ {
444
+ checked: selected.includes(option.value),
445
+ onChange: (event) => setValue(
446
+ name,
447
+ event.target.checked ? [...selected, option.value] : selected.filter((entry) => entry !== option.value),
448
+ type
449
+ ),
450
+ type: "checkbox"
451
+ }
452
+ ),
453
+ option.label
454
+ ] }, option.value)) })
455
+ );
456
+ }
457
+ if (type === "checkbox") {
458
+ return /* @__PURE__ */ jsxs("div", { className: `${cls.field}${error ? ` ${cls.fieldError}` : ""}`, children: [
459
+ /* @__PURE__ */ jsxs("label", { className: cls.checkbox, htmlFor: id, children: [
460
+ /* @__PURE__ */ jsx(
461
+ "input",
462
+ {
463
+ checked: value === true,
464
+ id,
465
+ onChange: (event) => setValue(name, event.target.checked, type),
466
+ type: "checkbox"
467
+ }
468
+ ),
469
+ label,
470
+ field.required ? " *" : ""
471
+ ] }),
472
+ error ? /* @__PURE__ */ jsx("p", { className: cls.errorText, role: "alert", children: error }) : null
473
+ ] }, name);
474
+ }
475
+ const inputType = type === "email" ? "email" : type === "date" ? "date" : type === "number" ? "text" : "text";
476
+ const inputMode = type === "phone" ? "tel" : type === "email" ? "email" : type === "number" ? "decimal" : void 0;
477
+ return wrap(
478
+ /* @__PURE__ */ jsx(
479
+ "input",
480
+ {
481
+ className: cls.input,
482
+ id,
483
+ inputMode,
484
+ onChange: (event) => setValue(name, event.target.value, type),
485
+ placeholder: field.placeholder,
486
+ type: inputType,
487
+ value: typeof value === "string" ? value : ""
488
+ }
489
+ )
490
+ );
491
+ };
492
+ return /* @__PURE__ */ jsxs("form", { className: cls.form, noValidate: true, onSubmit: submit, children: [
493
+ title,
494
+ intro,
495
+ /* @__PURE__ */ jsx(
496
+ "input",
497
+ {
498
+ "aria-hidden": "true",
499
+ autoComplete: "off",
500
+ className: "ocf-honeypot",
501
+ name: HONEYPOT_FIELD_NAME,
502
+ onChange: (event) => setHoneypot(event.target.value),
503
+ style: { position: "absolute", left: "-9999px", height: 0, width: 0, opacity: 0 },
504
+ tabIndex: -1,
505
+ value: honeypot
506
+ }
507
+ ),
508
+ steps.length > 1 ? /* @__PURE__ */ jsxs("p", { className: cls.stepTitle, children: [
509
+ "Step ",
510
+ stepIndex + 1,
511
+ " of ",
512
+ steps.length,
513
+ steps[stepIndex]?.title ? ` \u2014 ${steps[stepIndex].title}` : ""
514
+ ] }) : null,
515
+ visibleSteps.map((step) => (step.fields || []).map(renderField)),
516
+ state === "error" ? /* @__PURE__ */ jsx("p", { className: cls.formError, role: "alert", children: "The request could not be sent. Please try again." }) : null,
517
+ /* @__PURE__ */ jsxs("div", { className: "ocf-actions", children: [
518
+ steps.length > 1 && stepIndex > 0 ? /* @__PURE__ */ jsx("button", { className: cls.button, onClick: () => setStepIndex(stepIndex - 1), type: "button", children: "Back" }) : null,
519
+ !isLastStep ? /* @__PURE__ */ jsx("button", { className: cls.button, onClick: nextStep, type: "button", children: "Next" }) : /* @__PURE__ */ jsx("button", { className: cls.button, disabled: state === "submitting" || preview, type: "submit", children: state === "submitting" ? "Sending\u2026" : submitLabel })
520
+ ] })
521
+ ] });
522
+ }
523
+
524
+ export {
525
+ FORM_FIELD_TYPES,
526
+ FormRenderer
527
+ };
@@ -0,0 +1,51 @@
1
+ import { PageLayout } from '../blocks/index.js';
2
+ import 'react';
3
+ import 'zod';
4
+
5
+ /** Cache tag for published content. Frontends tag their reads with it so the
6
+ * API's revalidateContent() invalidates them on publish/sync. */
7
+ declare const CONTENT_CACHE_TAG = "orion-content";
8
+ /**
9
+ * Public read layer for site frontends. Uses the anon key — RLS restricts
10
+ * anonymous access to published content only, so this client is safe in any
11
+ * server component. Wrap calls in your framework's cache (the site template
12
+ * ships with this pre-wired using CONTENT_CACHE_TAG revalidation).
13
+ */
14
+ type PublicPage = {
15
+ id: string;
16
+ slug: string;
17
+ path: string;
18
+ title: string;
19
+ seo: Record<string, unknown>;
20
+ layout: PageLayout;
21
+ };
22
+ type ContentClientOptions = {
23
+ supabaseUrl?: string;
24
+ anonKey?: string;
25
+ };
26
+ type ContentClient = {
27
+ getPageByPath(path: string): Promise<PublicPage | null>;
28
+ getPageBySlug(slug: string): Promise<PublicPage | null>;
29
+ listPublishedPaths(): Promise<string[]>;
30
+ getGlobal<T extends Record<string, unknown> = Record<string, unknown>>(key: string): Promise<T>;
31
+ getFormConfig(slug: string): Promise<{
32
+ slug: string;
33
+ title: string;
34
+ config: Record<string, unknown>;
35
+ successMessage: string;
36
+ } | null>;
37
+ /** Looks up a redirect for a path — consulted by sites on would-be 404s. */
38
+ getRedirect(path: string): Promise<{
39
+ toPath: string;
40
+ permanent: boolean;
41
+ } | null>;
42
+ /** Builds a Supabase Storage transform URL for a media storage path. */
43
+ mediaUrl(storagePath: string, options?: {
44
+ width?: number;
45
+ height?: number;
46
+ quality?: number;
47
+ }): string;
48
+ };
49
+ declare function createContentClient(options?: ContentClientOptions): ContentClient;
50
+
51
+ export { CONTENT_CACHE_TAG, type ContentClient, type ContentClientOptions, type PublicPage, createContentClient };
@@ -0,0 +1,8 @@
1
+ import {
2
+ CONTENT_CACHE_TAG,
3
+ createContentClient
4
+ } from "../chunk-HVJCF2IZ.js";
5
+ export {
6
+ CONTENT_CACHE_TAG,
7
+ createContentClient
8
+ };
@@ -0,0 +1,70 @@
1
+ export { F as FormConfig, a as FormFieldConfig, b as FormNotifyConfig, H as HONEYPOT_FIELD_NAME, P as ProcessSubmissionArgs, c as ProcessSubmissionResult, R as RateLimitStore, d as createMemoryRateLimitStore, p as processSubmission } from '../submission-BKdBedOe.js';
2
+
3
+ /**
4
+ * Shared, isomorphic form validation and normalization.
5
+ *
6
+ * This module is the single source of truth mandated by the Studio Website
7
+ * Setup Guide: the client form renderer and the server submission route must
8
+ * both use these helpers so values are trimmed, normalized, and validated the
9
+ * same way everywhere. It has no server-only dependencies and is safe to
10
+ * import from client components.
11
+ */
12
+ type FieldValidationResult = {
13
+ valid: boolean;
14
+ /** Trimmed/normalized value. Use this for display, persistence, and emails. */
15
+ normalized: string;
16
+ /** Human-readable error message when invalid. */
17
+ message?: string;
18
+ /** Correction hint, e.g. `name@yahoo.com` when the domain looks like a typo. */
19
+ suggestion?: string;
20
+ };
21
+ declare function normalizeEmail(value: string): string;
22
+ type ValidateEmailOptions = {
23
+ /** Additional domains that must never be flagged as typos. */
24
+ knownGoodDomains?: string[];
25
+ };
26
+ /**
27
+ * Validates an email per the Studio contract: trims before validation, and
28
+ * rejects obvious misspellings of major consumer providers with a
29
+ * `Did you mean …?` suggestion.
30
+ */
31
+ declare function validateEmail(value: string, options?: ValidateEmailOptions): FieldValidationResult;
32
+ /** Strips formatting and a leading US country code, returning digits only. */
33
+ declare function normalizePhone(value: string): string;
34
+ /**
35
+ * Progressive US phone formatting for use while typing: `(555) 555-5555`.
36
+ * Feed it the raw input value on every change and write the result back.
37
+ */
38
+ declare function formatPhoneUS(value: string): string;
39
+ /** Validates against the normalized 10-digit value, per the Studio contract. */
40
+ declare function validatePhoneUS(value: string): FieldValidationResult;
41
+ /** Normalizes bare domains to `https://…` before validation, per the Studio contract. */
42
+ declare function normalizeUrl(value: string): string;
43
+ declare function validateUrl(value: string): FieldValidationResult;
44
+ declare function validateRequired(value: unknown, label?: string): FieldValidationResult;
45
+ type StudioFormFieldType = 'text' | 'textarea' | 'email' | 'phone' | 'url' | 'select' | 'radio' | 'checkbox' | 'date' | 'number' | 'hidden';
46
+ /** All field types the Studio form editor can assign. */
47
+ declare const FORM_FIELD_TYPES: StudioFormFieldType[];
48
+ /**
49
+ * Normalizes a single submitted value by field type using the same rules on
50
+ * client and server. Unknown types are trimmed only.
51
+ */
52
+ declare function normalizeFieldValue(value: string, fieldType: StudioFormFieldType): string;
53
+ /** Validates a date value (YYYY-MM-DD, per <input type="date">). */
54
+ declare function validateDate(value: string): FieldValidationResult;
55
+ /** Validates a numeric value; normalizes to the canonical decimal string. */
56
+ declare function validateNumber(value: string): FieldValidationResult;
57
+ /** Infers a normalization type from a form field definition (type first, then name heuristics). */
58
+ declare function inferFieldType(field: {
59
+ name?: unknown;
60
+ type?: unknown;
61
+ }): StudioFormFieldType;
62
+ /** Normalizes a field's configured options to `{label, value}` pairs. */
63
+ declare function fieldOptions(field: {
64
+ options?: unknown;
65
+ }): Array<{
66
+ label: string;
67
+ value: string;
68
+ }>;
69
+
70
+ export { FORM_FIELD_TYPES, type FieldValidationResult, type StudioFormFieldType, type ValidateEmailOptions, fieldOptions, formatPhoneUS, inferFieldType, normalizeEmail, normalizeFieldValue, normalizePhone, normalizeUrl, validateDate, validateEmail, validateNumber, validatePhoneUS, validateRequired, validateUrl };
@@ -0,0 +1,38 @@
1
+ import {
2
+ FORM_FIELD_TYPES,
3
+ HONEYPOT_FIELD_NAME,
4
+ createMemoryRateLimitStore,
5
+ fieldOptions,
6
+ formatPhoneUS,
7
+ inferFieldType,
8
+ normalizeEmail,
9
+ normalizeFieldValue,
10
+ normalizePhone,
11
+ normalizeUrl,
12
+ processSubmission,
13
+ validateDate,
14
+ validateEmail,
15
+ validateNumber,
16
+ validatePhoneUS,
17
+ validateRequired,
18
+ validateUrl
19
+ } from "../chunk-VPUODCNH.js";
20
+ export {
21
+ FORM_FIELD_TYPES,
22
+ HONEYPOT_FIELD_NAME,
23
+ createMemoryRateLimitStore,
24
+ fieldOptions,
25
+ formatPhoneUS,
26
+ inferFieldType,
27
+ normalizeEmail,
28
+ normalizeFieldValue,
29
+ normalizePhone,
30
+ normalizeUrl,
31
+ processSubmission,
32
+ validateDate,
33
+ validateEmail,
34
+ validateNumber,
35
+ validatePhoneUS,
36
+ validateRequired,
37
+ validateUrl
38
+ };