@btst/stack 1.5.2 → 1.6.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.
Files changed (201) hide show
  1. package/dist/node_modules/.pnpm/@dnd-kit_accessibility@3.1.1_react@19.2.0/node_modules/@dnd-kit/accessibility/dist/accessibility.esm.cjs +68 -0
  2. package/dist/node_modules/.pnpm/@dnd-kit_accessibility@3.1.1_react@19.2.0/node_modules/@dnd-kit/accessibility/dist/accessibility.esm.mjs +60 -0
  3. package/dist/node_modules/.pnpm/@dnd-kit_core@6.3.1_react-dom@19.2.0_react@19.2.0__react@19.2.0/node_modules/@dnd-kit/core/dist/core.esm.cjs +3937 -0
  4. package/dist/node_modules/.pnpm/@dnd-kit_core@6.3.1_react-dom@19.2.0_react@19.2.0__react@19.2.0/node_modules/@dnd-kit/core/dist/core.esm.mjs +3907 -0
  5. package/dist/node_modules/.pnpm/@dnd-kit_modifiers@9.0.0_@dnd-kit_core@6.3.1_react-dom@19.2.0_react@19.2.0__react@19.2.0__react@19.2.0/node_modules/@dnd-kit/modifiers/dist/modifiers.esm.cjs +30 -0
  6. package/dist/node_modules/.pnpm/@dnd-kit_modifiers@9.0.0_@dnd-kit_core@6.3.1_react-dom@19.2.0_react@19.2.0__react@19.2.0__react@19.2.0/node_modules/@dnd-kit/modifiers/dist/modifiers.esm.mjs +28 -0
  7. package/dist/node_modules/.pnpm/@dnd-kit_sortable@10.0.0_@dnd-kit_core@6.3.1_react-dom@19.2.0_react@19.2.0__react@19.2.0__react@19.2.0/node_modules/@dnd-kit/sortable/dist/sortable.esm.cjs +675 -0
  8. package/dist/node_modules/.pnpm/@dnd-kit_sortable@10.0.0_@dnd-kit_core@6.3.1_react-dom@19.2.0_react@19.2.0__react@19.2.0__react@19.2.0/node_modules/@dnd-kit/sortable/dist/sortable.esm.mjs +661 -0
  9. package/dist/node_modules/.pnpm/@dnd-kit_utilities@3.2.2_react@19.2.0/node_modules/@dnd-kit/utilities/dist/utilities.esm.cjs +358 -0
  10. package/dist/node_modules/.pnpm/@dnd-kit_utilities@3.2.2_react@19.2.0/node_modules/@dnd-kit/utilities/dist/utilities.esm.mjs +332 -0
  11. package/dist/node_modules/.pnpm/@radix-ui_react-tabs@1.1.13_@types_react-dom@19.2.3_@types_react@19.2.6__@types_react@1_865f042350eb43f3338b0fffb33f6246/node_modules/@radix-ui/react-tabs/dist/index.cjs +211 -0
  12. package/dist/node_modules/.pnpm/@radix-ui_react-tabs@1.1.13_@types_react-dom@19.2.3_@types_react@19.2.6__@types_react@1_865f042350eb43f3338b0fffb33f6246/node_modules/@radix-ui/react-tabs/dist/index.mjs +188 -0
  13. package/dist/packages/better-stack/src/plugins/cms/api/plugin.cjs +3 -2
  14. package/dist/packages/better-stack/src/plugins/cms/api/plugin.mjs +3 -2
  15. package/dist/packages/better-stack/src/plugins/cms/client/components/forms/content-form.cjs +15 -15
  16. package/dist/packages/better-stack/src/plugins/cms/client/components/forms/content-form.mjs +16 -16
  17. package/dist/packages/better-stack/src/plugins/form-builder/api/plugin.cjs +588 -0
  18. package/dist/packages/better-stack/src/plugins/form-builder/api/plugin.mjs +586 -0
  19. package/dist/packages/better-stack/src/plugins/form-builder/client/components/forms/form-renderer.cjs +131 -0
  20. package/dist/packages/better-stack/src/plugins/form-builder/client/components/forms/form-renderer.mjs +129 -0
  21. package/dist/packages/better-stack/src/plugins/form-builder/client/components/loading/form-builder-skeleton.cjs +32 -0
  22. package/dist/packages/better-stack/src/plugins/form-builder/client/components/loading/form-builder-skeleton.mjs +30 -0
  23. package/dist/packages/better-stack/src/plugins/form-builder/client/components/loading/form-list-skeleton.cjs +21 -0
  24. package/dist/packages/better-stack/src/plugins/form-builder/client/components/loading/form-list-skeleton.mjs +19 -0
  25. package/dist/packages/better-stack/src/plugins/form-builder/client/components/loading/submissions-skeleton.cjs +34 -0
  26. package/dist/packages/better-stack/src/plugins/form-builder/client/components/loading/submissions-skeleton.mjs +32 -0
  27. package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/404-page.cjs +20 -0
  28. package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/404-page.mjs +18 -0
  29. package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/form-builder-page.cjs +19 -0
  30. package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/form-builder-page.internal.cjs +186 -0
  31. package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/form-builder-page.internal.mjs +184 -0
  32. package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/form-builder-page.mjs +17 -0
  33. package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/form-list-page.cjs +19 -0
  34. package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/form-list-page.internal.cjs +165 -0
  35. package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/form-list-page.internal.mjs +163 -0
  36. package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/form-list-page.mjs +17 -0
  37. package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/submissions-page.cjs +19 -0
  38. package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/submissions-page.internal.cjs +177 -0
  39. package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/submissions-page.internal.mjs +175 -0
  40. package/dist/packages/better-stack/src/plugins/form-builder/client/components/pages/submissions-page.mjs +17 -0
  41. package/dist/packages/better-stack/src/plugins/form-builder/client/components/shared/default-error.cjs +17 -0
  42. package/dist/packages/better-stack/src/plugins/form-builder/client/components/shared/default-error.mjs +15 -0
  43. package/dist/packages/better-stack/src/plugins/form-builder/client/components/shared/empty-state.cjs +16 -0
  44. package/dist/packages/better-stack/src/plugins/form-builder/client/components/shared/empty-state.mjs +14 -0
  45. package/dist/packages/better-stack/src/plugins/form-builder/client/components/shared/page-wrapper.cjs +27 -0
  46. package/dist/packages/better-stack/src/plugins/form-builder/client/components/shared/page-wrapper.mjs +25 -0
  47. package/dist/packages/better-stack/src/plugins/form-builder/client/components/shared/pagination.cjs +39 -0
  48. package/dist/packages/better-stack/src/plugins/form-builder/client/components/shared/pagination.mjs +37 -0
  49. package/dist/packages/better-stack/src/plugins/form-builder/client/hooks/form-builder-hooks.cjs +551 -0
  50. package/dist/packages/better-stack/src/plugins/form-builder/client/hooks/form-builder-hooks.mjs +537 -0
  51. package/dist/packages/better-stack/src/plugins/form-builder/client/localization/form-builder-common.cjs +36 -0
  52. package/dist/packages/better-stack/src/plugins/form-builder/client/localization/form-builder-common.mjs +34 -0
  53. package/dist/packages/better-stack/src/plugins/form-builder/client/localization/form-builder-editor.cjs +19 -0
  54. package/dist/packages/better-stack/src/plugins/form-builder/client/localization/form-builder-editor.mjs +17 -0
  55. package/dist/packages/better-stack/src/plugins/form-builder/client/localization/form-builder-list.cjs +21 -0
  56. package/dist/packages/better-stack/src/plugins/form-builder/client/localization/form-builder-list.mjs +19 -0
  57. package/dist/packages/better-stack/src/plugins/form-builder/client/localization/form-builder-submissions.cjs +19 -0
  58. package/dist/packages/better-stack/src/plugins/form-builder/client/localization/form-builder-submissions.mjs +17 -0
  59. package/dist/packages/better-stack/src/plugins/form-builder/client/localization/form-builder-toasts.cjs +14 -0
  60. package/dist/packages/better-stack/src/plugins/form-builder/client/localization/form-builder-toasts.mjs +12 -0
  61. package/dist/packages/better-stack/src/plugins/form-builder/client/localization/index.cjs +17 -0
  62. package/dist/packages/better-stack/src/plugins/form-builder/client/localization/index.mjs +15 -0
  63. package/dist/packages/better-stack/src/plugins/form-builder/client/plugin.cjs +278 -0
  64. package/dist/packages/better-stack/src/plugins/form-builder/client/plugin.mjs +276 -0
  65. package/dist/packages/better-stack/src/plugins/form-builder/db.cjs +99 -0
  66. package/dist/packages/better-stack/src/plugins/form-builder/db.mjs +97 -0
  67. package/dist/packages/better-stack/src/plugins/form-builder/schemas.cjs +82 -0
  68. package/dist/packages/better-stack/src/plugins/form-builder/schemas.mjs +74 -0
  69. package/dist/packages/better-stack/src/plugins/form-builder/utils.cjs +37 -0
  70. package/dist/packages/better-stack/src/plugins/form-builder/utils.mjs +29 -0
  71. package/dist/packages/ui/src/components/auto-form/index.cjs +2 -12
  72. package/dist/packages/ui/src/components/auto-form/index.mjs +2 -9
  73. package/dist/packages/ui/src/components/auto-form/stepped-auto-form.cjs +377 -0
  74. package/dist/packages/ui/src/components/auto-form/stepped-auto-form.mjs +368 -0
  75. package/dist/packages/ui/src/components/auto-form/utils.cjs +1 -56
  76. package/dist/packages/ui/src/components/auto-form/utils.mjs +2 -56
  77. package/dist/packages/ui/src/components/form-builder/canvas.cjs +111 -0
  78. package/dist/packages/ui/src/components/form-builder/canvas.mjs +109 -0
  79. package/dist/packages/ui/src/components/form-builder/components/index.cjs +570 -0
  80. package/dist/packages/ui/src/components/form-builder/components/index.mjs +553 -0
  81. package/dist/packages/ui/src/components/form-builder/edit-field-dialog.cjs +131 -0
  82. package/dist/packages/ui/src/components/form-builder/edit-field-dialog.mjs +129 -0
  83. package/dist/packages/ui/src/components/form-builder/form-preview.cjs +73 -0
  84. package/dist/packages/ui/src/components/form-builder/form-preview.mjs +71 -0
  85. package/dist/packages/ui/src/components/form-builder/index.cjs +353 -0
  86. package/dist/packages/ui/src/components/form-builder/index.mjs +344 -0
  87. package/dist/packages/ui/src/components/form-builder/nested-field-editor-dialog.cjs +263 -0
  88. package/dist/packages/ui/src/components/form-builder/nested-field-editor-dialog.mjs +261 -0
  89. package/dist/packages/ui/src/components/form-builder/palette.cjs +52 -0
  90. package/dist/packages/ui/src/components/form-builder/palette.mjs +49 -0
  91. package/dist/packages/ui/src/components/form-builder/schema-utils.cjs +120 -0
  92. package/dist/packages/ui/src/components/form-builder/schema-utils.mjs +114 -0
  93. package/dist/packages/ui/src/components/form-builder/sortable-field.cjs +151 -0
  94. package/dist/packages/ui/src/components/form-builder/sortable-field.mjs +148 -0
  95. package/dist/packages/ui/src/components/form-builder/step-tabs.cjs +180 -0
  96. package/dist/packages/ui/src/components/form-builder/step-tabs.mjs +178 -0
  97. package/dist/packages/ui/src/components/form-builder/types.cjs +7 -0
  98. package/dist/packages/ui/src/components/form-builder/types.mjs +5 -0
  99. package/dist/packages/ui/src/components/form-builder/validation-schemas.cjs +67 -0
  100. package/dist/packages/ui/src/components/form-builder/validation-schemas.mjs +56 -0
  101. package/dist/packages/ui/src/components/tabs.cjs +70 -0
  102. package/dist/packages/ui/src/components/tabs.mjs +65 -0
  103. package/dist/packages/ui/src/lib/schema-converter.cjs +130 -0
  104. package/dist/packages/ui/src/lib/schema-converter.mjs +124 -0
  105. package/dist/plugins/blog/api/index.d.cts +1 -1
  106. package/dist/plugins/blog/api/index.d.mts +1 -1
  107. package/dist/plugins/blog/api/index.d.ts +1 -1
  108. package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
  109. package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
  110. package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
  111. package/dist/plugins/blog/client/index.d.cts +1 -1
  112. package/dist/plugins/blog/client/index.d.mts +1 -1
  113. package/dist/plugins/blog/client/index.d.ts +1 -1
  114. package/dist/plugins/blog/query-keys.d.cts +2 -2
  115. package/dist/plugins/blog/query-keys.d.mts +2 -2
  116. package/dist/plugins/blog/query-keys.d.ts +2 -2
  117. package/dist/plugins/cms/client/index.cjs +6 -0
  118. package/dist/plugins/cms/client/index.d.cts +6 -113
  119. package/dist/plugins/cms/client/index.d.mts +6 -113
  120. package/dist/plugins/cms/client/index.d.ts +6 -113
  121. package/dist/plugins/cms/client/index.mjs +1 -0
  122. package/dist/plugins/form-builder/api/index.cjs +7 -0
  123. package/dist/plugins/form-builder/api/index.d.cts +141 -0
  124. package/dist/plugins/form-builder/api/index.d.mts +141 -0
  125. package/dist/plugins/form-builder/api/index.d.ts +141 -0
  126. package/dist/plugins/form-builder/api/index.mjs +1 -0
  127. package/dist/plugins/form-builder/client/components/index.cjs +29 -0
  128. package/dist/plugins/form-builder/client/components/index.d.cts +93 -0
  129. package/dist/plugins/form-builder/client/components/index.d.mts +93 -0
  130. package/dist/plugins/form-builder/client/components/index.d.ts +93 -0
  131. package/dist/plugins/form-builder/client/components/index.mjs +18 -0
  132. package/dist/plugins/form-builder/client/hooks/index.cjs +19 -0
  133. package/dist/plugins/form-builder/client/hooks/index.d.cts +154 -0
  134. package/dist/plugins/form-builder/client/hooks/index.d.mts +154 -0
  135. package/dist/plugins/form-builder/client/hooks/index.d.ts +154 -0
  136. package/dist/plugins/form-builder/client/hooks/index.mjs +1 -0
  137. package/dist/plugins/form-builder/client/index.cjs +13 -0
  138. package/dist/plugins/form-builder/client/index.d.cts +381 -0
  139. package/dist/plugins/form-builder/client/index.d.mts +381 -0
  140. package/dist/plugins/form-builder/client/index.d.ts +381 -0
  141. package/dist/plugins/form-builder/client/index.mjs +2 -0
  142. package/dist/plugins/form-builder/client.css +3 -0
  143. package/dist/plugins/form-builder/query-keys.cjs +143 -0
  144. package/dist/plugins/form-builder/query-keys.d.cts +74 -0
  145. package/dist/plugins/form-builder/query-keys.d.mts +74 -0
  146. package/dist/plugins/form-builder/query-keys.d.ts +74 -0
  147. package/dist/plugins/form-builder/query-keys.mjs +141 -0
  148. package/dist/plugins/form-builder/style.css +19 -0
  149. package/dist/shared/stack.AX5nZ6A3.d.cts +86 -0
  150. package/dist/shared/stack.AX5nZ6A3.d.mts +86 -0
  151. package/dist/shared/stack.AX5nZ6A3.d.ts +86 -0
  152. package/dist/shared/stack.BIh2AXaW.d.cts +123 -0
  153. package/dist/shared/stack.BIh2AXaW.d.mts +123 -0
  154. package/dist/shared/stack.BIh2AXaW.d.ts +123 -0
  155. package/dist/shared/stack.DzH_wcvr.d.cts +195 -0
  156. package/dist/shared/stack.DzH_wcvr.d.mts +195 -0
  157. package/dist/shared/stack.DzH_wcvr.d.ts +195 -0
  158. package/package.json +54 -1
  159. package/src/plugins/cms/api/plugin.ts +9 -4
  160. package/src/plugins/cms/client/components/forms/content-form.tsx +23 -25
  161. package/src/plugins/cms/client/index.ts +11 -0
  162. package/src/plugins/form-builder/api/index.ts +1 -0
  163. package/src/plugins/form-builder/api/plugin.ts +776 -0
  164. package/src/plugins/form-builder/client/components/forms/form-renderer.tsx +253 -0
  165. package/src/plugins/form-builder/client/components/index.tsx +24 -0
  166. package/src/plugins/form-builder/client/components/loading/form-builder-skeleton.tsx +42 -0
  167. package/src/plugins/form-builder/client/components/loading/form-list-skeleton.tsx +25 -0
  168. package/src/plugins/form-builder/client/components/loading/index.tsx +3 -0
  169. package/src/plugins/form-builder/client/components/loading/submissions-skeleton.tsx +40 -0
  170. package/src/plugins/form-builder/client/components/pages/404-page.tsx +28 -0
  171. package/src/plugins/form-builder/client/components/pages/form-builder-page.internal.tsx +253 -0
  172. package/src/plugins/form-builder/client/components/pages/form-builder-page.tsx +26 -0
  173. package/src/plugins/form-builder/client/components/pages/form-list-page.internal.tsx +231 -0
  174. package/src/plugins/form-builder/client/components/pages/form-list-page.tsx +22 -0
  175. package/src/plugins/form-builder/client/components/pages/submissions-page.internal.tsx +268 -0
  176. package/src/plugins/form-builder/client/components/pages/submissions-page.tsx +26 -0
  177. package/src/plugins/form-builder/client/components/shared/default-error.tsx +30 -0
  178. package/src/plugins/form-builder/client/components/shared/empty-state.tsx +26 -0
  179. package/src/plugins/form-builder/client/components/shared/page-wrapper.tsx +32 -0
  180. package/src/plugins/form-builder/client/components/shared/pagination.tsx +52 -0
  181. package/src/plugins/form-builder/client/hooks/form-builder-hooks.tsx +799 -0
  182. package/src/plugins/form-builder/client/hooks/index.tsx +1 -0
  183. package/src/plugins/form-builder/client/index.ts +22 -0
  184. package/src/plugins/form-builder/client/localization/form-builder-common.ts +36 -0
  185. package/src/plugins/form-builder/client/localization/form-builder-editor.ts +18 -0
  186. package/src/plugins/form-builder/client/localization/form-builder-list.ts +17 -0
  187. package/src/plugins/form-builder/client/localization/form-builder-submissions.ts +17 -0
  188. package/src/plugins/form-builder/client/localization/form-builder-toasts.ts +10 -0
  189. package/src/plugins/form-builder/client/localization/index.ts +15 -0
  190. package/src/plugins/form-builder/client/overrides.ts +146 -0
  191. package/src/plugins/form-builder/client/plugin.tsx +488 -0
  192. package/src/plugins/form-builder/client.css +3 -0
  193. package/src/plugins/form-builder/db.ts +99 -0
  194. package/src/plugins/form-builder/query-keys.ts +198 -0
  195. package/src/plugins/form-builder/schemas.ts +122 -0
  196. package/src/plugins/form-builder/style.css +19 -0
  197. package/src/plugins/form-builder/types.ts +317 -0
  198. package/src/plugins/form-builder/utils.ts +63 -0
  199. package/dist/shared/{stack.DLhzx1-D.d.cts → stack.CcI4sYJP.d.cts} +1 -1
  200. package/dist/shared/{stack.DLhzx1-D.d.mts → stack.CcI4sYJP.d.mts} +1 -1
  201. package/dist/shared/{stack.DLhzx1-D.d.ts → stack.CcI4sYJP.d.ts} +1 -1
@@ -0,0 +1,253 @@
1
+ "use client";
2
+
3
+ import { useState, useMemo, type ComponentType } from "react";
4
+ import { usePluginOverrides } from "@btst/stack/context";
5
+ import { SteppedAutoForm } from "@workspace/ui/components/auto-form/stepped-auto-form";
6
+ import { buildFieldConfigFromJsonSchema } from "@workspace/ui/components/auto-form/utils";
7
+ import { formSchemaToZod } from "@workspace/ui/lib/schema-converter";
8
+ import { Skeleton } from "@workspace/ui/components/skeleton";
9
+ import { AlertCircle, CheckCircle } from "lucide-react";
10
+ import type { AutoFormInputComponentProps } from "@workspace/ui/components/auto-form/types";
11
+
12
+ import { useFormBySlug, useSubmitForm } from "../../hooks/form-builder-hooks";
13
+ import type { FormBuilderPluginOverrides } from "../../overrides";
14
+ import { FORM_BUILDER_LOCALIZATION } from "../../localization";
15
+ import type { SerializedFormSubmission } from "../../../types";
16
+
17
+ export interface FormRendererProps {
18
+ /** Form slug to render */
19
+ slug: string;
20
+ /** Callback when form submission succeeds */
21
+ onSuccess?: (
22
+ submission: SerializedFormSubmission & {
23
+ form: { successMessage?: string; redirectUrl?: string };
24
+ },
25
+ ) => void;
26
+ /** Callback when form submission fails */
27
+ onError?: (error: Error) => void;
28
+ /** Custom field components (same as FormBuilder) */
29
+ fieldComponents?: Record<string, ComponentType<AutoFormInputComponentProps>>;
30
+ /** Override success message */
31
+ successMessage?: React.ReactNode;
32
+ /** Override submit button text */
33
+ submitButtonText?: string;
34
+ /** Custom loading component */
35
+ LoadingComponent?: ComponentType;
36
+ /** Custom error component */
37
+ ErrorComponent?: ComponentType<{ error: Error }>;
38
+ /** Class name for the form container */
39
+ className?: string;
40
+ }
41
+
42
+ function DefaultLoadingComponent() {
43
+ return (
44
+ <div className="space-y-4">
45
+ <Skeleton className="h-10 w-full" />
46
+ <Skeleton className="h-10 w-full" />
47
+ <Skeleton className="h-10 w-full" />
48
+ <Skeleton className="h-10 w-32" />
49
+ </div>
50
+ );
51
+ }
52
+
53
+ function DefaultErrorComponent({ error }: { error: Error }) {
54
+ return (
55
+ <div className="flex flex-col items-center justify-center py-8 text-center">
56
+ <div className="rounded-full bg-destructive/10 p-3 mb-4">
57
+ <AlertCircle className="h-6 w-6 text-destructive" />
58
+ </div>
59
+ <h3 className="text-lg font-medium text-foreground mb-2">
60
+ Failed to load form
61
+ </h3>
62
+ <p className="text-sm text-muted-foreground max-w-sm">
63
+ {error.message || "An unexpected error occurred"}
64
+ </p>
65
+ </div>
66
+ );
67
+ }
68
+
69
+ function DefaultSuccessComponent({ message }: { message: React.ReactNode }) {
70
+ return (
71
+ <div className="flex flex-col items-center justify-center py-8 text-center">
72
+ <div className="rounded-full bg-green-100 dark:bg-green-900 p-3 mb-4">
73
+ <CheckCircle className="h-6 w-6 text-green-600 dark:text-green-400" />
74
+ </div>
75
+ <h3 className="text-lg font-medium text-foreground mb-2">
76
+ Form Submitted
77
+ </h3>
78
+ <p className="text-sm text-muted-foreground max-w-sm">{message}</p>
79
+ </div>
80
+ );
81
+ }
82
+
83
+ /**
84
+ * FormRenderer component for rendering forms on the frontend.
85
+ *
86
+ * Uses SteppedAutoForm which automatically handles both single-step and multi-step forms.
87
+ *
88
+ * @example
89
+ * ```tsx
90
+ * <FormRenderer
91
+ * slug="contact-form"
92
+ * onSuccess={() => {
93
+ * toast.success("Thank you!");
94
+ * }}
95
+ * onError={(error) => {
96
+ * toast.error("Something went wrong");
97
+ * }}
98
+ * />
99
+ * ```
100
+ */
101
+ export function FormRenderer({
102
+ slug,
103
+ onSuccess,
104
+ onError,
105
+ fieldComponents: propFieldComponents,
106
+ successMessage: propSuccessMessage,
107
+ submitButtonText,
108
+ LoadingComponent = DefaultLoadingComponent,
109
+ ErrorComponent = DefaultErrorComponent,
110
+ className,
111
+ }: FormRendererProps) {
112
+ const { fieldComponents: overrideFieldComponents, localization } =
113
+ usePluginOverrides<
114
+ FormBuilderPluginOverrides,
115
+ Partial<FormBuilderPluginOverrides>
116
+ >("form-builder", {
117
+ localization: FORM_BUILDER_LOCALIZATION,
118
+ });
119
+
120
+ const loc = localization || FORM_BUILDER_LOCALIZATION;
121
+
122
+ const { form, isLoading, error } = useFormBySlug(slug);
123
+ const submitMutation = useSubmitForm(slug);
124
+
125
+ const [submitted, setSubmitted] = useState(false);
126
+ const [finalSuccessMessage, setFinalSuccessMessage] = useState<string | null>(
127
+ null,
128
+ );
129
+
130
+ // Merge field components from props and overrides
131
+ const mergedFieldComponents = useMemo(
132
+ () => ({
133
+ ...overrideFieldComponents,
134
+ ...propFieldComponents,
135
+ }),
136
+ [overrideFieldComponents, propFieldComponents],
137
+ );
138
+
139
+ // Parse JSON Schema and create Zod schema
140
+ const { zodSchema, fieldConfig } = useMemo(() => {
141
+ if (!form?.schema) {
142
+ return { zodSchema: null, fieldConfig: {} };
143
+ }
144
+
145
+ try {
146
+ const parsedSchema = JSON.parse(form.schema);
147
+ const zod = formSchemaToZod(parsedSchema);
148
+ const config = buildFieldConfigFromJsonSchema(
149
+ parsedSchema,
150
+ mergedFieldComponents,
151
+ );
152
+
153
+ return { zodSchema: zod, fieldConfig: config };
154
+ } catch {
155
+ return { zodSchema: null, fieldConfig: {} };
156
+ }
157
+ }, [form?.schema, mergedFieldComponents]);
158
+
159
+ const handleSubmit = async (data: Record<string, unknown>) => {
160
+ try {
161
+ const result = await submitMutation.mutateAsync({ data });
162
+
163
+ // Set success message
164
+ const message =
165
+ propSuccessMessage ||
166
+ result.form.successMessage ||
167
+ "Thank you for your submission!";
168
+ setFinalSuccessMessage(message as string);
169
+ setSubmitted(true);
170
+
171
+ // Call onSuccess callback before any redirect
172
+ onSuccess?.(result);
173
+
174
+ // Handle redirect
175
+ if (result.form.redirectUrl) {
176
+ window.location.href = result.form.redirectUrl;
177
+ return;
178
+ }
179
+ } catch (err) {
180
+ onError?.(err as Error);
181
+ }
182
+ };
183
+
184
+ // Loading state
185
+ if (isLoading) {
186
+ return (
187
+ <div className={className}>
188
+ <LoadingComponent />
189
+ </div>
190
+ );
191
+ }
192
+
193
+ // Error state
194
+ if (error) {
195
+ return (
196
+ <div className={className}>
197
+ <ErrorComponent error={error} />
198
+ </div>
199
+ );
200
+ }
201
+
202
+ // Form not found
203
+ if (!form) {
204
+ return (
205
+ <div className={className}>
206
+ <ErrorComponent error={new Error("Form not found")} />
207
+ </div>
208
+ );
209
+ }
210
+
211
+ // Form not active
212
+ if (form.status !== "active") {
213
+ return (
214
+ <div className={className}>
215
+ <ErrorComponent
216
+ error={new Error("This form is not currently accepting submissions")}
217
+ />
218
+ </div>
219
+ );
220
+ }
221
+
222
+ // Schema parsing failed
223
+ if (!zodSchema) {
224
+ return (
225
+ <div className={className}>
226
+ <ErrorComponent error={new Error("Failed to parse form schema")} />
227
+ </div>
228
+ );
229
+ }
230
+
231
+ // Success state
232
+ if (submitted && finalSuccessMessage) {
233
+ return (
234
+ <div className={className}>
235
+ <DefaultSuccessComponent message={finalSuccessMessage} />
236
+ </div>
237
+ );
238
+ }
239
+
240
+ // Render form using SteppedAutoForm
241
+ // It automatically handles both single-step and multi-step forms
242
+ return (
243
+ <div className={className} data-testid="form-renderer">
244
+ <SteppedAutoForm
245
+ formSchema={zodSchema}
246
+ fieldConfig={fieldConfig}
247
+ onSubmit={(values) => handleSubmit(values as Record<string, unknown>)}
248
+ isSubmitting={submitMutation.isPending}
249
+ submitButtonText={submitButtonText || loc.FORM_BUILDER_BUTTON_SUBMIT}
250
+ />
251
+ </div>
252
+ );
253
+ }
@@ -0,0 +1,24 @@
1
+ "use client";
2
+
3
+ import { FormListPageComponent as FormListPageImpl } from "./pages/form-list-page";
4
+ import { FormBuilderPageComponent as FormBuilderPageImpl } from "./pages/form-builder-page";
5
+ import { SubmissionsPageComponent as SubmissionsPageImpl } from "./pages/submissions-page";
6
+ import { FormRenderer as FormRendererImpl } from "./forms/form-renderer";
7
+
8
+ // Re-export to ensure the client boundary is preserved
9
+ export const FormListPage = FormListPageImpl;
10
+ export const FormBuilderPage = FormBuilderPageImpl;
11
+ export const SubmissionsPage = SubmissionsPageImpl;
12
+ export const FormRenderer = FormRendererImpl;
13
+
14
+ // Export loading skeletons
15
+ export {
16
+ FormListSkeleton,
17
+ FormBuilderSkeleton,
18
+ SubmissionsSkeleton,
19
+ } from "./loading";
20
+
21
+ // Export shared components
22
+ export { DefaultError } from "./shared/default-error";
23
+ export { EmptyState } from "./shared/empty-state";
24
+ export { NotFoundPage } from "./pages/404-page";
@@ -0,0 +1,42 @@
1
+ "use client";
2
+
3
+ import { Skeleton } from "@workspace/ui/components/skeleton";
4
+
5
+ export function FormBuilderSkeleton() {
6
+ return (
7
+ <div className="flex h-full flex-col" data-testid="form-builder-skeleton">
8
+ {/* Header */}
9
+ <div className="flex items-center gap-4 border-b p-4">
10
+ <Skeleton className="h-10 w-48" />
11
+ <Skeleton className="h-10 w-32" />
12
+ <div className="ml-auto">
13
+ <Skeleton className="h-10 w-24" />
14
+ </div>
15
+ </div>
16
+
17
+ {/* Main content */}
18
+ <div className="flex flex-1">
19
+ {/* Palette */}
20
+ <div className="w-64 border-r p-4 space-y-4">
21
+ <Skeleton className="h-6 w-24" />
22
+ {Array.from({ length: 8 }).map((_, i) => (
23
+ <Skeleton key={i} className="h-12 rounded-lg" />
24
+ ))}
25
+ </div>
26
+
27
+ {/* Canvas */}
28
+ <div className="flex-1 p-4 space-y-4">
29
+ <Skeleton className="h-6 w-32" />
30
+ <Skeleton className="h-48 rounded-lg" />
31
+ <Skeleton className="h-24 rounded-lg" />
32
+ </div>
33
+
34
+ {/* Preview panel */}
35
+ <div className="w-80 border-l p-4 space-y-4">
36
+ <Skeleton className="h-6 w-20" />
37
+ <Skeleton className="h-full rounded-lg" />
38
+ </div>
39
+ </div>
40
+ </div>
41
+ );
42
+ }
@@ -0,0 +1,25 @@
1
+ "use client";
2
+
3
+ import { Skeleton } from "@workspace/ui/components/skeleton";
4
+ import { PageWrapper } from "../shared/page-wrapper";
5
+
6
+ export function FormListSkeleton() {
7
+ return (
8
+ <PageWrapper testId="form-list-skeleton">
9
+ <div className="w-full max-w-5xl space-y-6">
10
+ <div className="flex items-center justify-between">
11
+ <div className="space-y-2">
12
+ <Skeleton className="h-8 w-32" />
13
+ <Skeleton className="h-4 w-48" />
14
+ </div>
15
+ <Skeleton className="h-10 w-28" />
16
+ </div>
17
+ <div className="space-y-4">
18
+ {Array.from({ length: 5 }).map((_, i) => (
19
+ <Skeleton key={i} className="h-16 rounded-lg" />
20
+ ))}
21
+ </div>
22
+ </div>
23
+ </PageWrapper>
24
+ );
25
+ }
@@ -0,0 +1,3 @@
1
+ export { FormListSkeleton } from "./form-list-skeleton";
2
+ export { FormBuilderSkeleton } from "./form-builder-skeleton";
3
+ export { SubmissionsSkeleton } from "./submissions-skeleton";
@@ -0,0 +1,40 @@
1
+ "use client";
2
+
3
+ import { Skeleton } from "@workspace/ui/components/skeleton";
4
+ import { PageWrapper } from "../shared/page-wrapper";
5
+
6
+ export function SubmissionsSkeleton() {
7
+ return (
8
+ <PageWrapper testId="submissions-skeleton">
9
+ <div className="w-full max-w-5xl space-y-6">
10
+ <div className="flex items-center justify-between">
11
+ <div className="space-y-2">
12
+ <Skeleton className="h-8 w-48" />
13
+ <Skeleton className="h-4 w-64" />
14
+ </div>
15
+ <Skeleton className="h-10 w-32" />
16
+ </div>
17
+ <div className="rounded-lg border">
18
+ <div className="border-b p-4">
19
+ <div className="flex gap-4">
20
+ <Skeleton className="h-4 w-24" />
21
+ <Skeleton className="h-4 w-48" />
22
+ <Skeleton className="h-4 w-32" />
23
+ <Skeleton className="h-4 w-20" />
24
+ </div>
25
+ </div>
26
+ {Array.from({ length: 5 }).map((_, i) => (
27
+ <div key={i} className="border-b p-4 last:border-0">
28
+ <div className="flex gap-4">
29
+ <Skeleton className="h-4 w-24" />
30
+ <Skeleton className="h-4 w-48" />
31
+ <Skeleton className="h-4 w-32" />
32
+ <Skeleton className="h-4 w-20" />
33
+ </div>
34
+ </div>
35
+ ))}
36
+ </div>
37
+ </div>
38
+ </PageWrapper>
39
+ );
40
+ }
@@ -0,0 +1,28 @@
1
+ "use client";
2
+
3
+ import { Button } from "@workspace/ui/components/button";
4
+ import { usePluginOverrides, useBasePath } from "@btst/stack/context";
5
+ import type { FormBuilderPluginOverrides } from "../../overrides";
6
+
7
+ export function NotFoundPage() {
8
+ const { navigate, Link } =
9
+ usePluginOverrides<FormBuilderPluginOverrides>("form-builder");
10
+ const basePath = useBasePath();
11
+
12
+ const LinkComponent = Link || "a";
13
+
14
+ return (
15
+ <div className="flex flex-col items-center justify-center py-12 px-4 text-center">
16
+ <h1 className="text-6xl font-bold text-muted-foreground mb-4">404</h1>
17
+ <h2 className="text-xl font-medium text-foreground mb-2">
18
+ Page not found
19
+ </h2>
20
+ <p className="text-sm text-muted-foreground mb-6 max-w-sm">
21
+ The page you're looking for doesn't exist or has been moved.
22
+ </p>
23
+ <Button asChild>
24
+ <LinkComponent href={`${basePath}/forms`}>Back to Forms</LinkComponent>
25
+ </Button>
26
+ </div>
27
+ );
28
+ }
@@ -0,0 +1,253 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect, useCallback } from "react";
4
+ import { usePluginOverrides, useBasePath } from "@btst/stack/context";
5
+ import { Button } from "@workspace/ui/components/button";
6
+ import { Input } from "@workspace/ui/components/input";
7
+ import { Label } from "@workspace/ui/components/label";
8
+ import {
9
+ Select,
10
+ SelectContent,
11
+ SelectItem,
12
+ SelectTrigger,
13
+ SelectValue,
14
+ } from "@workspace/ui/components/select";
15
+ import { ArrowLeft, Save } from "lucide-react";
16
+ import { toast } from "sonner";
17
+ import { FormBuilder } from "@workspace/ui/components/form-builder";
18
+ import type { JSONSchema } from "@workspace/ui/components/form-builder/types";
19
+
20
+ import {
21
+ useSuspenseFormById,
22
+ useCreateForm,
23
+ useUpdateForm,
24
+ } from "../../hooks/form-builder-hooks";
25
+ import type { FormBuilderPluginOverrides } from "../../overrides";
26
+ import { FORM_BUILDER_LOCALIZATION } from "../../localization";
27
+ import { slugify } from "../../../utils";
28
+ import type { SerializedForm } from "../../../types";
29
+
30
+ export interface FormBuilderPageProps {
31
+ id?: string;
32
+ }
33
+
34
+ /**
35
+ * Entry point component that conditionally renders the appropriate
36
+ * sub-component based on whether we're creating or editing a form.
37
+ * This avoids conditional hook calls which violate React's Rules of Hooks.
38
+ */
39
+ export function FormBuilderPage({ id }: FormBuilderPageProps) {
40
+ if (id) {
41
+ return <EditFormBuilderPage id={id} />;
42
+ }
43
+ return <CreateFormBuilderPage />;
44
+ }
45
+
46
+ /**
47
+ * Component for editing an existing form.
48
+ * Uses useSuspenseFormById unconditionally since id is always defined.
49
+ */
50
+ function EditFormBuilderPage({ id }: { id: string }) {
51
+ const { form: existingForm } = useSuspenseFormById(id);
52
+ return <FormBuilderPageContent id={id} existingForm={existingForm} />;
53
+ }
54
+
55
+ /**
56
+ * Component for creating a new form.
57
+ * No data fetching needed.
58
+ */
59
+ function CreateFormBuilderPage() {
60
+ return <FormBuilderPageContent />;
61
+ }
62
+
63
+ interface FormBuilderPageContentProps {
64
+ id?: string;
65
+ existingForm?: SerializedForm | null;
66
+ }
67
+
68
+ function FormBuilderPageContent({
69
+ id,
70
+ existingForm,
71
+ }: FormBuilderPageContentProps) {
72
+ const { navigate, Link, localization } = usePluginOverrides<
73
+ FormBuilderPluginOverrides,
74
+ Partial<FormBuilderPluginOverrides>
75
+ >("form-builder", {
76
+ localization: FORM_BUILDER_LOCALIZATION,
77
+ });
78
+ const basePath = useBasePath();
79
+
80
+ const createMutation = useCreateForm();
81
+ const updateMutation = useUpdateForm();
82
+
83
+ const loc = localization || FORM_BUILDER_LOCALIZATION;
84
+ const LinkComponent = Link || "a";
85
+
86
+ // Form state
87
+ const [name, setName] = useState(existingForm?.name || "");
88
+ const [slug, setSlug] = useState(existingForm?.slug || "");
89
+ const [status, setStatus] = useState<"active" | "inactive" | "archived">(
90
+ (existingForm?.status as "active" | "inactive" | "archived") || "active",
91
+ );
92
+ const [schema, setSchema] = useState<JSONSchema | undefined>(() => {
93
+ if (existingForm?.schema) {
94
+ try {
95
+ return JSON.parse(existingForm.schema) as JSONSchema;
96
+ } catch {
97
+ return undefined;
98
+ }
99
+ }
100
+ return undefined;
101
+ });
102
+
103
+ // Auto-generate slug from name
104
+ const [autoSlug, setAutoSlug] = useState(!id);
105
+
106
+ useEffect(() => {
107
+ if (autoSlug && name) {
108
+ setSlug(slugify(name));
109
+ }
110
+ }, [name, autoSlug]);
111
+
112
+ const handleSchemaChange = useCallback((newSchema: JSONSchema) => {
113
+ setSchema(newSchema);
114
+ }, []);
115
+
116
+ const handleSave = async () => {
117
+ if (!name.trim()) {
118
+ toast.error("Name is required");
119
+ return;
120
+ }
121
+ if (!slug.trim()) {
122
+ toast.error("Slug is required");
123
+ return;
124
+ }
125
+ if (!schema) {
126
+ toast.error("Please add at least one field to the form");
127
+ return;
128
+ }
129
+
130
+ try {
131
+ const schemaStr = JSON.stringify(schema);
132
+
133
+ if (id) {
134
+ await updateMutation.mutateAsync({
135
+ id,
136
+ data: {
137
+ name,
138
+ schema: schemaStr,
139
+ status,
140
+ },
141
+ });
142
+ toast.success(loc.FORM_BUILDER_TOAST_UPDATE_SUCCESS);
143
+ } else {
144
+ const newForm = await createMutation.mutateAsync({
145
+ name,
146
+ slug,
147
+ schema: schemaStr,
148
+ status,
149
+ });
150
+ toast.success(loc.FORM_BUILDER_TOAST_CREATE_SUCCESS);
151
+ navigate?.(`${basePath}/forms/${newForm.id}/edit`);
152
+ }
153
+ } catch (error) {
154
+ const message = error instanceof Error ? error.message : "Unknown error";
155
+ if (message.includes("slug already exists")) {
156
+ toast.error(loc.FORM_BUILDER_TOAST_DUPLICATE_SLUG);
157
+ } else {
158
+ toast.error(loc.FORM_BUILDER_TOAST_ERROR);
159
+ }
160
+ }
161
+ };
162
+
163
+ const isSaving = createMutation.isPending || updateMutation.isPending;
164
+
165
+ return (
166
+ <div className="flex h-full flex-col" data-testid="form-builder-page">
167
+ {/* Header */}
168
+ <div className="flex items-center gap-4 border-b p-4">
169
+ <Button variant="ghost" size="icon" asChild>
170
+ <LinkComponent href={`${basePath}/forms`}>
171
+ <ArrowLeft className="h-4 w-4" />
172
+ </LinkComponent>
173
+ </Button>
174
+
175
+ <div className="flex flex-col gap-1">
176
+ <Label htmlFor="form-name" className="text-xs text-muted-foreground">
177
+ {loc.FORM_BUILDER_LABEL_NAME}
178
+ </Label>
179
+ <Input
180
+ id="form-name"
181
+ value={name}
182
+ onChange={(e) => setName(e.target.value)}
183
+ placeholder={loc.FORM_BUILDER_EDITOR_NAME_PLACEHOLDER}
184
+ className="h-8 w-48"
185
+ />
186
+ </div>
187
+
188
+ <div className="flex flex-col gap-1">
189
+ <Label htmlFor="form-slug" className="text-xs text-muted-foreground">
190
+ {loc.FORM_BUILDER_LABEL_SLUG}
191
+ </Label>
192
+ <Input
193
+ id="form-slug"
194
+ value={slug}
195
+ onChange={(e) => {
196
+ setSlug(e.target.value);
197
+ setAutoSlug(false);
198
+ }}
199
+ placeholder={loc.FORM_BUILDER_EDITOR_SLUG_PLACEHOLDER}
200
+ className="h-8 w-48 font-mono text-sm"
201
+ disabled={!!id}
202
+ />
203
+ </div>
204
+
205
+ <div className="flex flex-col gap-1">
206
+ <Label
207
+ htmlFor="form-status"
208
+ className="text-xs text-muted-foreground"
209
+ >
210
+ {loc.FORM_BUILDER_LABEL_STATUS}
211
+ </Label>
212
+ <Select
213
+ value={status}
214
+ onValueChange={(v) => setStatus(v as typeof status)}
215
+ >
216
+ <SelectTrigger className="h-8 w-28">
217
+ <SelectValue />
218
+ </SelectTrigger>
219
+ <SelectContent>
220
+ <SelectItem value="active">
221
+ {loc.FORM_BUILDER_STATUS_ACTIVE}
222
+ </SelectItem>
223
+ <SelectItem value="inactive">
224
+ {loc.FORM_BUILDER_STATUS_INACTIVE}
225
+ </SelectItem>
226
+ <SelectItem value="archived">
227
+ {loc.FORM_BUILDER_STATUS_ARCHIVED}
228
+ </SelectItem>
229
+ </SelectContent>
230
+ </Select>
231
+ </div>
232
+
233
+ <div className="ml-auto">
234
+ <Button onClick={handleSave} disabled={isSaving}>
235
+ <Save className="mr-2 h-4 w-4" />
236
+ {isSaving
237
+ ? loc.FORM_BUILDER_STATUS_SAVING
238
+ : id
239
+ ? loc.FORM_BUILDER_BUTTON_SAVE
240
+ : loc.FORM_BUILDER_BUTTON_CREATE}
241
+ </Button>
242
+ </div>
243
+ </div>
244
+
245
+ {/* Form Builder */}
246
+ <FormBuilder
247
+ value={schema}
248
+ onChange={handleSchemaChange}
249
+ className="flex-1"
250
+ />
251
+ </div>
252
+ );
253
+ }
@@ -0,0 +1,26 @@
1
+ "use client";
2
+
3
+ import { lazy, Suspense } from "react";
4
+ import { FormBuilderSkeleton } from "../loading/form-builder-skeleton";
5
+ import { ErrorBoundary } from "react-error-boundary";
6
+ import { DefaultError } from "../shared/default-error";
7
+
8
+ const FormBuilderPage = lazy(() =>
9
+ import("./form-builder-page.internal").then((m) => ({
10
+ default: m.FormBuilderPage,
11
+ })),
12
+ );
13
+
14
+ export interface FormBuilderPageProps {
15
+ id?: string;
16
+ }
17
+
18
+ export function FormBuilderPageComponent({ id }: FormBuilderPageProps) {
19
+ return (
20
+ <ErrorBoundary FallbackComponent={DefaultError}>
21
+ <Suspense fallback={<FormBuilderSkeleton />}>
22
+ <FormBuilderPage id={id} />
23
+ </Suspense>
24
+ </ErrorBoundary>
25
+ );
26
+ }