@saastro/forms 0.1.3 → 0.2.1

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.js CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  useHasComponentProvider,
12
12
  usePartialComponents,
13
13
  withComponents
14
- } from "./chunk-GHDCNAWC.js";
14
+ } from "./chunk-G2ZSBYVS.js";
15
15
  import {
16
16
  applyBuiltinTransform,
17
17
  applyFieldMapping,
@@ -1362,7 +1362,7 @@ function renderSwitchGroupField(fieldConfig, ctx) {
1362
1362
  import { Suspense, lazy } from "react";
1363
1363
  import { jsx as jsx7 } from "react/jsx-runtime";
1364
1364
  var DateRenderersLazy = lazy(
1365
- () => import("./DateRenderers-3JUQNLKJ.js").then((m) => ({ default: m.DateRenderers }))
1365
+ () => import("./DateRenderers-QKRBXFC6.js").then((m) => ({ default: m.DateRenderers }))
1366
1366
  );
1367
1367
  function renderDateField(fieldConfig, ctx, control, name) {
1368
1368
  return /* @__PURE__ */ jsx7(
@@ -6799,6 +6799,391 @@ var BUILTIN_RESOLVERS = [
6799
6799
  { id: "urlParam", label: "URL Parameter", description: "Value from a URL query parameter" }
6800
6800
  ];
6801
6801
 
6802
+ // src/templates/contact.ts
6803
+ function createContactTemplate() {
6804
+ return FormBuilder.create("contact").layout("auto").columns(1).gap(4).addField(
6805
+ "name",
6806
+ (f) => f.type("text").label("Name").placeholder("Your full name").required("Please enter your name").minLength(2, "Name must be at least 2 characters")
6807
+ ).addField(
6808
+ "email",
6809
+ (f) => f.type("email").label("Email").placeholder("you@example.com").required("Please enter your email").email("Please enter a valid email address")
6810
+ ).addField(
6811
+ "message",
6812
+ (f) => f.type("textarea").label("Message").placeholder("How can we help?").required("Please write a short message").minLength(10, "Message must be at least 10 characters")
6813
+ ).addStep("main", ["name", "email", "message"]).initialStep("main").buttons({
6814
+ submit: { type: "submit", label: "Send message" },
6815
+ align: "end"
6816
+ }).build();
6817
+ }
6818
+
6819
+ // src/templates/newsletter.ts
6820
+ function createNewsletterTemplate() {
6821
+ return FormBuilder.create("newsletter").layout("auto").columns(1).gap(4).addField(
6822
+ "email",
6823
+ (f) => f.type("email").label("Email").placeholder("you@example.com").required("Please enter your email").email("Please enter a valid email address")
6824
+ ).addField(
6825
+ "consent",
6826
+ (f) => f.type("checkbox").label("I agree to receive newsletter emails").required("You must agree to receive the newsletter")
6827
+ ).addStep("main", ["email", "consent"]).initialStep("main").buttons({
6828
+ submit: { type: "submit", label: "Subscribe" },
6829
+ align: "end"
6830
+ }).build();
6831
+ }
6832
+
6833
+ // src/templates/lead-gen.ts
6834
+ function createLeadGenTemplate() {
6835
+ return FormBuilder.create("lead-gen").layout("auto").columns(2).gap(4).addField(
6836
+ "name",
6837
+ (f) => f.type("text").label("Full name").placeholder("Jane Doe").required("Please enter your name").minLength(2, "Name must be at least 2 characters").columns({ default: 1 })
6838
+ ).addField(
6839
+ "email",
6840
+ (f) => f.type("email").label("Work email").placeholder("jane@acme.com").required("Please enter your email").email("Please enter a valid email address").columns({ default: 1 })
6841
+ ).addField(
6842
+ "phone",
6843
+ (f) => f.type("tel").label("Phone").placeholder("+1 555 0123").optional().columns({ default: 1 })
6844
+ ).addField(
6845
+ "company",
6846
+ (f) => f.type("text").label("Company").placeholder("Acme Inc.").optional().columns({ default: 1 })
6847
+ ).addField(
6848
+ "message",
6849
+ (f) => f.type("textarea").label("What are you looking to solve?").placeholder("A few words about your project or use case").required("Please describe your need briefly").minLength(10, "Message must be at least 10 characters").columns({ default: 2 })
6850
+ ).addStep("main", ["name", "email", "phone", "company", "message"]).initialStep("main").buttons({
6851
+ submit: { type: "submit", label: "Request a call" },
6852
+ align: "end"
6853
+ }).build();
6854
+ }
6855
+
6856
+ // src/templates/event-rsvp.ts
6857
+ function createEventRsvpTemplate() {
6858
+ return FormBuilder.create("event-rsvp").layout("auto").columns(2).gap(4).addField(
6859
+ "name",
6860
+ (f) => f.type("text").label("Full name").placeholder("Jane Doe").required("Please enter your name").minLength(2, "Name must be at least 2 characters").columns({ default: 1 })
6861
+ ).addField(
6862
+ "email",
6863
+ (f) => f.type("email").label("Email").placeholder("jane@example.com").required("Please enter your email").email("Please enter a valid email address").columns({ default: 1 })
6864
+ ).addField(
6865
+ "attending",
6866
+ (f) => f.type("radio").label("Will you attend?").options([
6867
+ { label: "Yes, I will be there", value: "yes" },
6868
+ { label: "No, I cannot make it", value: "no" },
6869
+ { label: "Not sure yet", value: "maybe" }
6870
+ ]).required("Please pick one").columns({ default: 2 })
6871
+ ).addField(
6872
+ "guests",
6873
+ (f) => f.type("number").label("Number of guests (including yourself)").min(1).max(10).optional().columns({ default: 1 })
6874
+ ).addField(
6875
+ "dietary",
6876
+ (f) => f.type("textarea").label("Dietary notes").placeholder("Allergies, preferences, etc.").optional().columns({ default: 2 })
6877
+ ).addStep("main", ["name", "email", "attending", "guests", "dietary"]).initialStep("main").buttons({
6878
+ submit: { type: "submit", label: "Send RSVP" },
6879
+ align: "end"
6880
+ }).build();
6881
+ }
6882
+
6883
+ // src/templates/quote-request.ts
6884
+ function createQuoteRequestTemplate() {
6885
+ return FormBuilder.create("quote-request").layout("auto").columns(2).gap(4).addField(
6886
+ "name",
6887
+ (f) => f.type("text").label("Full name").placeholder("Jane Doe").required("Please enter your name").minLength(2, "Name must be at least 2 characters").columns({ default: 1 })
6888
+ ).addField(
6889
+ "email",
6890
+ (f) => f.type("email").label("Work email").placeholder("jane@acme.com").required("Please enter your email").email("Please enter a valid email address").columns({ default: 1 })
6891
+ ).addField(
6892
+ "company",
6893
+ (f) => f.type("text").label("Company").placeholder("Acme Inc.").required("Please enter your company name").columns({ default: 1 })
6894
+ ).addField(
6895
+ "service",
6896
+ (f) => f.type("select").label("Service type").placeholder("Pick the closest match").options([
6897
+ { label: "Design", value: "design" },
6898
+ { label: "Development", value: "development" },
6899
+ { label: "Consulting", value: "consulting" },
6900
+ { label: "Other", value: "other" }
6901
+ ]).required("Please pick a service").columns({ default: 1 })
6902
+ ).addField(
6903
+ "budget",
6904
+ (f) => f.type("select").label("Estimated budget").placeholder("Pick a range").options([
6905
+ { label: "Under $5k", value: "under-5k" },
6906
+ { label: "$5k \u2013 $25k", value: "5k-25k" },
6907
+ { label: "$25k \u2013 $100k", value: "25k-100k" },
6908
+ { label: "$100k+", value: "100k+" },
6909
+ { label: "Not sure yet", value: "unknown" }
6910
+ ]).required("Please pick a budget range").columns({ default: 2 })
6911
+ ).addField(
6912
+ "details",
6913
+ (f) => f.type("textarea").label("Project details").placeholder("Goals, timeline, anything we should know").required("Please share a few sentences about the project").minLength(20, "Project details must be at least 20 characters").columns({ default: 2 })
6914
+ ).addStep("main", ["name", "email", "company", "service", "budget", "details"]).initialStep("main").buttons({
6915
+ submit: { type: "submit", label: "Request a quote" },
6916
+ align: "end"
6917
+ }).build();
6918
+ }
6919
+
6920
+ // src/templates/index.ts
6921
+ var TEMPLATES = {
6922
+ contact: createContactTemplate,
6923
+ newsletter: createNewsletterTemplate,
6924
+ "lead-gen": createLeadGenTemplate,
6925
+ "event-rsvp": createEventRsvpTemplate,
6926
+ "quote-request": createQuoteRequestTemplate
6927
+ };
6928
+ var TEMPLATE_META = [
6929
+ {
6930
+ id: "contact",
6931
+ label: "Contact",
6932
+ description: "Name, email, and a message. The classic three-field form.",
6933
+ fieldCount: 3
6934
+ },
6935
+ {
6936
+ id: "newsletter",
6937
+ label: "Newsletter",
6938
+ description: "Email plus a GDPR-friendly consent checkbox.",
6939
+ fieldCount: 2
6940
+ },
6941
+ {
6942
+ id: "lead-gen",
6943
+ label: "Lead generation",
6944
+ description: "Name, work email, phone, company, and a brief message.",
6945
+ fieldCount: 5
6946
+ },
6947
+ {
6948
+ id: "event-rsvp",
6949
+ label: "Event RSVP",
6950
+ description: "Name, email, attendance answer, guest count, and dietary notes.",
6951
+ fieldCount: 5
6952
+ },
6953
+ {
6954
+ id: "quote-request",
6955
+ label: "Quote request",
6956
+ description: "Contact info plus service type, budget range, and a details field.",
6957
+ fieldCount: 6
6958
+ }
6959
+ ];
6960
+
6961
+ // src/lib/createHubFormSubmit.ts
6962
+ function publicBase({ hubUrl, siteId, formSlug }) {
6963
+ const cleanHub = hubUrl.replace(/\/+$/, "");
6964
+ return `${cleanHub}/api/public/forms/${encodeURIComponent(siteId)}/${encodeURIComponent(formSlug)}`;
6965
+ }
6966
+ async function presignAndUpload(base, file) {
6967
+ const presignRes = await fetch(`${base}/upload-url`, {
6968
+ method: "POST",
6969
+ headers: { "content-type": "application/json" },
6970
+ body: JSON.stringify({
6971
+ filename: file.name,
6972
+ size: file.size,
6973
+ mime: file.type || "application/octet-stream"
6974
+ })
6975
+ });
6976
+ const presignBody = await presignRes.json().catch(() => ({}));
6977
+ if (!presignRes.ok || !presignBody.ok || !presignBody.uploadUrl || !presignBody.key) {
6978
+ const detail = presignBody.detail ?? presignBody.error ?? presignRes.statusText;
6979
+ throw new Error(`upload-url failed: ${detail}`);
6980
+ }
6981
+ const putRes = await fetch(presignBody.uploadUrl, {
6982
+ method: "PUT",
6983
+ headers: { "content-type": file.type || "application/octet-stream" },
6984
+ body: file
6985
+ });
6986
+ if (!putRes.ok) {
6987
+ throw new Error(`upload PUT to R2 failed: ${putRes.status} ${putRes.statusText}`);
6988
+ }
6989
+ return {
6990
+ key: presignBody.key,
6991
+ filename: file.name,
6992
+ size: file.size,
6993
+ mime: file.type || "application/octet-stream"
6994
+ };
6995
+ }
6996
+ function partitionValues(values) {
6997
+ const payload = {};
6998
+ const files = [];
6999
+ let turnstileToken = null;
7000
+ const honeypotEntries = [];
7001
+ for (const [key, value] of Object.entries(values)) {
7002
+ if (key === "_turnstile") {
7003
+ if (typeof value === "string") turnstileToken = value;
7004
+ continue;
7005
+ }
7006
+ if (key.startsWith("_hp") || key === "_hp") {
7007
+ if (typeof value === "string") {
7008
+ honeypotEntries.push({ field: key, value });
7009
+ }
7010
+ continue;
7011
+ }
7012
+ if (typeof File !== "undefined" && value instanceof File) {
7013
+ files.push({ field: key, file: value });
7014
+ continue;
7015
+ }
7016
+ if (Array.isArray(value)) {
7017
+ const onlyFiles = value.every((v) => typeof File !== "undefined" && v instanceof File);
7018
+ if (onlyFiles && value.length > 0) {
7019
+ for (const f of value) files.push({ field: key, file: f });
7020
+ continue;
7021
+ }
7022
+ }
7023
+ payload[key] = value;
7024
+ }
7025
+ return { payload, files, turnstileToken, honeypotEntries };
7026
+ }
7027
+ function createHubFormSubmit(opts) {
7028
+ const base = publicBase(opts);
7029
+ return {
7030
+ type: "custom",
7031
+ async onSubmit(values) {
7032
+ const { payload, files, turnstileToken, honeypotEntries } = partitionValues(values);
7033
+ const attachments = files.length ? await Promise.all(files.map(({ file }) => presignAndUpload(base, file))) : void 0;
7034
+ const honeypotEntry = {};
7035
+ for (const { field, value } of honeypotEntries) honeypotEntry[field] = value;
7036
+ const submitRes = await fetch(`${base}/submit`, {
7037
+ method: "POST",
7038
+ headers: { "content-type": "application/json" },
7039
+ body: JSON.stringify({
7040
+ payload,
7041
+ attachments,
7042
+ turnstile: turnstileToken ?? void 0,
7043
+ ...honeypotEntry
7044
+ })
7045
+ });
7046
+ const body = await submitRes.json().catch(() => ({}));
7047
+ if (!submitRes.ok || !body.ok) {
7048
+ const detail = body.detail ?? body.error ?? submitRes.statusText;
7049
+ throw new Error(`submit failed: ${detail}`);
7050
+ }
7051
+ }
7052
+ };
7053
+ }
7054
+
7055
+ // src/components/HubForm.tsx
7056
+ import { useEffect as useEffect8, useMemo as useMemo7, useState as useState5 } from "react";
7057
+ import { Fragment as Fragment3, jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
7058
+ function isRenderableSchema(value) {
7059
+ if (!value || typeof value !== "object") return false;
7060
+ const schema = value;
7061
+ const fields = schema.fields;
7062
+ const steps = schema.steps;
7063
+ if (!fields || typeof fields !== "object" || Object.keys(fields).length === 0) {
7064
+ return false;
7065
+ }
7066
+ if (!steps || typeof steps !== "object" || Object.keys(steps).length === 0) {
7067
+ return false;
7068
+ }
7069
+ return true;
7070
+ }
7071
+ function HubForm({
7072
+ hubUrl,
7073
+ siteId,
7074
+ formSlug,
7075
+ loadingFallback,
7076
+ errorFallback,
7077
+ unconfiguredFallback,
7078
+ successFallback,
7079
+ onSuccess,
7080
+ onError,
7081
+ formProps
7082
+ }) {
7083
+ const [state, setState] = useState5({ status: "loading" });
7084
+ useEffect8(() => {
7085
+ let cancelled = false;
7086
+ const cleanHub = hubUrl.replace(/\/+$/, "");
7087
+ const url = `${cleanHub}/api/public/forms/${encodeURIComponent(siteId)}/${encodeURIComponent(formSlug)}.json`;
7088
+ fetch(url, { headers: { accept: "application/json" } }).then(async (res) => {
7089
+ if (!res.ok) throw new Error(`schema fetch failed: ${res.status} ${res.statusText}`);
7090
+ return res.json();
7091
+ }).then((schema) => {
7092
+ if (cancelled) return;
7093
+ if (isRenderableSchema(schema)) {
7094
+ setState({ status: "ready", schema });
7095
+ } else {
7096
+ setState({ status: "unconfigured" });
7097
+ }
7098
+ }).catch((err) => {
7099
+ if (!cancelled) {
7100
+ setState({
7101
+ status: "error",
7102
+ error: err instanceof Error ? err.message : String(err)
7103
+ });
7104
+ }
7105
+ });
7106
+ return () => {
7107
+ cancelled = true;
7108
+ };
7109
+ }, [hubUrl, siteId, formSlug]);
7110
+ const wrappedSubmit = useMemo7(() => {
7111
+ const base = createHubFormSubmit({ hubUrl, siteId, formSlug });
7112
+ return {
7113
+ type: "custom",
7114
+ async onSubmit(values) {
7115
+ try {
7116
+ await base.onSubmit(values);
7117
+ onSuccess?.(values);
7118
+ setState({ status: "submitted" });
7119
+ } catch (err) {
7120
+ const error = err instanceof Error ? err : new Error(String(err));
7121
+ onError?.(error);
7122
+ throw error;
7123
+ }
7124
+ }
7125
+ };
7126
+ }, [hubUrl, siteId, formSlug, onSuccess, onError]);
7127
+ if (state.status === "loading") {
7128
+ return loadingFallback ?? /* @__PURE__ */ jsx14("div", { "data-saastro-hubform-loading": true, style: loadingStyle, children: "Loading\u2026" });
7129
+ }
7130
+ if (state.status === "error") {
7131
+ if (errorFallback) return /* @__PURE__ */ jsx14(Fragment3, { children: errorFallback(state.error) });
7132
+ return /* @__PURE__ */ jsxs11("div", { role: "alert", style: errorStyle, children: [
7133
+ "Could not load form: ",
7134
+ state.error
7135
+ ] });
7136
+ }
7137
+ if (state.status === "unconfigured") {
7138
+ if (unconfiguredFallback !== void 0) return /* @__PURE__ */ jsx14(Fragment3, { children: unconfiguredFallback });
7139
+ return /* @__PURE__ */ jsx14("div", { role: "status", style: unconfiguredStyle, "data-saastro-hubform-unconfigured": true, children: "This form has not been configured yet. Open it in the Hub builder to add fields." });
7140
+ }
7141
+ if (state.status === "submitted") {
7142
+ if (successFallback !== void 0) return /* @__PURE__ */ jsx14(Fragment3, { children: successFallback });
7143
+ return /* @__PURE__ */ jsx14("div", { role: "status", style: successStyle, "data-saastro-hubform-submitted": true, children: "Thanks \u2014 we'll get back to you soon." });
7144
+ }
7145
+ const config = {
7146
+ ...state.schema,
7147
+ submit: wrappedSubmit
7148
+ };
7149
+ return /* @__PURE__ */ jsx14(Form, { ...formProps, config });
7150
+ }
7151
+ var loadingStyle = {
7152
+ padding: "2rem 1rem",
7153
+ textAlign: "center",
7154
+ color: "#888",
7155
+ fontFamily: "system-ui, -apple-system, sans-serif",
7156
+ fontSize: "0.9rem"
7157
+ };
7158
+ var errorStyle = {
7159
+ padding: "1rem",
7160
+ background: "#fef2f2",
7161
+ color: "#991b1b",
7162
+ border: "1px solid #fecaca",
7163
+ borderRadius: "0.375rem",
7164
+ fontFamily: "system-ui, -apple-system, sans-serif",
7165
+ fontSize: "0.875rem"
7166
+ };
7167
+ var unconfiguredStyle = {
7168
+ padding: "1rem",
7169
+ background: "#fffbeb",
7170
+ color: "#92400e",
7171
+ border: "1px solid #fcd34d",
7172
+ borderRadius: "0.375rem",
7173
+ fontFamily: "system-ui, -apple-system, sans-serif",
7174
+ fontSize: "0.875rem"
7175
+ };
7176
+ var successStyle = {
7177
+ padding: "1.25rem 1rem",
7178
+ background: "#ecfdf5",
7179
+ color: "#065f46",
7180
+ border: "1px solid #6ee7b7",
7181
+ borderRadius: "0.375rem",
7182
+ fontFamily: "system-ui, -apple-system, sans-serif",
7183
+ fontSize: "0.95rem",
7184
+ textAlign: "center"
7185
+ };
7186
+
6802
7187
  // src/utils/testDataGenerator.ts
6803
7188
  var SPANISH_PATTERNS = [
6804
7189
  "nombre",
@@ -7029,6 +7414,7 @@ export {
7029
7414
  Form,
7030
7415
  FormBuilder,
7031
7416
  FormComponentsProvider,
7417
+ HubForm,
7032
7418
  InternalComponentProvider,
7033
7419
  MissingComponentFallback,
7034
7420
  OPTION_BASED_TYPES,
@@ -7037,6 +7423,8 @@ export {
7037
7423
  StepsAccordion,
7038
7424
  StepsNavigation,
7039
7425
  StepsProgress,
7426
+ TEMPLATES,
7427
+ TEMPLATE_META,
7040
7428
  TYPE_SPECIFIC_PROPERTIES,
7041
7429
  VALIDATION_METHODS,
7042
7430
  analyticsPlugin,
@@ -7050,7 +7438,13 @@ export {
7050
7438
  configureComponents,
7051
7439
  coreComponents,
7052
7440
  createComponentRegistry,
7441
+ createContactTemplate,
7442
+ createEventRsvpTemplate,
7443
+ createHubFormSubmit,
7444
+ createLeadGenTemplate,
7053
7445
  createMissingComponentPlaceholder,
7446
+ createNewsletterTemplate,
7447
+ createQuoteRequestTemplate,
7054
7448
  createShadcnRegistry,
7055
7449
  databowlAction,
7056
7450
  databowlPlugin,