@evanschleret/formforgeclient 1.0.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 (121) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +94 -0
  3. package/dist/module.cjs +112 -0
  4. package/dist/module.d.cts +20 -0
  5. package/dist/module.d.mts +20 -0
  6. package/dist/module.d.ts +20 -0
  7. package/dist/module.json +12 -0
  8. package/dist/module.mjs +109 -0
  9. package/dist/runtime/api/categories.d.ts +9 -0
  10. package/dist/runtime/api/categories.js +83 -0
  11. package/dist/runtime/api/client.d.ts +45 -0
  12. package/dist/runtime/api/client.js +148 -0
  13. package/dist/runtime/api/drafts.d.ts +6 -0
  14. package/dist/runtime/api/drafts.js +77 -0
  15. package/dist/runtime/api/http.d.ts +3 -0
  16. package/dist/runtime/api/http.js +138 -0
  17. package/dist/runtime/api/index.d.ts +9 -0
  18. package/dist/runtime/api/index.js +11 -0
  19. package/dist/runtime/api/management.d.ts +19 -0
  20. package/dist/runtime/api/management.js +180 -0
  21. package/dist/runtime/api/request.d.ts +8 -0
  22. package/dist/runtime/api/request.js +52 -0
  23. package/dist/runtime/api/responses.d.ts +6 -0
  24. package/dist/runtime/api/responses.js +61 -0
  25. package/dist/runtime/api/schema.d.ts +7 -0
  26. package/dist/runtime/api/schema.js +56 -0
  27. package/dist/runtime/api/submission.d.ts +11 -0
  28. package/dist/runtime/api/submission.js +47 -0
  29. package/dist/runtime/api/upload.d.ts +8 -0
  30. package/dist/runtime/api/upload.js +37 -0
  31. package/dist/runtime/composables/index.d.ts +31 -0
  32. package/dist/runtime/composables/index.js +16 -0
  33. package/dist/runtime/composables/useFormForgeApi.d.ts +3 -0
  34. package/dist/runtime/composables/useFormForgeApi.js +4 -0
  35. package/dist/runtime/composables/useFormForgeBuilder.d.ts +57 -0
  36. package/dist/runtime/composables/useFormForgeBuilder.js +515 -0
  37. package/dist/runtime/composables/useFormForgeCategory.d.ts +61 -0
  38. package/dist/runtime/composables/useFormForgeCategory.js +248 -0
  39. package/dist/runtime/composables/useFormForgeClient.d.ts +3 -0
  40. package/dist/runtime/composables/useFormForgeClient.js +200 -0
  41. package/dist/runtime/composables/useFormForgeDrafts.d.ts +20 -0
  42. package/dist/runtime/composables/useFormForgeDrafts.js +78 -0
  43. package/dist/runtime/composables/useFormForgeForm.d.ts +26 -0
  44. package/dist/runtime/composables/useFormForgeForm.js +114 -0
  45. package/dist/runtime/composables/useFormForgeGetForm.d.ts +22 -0
  46. package/dist/runtime/composables/useFormForgeGetForm.js +36 -0
  47. package/dist/runtime/composables/useFormForgeI18n.d.ts +250 -0
  48. package/dist/runtime/composables/useFormForgeI18n.js +324 -0
  49. package/dist/runtime/composables/useFormForgeManagement.d.ts +40 -0
  50. package/dist/runtime/composables/useFormForgeManagement.js +153 -0
  51. package/dist/runtime/composables/useFormForgeResolver.d.ts +28 -0
  52. package/dist/runtime/composables/useFormForgeResolver.js +88 -0
  53. package/dist/runtime/composables/useFormForgeResponses.d.ts +45 -0
  54. package/dist/runtime/composables/useFormForgeResponses.js +206 -0
  55. package/dist/runtime/composables/useFormForgeSchema.d.ts +24 -0
  56. package/dist/runtime/composables/useFormForgeSchema.js +69 -0
  57. package/dist/runtime/composables/useFormForgeSubmission.d.ts +12 -0
  58. package/dist/runtime/composables/useFormForgeSubmission.js +4 -0
  59. package/dist/runtime/composables/useFormForgeSubmit.d.ts +29 -0
  60. package/dist/runtime/composables/useFormForgeSubmit.js +291 -0
  61. package/dist/runtime/composables/useFormForgeUploads.d.ts +21 -0
  62. package/dist/runtime/composables/useFormForgeUploads.js +37 -0
  63. package/dist/runtime/composables/useFormForgeWizard.d.ts +20 -0
  64. package/dist/runtime/composables/useFormForgeWizard.js +83 -0
  65. package/dist/runtime/index.d.ts +11 -0
  66. package/dist/runtime/index.js +14 -0
  67. package/dist/runtime/plugin.d.ts +3 -0
  68. package/dist/runtime/plugin.js +175 -0
  69. package/dist/runtime/renderers/default/FormForgeBuilder.d.vue.ts +40 -0
  70. package/dist/runtime/renderers/default/FormForgeBuilder.vue +1159 -0
  71. package/dist/runtime/renderers/default/FormForgeBuilder.vue.d.ts +40 -0
  72. package/dist/runtime/renderers/default/FormForgeCategoryCreateModal.d.vue.ts +16 -0
  73. package/dist/runtime/renderers/default/FormForgeCategoryCreateModal.vue +129 -0
  74. package/dist/runtime/renderers/default/FormForgeCategoryCreateModal.vue.d.ts +16 -0
  75. package/dist/runtime/renderers/default/FormForgeRenderer.d.vue.ts +72 -0
  76. package/dist/runtime/renderers/default/FormForgeRenderer.vue +1188 -0
  77. package/dist/runtime/renderers/default/FormForgeRenderer.vue.d.ts +72 -0
  78. package/dist/runtime/renderers/default/FormForgeResponse.d.vue.ts +18 -0
  79. package/dist/runtime/renderers/default/FormForgeResponse.vue +744 -0
  80. package/dist/runtime/renderers/default/FormForgeResponse.vue.d.ts +18 -0
  81. package/dist/runtime/renderers/default/index.d.ts +5 -0
  82. package/dist/runtime/renderers/default/index.js +4 -0
  83. package/dist/runtime/renderers/index.d.ts +2 -0
  84. package/dist/runtime/renderers/index.js +1 -0
  85. package/dist/runtime/types/api.d.ts +129 -0
  86. package/dist/runtime/types/api.js +0 -0
  87. package/dist/runtime/types/category.d.ts +42 -0
  88. package/dist/runtime/types/category.js +0 -0
  89. package/dist/runtime/types/errors.d.ts +16 -0
  90. package/dist/runtime/types/errors.js +0 -0
  91. package/dist/runtime/types/index.d.ts +8 -0
  92. package/dist/runtime/types/index.js +0 -0
  93. package/dist/runtime/types/json.d.ts +6 -0
  94. package/dist/runtime/types/json.js +0 -0
  95. package/dist/runtime/types/management.d.ts +46 -0
  96. package/dist/runtime/types/management.js +0 -0
  97. package/dist/runtime/types/nuxt.d.ts +13 -0
  98. package/dist/runtime/types/nuxt.js +1 -0
  99. package/dist/runtime/types/schema.d.ts +93 -0
  100. package/dist/runtime/types/schema.js +0 -0
  101. package/dist/runtime/utils/category.d.ts +5 -0
  102. package/dist/runtime/utils/category.js +101 -0
  103. package/dist/runtime/utils/form-data.d.ts +8 -0
  104. package/dist/runtime/utils/form-data.js +64 -0
  105. package/dist/runtime/utils/object.d.ts +8 -0
  106. package/dist/runtime/utils/object.js +43 -0
  107. package/dist/runtime/utils/schema.d.ts +3 -0
  108. package/dist/runtime/utils/schema.js +309 -0
  109. package/dist/runtime/utils/submission.d.ts +4 -0
  110. package/dist/runtime/utils/submission.js +45 -0
  111. package/dist/runtime/validation/errors.d.ts +5 -0
  112. package/dist/runtime/validation/errors.js +130 -0
  113. package/dist/runtime/validation/zod.d.ts +6 -0
  114. package/dist/runtime/validation/zod.js +203 -0
  115. package/dist/runtime.cjs +16 -0
  116. package/dist/runtime.d.cts +1 -0
  117. package/dist/runtime.d.mts +1 -0
  118. package/dist/runtime.d.ts +1 -0
  119. package/dist/runtime.mjs +1 -0
  120. package/dist/types.d.mts +3 -0
  121. package/package.json +60 -0
@@ -0,0 +1,1159 @@
1
+ <script setup>
2
+ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "#imports";
3
+ import { useOverlay } from "@nuxt/ui/composables/useOverlay";
4
+ import Draggable from "vuedraggable";
5
+ import {
6
+ FORM_FORGE_BUILDER_CONDITION_ACTIONS,
7
+ FORM_FORGE_BUILDER_CONDITION_MATCHES,
8
+ FORM_FORGE_BUILDER_CONDITION_OPERATORS,
9
+ FORM_FORGE_BUILDER_FIELD_TYPES,
10
+ useFormForgeBuilder
11
+ } from "../../composables/useFormForgeBuilder";
12
+ import { useFormForgeCategory, useFormForgeCategoryOptions } from "../../composables/useFormForgeCategory";
13
+ import { useFormForgeI18n } from "../../composables/useFormForgeI18n";
14
+ import FormForgeCategoryCreateModal from "./FormForgeCategoryCreateModal.vue";
15
+ const props = defineProps({
16
+ formUuid: { type: String, required: false, default: void 0 },
17
+ formKey: { type: String, required: false, default: void 0 },
18
+ endpoint: { type: String, required: false, default: void 0 },
19
+ loadFormKey: { type: String, required: false, default: void 0 },
20
+ loadFormVersion: { type: String, required: false, default: void 0 },
21
+ locale: { type: String, required: false, default: void 0 },
22
+ modelValue: { type: Object, required: false, default: void 0 },
23
+ autosave: { type: Boolean, required: false, default: true },
24
+ autosaveDelay: { type: Number, required: false, default: 5e3 },
25
+ readonly: { type: Boolean, required: false, default: false }
26
+ });
27
+ const emit = defineEmits(["update:modelValue", "save", "publish", "unpublish", "error"]);
28
+ const builderOptions = {
29
+ formUuid: props.formUuid,
30
+ formKey: props.formKey,
31
+ endpoint: props.endpoint,
32
+ initial: props.modelValue,
33
+ autosave: props.autosave,
34
+ autosaveDelay: props.autosaveDelay
35
+ };
36
+ const builder = useFormForgeBuilder(builderOptions);
37
+ const { t } = useFormForgeI18n({
38
+ locale: () => props.locale
39
+ });
40
+ const draft = builder.draft;
41
+ const isClientReady = ref(false);
42
+ const saving = builder.saving;
43
+ const publishing = builder.publishing;
44
+ const publishable = builder.publishable;
45
+ const lastSavedAt = builder.lastSavedAt;
46
+ const builderError = builder.error;
47
+ const overlay = useOverlay();
48
+ const categoryManager = useFormForgeCategory({
49
+ immediate: true,
50
+ initialQuery: {
51
+ per_page: 200
52
+ },
53
+ endpoint: props.endpoint
54
+ });
55
+ const categoryOptions = useFormForgeCategoryOptions({
56
+ source: categoryManager,
57
+ includeInactive: true
58
+ });
59
+ const CATEGORY_NONE_VALUE = "__formforge_no_category__";
60
+ const fieldElements = /* @__PURE__ */ new Map();
61
+ const builderRootElement = ref(null);
62
+ const builderColumnElement = ref(null);
63
+ const railTop = ref(120);
64
+ const railLeft = ref(0);
65
+ const loadingRemoteForm = ref(false);
66
+ let loadRequestId = 0;
67
+ const safePages = computed(() => {
68
+ const pages = draft.value?.pages;
69
+ if (!Array.isArray(pages)) {
70
+ return [];
71
+ }
72
+ return pages.filter((page) => page !== void 0 && page !== null);
73
+ });
74
+ const safeConditions = computed(() => {
75
+ const conditions = draft.value?.conditions;
76
+ if (!Array.isArray(conditions)) {
77
+ return [];
78
+ }
79
+ return conditions.filter((condition) => condition !== void 0 && condition !== null);
80
+ });
81
+ const draftTitle = computed({
82
+ get() {
83
+ return typeof draft.value?.title === "string" ? draft.value.title : "";
84
+ },
85
+ set(value) {
86
+ if (draft.value === void 0 || draft.value === null) {
87
+ return;
88
+ }
89
+ draft.value.title = value;
90
+ }
91
+ });
92
+ const draftCategory = computed({
93
+ get() {
94
+ return typeof draft.value?.category === "string" ? draft.value.category : null;
95
+ },
96
+ set(value) {
97
+ if (draft.value === void 0 || draft.value === null) {
98
+ return;
99
+ }
100
+ draft.value.category = value;
101
+ }
102
+ });
103
+ const draftCategorySelectValue = computed({
104
+ get() {
105
+ return draftCategory.value ?? CATEGORY_NONE_VALUE;
106
+ },
107
+ set(value) {
108
+ draftCategory.value = value === CATEGORY_NONE_VALUE ? null : value;
109
+ }
110
+ });
111
+ const categorySelectItems = computed(() => {
112
+ const items = [
113
+ {
114
+ label: t("builder.categoryNone"),
115
+ value: CATEGORY_NONE_VALUE,
116
+ disabled: false
117
+ },
118
+ ...categoryOptions.value
119
+ ];
120
+ if (typeof draftCategory.value === "string" && draftCategory.value !== "" && !items.some((item) => item.value === draftCategory.value)) {
121
+ items.push({
122
+ label: draftCategory.value,
123
+ value: draftCategory.value,
124
+ disabled: false
125
+ });
126
+ }
127
+ return items;
128
+ });
129
+ const draftPages = computed({
130
+ get() {
131
+ return safePages.value;
132
+ },
133
+ set(value) {
134
+ if (draft.value === void 0 || draft.value === null) {
135
+ return;
136
+ }
137
+ draft.value.pages = value;
138
+ }
139
+ });
140
+ const draftMutationIdentifier = computed(() => {
141
+ if (typeof draft.value?.uuid === "string" && draft.value.uuid !== "") {
142
+ return draft.value.uuid;
143
+ }
144
+ return null;
145
+ });
146
+ function twoDigits(value) {
147
+ return String(value).padStart(2, "0");
148
+ }
149
+ function isFormForgeCategory(value) {
150
+ if (value === null || typeof value !== "object") {
151
+ return false;
152
+ }
153
+ const candidate = value;
154
+ return typeof candidate.key === "string" && candidate.key !== "" && typeof candidate.name === "string";
155
+ }
156
+ const formattedLastSavedAt = computed(() => {
157
+ if (lastSavedAt.value === null) {
158
+ return null;
159
+ }
160
+ const parsed = new Date(lastSavedAt.value);
161
+ if (Number.isNaN(parsed.getTime())) {
162
+ return lastSavedAt.value;
163
+ }
164
+ const day = twoDigits(parsed.getDate());
165
+ const month = twoDigits(parsed.getMonth() + 1);
166
+ const year = parsed.getFullYear();
167
+ const hours = twoDigits(parsed.getHours());
168
+ const minutes = twoDigits(parsed.getMinutes());
169
+ return `${day}.${month}.${year} \xE0 ${hours}:${minutes}`;
170
+ });
171
+ const selectedPageKey = ref(safePages.value[0]?.page_key ?? null);
172
+ const selectedFieldKey = ref(safePages.value[0]?.fields[0]?.field_key ?? null);
173
+ const fieldTypeMeta = computed(() => ({
174
+ text: { label: t("builder.fieldType.text"), icon: "i-lucide-text-cursor-input" },
175
+ textarea: { label: t("builder.fieldType.textarea"), icon: "i-lucide-align-left" },
176
+ email: { label: t("builder.fieldType.email"), icon: "i-lucide-mail" },
177
+ number: { label: t("builder.fieldType.number"), icon: "i-lucide-hash" },
178
+ select: { label: t("builder.fieldType.select"), icon: "i-lucide-list" },
179
+ select_menu: { label: t("builder.fieldType.select_menu"), icon: "i-lucide-list-filter" },
180
+ radio: { label: t("builder.fieldType.radio"), icon: "i-lucide-circle-dot" },
181
+ checkbox: { label: t("builder.fieldType.checkbox"), icon: "i-lucide-check-square" },
182
+ checkbox_group: { label: t("builder.fieldType.checkbox_group"), icon: "i-lucide-list-checks" },
183
+ switch: { label: t("builder.fieldType.switch"), icon: "i-lucide-toggle-left" },
184
+ date: { label: t("builder.fieldType.date"), icon: "i-lucide-calendar" },
185
+ time: { label: t("builder.fieldType.time"), icon: "i-lucide-clock-3" },
186
+ datetime: { label: t("builder.fieldType.datetime"), icon: "i-lucide-calendar-clock" },
187
+ date_range: { label: t("builder.fieldType.date_range"), icon: "i-lucide-calendar-range" },
188
+ datetime_range: { label: t("builder.fieldType.datetime_range"), icon: "i-lucide-calendar-range" },
189
+ file: { label: t("builder.fieldType.file"), icon: "i-lucide-paperclip" }
190
+ }));
191
+ const fieldTypeItems = computed(() => FORM_FORGE_BUILDER_FIELD_TYPES.map((type) => ({
192
+ label: fieldTypeMeta.value[type].label,
193
+ value: type,
194
+ icon: fieldTypeMeta.value[type].icon
195
+ })));
196
+ function conditionActionLabel(action) {
197
+ const labels = {
198
+ show: t("builder.condition.action.show"),
199
+ hide: t("builder.condition.action.hide"),
200
+ skip: t("builder.condition.action.skip"),
201
+ require: t("builder.condition.action.require"),
202
+ disable: t("builder.condition.action.disable")
203
+ };
204
+ return labels[action];
205
+ }
206
+ function conditionMatchLabel(match) {
207
+ const labels = {
208
+ all: t("builder.condition.match.all"),
209
+ any: t("builder.condition.match.any")
210
+ };
211
+ return labels[match];
212
+ }
213
+ function conditionOperatorLabel(operator) {
214
+ const labels = {
215
+ eq: t("builder.condition.operator.eq"),
216
+ neq: t("builder.condition.operator.neq"),
217
+ in: t("builder.condition.operator.in"),
218
+ not_in: t("builder.condition.operator.not_in"),
219
+ gt: t("builder.condition.operator.gt"),
220
+ gte: t("builder.condition.operator.gte"),
221
+ lt: t("builder.condition.operator.lt"),
222
+ lte: t("builder.condition.operator.lte"),
223
+ contains: t("builder.condition.operator.contains"),
224
+ not_contains: t("builder.condition.operator.not_contains"),
225
+ is_empty: t("builder.condition.operator.is_empty"),
226
+ not_empty: t("builder.condition.operator.not_empty")
227
+ };
228
+ return labels[operator];
229
+ }
230
+ const conditionActionItems = computed(() => FORM_FORGE_BUILDER_CONDITION_ACTIONS.map((action) => ({
231
+ label: conditionActionLabel(action),
232
+ value: action
233
+ })));
234
+ const conditionMatchItems = computed(() => FORM_FORGE_BUILDER_CONDITION_MATCHES.map((match) => ({
235
+ label: conditionMatchLabel(match),
236
+ value: match
237
+ })));
238
+ const conditionOperatorItems = computed(() => FORM_FORGE_BUILDER_CONDITION_OPERATORS.map((operator) => ({
239
+ label: conditionOperatorLabel(operator),
240
+ value: operator
241
+ })));
242
+ const targetTypeItems = computed(() => [
243
+ { label: t("builder.targetType.page"), value: "page" },
244
+ { label: t("builder.targetType.field"), value: "field" }
245
+ ]);
246
+ const pageTargetItems = computed(() => {
247
+ const items = [];
248
+ for (const maybePage of safePages.value) {
249
+ const title = typeof maybePage.title === "string" ? maybePage.title : "";
250
+ items.push({
251
+ label: title === "" ? maybePage.page_key : `${title} (${maybePage.page_key})`,
252
+ value: maybePage.page_key
253
+ });
254
+ }
255
+ return items;
256
+ });
257
+ const fieldTargetItems = computed(() => {
258
+ const items = [];
259
+ for (const maybePage of safePages.value) {
260
+ if (!Array.isArray(maybePage.fields)) {
261
+ continue;
262
+ }
263
+ for (const maybeField of maybePage.fields) {
264
+ const label = maybeField.label === void 0 || maybeField.label === "" ? maybeField.field_key : `${maybeField.label} (${maybeField.field_key})`;
265
+ items.push({
266
+ label,
267
+ value: maybeField.field_key
268
+ });
269
+ }
270
+ }
271
+ return items;
272
+ });
273
+ function selectedPage() {
274
+ if (selectedPageKey.value === null) {
275
+ return safePages.value[0];
276
+ }
277
+ return safePages.value.find((page) => page.page_key === selectedPageKey.value) ?? safePages.value[0];
278
+ }
279
+ function syncSelectionWithDraft(pages) {
280
+ if (pages.length === 0) {
281
+ selectedPageKey.value = null;
282
+ selectedFieldKey.value = null;
283
+ return;
284
+ }
285
+ const nextPage = selectedPageKey.value === null ? pages[0] : pages.find((page) => page.page_key === selectedPageKey.value) ?? pages[0];
286
+ selectedPageKey.value = nextPage.page_key;
287
+ const fields = Array.isArray(nextPage.fields) ? nextPage.fields.filter((field) => field !== void 0 && field !== null) : [];
288
+ if (fields.length === 0) {
289
+ selectedFieldKey.value = null;
290
+ return;
291
+ }
292
+ const nextField = selectedFieldKey.value === null ? fields[0] : fields.find((field) => field.field_key === selectedFieldKey.value) ?? fields[0];
293
+ selectedFieldKey.value = nextField.field_key;
294
+ }
295
+ function resolveHTMLElement(element) {
296
+ if (element instanceof HTMLElement) {
297
+ return element;
298
+ }
299
+ if (typeof element !== "object" || element === null || !("$el" in element)) {
300
+ return null;
301
+ }
302
+ const componentElement = element.$el;
303
+ return componentElement instanceof HTMLElement ? componentElement : null;
304
+ }
305
+ function registerFieldElement(fieldKey, element) {
306
+ const htmlElement = resolveHTMLElement(element);
307
+ if (htmlElement !== null) {
308
+ fieldElements.set(fieldKey, htmlElement);
309
+ if (fieldKey === selectedFieldKey.value) {
310
+ nextTick(() => {
311
+ updateRailPosition();
312
+ });
313
+ }
314
+ return;
315
+ }
316
+ fieldElements.delete(fieldKey);
317
+ }
318
+ function updateRailPosition() {
319
+ if (!isClientReady.value || typeof window === "undefined") {
320
+ return;
321
+ }
322
+ const rootElement = builderRootElement.value;
323
+ if (rootElement === null) {
324
+ return;
325
+ }
326
+ const rootRect = rootElement.getBoundingClientRect();
327
+ const columnRect = builderColumnElement.value?.getBoundingClientRect() ?? rootRect;
328
+ const selectedElement = selectedFieldKey.value === null ? void 0 : fieldElements.get(selectedFieldKey.value);
329
+ const selectedRect = selectedElement?.getBoundingClientRect();
330
+ if (window.innerWidth <= 1024) {
331
+ railLeft.value = 0;
332
+ return;
333
+ }
334
+ const railWidth = 68;
335
+ const horizontalGap = 14;
336
+ const minLeft = 8;
337
+ const maxLeft = window.innerWidth - railWidth - 8;
338
+ const horizontalAnchor = selectedRect?.right ?? columnRect.right;
339
+ const desiredLeft = horizontalAnchor + horizontalGap;
340
+ railLeft.value = Math.min(Math.max(desiredLeft, minLeft), maxLeft);
341
+ const railHeight = 122;
342
+ const minTop = Math.max(88, columnRect.top + 8);
343
+ const maxTop = Math.max(minTop, Math.min(window.innerHeight - railHeight - 12, columnRect.bottom - railHeight - 8));
344
+ if (selectedRect === void 0) {
345
+ railTop.value = minTop;
346
+ return;
347
+ }
348
+ const desiredTop = selectedRect.top + selectedRect.height / 2 - railHeight / 2;
349
+ railTop.value = Math.min(Math.max(desiredTop, minTop), maxTop);
350
+ }
351
+ onMounted(() => {
352
+ isClientReady.value = true;
353
+ window.addEventListener("scroll", updateRailPosition, { passive: true });
354
+ window.addEventListener("resize", updateRailPosition);
355
+ nextTick(() => {
356
+ syncSelectionWithDraft(safePages.value);
357
+ updateRailPosition();
358
+ });
359
+ });
360
+ onBeforeUnmount(() => {
361
+ window.removeEventListener("scroll", updateRailPosition);
362
+ window.removeEventListener("resize", updateRailPosition);
363
+ fieldElements.clear();
364
+ });
365
+ function selectField(page, fieldKey) {
366
+ selectedPageKey.value = page.page_key;
367
+ selectedFieldKey.value = fieldKey;
368
+ nextTick(() => {
369
+ updateRailPosition();
370
+ });
371
+ }
372
+ function updatePageTitle(page, value) {
373
+ if (page === void 0) {
374
+ return;
375
+ }
376
+ page.title = value;
377
+ }
378
+ function readPageTitle(page) {
379
+ if (page === void 0 || page === null) {
380
+ return "";
381
+ }
382
+ return typeof page.title === "string" ? page.title : "";
383
+ }
384
+ function pageOrder(page) {
385
+ const index = safePages.value.findIndex((item) => item.page_key === page.page_key);
386
+ return index < 0 ? 1 : index + 1;
387
+ }
388
+ function canMergeWithPreviousPage(page) {
389
+ return safePages.value.length > 1 && pageOrder(page) > 1;
390
+ }
391
+ function mergePageWithPrevious(page) {
392
+ builder.mergePageWithPrevious(page.page_key);
393
+ nextTick(() => {
394
+ syncSelectionWithDraft(safePages.value);
395
+ updateRailPosition();
396
+ });
397
+ }
398
+ function isFieldSelected(fieldKey) {
399
+ return selectedFieldKey.value === fieldKey;
400
+ }
401
+ function showInlinePreview(field) {
402
+ return field.type === "text" || field.type === "textarea" || field.type === "email";
403
+ }
404
+ function fieldTypeLabel(type) {
405
+ return fieldTypeMeta.value[type]?.label ?? type;
406
+ }
407
+ function fieldTypeIcon(type) {
408
+ return fieldTypeMeta.value[type]?.icon ?? "i-lucide-square";
409
+ }
410
+ function isUuidLike(value) {
411
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
412
+ }
413
+ function cloneValue(value) {
414
+ if (typeof structuredClone === "function") {
415
+ return structuredClone(value);
416
+ }
417
+ return JSON.parse(JSON.stringify(value));
418
+ }
419
+ function resolveLoadedFormUuid() {
420
+ const fromProps = typeof props.formUuid === "string" && props.formUuid !== "" ? props.formUuid : null;
421
+ if (fromProps !== null) {
422
+ return fromProps;
423
+ }
424
+ const fromLoadKey = typeof props.loadFormKey === "string" && isUuidLike(props.loadFormKey) ? props.loadFormKey : null;
425
+ if (fromLoadKey !== null) {
426
+ return fromLoadKey;
427
+ }
428
+ return typeof draft.value?.uuid === "string" && draft.value.uuid !== "" ? draft.value.uuid : null;
429
+ }
430
+ function applyLoadedForm(schema) {
431
+ const nextUuid = resolveLoadedFormUuid();
432
+ draft.value = {
433
+ uuid: nextUuid,
434
+ key: schema.key,
435
+ title: schema.title,
436
+ category: schema.category ?? null,
437
+ pages: cloneValue(schema.pages),
438
+ conditions: cloneValue(schema.conditions),
439
+ drafts: cloneValue(schema.drafts)
440
+ };
441
+ }
442
+ async function loadFormIntoBuilder(key, version) {
443
+ const requestId = ++loadRequestId;
444
+ loadingRemoteForm.value = true;
445
+ try {
446
+ const schema = version !== void 0 && version !== "" ? await builder.client.getFormVersion(key, version, { endpoint: props.endpoint }) : await builder.client.getForm(key, { endpoint: props.endpoint });
447
+ if (requestId !== loadRequestId) {
448
+ return;
449
+ }
450
+ applyLoadedForm(schema);
451
+ nextTick(() => {
452
+ syncSelectionWithDraft(safePages.value);
453
+ updateRailPosition();
454
+ });
455
+ } catch (caughtError) {
456
+ const message = caughtError instanceof Error ? caughtError.message : t("builder.error.loadForm");
457
+ emit("error", message);
458
+ } finally {
459
+ if (requestId === loadRequestId) {
460
+ loadingRemoteForm.value = false;
461
+ }
462
+ }
463
+ }
464
+ function addField(page, type) {
465
+ builder.addField(page.page_key, type);
466
+ builder.normalizeFieldLocations();
467
+ const nextField = page.fields.at(-1);
468
+ if (nextField !== void 0) {
469
+ selectField(page, nextField.field_key);
470
+ }
471
+ }
472
+ function onFieldTypeChange(field, nextType) {
473
+ field.type = nextType;
474
+ if (nextType === "select" || nextType === "select_menu" || nextType === "radio" || nextType === "checkbox_group") {
475
+ if (field.options === void 0) {
476
+ field.options = [];
477
+ }
478
+ } else {
479
+ field.options = void 0;
480
+ }
481
+ if (nextType === "file") {
482
+ field.multiple = false;
483
+ if (!("accept" in field)) {
484
+ Object.assign(field, {
485
+ accept: []
486
+ });
487
+ }
488
+ }
489
+ }
490
+ function addOption(field) {
491
+ if (field.options === void 0) {
492
+ field.options = [];
493
+ }
494
+ field.options.push({
495
+ label: t("builder.optionDefaultLabel"),
496
+ value: `option_${field.options.length + 1}`
497
+ });
498
+ }
499
+ function removeOption(field, index) {
500
+ if (field.options === void 0) {
501
+ return;
502
+ }
503
+ field.options.splice(index, 1);
504
+ }
505
+ function optionLabel(option) {
506
+ if (option === void 0 || option === null) {
507
+ return "";
508
+ }
509
+ if (typeof option === "object" && "label" in option) {
510
+ return typeof option.label === "string" ? option.label : "";
511
+ }
512
+ return String(option);
513
+ }
514
+ function setOptionLabel(field, optionIndex, value) {
515
+ if (field.options === void 0) {
516
+ return;
517
+ }
518
+ const option = field.options[optionIndex];
519
+ if (option === void 0 || option === null) {
520
+ return;
521
+ }
522
+ if (typeof option === "object" && "value" in option) {
523
+ field.options[optionIndex] = {
524
+ ...option,
525
+ label: value
526
+ };
527
+ return;
528
+ }
529
+ field.options[optionIndex] = {
530
+ label: value,
531
+ value: option
532
+ };
533
+ }
534
+ async function save() {
535
+ try {
536
+ await builder.save();
537
+ emit("save", draft.value);
538
+ } catch (caughtError) {
539
+ const message = caughtError instanceof Error ? caughtError.message : t("builder.error.save");
540
+ emit("error", message);
541
+ }
542
+ }
543
+ async function publish() {
544
+ try {
545
+ await builder.publish();
546
+ emit("publish", draft.value);
547
+ } catch (caughtError) {
548
+ const message = caughtError instanceof Error ? caughtError.message : t("builder.error.publish");
549
+ emit("error", message);
550
+ }
551
+ }
552
+ async function unpublish() {
553
+ try {
554
+ await builder.unpublish();
555
+ emit("unpublish", draft.value);
556
+ } catch (caughtError) {
557
+ const message = caughtError instanceof Error ? caughtError.message : t("builder.error.unpublish");
558
+ emit("error", message);
559
+ }
560
+ }
561
+ function removeField(page, fieldKey) {
562
+ builder.removeField(page.page_key, fieldKey);
563
+ if (selectedFieldKey.value === fieldKey) {
564
+ selectedFieldKey.value = null;
565
+ }
566
+ }
567
+ function addQuestionFromRail() {
568
+ const page = selectedPage();
569
+ if (page === void 0) {
570
+ builder.addPage();
571
+ nextTick(() => {
572
+ syncSelectionWithDraft(safePages.value);
573
+ updateRailPosition();
574
+ });
575
+ return;
576
+ }
577
+ addField(page, "text");
578
+ }
579
+ function addPageFromRail() {
580
+ builder.addPage();
581
+ nextTick(() => {
582
+ syncSelectionWithDraft(safePages.value);
583
+ updateRailPosition();
584
+ });
585
+ }
586
+ async function openCategoryCreateModal() {
587
+ if (props.readonly) {
588
+ return;
589
+ }
590
+ try {
591
+ const createCategoryModal = overlay.create(FormForgeCategoryCreateModal, {
592
+ destroyOnClose: true
593
+ });
594
+ const result = await createCategoryModal.open({
595
+ locale: props.locale,
596
+ endpoint: props.endpoint
597
+ });
598
+ if (!isFormForgeCategory(result)) {
599
+ return;
600
+ }
601
+ categoryManager.list.value = [result, ...categoryManager.list.value.filter((item) => item.key !== result.key)];
602
+ draftCategory.value = result.key;
603
+ } catch (caughtError) {
604
+ const message = caughtError instanceof Error ? caughtError.message : t("builder.error.categoryCreate");
605
+ emit("error", message);
606
+ }
607
+ }
608
+ function duplicateField(page, fieldKey) {
609
+ builder.duplicateField(page.page_key, fieldKey);
610
+ builder.normalizeFieldLocations();
611
+ }
612
+ watch(() => draft.value, (value) => {
613
+ emit("update:modelValue", value);
614
+ }, {
615
+ deep: true
616
+ });
617
+ watch(() => props.autosave, (value) => {
618
+ builder.autosaveEnabled.value = value;
619
+ if (!value) {
620
+ builder.clearAutosave();
621
+ }
622
+ }, {
623
+ immediate: true
624
+ });
625
+ watch(() => props.autosaveDelay, (value) => {
626
+ builder.autosaveDelay.value = value;
627
+ }, {
628
+ immediate: true
629
+ });
630
+ watch(() => [props.loadFormKey, props.loadFormVersion], ([key, version]) => {
631
+ if (typeof key !== "string" || key === "") {
632
+ return;
633
+ }
634
+ loadFormIntoBuilder(key, version);
635
+ }, {
636
+ immediate: true
637
+ });
638
+ watch(() => safePages.value, (pages) => {
639
+ syncSelectionWithDraft(pages);
640
+ nextTick(() => {
641
+ updateRailPosition();
642
+ });
643
+ }, {
644
+ deep: true,
645
+ immediate: true
646
+ });
647
+ watch(() => selectedFieldKey.value, () => {
648
+ nextTick(() => {
649
+ updateRailPosition();
650
+ });
651
+ });
652
+ </script>
653
+
654
+ <template>
655
+ <div
656
+ v-if="isClientReady"
657
+ ref="builderRootElement"
658
+ class="builder-root"
659
+ >
660
+ <div class="builder-layout">
661
+ <div
662
+ ref="builderColumnElement"
663
+ class="builder-column"
664
+ >
665
+ <UCard
666
+ variant="subtle"
667
+ class="builder-card"
668
+ >
669
+ <div class="builder-toolbar">
670
+ <div class="builder-toolbar-grid">
671
+ <UInput
672
+ v-model="draftTitle"
673
+ :disabled="readonly"
674
+ :placeholder="t('builder.formTitlePlaceholder')"
675
+ />
676
+ <div class="builder-category-control">
677
+ <USelect
678
+ v-model="draftCategorySelectValue"
679
+ :items="categorySelectItems"
680
+ :disabled="readonly"
681
+ :placeholder="t('builder.categoryPlaceholder')"
682
+ />
683
+ <UTooltip :text="t('builder.tooltip.addCategory')">
684
+ <UButton
685
+ color="neutral"
686
+ variant="soft"
687
+ icon="i-lucide-folder-plus"
688
+ :disabled="readonly"
689
+ @click="openCategoryCreateModal"
690
+ />
691
+ </UTooltip>
692
+ </div>
693
+ </div>
694
+
695
+ <div class="builder-toolbar-actions">
696
+ <div class="builder-status">
697
+ <span v-if="loadingRemoteForm">{{ t("builder.loadingForm") }}</span>
698
+ <span v-if="formattedLastSavedAt !== null">{{ t("builder.lastSave", { value: formattedLastSavedAt }) }}</span>
699
+ <span
700
+ v-if="builderError !== null"
701
+ class="builder-error"
702
+ >{{ builderError }}</span>
703
+ </div>
704
+
705
+ <div class="builder-actions">
706
+ <UButton
707
+ :loading="saving"
708
+ :disabled="readonly"
709
+ @click="save"
710
+ >
711
+ {{ t("builder.save") }}
712
+ </UButton>
713
+ <UButton
714
+ color="primary"
715
+ :loading="publishing"
716
+ :disabled="readonly || !publishable"
717
+ @click="publish"
718
+ >
719
+ {{ t("builder.publish") }}
720
+ </UButton>
721
+ <UButton
722
+ color="neutral"
723
+ variant="soft"
724
+ :loading="publishing"
725
+ :disabled="readonly || draftMutationIdentifier === null"
726
+ @click="unpublish"
727
+ >
728
+ {{ t("builder.unpublish") }}
729
+ </UButton>
730
+ </div>
731
+ </div>
732
+ </div>
733
+ </UCard>
734
+
735
+ <Draggable
736
+ v-model="draftPages"
737
+ item-key="page_key"
738
+ handle=".page-drag-handle"
739
+ group="formforge-pages"
740
+ class="builder-stack pages-stack"
741
+ >
742
+ <template #item="{ element: page }">
743
+ <div
744
+ v-if="page !== void 0 && page !== null"
745
+ class="page-block"
746
+ >
747
+ <UCard
748
+ variant="soft"
749
+ class="builder-card page-meta-card"
750
+ >
751
+ <div class="page-chip-row">
752
+ <UBadge
753
+ color="primary"
754
+ variant="subtle"
755
+ >
756
+ {{ t("builder.pageCounter", { current: pageOrder(page), total: safePages.length }) }}
757
+ </UBadge>
758
+ </div>
759
+
760
+ <div class="page-header">
761
+ <span class="page-drag-handle drag-handle">⋮⋮</span>
762
+ <div class="page-header-main">
763
+ <UInput
764
+ :model-value="readPageTitle(page)"
765
+ :disabled="readonly"
766
+ class="grow"
767
+ :placeholder="t('builder.pageTitlePlaceholder')"
768
+ @update:model-value="(value) => updatePageTitle(page, value)"
769
+ />
770
+ <UTextarea
771
+ v-model="page.description"
772
+ :rows="2"
773
+ :disabled="readonly"
774
+ :placeholder="t('builder.pageDescriptionPlaceholder')"
775
+ />
776
+ </div>
777
+ <div class="page-actions">
778
+ <UTooltip :text="t('builder.tooltip.mergePage')">
779
+ <UButton
780
+ color="neutral"
781
+ variant="ghost"
782
+ icon="i-lucide-arrow-up-to-line"
783
+ :disabled="readonly || !canMergeWithPreviousPage(page)"
784
+ @click="mergePageWithPrevious(page)"
785
+ />
786
+ </UTooltip>
787
+ <UTooltip :text="t('builder.tooltip.deletePage')">
788
+ <UButton
789
+ color="neutral"
790
+ variant="ghost"
791
+ icon="i-lucide-trash-2"
792
+ :disabled="readonly || safePages.length <= 1"
793
+ @click="builder.removePage(page.page_key)"
794
+ />
795
+ </UTooltip>
796
+ </div>
797
+ </div>
798
+ </UCard>
799
+
800
+ <Draggable
801
+ v-model="page.fields"
802
+ item-key="field_key"
803
+ handle=".field-drag-handle"
804
+ :group="{ name: 'formforge-fields', pull: true, put: true }"
805
+ class="builder-stack page-questions-stack"
806
+ >
807
+ <template #item="{ element: field }">
808
+ <UCard
809
+ v-if="field !== void 0 && field !== null"
810
+ :ref="(element) => registerFieldElement(field.field_key, element)"
811
+ variant="subtle"
812
+ :class="[
813
+ 'field-card',
814
+ isFieldSelected(field.field_key) ? 'field-card--active' : ''
815
+ ]"
816
+ @click="selectField(page, field.field_key)"
817
+ @focusin="selectField(page, field.field_key)"
818
+ >
819
+ <div class="field-shell">
820
+ <div class="field-head">
821
+ <span class="field-drag-handle drag-handle">⋮⋮</span>
822
+ <UInput
823
+ v-model="field.label"
824
+ :disabled="readonly"
825
+ :placeholder="t('builder.questionPlaceholder')"
826
+ />
827
+ <USelectMenu
828
+ :model-value="field.type"
829
+ :items="fieldTypeItems"
830
+ value-key="value"
831
+ label-key="label"
832
+ :search-input="false"
833
+ :leading-icon="fieldTypeIcon(field.type)"
834
+ :disabled="readonly"
835
+ @update:model-value="(value) => onFieldTypeChange(field, value)"
836
+ />
837
+ </div>
838
+
839
+ <div
840
+ v-if="isFieldSelected(field.field_key)"
841
+ class="field-controls"
842
+ >
843
+ <UTooltip :text="t('builder.tooltip.duplicateQuestion')">
844
+ <UButton
845
+ color="neutral"
846
+ variant="ghost"
847
+ icon="i-lucide-copy"
848
+ :disabled="readonly"
849
+ @click.stop="duplicateField(page, field.field_key)"
850
+ />
851
+ </UTooltip>
852
+ <UTooltip :text="t('builder.tooltip.deleteQuestion')">
853
+ <UButton
854
+ color="error"
855
+ variant="ghost"
856
+ icon="i-lucide-trash-2"
857
+ :disabled="readonly || page.fields.length <= 1"
858
+ @click.stop="removeField(page, field.field_key)"
859
+ />
860
+ </UTooltip>
861
+ <div class="field-required-switch">
862
+ <span>{{ t("builder.required") }}</span>
863
+ <USwitch
864
+ v-model="field.required"
865
+ :disabled="readonly"
866
+ />
867
+ </div>
868
+ </div>
869
+
870
+ <div
871
+ v-if="isFieldSelected(field.field_key) && showInlinePreview(field)"
872
+ class="field-inline-preview"
873
+ >
874
+ <p class="field-inline-preview-label">
875
+ {{ fieldTypeLabel(field.type) }}
876
+ </p>
877
+ <div
878
+ v-if="field.type === 'textarea'"
879
+ class="field-inline-preview-textarea"
880
+ >
881
+ <span />
882
+ <span />
883
+ </div>
884
+ <div
885
+ v-else
886
+ class="field-inline-preview-line"
887
+ />
888
+ </div>
889
+
890
+ <div
891
+ v-if="isFieldSelected(field.field_key) && (field.type === 'select' || field.type === 'select_menu' || field.type === 'radio' || field.type === 'checkbox_group')"
892
+ class="field-options"
893
+ >
894
+ <div
895
+ v-for="(option, optionIndex) in field.options"
896
+ :key="optionIndex"
897
+ class="field-option-row"
898
+ >
899
+ <UInput
900
+ :model-value="optionLabel(option)"
901
+ :disabled="readonly"
902
+ :placeholder="t('builder.optionLabelPlaceholder')"
903
+ @update:model-value="(value) => setOptionLabel(field, optionIndex, value)"
904
+ />
905
+ <UButton
906
+ color="neutral"
907
+ variant="ghost"
908
+ icon="i-lucide-x"
909
+ :disabled="readonly"
910
+ @click="removeOption(field, optionIndex)"
911
+ />
912
+ </div>
913
+ <UButton
914
+ color="neutral"
915
+ variant="soft"
916
+ icon="i-lucide-plus"
917
+ :disabled="readonly"
918
+ @click="addOption(field)"
919
+ >
920
+ {{ t("builder.addOption") }}
921
+ </UButton>
922
+ </div>
923
+
924
+ <p
925
+ v-if="!isFieldSelected(field.field_key)"
926
+ class="field-preview"
927
+ >
928
+ {{ field.placeholder || field.help_text || (field.options?.length ? t("builder.optionsCount", { count: field.options.length }) : fieldTypeLabel(field.type)) }}
929
+ </p>
930
+
931
+ <details
932
+ v-if="isFieldSelected(field.field_key)"
933
+ class="field-advanced"
934
+ >
935
+ <summary class="field-advanced-summary">
936
+ {{ t("builder.advancedSettings") }}
937
+ </summary>
938
+
939
+ <div class="field-advanced-body">
940
+ <div class="field-advanced-grid">
941
+ <UInput
942
+ v-model="field.placeholder"
943
+ :disabled="readonly"
944
+ :placeholder="t('builder.placeholderPlaceholder')"
945
+ />
946
+ <UTextarea
947
+ v-model="field.help_text"
948
+ :disabled="readonly"
949
+ :rows="2"
950
+ :placeholder="t('builder.helpTextPlaceholder')"
951
+ />
952
+ </div>
953
+
954
+ <div
955
+ v-if="field.type === 'number'"
956
+ class="field-numbers"
957
+ >
958
+ <UInput
959
+ v-model="field.min"
960
+ :disabled="readonly"
961
+ :placeholder="t('builder.minPlaceholder')"
962
+ />
963
+ <UInput
964
+ v-model="field.max"
965
+ :disabled="readonly"
966
+ :placeholder="t('builder.maxPlaceholder')"
967
+ />
968
+ <UInput
969
+ v-model="field.step"
970
+ :disabled="readonly"
971
+ :placeholder="t('builder.stepPlaceholder')"
972
+ />
973
+ </div>
974
+
975
+ <div
976
+ v-if="field.type === 'file'"
977
+ class="field-file"
978
+ >
979
+ <UCheckbox
980
+ v-model="field.multiple"
981
+ :disabled="readonly"
982
+ :label="t('builder.multiple')"
983
+ />
984
+ <UInput
985
+ :model-value="field.accept?.join(',') ?? ''"
986
+ :disabled="readonly"
987
+ :placeholder="t('builder.acceptedExtensionsPlaceholder')"
988
+ @update:model-value="(value) => {
989
+ field.accept = value.split(',').map((item) => item.trim()).filter((item) => item !== '');
990
+ }"
991
+ />
992
+ </div>
993
+ </div>
994
+ </details>
995
+ </div>
996
+ </UCard>
997
+ </template>
998
+ </Draggable>
999
+ </div>
1000
+ </template>
1001
+ </Draggable>
1002
+
1003
+ <UCard
1004
+ variant="subtle"
1005
+ class="builder-card conditions-card"
1006
+ >
1007
+ <div class="builder-row">
1008
+ <h3 class="conditions-title">
1009
+ {{ t("builder.conditions") }}
1010
+ </h3>
1011
+ <UButton
1012
+ color="neutral"
1013
+ variant="soft"
1014
+ icon="i-lucide-plus"
1015
+ :disabled="readonly"
1016
+ @click="builder.addCondition()"
1017
+ >
1018
+ {{ t("builder.addCondition") }}
1019
+ </UButton>
1020
+ </div>
1021
+
1022
+ <div
1023
+ v-for="condition in safeConditions"
1024
+ :key="condition.condition_key"
1025
+ class="condition-card"
1026
+ >
1027
+ <div class="grid grid-cols-1 gap-2 md:grid-cols-4">
1028
+ <USelect
1029
+ v-model="condition.target_type"
1030
+ :items="targetTypeItems"
1031
+ :disabled="readonly"
1032
+ />
1033
+ <USelect
1034
+ v-if="condition.target_type === 'page'"
1035
+ v-model="condition.target_key"
1036
+ :items="pageTargetItems"
1037
+ :disabled="readonly"
1038
+ />
1039
+ <USelect
1040
+ v-else
1041
+ v-model="condition.target_key"
1042
+ :items="fieldTargetItems"
1043
+ :disabled="readonly"
1044
+ />
1045
+ <USelect
1046
+ v-model="condition.action"
1047
+ :items="conditionActionItems"
1048
+ :disabled="readonly"
1049
+ />
1050
+ <USelect
1051
+ v-model="condition.match"
1052
+ :items="conditionMatchItems"
1053
+ :disabled="readonly"
1054
+ />
1055
+ </div>
1056
+
1057
+ <div class="space-y-2">
1058
+ <div
1059
+ v-for="(clause, clauseIndex) in condition.when"
1060
+ :key="`${condition.condition_key}-${clauseIndex}`"
1061
+ class="grid grid-cols-1 gap-2 md:grid-cols-[1fr_180px_1fr_auto]"
1062
+ >
1063
+ <USelect
1064
+ v-model="clause.field_key"
1065
+ :items="fieldTargetItems"
1066
+ :disabled="readonly"
1067
+ />
1068
+ <USelect
1069
+ v-model="clause.operator"
1070
+ :items="conditionOperatorItems"
1071
+ :disabled="readonly"
1072
+ />
1073
+ <UInput
1074
+ v-if="clause.operator !== 'is_empty' && clause.operator !== 'not_empty'"
1075
+ :model-value="typeof clause.value === 'string' ? clause.value : String(clause.value ?? '')"
1076
+ :disabled="readonly"
1077
+ :placeholder="t('builder.valuePlaceholder')"
1078
+ @update:model-value="(value) => {
1079
+ clause.value = value;
1080
+ }"
1081
+ />
1082
+ <div v-else />
1083
+ <UButton
1084
+ color="neutral"
1085
+ variant="soft"
1086
+ :disabled="readonly || condition.when.length <= 1"
1087
+ @click="builder.removeConditionClause(condition.condition_key, clauseIndex)"
1088
+ >
1089
+ {{ t("builder.remove") }}
1090
+ </UButton>
1091
+ </div>
1092
+ </div>
1093
+
1094
+ <div class="condition-actions">
1095
+ <UButton
1096
+ color="neutral"
1097
+ variant="soft"
1098
+ :disabled="readonly"
1099
+ @click="builder.addConditionClause(condition.condition_key)"
1100
+ >
1101
+ {{ t("builder.addClause") }}
1102
+ </UButton>
1103
+ <UButton
1104
+ color="error"
1105
+ variant="soft"
1106
+ :disabled="readonly"
1107
+ @click="builder.removeCondition(condition.condition_key)"
1108
+ >
1109
+ {{ t("builder.deleteCondition") }}
1110
+ </UButton>
1111
+ </div>
1112
+ </div>
1113
+ </UCard>
1114
+ </div>
1115
+
1116
+ <aside class="builder-rail-column">
1117
+ <div
1118
+ class="builder-rail"
1119
+ :style="{ top: `${railTop}px`, left: `${railLeft}px` }"
1120
+ >
1121
+ <UTooltip :text="t('builder.rail.addQuestion')">
1122
+ <UButton
1123
+ color="neutral"
1124
+ variant="soft"
1125
+ icon="i-lucide-circle-plus"
1126
+ class="rail-action"
1127
+ :disabled="readonly"
1128
+ @click="addQuestionFromRail"
1129
+ />
1130
+ </UTooltip>
1131
+ <UTooltip :text="t('builder.rail.addPage')">
1132
+ <UButton
1133
+ color="neutral"
1134
+ variant="soft"
1135
+ icon="i-lucide-file-plus-2"
1136
+ class="rail-action"
1137
+ :disabled="readonly"
1138
+ @click="addPageFromRail"
1139
+ />
1140
+ </UTooltip>
1141
+ </div>
1142
+ </aside>
1143
+ </div>
1144
+ </div>
1145
+ <div
1146
+ v-else
1147
+ class="space-y-6"
1148
+ >
1149
+ <UCard>
1150
+ <div class="text-sm text-muted">
1151
+ {{ t("builder.loadingBuilder") }}
1152
+ </div>
1153
+ </UCard>
1154
+ </div>
1155
+ </template>
1156
+
1157
+ <style scoped>
1158
+ .builder-root{width:100%}.builder-layout{align-items:start;display:grid;gap:1rem;grid-template-columns:minmax(0,1fr) auto;width:100%}.builder-column{display:grid;gap:1.5rem;min-width:0;width:100%}.builder-card{border-radius:16px}.builder-toolbar{display:grid;gap:1rem}.builder-toolbar-grid{display:grid;gap:.85rem;grid-template-columns:1fr}.builder-category-control{align-items:center;display:grid;gap:.5rem;grid-template-columns:minmax(0,1fr) auto}.builder-toolbar-actions{align-items:center;border-top:1px solid var(--ui-border-muted);display:flex;flex-wrap:wrap;gap:.75rem;justify-content:space-between;padding-top:.85rem}.builder-status{color:var(--ui-text-muted);font-size:.82rem}.builder-error{color:var(--ui-error);margin-left:.5rem}.builder-actions{align-items:center;display:flex;flex-wrap:wrap;gap:.5rem}.builder-stack{display:grid;gap:1rem}.pages-stack{gap:2.25rem}.drag-handle{color:var(--ui-text-muted);cursor:grab;padding-top:.4rem;-webkit-user-select:none;-moz-user-select:none;user-select:none}.page-block{display:grid;gap:1rem}.page-meta-card{border-radius:16px}.page-chip-row{margin-bottom:.65rem}.page-header{align-items:start;display:grid;gap:.75rem;grid-template-columns:auto minmax(0,1fr) auto}.page-header-main{display:grid;gap:.6rem;min-width:0}.page-actions{align-items:center;display:inline-flex;gap:.25rem}.page-questions-stack{gap:1rem}.field-card{border-left:4px solid transparent;border-radius:14px;cursor:pointer}.field-card--active{border-left-color:var(--ui-primary)}.field-shell{display:grid;gap:.85rem}.field-head{align-items:center;display:grid;gap:.75rem;grid-template-columns:auto minmax(0,1fr) minmax(220px,280px)}.field-controls{align-items:center;display:flex;flex-wrap:wrap;gap:.35rem}.field-required-switch{align-items:center;color:var(--ui-text-toned);display:inline-flex;font-size:.82rem;gap:.5rem;margin-left:auto}.field-preview{color:var(--ui-text-muted);font-size:.84rem;margin:0}.field-inline-preview{background:var(--ui-bg-muted);border-radius:10px;display:grid;gap:.5rem;padding:.75rem}.field-inline-preview-label{color:var(--ui-text-muted);font-size:.8rem;margin:0}.field-inline-preview-line{border-bottom:1px dashed var(--ui-border-accented);height:1.4rem;width:min(32rem,100%)}.field-inline-preview-textarea{display:grid;gap:.5rem;width:min(32rem,100%)}.field-inline-preview-textarea>span{border-bottom:1px dashed var(--ui-border-accented);display:block;height:1.2rem}.field-advanced{border-top:1px solid var(--ui-border-muted);padding-top:.75rem}.field-advanced-summary{color:var(--ui-text-toned);cursor:pointer;font-size:.82rem;font-weight:500}.field-advanced-body{background:var(--ui-bg-elevated);border-radius:12px;display:grid;gap:.75rem;margin-top:.75rem;padding:.75rem}.field-advanced-grid{display:grid;gap:.65rem}.field-toggle-grid{grid-template-columns:repeat(3,minmax(0,1fr))}.field-options,.field-toggle-grid{display:grid;gap:.6rem}.field-option-row{align-items:center;display:grid;gap:.45rem;grid-template-columns:minmax(0,1fr) auto}.field-numbers{grid-template-columns:repeat(3,minmax(0,1fr))}.field-file,.field-numbers{display:grid;gap:.55rem}.conditions-card{border-radius:16px}.builder-row{align-items:center;display:flex;gap:.75rem;justify-content:space-between}.conditions-title{font-size:.95rem;font-weight:600}.condition-card{border:1px solid var(--ui-border-muted);border-radius:12px;display:grid;gap:.75rem;padding:.75rem}.condition-actions{align-items:center;display:flex;flex-wrap:wrap;gap:.5rem}.builder-rail-column{display:flex;justify-content:center;width:4.25rem}.builder-rail{background:var(--ui-bg);border:1px solid var(--ui-border-muted);border-radius:14px;box-shadow:0 8px 24px color-mix(in srgb,var(--ui-text) 8%,transparent);display:flex;flex-direction:column;gap:.5rem;padding:.5rem;position:fixed;transition:top .12s ease,left .12s ease;z-index:20}.rail-action{height:2.75rem;justify-content:center;width:2.75rem}@media (min-width:1024px){.builder-toolbar-grid{grid-template-columns:1.2fr .8fr}}@media (max-width:1024px){.builder-layout{display:block}.builder-rail{bottom:.85rem;flex-direction:row;left:auto!important;position:fixed;right:.85rem;top:auto!important}}@media (max-width:768px){.page-header{grid-template-columns:minmax(0,1fr) auto}.page-drag-handle{display:none}.field-head{grid-template-columns:minmax(0,1fr)}.field-drag-handle{display:none}.field-numbers,.field-toggle-grid{grid-template-columns:1fr}.field-required-switch{margin-left:0}}
1159
+ </style>