@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,198 @@
1
+ import {
2
+ mergeQueryKeys,
3
+ createQueryKeys,
4
+ } from "@lukemorales/query-key-factory";
5
+ import type { FormBuilderApiRouter } from "./api";
6
+ import { createApiClient } from "@btst/stack/plugins/client";
7
+ import type {
8
+ SerializedForm,
9
+ PaginatedForms,
10
+ PaginatedFormSubmissions,
11
+ SerializedFormSubmissionWithData,
12
+ } from "./types";
13
+
14
+ interface FormListParams {
15
+ status?: "active" | "inactive" | "archived";
16
+ limit?: number;
17
+ offset?: number;
18
+ }
19
+
20
+ interface SubmissionListParams {
21
+ formId: string;
22
+ limit?: number;
23
+ offset?: number;
24
+ }
25
+
26
+ // Type guard for better-call error responses
27
+ function isErrorResponse(
28
+ response: unknown,
29
+ ): response is { error: unknown; data?: never } {
30
+ if (typeof response !== "object" || response === null) {
31
+ return false;
32
+ }
33
+ const obj = response as Record<string, unknown>;
34
+ return "error" in obj && obj.error !== null && obj.error !== undefined;
35
+ }
36
+
37
+ // Helper to convert error to a proper Error object with meaningful message
38
+ function toError(error: unknown): Error {
39
+ if (error instanceof Error) {
40
+ return error;
41
+ }
42
+
43
+ if (typeof error === "object" && error !== null) {
44
+ const errorObj = error as Record<string, unknown>;
45
+ const message =
46
+ (typeof errorObj.message === "string" ? errorObj.message : null) ||
47
+ (typeof errorObj.error === "string" ? errorObj.error : null) ||
48
+ JSON.stringify(error);
49
+
50
+ const err = new Error(message);
51
+ Object.assign(err, error);
52
+ return err;
53
+ }
54
+
55
+ return new Error(String(error));
56
+ }
57
+
58
+ /**
59
+ * Create Form Builder query keys for React Query
60
+ * Used by consumers to fetch forms and submissions
61
+ */
62
+ export function createFormBuilderQueryKeys(
63
+ client: ReturnType<typeof createApiClient<FormBuilderApiRouter>>,
64
+ headers?: HeadersInit,
65
+ ) {
66
+ const forms = createFormsQueries(client, headers);
67
+ const submissions = createSubmissionsQueries(client, headers);
68
+
69
+ return mergeQueryKeys(forms, submissions);
70
+ }
71
+
72
+ function createFormsQueries(
73
+ client: ReturnType<typeof createApiClient<FormBuilderApiRouter>>,
74
+ headers?: HeadersInit,
75
+ ) {
76
+ return createQueryKeys("forms", {
77
+ list: (params: FormListParams = {}) => ({
78
+ queryKey: ["list", params],
79
+ queryFn: async () => {
80
+ try {
81
+ const response: unknown = await client("/forms", {
82
+ method: "GET",
83
+ query: {
84
+ status: params.status,
85
+ limit: params.limit ?? 20,
86
+ offset: params.offset ?? 0,
87
+ },
88
+ headers,
89
+ });
90
+ if (isErrorResponse(response)) {
91
+ throw toError(response.error);
92
+ }
93
+ return (response as { data?: unknown }).data as PaginatedForms;
94
+ } catch (error) {
95
+ throw error;
96
+ }
97
+ },
98
+ }),
99
+
100
+ bySlug: (slug: string) => ({
101
+ queryKey: ["bySlug", slug],
102
+ queryFn: async () => {
103
+ if (!slug) return null;
104
+
105
+ try {
106
+ const response: unknown = await client("/forms/:slug", {
107
+ method: "GET",
108
+ params: { slug },
109
+ headers,
110
+ });
111
+ if (isErrorResponse(response)) {
112
+ throw toError(response.error);
113
+ }
114
+ return (response as { data?: unknown }).data as SerializedForm | null;
115
+ } catch (error) {
116
+ throw error;
117
+ }
118
+ },
119
+ }),
120
+
121
+ byId: (id: string) => ({
122
+ queryKey: ["byId", id],
123
+ queryFn: async () => {
124
+ if (!id) return null;
125
+
126
+ try {
127
+ const response: unknown = await client("/forms/id/:id", {
128
+ method: "GET",
129
+ params: { id },
130
+ headers,
131
+ });
132
+ if (isErrorResponse(response)) {
133
+ throw toError(response.error);
134
+ }
135
+ return (response as { data?: unknown }).data as SerializedForm | null;
136
+ } catch (error) {
137
+ throw error;
138
+ }
139
+ },
140
+ }),
141
+ });
142
+ }
143
+
144
+ function createSubmissionsQueries(
145
+ client: ReturnType<typeof createApiClient<FormBuilderApiRouter>>,
146
+ headers?: HeadersInit,
147
+ ) {
148
+ return createQueryKeys("formSubmissions", {
149
+ list: (params: SubmissionListParams) => ({
150
+ queryKey: [params],
151
+ queryFn: async () => {
152
+ try {
153
+ const response: unknown = await client("/forms/:formId/submissions", {
154
+ method: "GET",
155
+ params: { formId: params.formId },
156
+ query: {
157
+ limit: params.limit ?? 20,
158
+ offset: params.offset ?? 0,
159
+ },
160
+ headers,
161
+ });
162
+ if (isErrorResponse(response)) {
163
+ throw toError(response.error);
164
+ }
165
+ return (response as { data?: unknown })
166
+ .data as PaginatedFormSubmissions;
167
+ } catch (error) {
168
+ throw error;
169
+ }
170
+ },
171
+ }),
172
+
173
+ detail: (formId: string, subId: string) => ({
174
+ queryKey: [formId, subId],
175
+ queryFn: async () => {
176
+ if (!formId || !subId) return null;
177
+
178
+ try {
179
+ const response: unknown = await client(
180
+ "/forms/:formId/submissions/:subId",
181
+ {
182
+ method: "GET",
183
+ params: { formId, subId },
184
+ headers,
185
+ },
186
+ );
187
+ if (isErrorResponse(response)) {
188
+ throw toError(response.error);
189
+ }
190
+ return (response as { data?: unknown })
191
+ .data as SerializedFormSubmissionWithData | null;
192
+ } catch (error) {
193
+ throw error;
194
+ }
195
+ },
196
+ }),
197
+ });
198
+ }
@@ -0,0 +1,122 @@
1
+ import { z } from "zod";
2
+
3
+ /**
4
+ * Schema for listing forms with pagination
5
+ */
6
+ export const listFormsQuerySchema = z.object({
7
+ status: z.enum(["active", "inactive", "archived"]).optional(),
8
+ limit: z.coerce.number().min(1).max(100).optional().default(20),
9
+ offset: z.coerce.number().min(0).optional().default(0),
10
+ });
11
+
12
+ /**
13
+ * Schema for creating a form
14
+ */
15
+ export const createFormSchema = z.object({
16
+ name: z.string().min(1, "Name is required"),
17
+ slug: z.string().min(1, "Slug is required"),
18
+ description: z.string().optional(),
19
+ schema: z.string().min(1, "Schema is required"),
20
+ successMessage: z.string().optional(),
21
+ redirectUrl: z.string().url().optional().or(z.literal("")),
22
+ status: z
23
+ .enum(["active", "inactive", "archived"])
24
+ .optional()
25
+ .default("active"),
26
+ });
27
+
28
+ /**
29
+ * Schema for updating a form
30
+ */
31
+ export const updateFormSchema = z.object({
32
+ name: z.string().min(1, "Name is required").optional(),
33
+ slug: z.string().min(1, "Slug is required").optional(),
34
+ description: z.string().optional(),
35
+ schema: z.string().min(1, "Schema is required").optional(),
36
+ successMessage: z.string().optional(),
37
+ redirectUrl: z.string().url().optional().or(z.literal("")),
38
+ status: z.enum(["active", "inactive", "archived"]).optional(),
39
+ });
40
+
41
+ /**
42
+ * Schema for form response
43
+ */
44
+ export const formResponseSchema = z.object({
45
+ id: z.string(),
46
+ name: z.string(),
47
+ slug: z.string(),
48
+ description: z.string().nullable().optional(),
49
+ schema: z.string(),
50
+ successMessage: z.string().nullable().optional(),
51
+ redirectUrl: z.string().nullable().optional(),
52
+ status: z.string(),
53
+ createdBy: z.string().nullable().optional(),
54
+ createdAt: z.string(),
55
+ updatedAt: z.string(),
56
+ });
57
+
58
+ /**
59
+ * Schema for paginated forms response
60
+ */
61
+ export const paginatedFormsResponseSchema = z.object({
62
+ items: z.array(formResponseSchema),
63
+ total: z.number(),
64
+ limit: z.number(),
65
+ offset: z.number(),
66
+ });
67
+
68
+ /**
69
+ * Schema for listing form submissions with pagination
70
+ */
71
+ export const listSubmissionsQuerySchema = z.object({
72
+ limit: z.coerce.number().min(1).max(100).optional().default(20),
73
+ offset: z.coerce.number().min(0).optional().default(0),
74
+ });
75
+
76
+ /**
77
+ * Schema for submitting a form (public)
78
+ */
79
+ export const submitFormSchema = z.object({
80
+ // Use passthrough object for dynamic form data validation
81
+ data: z.object({}).passthrough(),
82
+ });
83
+
84
+ /**
85
+ * Schema for form submission response
86
+ */
87
+ export const formSubmissionResponseSchema = z.object({
88
+ id: z.string(),
89
+ formId: z.string(),
90
+ data: z.string(),
91
+ submittedAt: z.string(),
92
+ submittedBy: z.string().nullable().optional(),
93
+ ipAddress: z.string().nullable().optional(),
94
+ userAgent: z.string().nullable().optional(),
95
+ });
96
+
97
+ /**
98
+ * Schema for form submission with parsed data response
99
+ */
100
+ export const formSubmissionWithDataResponseSchema =
101
+ formSubmissionResponseSchema.extend({
102
+ // Use passthrough object for dynamic parsed data
103
+ parsedData: z.object({}).passthrough(),
104
+ form: formResponseSchema.optional(),
105
+ });
106
+
107
+ /**
108
+ * Schema for paginated submissions response
109
+ */
110
+ export const paginatedSubmissionsResponseSchema = z.object({
111
+ items: z.array(formSubmissionWithDataResponseSchema),
112
+ total: z.number(),
113
+ limit: z.number(),
114
+ offset: z.number(),
115
+ });
116
+
117
+ // Export inferred types
118
+ export type ListFormsQuery = z.infer<typeof listFormsQuerySchema>;
119
+ export type CreateFormInput = z.infer<typeof createFormSchema>;
120
+ export type UpdateFormInput = z.infer<typeof updateFormSchema>;
121
+ export type ListSubmissionsQuery = z.infer<typeof listSubmissionsQuerySchema>;
122
+ export type SubmitFormInput = z.infer<typeof submitFormSchema>;
@@ -0,0 +1,19 @@
1
+ @import "./client.css";
2
+
3
+ /*
4
+ * Form Builder Plugin CSS - Includes Tailwind class scanning
5
+ *
6
+ * When consumed from npm, Tailwind v4 will automatically scan this package's
7
+ * source files for Tailwind classes. Consumers only need:
8
+ * @import "@btst/stack/plugins/form-builder/css";
9
+ */
10
+
11
+ /* Scan this package's source files for Tailwind classes */
12
+ @source "../../../src/**/*.{ts,tsx}";
13
+
14
+ /* Scan UI package components (when installed as npm package the UI package will be in this dir) */
15
+ @source "../../packages/ui/src";
16
+
17
+ /*
18
+ * alternatively consumer can use @source "../node_modules/@btst/stack/src/**\/*.{ts,tsx}";
19
+ */
@@ -0,0 +1,317 @@
1
+ /**
2
+ * Form Builder Plugin Types
3
+ *
4
+ * Key distinction from CMS Plugin:
5
+ * - CMS uses developer-defined Zod schemas in code
6
+ * - Form Builder allows non-technical admins to build forms via drag-and-drop UI
7
+ * - Forms are serialized to/from JSON Schema for storage
8
+ */
9
+
10
+ /**
11
+ * Form stored in the database
12
+ */
13
+ export type Form = {
14
+ id: string;
15
+ /** Display name for the form */
16
+ name: string;
17
+ /** URL-friendly slug - unique identifier for public access */
18
+ slug: string;
19
+ /** Optional description for admin UI */
20
+ description?: string;
21
+ /** JSON Schema stored as string (includes steps, fieldType, stepGroup, etc.) */
22
+ schema: string;
23
+ /** Optional custom success message after submission */
24
+ successMessage?: string;
25
+ /** Optional redirect URL after submission */
26
+ redirectUrl?: string;
27
+ /** Form status: active, inactive, archived */
28
+ status: "active" | "inactive" | "archived";
29
+ /** User who created the form */
30
+ createdBy?: string;
31
+ createdAt: Date;
32
+ updatedAt: Date;
33
+ };
34
+
35
+ /**
36
+ * Form submission stored in the database
37
+ */
38
+ export type FormSubmission = {
39
+ id: string;
40
+ /** Reference to the form */
41
+ formId: string;
42
+ /** Submitted data as JSON string */
43
+ data: string;
44
+ /** Submission timestamp */
45
+ submittedAt: Date;
46
+ /** Optional user ID if authenticated */
47
+ submittedBy?: string;
48
+ /** Client IP address for rate limiting and spam protection */
49
+ ipAddress?: string;
50
+ /** User agent for analytics */
51
+ userAgent?: string;
52
+ };
53
+
54
+ /**
55
+ * Form submission with its parent form joined
56
+ */
57
+ export type FormSubmissionWithForm = FormSubmission & {
58
+ form?: Form;
59
+ };
60
+
61
+ /**
62
+ * Serialized form for API responses (dates as strings)
63
+ */
64
+ export interface SerializedForm
65
+ extends Omit<Form, "createdAt" | "updatedAt" | "status"> {
66
+ status: string;
67
+ createdAt: string;
68
+ updatedAt: string;
69
+ }
70
+
71
+ /**
72
+ * Serialized form submission for API responses (dates as strings)
73
+ */
74
+ export interface SerializedFormSubmission
75
+ extends Omit<FormSubmission, "submittedAt"> {
76
+ submittedAt: string;
77
+ }
78
+
79
+ /**
80
+ * Serialized form submission with parsed data
81
+ */
82
+ export interface SerializedFormSubmissionWithData<
83
+ TData = Record<string, unknown>,
84
+ > extends SerializedFormSubmission {
85
+ /** Parsed data object (JSON.parse of data field) */
86
+ parsedData: TData;
87
+ /** Joined form */
88
+ form?: SerializedForm;
89
+ }
90
+
91
+ /**
92
+ * Paginated list response for forms
93
+ */
94
+ export interface PaginatedForms {
95
+ items: SerializedForm[];
96
+ total: number;
97
+ limit: number;
98
+ offset: number;
99
+ }
100
+
101
+ /**
102
+ * Paginated list response for form submissions
103
+ */
104
+ export interface PaginatedFormSubmissions<TData = Record<string, unknown>> {
105
+ items: SerializedFormSubmissionWithData<TData>[];
106
+ total: number;
107
+ limit: number;
108
+ offset: number;
109
+ }
110
+
111
+ // ============================================================================
112
+ // BACKEND HOOKS
113
+ // ============================================================================
114
+
115
+ /**
116
+ * Context passed to all backend hooks
117
+ */
118
+ export interface FormBuilderHookContext {
119
+ /** User ID if authenticated */
120
+ userId?: string;
121
+ /** Request headers */
122
+ headers?: Headers;
123
+ /** Client IP address (for rate limiting) */
124
+ ipAddress?: string;
125
+ /** User agent string */
126
+ userAgent?: string;
127
+ }
128
+
129
+ /**
130
+ * Context for submission-specific hooks
131
+ */
132
+ export interface SubmissionHookContext extends FormBuilderHookContext {
133
+ /** Form slug being submitted */
134
+ formSlug: string;
135
+ /** Form ID */
136
+ formId: string;
137
+ }
138
+
139
+ /**
140
+ * Input data for creating a form
141
+ */
142
+ export interface FormInput {
143
+ name: string;
144
+ slug: string;
145
+ description?: string;
146
+ schema: string;
147
+ successMessage?: string;
148
+ redirectUrl?: string;
149
+ status?: "active" | "inactive" | "archived";
150
+ createdBy?: string;
151
+ }
152
+
153
+ /**
154
+ * Input data for updating a form
155
+ */
156
+ export interface FormUpdate {
157
+ name?: string;
158
+ slug?: string;
159
+ description?: string;
160
+ schema?: string;
161
+ successMessage?: string;
162
+ redirectUrl?: string;
163
+ status?: "active" | "inactive" | "archived";
164
+ }
165
+
166
+ /**
167
+ * Backend hooks for Form Builder plugin
168
+ *
169
+ * All CRUD hooks receive ipAddress and headers for auth/rate limiting.
170
+ * Return false from onBefore* hooks to reject the operation (throws 403).
171
+ */
172
+ export interface FormBuilderBackendHooks {
173
+ // ============================================================================
174
+ // FORM CRUD HOOKS (Admin operations)
175
+ // ============================================================================
176
+
177
+ /** Called before listing forms. Return false to deny access (403). */
178
+ onBeforeListForms?: (
179
+ ctx: FormBuilderHookContext,
180
+ ) => Promise<boolean> | boolean;
181
+
182
+ /** Called before creating a form. Return false to deny, or modified data. */
183
+ onBeforeFormCreated?: (
184
+ data: FormInput,
185
+ ctx: FormBuilderHookContext,
186
+ ) => Promise<FormInput | false> | FormInput | false;
187
+
188
+ /** Called after a form is created */
189
+ onAfterFormCreated?: (
190
+ form: SerializedForm,
191
+ ctx: FormBuilderHookContext,
192
+ ) => Promise<void> | void;
193
+
194
+ /** Called before getting a form by ID or slug. Return false to deny access. */
195
+ onBeforeGetForm?: (
196
+ idOrSlug: string,
197
+ ctx: FormBuilderHookContext,
198
+ ) => Promise<boolean> | boolean;
199
+
200
+ /** Called before updating a form. Return false to deny, or modified data. */
201
+ onBeforeFormUpdated?: (
202
+ id: string,
203
+ data: FormUpdate,
204
+ ctx: FormBuilderHookContext,
205
+ ) => Promise<FormUpdate | false> | FormUpdate | false;
206
+
207
+ /** Called after a form is updated */
208
+ onAfterFormUpdated?: (
209
+ form: SerializedForm,
210
+ ctx: FormBuilderHookContext,
211
+ ) => Promise<void> | void;
212
+
213
+ /** Called before deleting a form. Return false to deny. */
214
+ onBeforeFormDeleted?: (
215
+ id: string,
216
+ ctx: FormBuilderHookContext,
217
+ ) => Promise<boolean> | boolean;
218
+
219
+ /** Called after a form is deleted */
220
+ onAfterFormDeleted?: (
221
+ id: string,
222
+ ctx: FormBuilderHookContext,
223
+ ) => Promise<void> | void;
224
+
225
+ // ============================================================================
226
+ // SUBMISSION HOOKS (Public form submissions)
227
+ // ============================================================================
228
+
229
+ /**
230
+ * Called before processing a form submission.
231
+ * Use for: spam protection, rate limiting, data validation/enrichment.
232
+ *
233
+ * @returns false to reject submission (400), or modified data to continue
234
+ */
235
+ onBeforeSubmission?: (
236
+ formSlug: string,
237
+ data: Record<string, unknown>,
238
+ ctx: SubmissionHookContext,
239
+ ) =>
240
+ | Promise<Record<string, unknown> | false>
241
+ | Record<string, unknown>
242
+ | false;
243
+
244
+ /**
245
+ * Called after a submission is saved.
246
+ * Use for: sending emails, storing in CRM, triggering workflows.
247
+ */
248
+ onAfterSubmission?: (
249
+ submission: SerializedFormSubmission,
250
+ form: SerializedForm,
251
+ ctx: SubmissionHookContext,
252
+ ) => Promise<void> | void;
253
+
254
+ /** Called when a submission fails */
255
+ onSubmissionError?: (
256
+ error: Error,
257
+ formSlug: string,
258
+ data: Record<string, unknown>,
259
+ ctx: SubmissionHookContext,
260
+ ) => Promise<void> | void;
261
+
262
+ // ============================================================================
263
+ // SUBMISSIONS MANAGEMENT HOOKS (Admin viewing submissions)
264
+ // ============================================================================
265
+
266
+ /** Called before listing submissions. Return false to deny access (403). */
267
+ onBeforeListSubmissions?: (
268
+ formId: string,
269
+ ctx: FormBuilderHookContext,
270
+ ) => Promise<boolean> | boolean;
271
+
272
+ /** Called before getting a submission. Return false to deny access. */
273
+ onBeforeGetSubmission?: (
274
+ submissionId: string,
275
+ ctx: FormBuilderHookContext,
276
+ ) => Promise<boolean> | boolean;
277
+
278
+ /** Called before deleting a submission. Return false to deny. */
279
+ onBeforeSubmissionDeleted?: (
280
+ submissionId: string,
281
+ ctx: FormBuilderHookContext,
282
+ ) => Promise<boolean> | boolean;
283
+
284
+ /** Called after a submission is deleted */
285
+ onAfterSubmissionDeleted?: (
286
+ submissionId: string,
287
+ ctx: FormBuilderHookContext,
288
+ ) => Promise<void> | void;
289
+
290
+ // ============================================================================
291
+ // ERROR HOOK
292
+ // ============================================================================
293
+
294
+ /** Called on any error */
295
+ onError?: (
296
+ error: Error,
297
+ operation:
298
+ | "list"
299
+ | "get"
300
+ | "create"
301
+ | "update"
302
+ | "delete"
303
+ | "submit"
304
+ | "listSubmissions"
305
+ | "getSubmission"
306
+ | "deleteSubmission",
307
+ ctx: FormBuilderHookContext,
308
+ ) => Promise<void> | void;
309
+ }
310
+
311
+ /**
312
+ * Configuration for the Form Builder backend plugin
313
+ */
314
+ export interface FormBuilderBackendConfig {
315
+ /** Optional hooks for customizing behavior */
316
+ hooks?: FormBuilderBackendHooks;
317
+ }
@@ -0,0 +1,63 @@
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+ import slug from "slug";
4
+
5
+ /**
6
+ * Converts text to a URL-friendly slug
7
+ */
8
+ export function slugify(text: string, locale: string = "en"): string {
9
+ return slug(text, { lower: true, locale });
10
+ }
11
+
12
+ /**
13
+ * Merges class names using clsx and tailwind-merge
14
+ */
15
+ export function cn(...inputs: ClassValue[]) {
16
+ return twMerge(clsx(inputs));
17
+ }
18
+
19
+ /**
20
+ * Safely parses JSON with a fallback
21
+ */
22
+ export function safeJsonParse<T>(json: string, fallback: T): T {
23
+ try {
24
+ return JSON.parse(json) as T;
25
+ } catch {
26
+ return fallback;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Extracts client IP address from request headers
32
+ * Handles common proxy headers
33
+ */
34
+ export function extractIpAddress(headers?: Headers): string | undefined {
35
+ if (!headers) return undefined;
36
+
37
+ // Check common proxy headers in order of preference
38
+ const forwardedFor = headers.get("x-forwarded-for");
39
+ if (forwardedFor) {
40
+ // x-forwarded-for can contain multiple IPs, take the first one
41
+ return forwardedFor.split(",")[0]?.trim();
42
+ }
43
+
44
+ const realIp = headers.get("x-real-ip");
45
+ if (realIp) {
46
+ return realIp.trim();
47
+ }
48
+
49
+ const cfConnectingIp = headers.get("cf-connecting-ip");
50
+ if (cfConnectingIp) {
51
+ return cfConnectingIp.trim();
52
+ }
53
+
54
+ return undefined;
55
+ }
56
+
57
+ /**
58
+ * Extracts user agent from request headers
59
+ */
60
+ export function extractUserAgent(headers?: Headers): string | undefined {
61
+ if (!headers) return undefined;
62
+ return headers.get("user-agent") || undefined;
63
+ }