@orion-studios/cms 0.5.6 → 0.5.8
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/README.md +21 -4
- package/dist/analytics/react.d.ts +3 -1
- package/dist/analytics/react.js +5 -0
- package/dist/chunk-DPKXKG2S.js +8 -0
- package/dist/{chunk-AYV6KYDP.js → chunk-NSAZCP4I.js} +20 -1
- package/dist/{chunk-ULE565KD.js → chunk-Z52R6XR7.js} +227 -72
- package/dist/content/index.js +1 -1
- package/dist/forms/react.d.ts +12 -2
- package/dist/forms/react.js +6 -3
- package/dist/server/index.d.ts +93 -21
- package/dist/server/index.js +803 -143
- package/dist/studio/index.js +29 -7
- package/package.json +1 -1
- package/sql/bootstrap.sql +313 -6
- package/sql/migrations/20260829032027_cms_security_and_sync_contracts.sql +390 -0
package/README.md
CHANGED
|
@@ -41,7 +41,14 @@ Requires Next.js (App Router), React 19, Zod 3. Supabase only in supabase mode.
|
|
|
41
41
|
import { createCmsRoutes } from '@orion-studios/cms/server'
|
|
42
42
|
import { registry } from '@/cms/registry'
|
|
43
43
|
export const { GET, POST, PATCH, DELETE } = createCmsRoutes({
|
|
44
|
-
registry,
|
|
44
|
+
registry,
|
|
45
|
+
syncToken: process.env.CMS_SYNC_TOKEN,
|
|
46
|
+
cronToken: process.env.CRON_SECRET,
|
|
47
|
+
previewSecret: process.env.CMS_PREVIEW_SECRET,
|
|
48
|
+
previewKeyId: process.env.CMS_PREVIEW_KEY_ID,
|
|
49
|
+
analyticsSecret: process.env.CMS_ANALYTICS_HASH_SECRET,
|
|
50
|
+
autoReplyHashSecret: process.env.CMS_AUTO_REPLY_HASH_SECRET,
|
|
51
|
+
projectRef: process.env.CMS_EXPECTED_SUPABASE_PROJECT_REF,
|
|
45
52
|
})
|
|
46
53
|
|
|
47
54
|
// src/app/studio/page.tsx — the admin
|
|
@@ -75,10 +82,20 @@ own role, never delete yourself.
|
|
|
75
82
|
| Variable | Where | Purpose |
|
|
76
83
|
| --- | --- | --- |
|
|
77
84
|
| `NEXT_PUBLIC_SUPABASE_URL` | local + Vercel | project URL (also selects supabase mode) |
|
|
78
|
-
| `
|
|
79
|
-
| `
|
|
80
|
-
| `
|
|
85
|
+
| `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY` | local + Vercel | public reads (RLS: published content only) |
|
|
86
|
+
| `SUPABASE_SECRET_KEY` | local + Vercel | server-only API access; the legacy service-role alias remains supported |
|
|
87
|
+
| `CMS_EXPECTED_SUPABASE_PROJECT_REF` | local + Vercel | binds the project URL, server credential, previews, and operator database target |
|
|
88
|
+
| `CMS_DATABASE_URL` | local operator only | direct or port 5432 session connection for bootstrap and migrations |
|
|
89
|
+
| `CMS_EXPECTED_DATABASE_ROLE` | local operator only | dedicated CMS migration role with no `app` schema access |
|
|
90
|
+
| `CMS_EXPECTED_DATABASE_NAME` | local operator only | exact database name required by the target guard |
|
|
91
|
+
| `CMS_DATABASE_CONFIRMATION` | local operator only | exact redacted target-receipt hash required before database writes |
|
|
92
|
+
| `CMS_PREVIEW_SECRET` | local + Vercel | signs short-lived, revocable preview grants |
|
|
93
|
+
| `CMS_PREVIEW_KEY_ID` | local + Vercel | non-secret identifier bound into preview grants |
|
|
94
|
+
| `CMS_PREVIEW_PREVIOUS_SECRET`, `CMS_PREVIEW_PREVIOUS_KEY_ID`, `CMS_PREVIEW_PREVIOUS_VALID_UNTIL` | local + Vercel | optional bounded preview-key rotation overlap |
|
|
95
|
+
| `CMS_ANALYTICS_HASH_SECRET` | local + Vercel | hashes analytics session and visitor identifiers |
|
|
96
|
+
| `CMS_AUTO_REPLY_HASH_SECRET` | local + Vercel | hashes form auto-reply rate-limit keys |
|
|
81
97
|
| `CMS_SYNC_TOKEN` | local + Vercel | authorizes `POST /api/cms/sync` |
|
|
98
|
+
| `CRON_SECRET` | local + Vercel | authorizes scheduled publication only |
|
|
82
99
|
| `CMS_STATIC=true` | optional | static mode (no CMS) |
|
|
83
100
|
| `CMS_MEMORY=true` | optional | force memory mode |
|
|
84
101
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
declare const ANALYTICS_READY_EVENT = "orion-analytics-ready";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* The site-side analytics tracker: small, first-party, and zero dependency.
|
|
3
5
|
* Mount <Analytics /> once in the site layout. It captures:
|
|
@@ -50,4 +52,4 @@ declare function AnalyticsNotFound(): null;
|
|
|
50
52
|
/** Programmatic tracking for custom site components. No-op when analytics is off. */
|
|
51
53
|
declare function trackEvent(name: string, meta?: Record<string, unknown>): void;
|
|
52
54
|
|
|
53
|
-
export { ANALYTICS_CONSENT_COOKIE, ANALYTICS_CONSENT_EVENT, ANALYTICS_VISITOR_COOKIE, Analytics, type AnalyticsConsent, AnalyticsNotFound, type AnalyticsProps, EXCLUDE_FLAG, getAnalyticsConsent, setAnalyticsConsent, trackEvent };
|
|
55
|
+
export { ANALYTICS_CONSENT_COOKIE, ANALYTICS_CONSENT_EVENT, ANALYTICS_READY_EVENT, ANALYTICS_VISITOR_COOKIE, Analytics, type AnalyticsConsent, AnalyticsNotFound, type AnalyticsProps, EXCLUDE_FLAG, getAnalyticsConsent, setAnalyticsConsent, trackEvent };
|
package/dist/analytics/react.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
"use client";
|
|
3
|
+
import {
|
|
4
|
+
ANALYTICS_READY_EVENT
|
|
5
|
+
} from "../chunk-DPKXKG2S.js";
|
|
3
6
|
|
|
4
7
|
// src/analytics/react.tsx
|
|
5
8
|
import { useEffect, useRef, useState } from "react";
|
|
@@ -105,6 +108,7 @@ function Analytics({
|
|
|
105
108
|
track({ type, name, path: window.location.pathname, meta });
|
|
106
109
|
if (type === "form" || type === "not_found") flush();
|
|
107
110
|
};
|
|
111
|
+
window.dispatchEvent(new Event(ANALYTICS_READY_EVENT));
|
|
108
112
|
const onClick = (event) => {
|
|
109
113
|
const target = event.target;
|
|
110
114
|
const tagged = target?.closest("[data-analytics]");
|
|
@@ -185,6 +189,7 @@ function trackEvent(name, meta) {
|
|
|
185
189
|
export {
|
|
186
190
|
ANALYTICS_CONSENT_COOKIE,
|
|
187
191
|
ANALYTICS_CONSENT_EVENT,
|
|
192
|
+
ANALYTICS_READY_EVENT,
|
|
188
193
|
ANALYTICS_VISITOR_COOKIE,
|
|
189
194
|
Analytics,
|
|
190
195
|
AnalyticsNotFound,
|
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
// src/content/index.ts
|
|
2
2
|
import { createClient } from "@supabase/supabase-js";
|
|
3
|
+
|
|
4
|
+
// src/supabase-keys.ts
|
|
5
|
+
var isServerSupabaseKey = (key) => {
|
|
6
|
+
if (key.startsWith("sb_secret_")) return true;
|
|
7
|
+
const payload = key.split(".")[1];
|
|
8
|
+
if (!payload) return false;
|
|
9
|
+
try {
|
|
10
|
+
const normalized = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
11
|
+
const decoded = JSON.parse(atob(normalized));
|
|
12
|
+
return decoded.role === "service_role";
|
|
13
|
+
} catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// src/content/index.ts
|
|
3
19
|
var CONTENT_CACHE_TAG = "orion-content";
|
|
4
20
|
var toPublicPage = (row) => ({
|
|
5
21
|
id: String(row.id),
|
|
@@ -11,12 +27,15 @@ var toPublicPage = (row) => ({
|
|
|
11
27
|
});
|
|
12
28
|
function createContentClient(options = {}) {
|
|
13
29
|
const supabaseUrl = options.supabaseUrl || process.env.NEXT_PUBLIC_SUPABASE_URL || process.env.SUPABASE_URL || "";
|
|
14
|
-
const anonKey = options.anonKey || process.env.
|
|
30
|
+
const anonKey = options.anonKey || process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || process.env.SUPABASE_PUBLISHABLE_KEY || process.env.SUPABASE_ANON_KEY || "";
|
|
15
31
|
if (!supabaseUrl || !anonKey) {
|
|
16
32
|
throw new Error(
|
|
17
33
|
"Orion CMS: NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY (or NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY) must be set."
|
|
18
34
|
);
|
|
19
35
|
}
|
|
36
|
+
if (isServerSupabaseKey(anonKey)) {
|
|
37
|
+
throw new Error("Orion CMS: a Supabase server key cannot be used for public content reads.");
|
|
38
|
+
}
|
|
20
39
|
const client = createClient(supabaseUrl, anonKey, {
|
|
21
40
|
auth: { persistSession: false, autoRefreshToken: false }
|
|
22
41
|
});
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
'use client';
|
|
2
|
+
import {
|
|
3
|
+
ANALYTICS_READY_EVENT
|
|
4
|
+
} from "./chunk-DPKXKG2S.js";
|
|
2
5
|
|
|
3
6
|
// src/forms/react.tsx
|
|
4
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
useCallback,
|
|
9
|
+
useEffect,
|
|
10
|
+
useMemo,
|
|
11
|
+
useRef,
|
|
12
|
+
useState
|
|
13
|
+
} from "react";
|
|
5
14
|
|
|
6
15
|
// src/forms/validation.ts
|
|
7
16
|
var MAJOR_EMAIL_PROVIDERS = [
|
|
@@ -248,8 +257,135 @@ function getAutoReplyEmailFields(config) {
|
|
|
248
257
|
return [...fieldTypes.entries()].filter(([, types]) => types.size === 1 && types.has("email")).map(([name]) => name);
|
|
249
258
|
}
|
|
250
259
|
|
|
260
|
+
// src/forms/funnel.ts
|
|
261
|
+
var FORM_VIEW_MIN_VISIBLE_RATIO = 0.75;
|
|
262
|
+
var FORM_VIEW_DWELL_MS = 2e3;
|
|
263
|
+
function createFormFunnelSession(trackStage) {
|
|
264
|
+
const trackedStages = /* @__PURE__ */ new Set();
|
|
265
|
+
const trackOnce = (stage) => {
|
|
266
|
+
if (trackedStages.has(stage) || !trackStage(stage)) return false;
|
|
267
|
+
trackedStages.add(stage);
|
|
268
|
+
return true;
|
|
269
|
+
};
|
|
270
|
+
return {
|
|
271
|
+
hasViewed: () => trackedStages.has("viewed"),
|
|
272
|
+
trackStarted: () => trackOnce("start"),
|
|
273
|
+
trackViewed: () => trackOnce("viewed")
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function isQualifiedFormVisibility(intersectionRatio, isIntersecting, visibilityState) {
|
|
277
|
+
return isIntersecting && intersectionRatio >= FORM_VIEW_MIN_VISIBLE_RATIO && visibilityState === "visible";
|
|
278
|
+
}
|
|
279
|
+
|
|
251
280
|
// src/forms/react.tsx
|
|
252
281
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
282
|
+
var analyticsTrack = () => window.__orionTrack;
|
|
283
|
+
var isActionableFieldTarget = (target) => {
|
|
284
|
+
if (!(target instanceof Element)) return false;
|
|
285
|
+
const directField = target.closest("input, select, textarea");
|
|
286
|
+
const labelledField = target.closest("label")?.control;
|
|
287
|
+
const field = directField || labelledField;
|
|
288
|
+
if (!(field instanceof HTMLInputElement || field instanceof HTMLSelectElement || field instanceof HTMLTextAreaElement) || field.name === HONEYPOT_FIELD_NAME || field.disabled || "readOnly" in field && field.readOnly) {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
return true;
|
|
292
|
+
};
|
|
293
|
+
function useFormFunnel(slug, options = {}) {
|
|
294
|
+
const { preview = false } = options;
|
|
295
|
+
const [attentionTarget, setAttentionTarget] = useState(null);
|
|
296
|
+
const visibleRef = useRef(false);
|
|
297
|
+
const timerRef = useRef(null);
|
|
298
|
+
const clearViewTimer = useCallback(() => {
|
|
299
|
+
if (timerRef.current === null) return;
|
|
300
|
+
window.clearTimeout(timerRef.current);
|
|
301
|
+
timerRef.current = null;
|
|
302
|
+
}, []);
|
|
303
|
+
const trackStage = useCallback(
|
|
304
|
+
(stage) => {
|
|
305
|
+
if (preview || typeof window === "undefined") return false;
|
|
306
|
+
const track = analyticsTrack();
|
|
307
|
+
if (!track) return false;
|
|
308
|
+
track("form", `${slug}:${stage}`);
|
|
309
|
+
return true;
|
|
310
|
+
},
|
|
311
|
+
[preview, slug]
|
|
312
|
+
);
|
|
313
|
+
const session = useMemo(() => createFormFunnelSession(trackStage), [trackStage]);
|
|
314
|
+
const trackViewed = useCallback(() => {
|
|
315
|
+
if (session.trackViewed()) clearViewTimer();
|
|
316
|
+
}, [clearViewTimer, session]);
|
|
317
|
+
const startViewTimer = useCallback(() => {
|
|
318
|
+
if (session.hasViewed() || timerRef.current !== null || !visibleRef.current || document.visibilityState !== "visible" || !analyticsTrack()) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
timerRef.current = window.setTimeout(() => {
|
|
322
|
+
timerRef.current = null;
|
|
323
|
+
if (visibleRef.current && document.visibilityState === "visible") trackViewed();
|
|
324
|
+
}, FORM_VIEW_DWELL_MS);
|
|
325
|
+
}, [session, trackViewed]);
|
|
326
|
+
useEffect(() => {
|
|
327
|
+
visibleRef.current = false;
|
|
328
|
+
clearViewTimer();
|
|
329
|
+
if (preview || !attentionTarget || typeof IntersectionObserver === "undefined") return;
|
|
330
|
+
const observer = new IntersectionObserver(
|
|
331
|
+
([entry]) => {
|
|
332
|
+
visibleRef.current = Boolean(
|
|
333
|
+
entry && entry.isIntersecting && entry.intersectionRatio >= FORM_VIEW_MIN_VISIBLE_RATIO
|
|
334
|
+
);
|
|
335
|
+
if (entry && isQualifiedFormVisibility(
|
|
336
|
+
entry.intersectionRatio,
|
|
337
|
+
entry.isIntersecting,
|
|
338
|
+
document.visibilityState
|
|
339
|
+
)) {
|
|
340
|
+
startViewTimer();
|
|
341
|
+
} else clearViewTimer();
|
|
342
|
+
},
|
|
343
|
+
{ threshold: FORM_VIEW_MIN_VISIBLE_RATIO }
|
|
344
|
+
);
|
|
345
|
+
const onVisibilityChange = () => {
|
|
346
|
+
if (document.visibilityState !== "visible") {
|
|
347
|
+
clearViewTimer();
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (visibleRef.current) startViewTimer();
|
|
351
|
+
};
|
|
352
|
+
const onAnalyticsReady = () => {
|
|
353
|
+
if (visibleRef.current) startViewTimer();
|
|
354
|
+
};
|
|
355
|
+
observer.observe(attentionTarget);
|
|
356
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
357
|
+
window.addEventListener(ANALYTICS_READY_EVENT, onAnalyticsReady);
|
|
358
|
+
return () => {
|
|
359
|
+
observer.disconnect();
|
|
360
|
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
361
|
+
window.removeEventListener(ANALYTICS_READY_EVENT, onAnalyticsReady);
|
|
362
|
+
clearViewTimer();
|
|
363
|
+
};
|
|
364
|
+
}, [attentionTarget, clearViewTimer, preview, slug, startViewTimer]);
|
|
365
|
+
const onFieldPointerDownCapture = useCallback(
|
|
366
|
+
(event) => {
|
|
367
|
+
if (!event.nativeEvent.isTrusted || !isActionableFieldTarget(event.target)) return;
|
|
368
|
+
trackViewed();
|
|
369
|
+
},
|
|
370
|
+
[trackViewed]
|
|
371
|
+
);
|
|
372
|
+
const onFieldKeyDownCapture = useCallback(
|
|
373
|
+
(event) => {
|
|
374
|
+
if (!event.nativeEvent.isTrusted || !isActionableFieldTarget(event.target)) return;
|
|
375
|
+
trackViewed();
|
|
376
|
+
},
|
|
377
|
+
[trackViewed]
|
|
378
|
+
);
|
|
379
|
+
const trackStarted = useCallback(() => {
|
|
380
|
+
session.trackStarted();
|
|
381
|
+
}, [session]);
|
|
382
|
+
return {
|
|
383
|
+
attentionRef: setAttentionTarget,
|
|
384
|
+
onFieldKeyDownCapture,
|
|
385
|
+
onFieldPointerDownCapture,
|
|
386
|
+
trackStarted
|
|
387
|
+
};
|
|
388
|
+
}
|
|
253
389
|
var DEFAULT_CLASSES = {
|
|
254
390
|
form: "ocf-form",
|
|
255
391
|
field: "ocf-field",
|
|
@@ -291,26 +427,17 @@ function FormRenderer({
|
|
|
291
427
|
const [state, setState] = useState("idle");
|
|
292
428
|
const [honeypot, setHoneypot] = useState("");
|
|
293
429
|
const renderedAt = useRef(Date.now());
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
);
|
|
301
|
-
};
|
|
302
|
-
useEffect(() => {
|
|
303
|
-
const timer = setTimeout(() => funnel("view"), 100);
|
|
304
|
-
return () => clearTimeout(timer);
|
|
305
|
-
}, [slug]);
|
|
430
|
+
const {
|
|
431
|
+
attentionRef,
|
|
432
|
+
onFieldKeyDownCapture,
|
|
433
|
+
onFieldPointerDownCapture,
|
|
434
|
+
trackStarted
|
|
435
|
+
} = useFormFunnel(slug, { preview });
|
|
306
436
|
useEffect(() => {
|
|
307
437
|
setStepIndex((current) => Math.min(current, Math.max(steps.length - 1, 0)));
|
|
308
438
|
}, [steps.length]);
|
|
309
439
|
const setValue = (name, next, type) => {
|
|
310
|
-
|
|
311
|
-
startedRef.current = true;
|
|
312
|
-
funnel("start");
|
|
313
|
-
}
|
|
440
|
+
trackStarted();
|
|
314
441
|
setValues((current) => ({
|
|
315
442
|
...current,
|
|
316
443
|
[name]: type === "phone" && typeof next === "string" ? formatPhoneUS(next) : next
|
|
@@ -393,6 +520,7 @@ function FormRenderer({
|
|
|
393
520
|
}
|
|
394
521
|
const isLastStep = stepIndex >= steps.length - 1;
|
|
395
522
|
const visibleSteps = steps.length > 0 ? [steps[stepIndex]] : [];
|
|
523
|
+
const firstActionableFieldKey = visibleSteps.flatMap((step) => step.fields || []).map((field, index) => ({ field, key: fieldKey(field, index) })).find(({ field }) => inferFieldType(field) !== "hidden")?.key;
|
|
396
524
|
const renderField = (field, index) => {
|
|
397
525
|
const name = fieldKey(field, index);
|
|
398
526
|
const type = inferFieldType(field);
|
|
@@ -402,15 +530,23 @@ function FormRenderer({
|
|
|
402
530
|
const id = `ocf-${slug}-${name}`;
|
|
403
531
|
const value = values[name];
|
|
404
532
|
if (type === "hidden") return null;
|
|
405
|
-
const wrap = (control) => /* @__PURE__ */ jsxs(
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
field
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
533
|
+
const wrap = (control) => /* @__PURE__ */ jsxs(
|
|
534
|
+
"div",
|
|
535
|
+
{
|
|
536
|
+
className: `${cls.field}${error ? ` ${cls.fieldError}` : ""}`,
|
|
537
|
+
ref: name === firstActionableFieldKey ? attentionRef : void 0,
|
|
538
|
+
children: [
|
|
539
|
+
type !== "checkbox" || options.length > 0 ? /* @__PURE__ */ jsxs("label", { className: cls.label, htmlFor: id, children: [
|
|
540
|
+
label,
|
|
541
|
+
field.required ? " *" : ""
|
|
542
|
+
] }) : null,
|
|
543
|
+
control,
|
|
544
|
+
field.help ? /* @__PURE__ */ jsx("p", { className: "ocf-help", children: field.help }) : null,
|
|
545
|
+
error ? /* @__PURE__ */ jsx("p", { className: cls.errorText, role: "alert", children: error }) : null
|
|
546
|
+
]
|
|
547
|
+
},
|
|
548
|
+
name
|
|
549
|
+
);
|
|
414
550
|
if (type === "textarea") {
|
|
415
551
|
return wrap(
|
|
416
552
|
/* @__PURE__ */ jsx(
|
|
@@ -480,22 +616,30 @@ function FormRenderer({
|
|
|
480
616
|
);
|
|
481
617
|
}
|
|
482
618
|
if (type === "checkbox") {
|
|
483
|
-
return /* @__PURE__ */ jsxs(
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
619
|
+
return /* @__PURE__ */ jsxs(
|
|
620
|
+
"div",
|
|
621
|
+
{
|
|
622
|
+
className: `${cls.field}${error ? ` ${cls.fieldError}` : ""}`,
|
|
623
|
+
ref: name === firstActionableFieldKey ? attentionRef : void 0,
|
|
624
|
+
children: [
|
|
625
|
+
/* @__PURE__ */ jsxs("label", { className: cls.checkbox, htmlFor: id, children: [
|
|
626
|
+
/* @__PURE__ */ jsx(
|
|
627
|
+
"input",
|
|
628
|
+
{
|
|
629
|
+
checked: value === true,
|
|
630
|
+
id,
|
|
631
|
+
onChange: (event) => setValue(name, event.target.checked, type),
|
|
632
|
+
type: "checkbox"
|
|
633
|
+
}
|
|
634
|
+
),
|
|
635
|
+
label,
|
|
636
|
+
field.required ? " *" : ""
|
|
637
|
+
] }),
|
|
638
|
+
error ? /* @__PURE__ */ jsx("p", { className: cls.errorText, role: "alert", children: error }) : null
|
|
639
|
+
]
|
|
640
|
+
},
|
|
641
|
+
name
|
|
642
|
+
);
|
|
499
643
|
}
|
|
500
644
|
const inputType = type === "email" ? "email" : type === "date" ? "date" : type === "number" ? "text" : "text";
|
|
501
645
|
const inputMode = type === "phone" ? "tel" : type === "email" ? "email" : type === "number" ? "decimal" : void 0;
|
|
@@ -514,40 +658,51 @@ function FormRenderer({
|
|
|
514
658
|
)
|
|
515
659
|
);
|
|
516
660
|
};
|
|
517
|
-
return /* @__PURE__ */ jsxs(
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
661
|
+
return /* @__PURE__ */ jsxs(
|
|
662
|
+
"form",
|
|
663
|
+
{
|
|
664
|
+
className: cls.form,
|
|
665
|
+
noValidate: true,
|
|
666
|
+
onKeyDownCapture: onFieldKeyDownCapture,
|
|
667
|
+
onPointerDownCapture: onFieldPointerDownCapture,
|
|
668
|
+
onSubmit: submit,
|
|
669
|
+
children: [
|
|
670
|
+
title,
|
|
671
|
+
intro,
|
|
672
|
+
/* @__PURE__ */ jsx(
|
|
673
|
+
"input",
|
|
674
|
+
{
|
|
675
|
+
"aria-hidden": "true",
|
|
676
|
+
autoComplete: "off",
|
|
677
|
+
className: "ocf-honeypot",
|
|
678
|
+
name: HONEYPOT_FIELD_NAME,
|
|
679
|
+
onChange: (event) => setHoneypot(event.target.value),
|
|
680
|
+
style: { position: "absolute", left: "-9999px", height: 0, width: 0, opacity: 0 },
|
|
681
|
+
tabIndex: -1,
|
|
682
|
+
value: honeypot
|
|
683
|
+
}
|
|
684
|
+
),
|
|
685
|
+
steps.length > 1 ? /* @__PURE__ */ jsxs("p", { className: cls.stepTitle, children: [
|
|
686
|
+
"Step ",
|
|
687
|
+
stepIndex + 1,
|
|
688
|
+
" of ",
|
|
689
|
+
steps.length,
|
|
690
|
+
steps[stepIndex]?.title ? ` \u2014 ${steps[stepIndex].title}` : ""
|
|
691
|
+
] }) : null,
|
|
692
|
+
visibleSteps.map((step) => (step.fields || []).map(renderField)),
|
|
693
|
+
state === "error" ? /* @__PURE__ */ jsx("p", { className: cls.formError, role: "alert", children: "The request could not be sent. Please try again." }) : null,
|
|
694
|
+
/* @__PURE__ */ jsxs("div", { className: "ocf-actions", children: [
|
|
695
|
+
steps.length > 1 && stepIndex > 0 ? /* @__PURE__ */ jsx("button", { className: cls.button, onClick: () => setStepIndex(stepIndex - 1), type: "button", children: "Back" }) : null,
|
|
696
|
+
!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 })
|
|
697
|
+
] })
|
|
698
|
+
]
|
|
699
|
+
}
|
|
700
|
+
);
|
|
547
701
|
}
|
|
548
702
|
|
|
549
703
|
export {
|
|
550
704
|
FORM_FIELD_TYPES,
|
|
551
705
|
getAutoReplyEmailFields,
|
|
706
|
+
useFormFunnel,
|
|
552
707
|
FormRenderer
|
|
553
708
|
};
|
package/dist/content/index.js
CHANGED
package/dist/forms/react.d.ts
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { ReactNode } from 'react';
|
|
2
|
+
import { KeyboardEvent, PointerEvent, ReactNode } from 'react';
|
|
3
3
|
import { F as FormConfig } from '../submission-CKZgx1h7.js';
|
|
4
4
|
|
|
5
|
+
type FormFunnelOptions = {
|
|
6
|
+
preview?: boolean;
|
|
7
|
+
};
|
|
8
|
+
type FormFunnelTracking = {
|
|
9
|
+
attentionRef: (node: HTMLElement | null) => void;
|
|
10
|
+
onFieldKeyDownCapture: (event: KeyboardEvent<HTMLElement>) => void;
|
|
11
|
+
onFieldPointerDownCapture: (event: PointerEvent<HTMLElement>) => void;
|
|
12
|
+
trackStarted: () => void;
|
|
13
|
+
};
|
|
14
|
+
declare function useFormFunnel(slug: string, options?: FormFunnelOptions): FormFunnelTracking;
|
|
5
15
|
/**
|
|
6
16
|
* The shared, config-driven form renderer: the same component renders a form
|
|
7
17
|
* in the Studio's form editor preview and on the public site, from the same
|
|
@@ -42,4 +52,4 @@ type FormRendererProps = {
|
|
|
42
52
|
};
|
|
43
53
|
declare function FormRenderer({ slug, config, successMessage, basePath, classNames, title, intro, submitLabel, preview, onSuccess, }: FormRendererProps): react.JSX.Element;
|
|
44
54
|
|
|
45
|
-
export { FormRenderer, type FormRendererClassNames, type FormRendererProps };
|
|
55
|
+
export { type FormFunnelOptions, type FormFunnelTracking, FormRenderer, type FormRendererClassNames, type FormRendererProps, useFormFunnel };
|
package/dist/forms/react.js
CHANGED