@evanschleret/formforgeclient 1.2.4 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +10 -0
  2. package/dist/module.cjs +1 -0
  3. package/dist/module.d.cts +1 -0
  4. package/dist/module.d.mts +1 -0
  5. package/dist/module.d.ts +1 -0
  6. package/dist/module.json +1 -1
  7. package/dist/module.mjs +1 -0
  8. package/dist/runtime/api/client.js +4 -2
  9. package/dist/runtime/api/request.d.ts +1 -0
  10. package/dist/runtime/api/schema.js +4 -4
  11. package/dist/runtime/assets/formforge.css +1 -0
  12. package/dist/runtime/composables/index.d.ts +1 -1
  13. package/dist/runtime/composables/useFormForgeBuilder.d.ts +24 -2
  14. package/dist/runtime/composables/useFormForgeBuilder.js +299 -43
  15. package/dist/runtime/composables/useFormForgeForm.js +15 -5
  16. package/dist/runtime/composables/useFormForgeI18n.d.ts +299 -19
  17. package/dist/runtime/composables/useFormForgeI18n.js +299 -19
  18. package/dist/runtime/composables/useFormForgeSubmit.js +31 -9
  19. package/dist/runtime/index.d.ts +1 -0
  20. package/dist/runtime/renderers/default/FormForgeBuilder.d.vue.ts +21 -2
  21. package/dist/runtime/renderers/default/FormForgeBuilder.vue +689 -738
  22. package/dist/runtime/renderers/default/FormForgeBuilder.vue.d.ts +21 -2
  23. package/dist/runtime/renderers/default/FormForgeBuilderBlockSettingsModal.d.vue.ts +17 -0
  24. package/dist/runtime/renderers/default/FormForgeBuilderBlockSettingsModal.vue +32 -0
  25. package/dist/runtime/renderers/default/FormForgeBuilderBlockSettingsModal.vue.d.ts +17 -0
  26. package/dist/runtime/renderers/default/FormForgeRenderer.d.vue.ts +3 -4
  27. package/dist/runtime/renderers/default/FormForgeRenderer.vue +344 -294
  28. package/dist/runtime/renderers/default/FormForgeRenderer.vue.d.ts +3 -4
  29. package/dist/runtime/renderers/default/FormForgeRendererField.d.vue.ts +22 -0
  30. package/dist/runtime/renderers/default/FormForgeRendererField.vue +237 -0
  31. package/dist/runtime/renderers/default/FormForgeRendererField.vue.d.ts +22 -0
  32. package/dist/runtime/renderers/default/FormForgeRendererPage.d.vue.ts +18 -0
  33. package/dist/runtime/renderers/default/FormForgeRendererPage.vue +31 -0
  34. package/dist/runtime/renderers/default/FormForgeRendererPage.vue.d.ts +18 -0
  35. package/dist/runtime/renderers/default/FormForgeResponse.vue +4 -3
  36. package/dist/runtime/renderers/default/builder/FormForgeBuilderAddressFieldsCard.d.vue.ts +11 -0
  37. package/dist/runtime/renderers/default/builder/FormForgeBuilderAddressFieldsCard.vue +118 -0
  38. package/dist/runtime/renderers/default/builder/FormForgeBuilderAddressFieldsCard.vue.d.ts +11 -0
  39. package/dist/runtime/renderers/default/builder/FormForgeBuilderBlockCard.d.vue.ts +46 -0
  40. package/dist/runtime/renderers/default/builder/FormForgeBuilderBlockCard.vue +205 -0
  41. package/dist/runtime/renderers/default/builder/FormForgeBuilderBlockCard.vue.d.ts +46 -0
  42. package/dist/runtime/renderers/default/builder/FormForgeBuilderChoiceDisplayField.d.vue.ts +11 -0
  43. package/dist/runtime/renderers/default/builder/FormForgeBuilderChoiceDisplayField.vue +37 -0
  44. package/dist/runtime/renderers/default/builder/FormForgeBuilderChoiceDisplayField.vue.d.ts +11 -0
  45. package/dist/runtime/renderers/default/builder/FormForgeBuilderChoiceOptionsField.d.vue.ts +11 -0
  46. package/dist/runtime/renderers/default/builder/FormForgeBuilderChoiceOptionsField.vue +195 -0
  47. package/dist/runtime/renderers/default/builder/FormForgeBuilderChoiceOptionsField.vue.d.ts +11 -0
  48. package/dist/runtime/renderers/default/builder/FormForgeBuilderDescriptionField.d.vue.ts +14 -0
  49. package/dist/runtime/renderers/default/builder/FormForgeBuilderDescriptionField.vue +91 -0
  50. package/dist/runtime/renderers/default/builder/FormForgeBuilderDescriptionField.vue.d.ts +14 -0
  51. package/dist/runtime/renderers/default/builder/FormForgeBuilderLogicPanel.d.vue.ts +13 -0
  52. package/dist/runtime/renderers/default/builder/FormForgeBuilderLogicPanel.vue +387 -0
  53. package/dist/runtime/renderers/default/builder/FormForgeBuilderLogicPanel.vue.d.ts +13 -0
  54. package/dist/runtime/renderers/default/builder/FormForgeBuilderQuestionRow.d.vue.ts +44 -0
  55. package/dist/runtime/renderers/default/builder/FormForgeBuilderQuestionRow.vue +328 -0
  56. package/dist/runtime/renderers/default/builder/FormForgeBuilderQuestionRow.vue.d.ts +44 -0
  57. package/dist/runtime/renderers/default/builder/FormForgeBuilderTemporalModeField.d.vue.ts +11 -0
  58. package/dist/runtime/renderers/default/builder/FormForgeBuilderTemporalModeField.vue +47 -0
  59. package/dist/runtime/renderers/default/builder/FormForgeBuilderTemporalModeField.vue.d.ts +11 -0
  60. package/dist/runtime/renderers/default/builder/FormForgeBuilderValidationRulesSection.d.vue.ts +14 -0
  61. package/dist/runtime/renderers/default/builder/FormForgeBuilderValidationRulesSection.vue +595 -0
  62. package/dist/runtime/renderers/default/builder/FormForgeBuilderValidationRulesSection.vue.d.ts +14 -0
  63. package/dist/runtime/renderers/default/builder/builderFieldHelpers.d.ts +3 -0
  64. package/dist/runtime/renderers/default/builder/builderFieldHelpers.js +4 -0
  65. package/dist/runtime/types/index.d.ts +1 -1
  66. package/dist/runtime/types/management.d.ts +12 -0
  67. package/dist/runtime/types/schema.d.ts +72 -4
  68. package/dist/runtime/utils/defaults.d.ts +7 -0
  69. package/dist/runtime/utils/defaults.js +86 -0
  70. package/dist/runtime/utils/page-logic.d.ts +24 -0
  71. package/dist/runtime/utils/page-logic.js +351 -0
  72. package/dist/runtime/utils/rich-text.d.ts +3 -0
  73. package/dist/runtime/utils/rich-text.js +72 -0
  74. package/dist/runtime/utils/schema.d.ts +1 -1
  75. package/dist/runtime/utils/schema.js +70 -16
  76. package/dist/runtime/utils/temporal.d.ts +10 -0
  77. package/dist/runtime/utils/temporal.js +28 -0
  78. package/dist/runtime/utils/validation.d.ts +5 -0
  79. package/dist/runtime/utils/validation.js +36 -0
  80. package/dist/runtime/validation/zod.d.ts +5 -2
  81. package/dist/runtime/validation/zod.js +563 -54
  82. package/dist/types.d.mts +2 -0
  83. package/package.json +18 -14
@@ -1,18 +1,24 @@
1
1
  <script setup>
2
- import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "#imports";
2
+ import { computed, nextTick, ref, watch } from "#imports";
3
+ import { parseDate, parseTime } from "@internationalized/date";
3
4
  import { useOverlay } from "@nuxt/ui/composables/useOverlay";
4
5
  import { useToast } from "@nuxt/ui/composables/useToast";
5
6
  import Draggable from "vuedraggable";
6
7
  import {
7
- FORM_FORGE_BUILDER_CONDITION_ACTIONS,
8
- FORM_FORGE_BUILDER_CONDITION_MATCHES,
9
- FORM_FORGE_BUILDER_CONDITION_OPERATORS,
10
8
  FORM_FORGE_BUILDER_FIELD_TYPES,
11
9
  useFormForgeBuilder
12
10
  } from "../../composables/useFormForgeBuilder";
13
11
  import { useFormForgeCategory, useFormForgeCategoryOptions } from "../../composables/useFormForgeCategory";
14
12
  import { useFormForgeI18n } from "../../composables/useFormForgeI18n";
13
+ import {
14
+ createDefaultAddressFields,
15
+ resolveDefaultConsentLabel
16
+ } from "../../utils/defaults";
17
+ import {
18
+ ensurePageLogic
19
+ } from "../../utils/page-logic";
15
20
  import FormForgeCategoryCreateModal from "./FormForgeCategoryCreateModal.vue";
21
+ import FormForgeBuilderBlockCard from "./builder/FormForgeBuilderBlockCard.vue";
16
22
  const props = defineProps({
17
23
  formUuid: { type: String, required: false, default: void 0 },
18
24
  formKey: { type: String, required: false, default: void 0 },
@@ -29,24 +35,29 @@ const props = defineProps({
29
35
  disableTitleInput: { type: Boolean, required: false, default: false },
30
36
  disableCategoryControl: { type: Boolean, required: false, default: false },
31
37
  disablePublishAction: { type: Boolean, required: false, default: false },
32
- defaultPublished: { type: Boolean, required: false, default: false }
38
+ disableSettingsTab: { type: Boolean, required: false, default: false },
39
+ hideSettings: { type: Boolean, required: false, default: false },
40
+ autoPublishOnSave: { type: Boolean, required: false, default: false },
41
+ defaultPublished: { type: Boolean, required: false, default: false },
42
+ standalone: { type: Boolean, required: false, default: false }
33
43
  });
34
- const emit = defineEmits(["update:modelValue", "save", "publish", "unpublish", "error"]);
44
+ const emit = defineEmits(["update:modelValue", "save", "publish", "unpublish", "error", "selection-change"]);
35
45
  const builderOptions = {
36
46
  formUuid: props.formUuid,
37
47
  formKey: props.formKey,
38
48
  endpoint: props.endpoint,
49
+ locale: props.locale,
39
50
  initial: props.modelValue,
40
51
  autosave: props.autosave,
41
- autosaveDelay: props.autosaveDelay
52
+ autosaveDelay: props.autosaveDelay,
53
+ autoPublishOnSave: props.autoPublishOnSave
42
54
  };
43
55
  const builder = useFormForgeBuilder(builderOptions);
44
- const { t } = useFormForgeI18n({
56
+ const { t, locale } = useFormForgeI18n({
45
57
  locale: () => props.locale
46
58
  });
47
59
  const toast = useToast();
48
60
  const draft = builder.draft;
49
- const isClientReady = ref(false);
50
61
  const saving = builder.saving;
51
62
  const publishing = builder.publishing;
52
63
  const publishable = builder.publishable;
@@ -66,14 +77,10 @@ const categoryOptions = useFormForgeCategoryOptions({
66
77
  includeInactive: true
67
78
  });
68
79
  const CATEGORY_NONE_VALUE = "__formforge_no_category__";
69
- const fieldElements = /* @__PURE__ */ new Map();
70
- const builderRootElement = ref(null);
71
- const builderColumnElement = ref(null);
72
- const railTop = ref(120);
73
- const railLeft = ref(0);
74
80
  const loadingRemoteForm = ref(false);
75
81
  const isPublished = ref(props.defaultPublished);
76
82
  let loadRequestId = 0;
83
+ const settingsHidden = computed(() => props.hideSettings || props.disableSettingsTab);
77
84
  const safePages = computed(() => {
78
85
  const pages = draft.value?.pages;
79
86
  if (!Array.isArray(pages)) {
@@ -81,13 +88,6 @@ const safePages = computed(() => {
81
88
  }
82
89
  return pages.filter((page) => page !== void 0 && page !== null);
83
90
  });
84
- const safeConditions = computed(() => {
85
- const conditions = draft.value?.conditions;
86
- if (!Array.isArray(conditions)) {
87
- return [];
88
- }
89
- return conditions.filter((condition) => condition !== void 0 && condition !== null);
90
- });
91
91
  const draftTitle = computed({
92
92
  get() {
93
93
  return typeof draft.value?.title === "string" ? draft.value.title : "";
@@ -153,8 +153,8 @@ const draftMutationIdentifier = computed(() => {
153
153
  }
154
154
  return null;
155
155
  });
156
- const showTopControls = computed(() => {
157
- return !props.disableTitleInput || !props.disableCategoryControl;
156
+ const builderLayoutClass = computed(() => {
157
+ return props.standalone ? "grid gap-4" : "grid gap-4 lg:grid-cols-[minmax(0,1fr)_4rem]";
158
158
  });
159
159
  function twoDigits(value) {
160
160
  return String(value).padStart(2, "0");
@@ -181,6 +181,239 @@ const formattedLastSavedAt = computed(() => {
181
181
  const minutes = twoDigits(parsed.getMinutes());
182
182
  return `${day}.${month}.${year} \xE0 ${hours}:${minutes}`;
183
183
  });
184
+ function parseDateTimeValue(value) {
185
+ if (typeof value !== "string" || value.trim() === "") {
186
+ return {
187
+ date: null,
188
+ time: null
189
+ };
190
+ }
191
+ const parsed = new Date(value);
192
+ if (Number.isNaN(parsed.getTime())) {
193
+ return {
194
+ date: null,
195
+ time: null
196
+ };
197
+ }
198
+ return {
199
+ date: parseDate([
200
+ parsed.getFullYear(),
201
+ twoDigits(parsed.getMonth() + 1),
202
+ twoDigits(parsed.getDate())
203
+ ].join("-")),
204
+ time: parseTime([
205
+ twoDigits(parsed.getHours()),
206
+ twoDigits(parsed.getMinutes()),
207
+ twoDigits(parsed.getSeconds())
208
+ ].join(":"))
209
+ };
210
+ }
211
+ function serializeDateTimeValue(date, time) {
212
+ if (date === null) {
213
+ return null;
214
+ }
215
+ const year = date.year;
216
+ const month = twoDigits(date.month);
217
+ const day = twoDigits(date.day);
218
+ const hours = time !== null ? twoDigits(time.hour) : "00";
219
+ const minutes = time !== null ? twoDigits(time.minute) : "00";
220
+ const seconds = time !== null ? twoDigits(time.second) : "00";
221
+ return (/* @__PURE__ */ new Date(`${year}-${month}-${day}T${hours}:${minutes}:${seconds}`)).toISOString();
222
+ }
223
+ function currentDateTimeValue() {
224
+ return (/* @__PURE__ */ new Date()).toISOString();
225
+ }
226
+ const selectedTab = ref("builder");
227
+ const publishAtParts = computed(() => parseDateTimeValue(draft.value?.publish_at));
228
+ const pauseAtParts = computed(() => parseDateTimeValue(draft.value?.pause_at));
229
+ const publishAtDate = computed({
230
+ get() {
231
+ return publishAtParts.value.date;
232
+ },
233
+ set(value) {
234
+ if (draft.value === void 0 || draft.value === null) {
235
+ return;
236
+ }
237
+ draft.value.publish_at = serializeDateTimeValue(value, publishAtParts.value.time);
238
+ }
239
+ });
240
+ const publishAtTime = computed({
241
+ get() {
242
+ return publishAtParts.value.time;
243
+ },
244
+ set(value) {
245
+ if (draft.value === void 0 || draft.value === null) {
246
+ return;
247
+ }
248
+ draft.value.publish_at = serializeDateTimeValue(publishAtParts.value.date, value);
249
+ }
250
+ });
251
+ const pauseAtDate = computed({
252
+ get() {
253
+ return pauseAtParts.value.date;
254
+ },
255
+ set(value) {
256
+ if (draft.value === void 0 || draft.value === null) {
257
+ return;
258
+ }
259
+ draft.value.pause_at = serializeDateTimeValue(value, pauseAtParts.value.time);
260
+ }
261
+ });
262
+ const pauseAtTime = computed({
263
+ get() {
264
+ return pauseAtParts.value.time;
265
+ },
266
+ set(value) {
267
+ if (draft.value === void 0 || draft.value === null) {
268
+ return;
269
+ }
270
+ draft.value.pause_at = serializeDateTimeValue(pauseAtParts.value.date, value);
271
+ }
272
+ });
273
+ const responseLimitValue = computed({
274
+ get() {
275
+ return typeof draft.value?.response_limit === "number" ? draft.value.response_limit : null;
276
+ },
277
+ set(value) {
278
+ if (draft.value === void 0 || draft.value === null) {
279
+ return;
280
+ }
281
+ draft.value.response_limit = typeof value === "number" && Number.isFinite(value) ? value : null;
282
+ }
283
+ });
284
+ const openingEnabled = computed({
285
+ get() {
286
+ return draft.value?.publish_at !== null && draft.value?.publish_at !== void 0;
287
+ },
288
+ set(value) {
289
+ if (draft.value === void 0 || draft.value === null) {
290
+ return;
291
+ }
292
+ if (value && (draft.value.publish_at === null || draft.value.publish_at === void 0)) {
293
+ draft.value.publish_at = currentDateTimeValue();
294
+ return;
295
+ }
296
+ if (!value) {
297
+ draft.value.publish_at = null;
298
+ }
299
+ }
300
+ });
301
+ const closingEnabled = computed({
302
+ get() {
303
+ return draft.value?.pause_at !== null && draft.value?.pause_at !== void 0;
304
+ },
305
+ set(value) {
306
+ if (draft.value === void 0 || draft.value === null) {
307
+ return;
308
+ }
309
+ if (value && (draft.value.pause_at === null || draft.value.pause_at === void 0)) {
310
+ draft.value.pause_at = currentDateTimeValue();
311
+ return;
312
+ }
313
+ if (!value) {
314
+ draft.value.pause_at = null;
315
+ }
316
+ }
317
+ });
318
+ const responseLimitEnabled = computed({
319
+ get() {
320
+ return draft.value?.response_limit !== null && draft.value?.response_limit !== void 0;
321
+ },
322
+ set(value) {
323
+ if (draft.value === void 0 || draft.value === null) {
324
+ return;
325
+ }
326
+ if (value && (draft.value.response_limit === null || draft.value.response_limit === void 0)) {
327
+ draft.value.response_limit = 1;
328
+ return;
329
+ }
330
+ if (!value) {
331
+ draft.value.response_limit = null;
332
+ }
333
+ }
334
+ });
335
+ const submissionPinEnabled = computed({
336
+ get() {
337
+ return draft.value?.submission_code_required === true;
338
+ },
339
+ set(value) {
340
+ if (draft.value === void 0 || draft.value === null) {
341
+ return;
342
+ }
343
+ draft.value.submission_code_required = value;
344
+ if (!value) {
345
+ draft.value.submission_code = null;
346
+ }
347
+ }
348
+ });
349
+ const submissionPinValue = computed({
350
+ get() {
351
+ return typeof draft.value?.submission_code === "string" ? draft.value.submission_code : "";
352
+ },
353
+ set(value) {
354
+ if (draft.value === void 0 || draft.value === null) {
355
+ return;
356
+ }
357
+ draft.value.submission_code = value;
358
+ }
359
+ });
360
+ const publicUrlValue = computed(() => {
361
+ return typeof draft.value?.public_url === "string" ? draft.value.public_url : "";
362
+ });
363
+ function ensureSubmissionApi() {
364
+ if (draft.value === void 0 || draft.value === null) {
365
+ return {};
366
+ }
367
+ if (typeof draft.value.api !== "object" || draft.value.api === null || Array.isArray(draft.value.api)) {
368
+ draft.value.api = {};
369
+ }
370
+ const api = draft.value.api;
371
+ if (typeof api.submission !== "object" || api.submission === null || Array.isArray(api.submission)) {
372
+ api.submission = {};
373
+ }
374
+ return api.submission;
375
+ }
376
+ const submissionIsPublic = computed({
377
+ get() {
378
+ const api = draft.value?.api;
379
+ if (typeof api !== "object" || api === null || Array.isArray(api)) {
380
+ return true;
381
+ }
382
+ const submission = api.submission;
383
+ if (typeof submission !== "object" || submission === null || Array.isArray(submission)) {
384
+ return true;
385
+ }
386
+ if (typeof submission.public === "boolean") {
387
+ return submission.public;
388
+ }
389
+ if (typeof submission.auth === "string") {
390
+ return submission.auth === "public";
391
+ }
392
+ return true;
393
+ },
394
+ set(value) {
395
+ const submission = ensureSubmissionApi();
396
+ submission.public = value;
397
+ submission.auth = value ? "public" : "required";
398
+ }
399
+ });
400
+ const builderTabs = computed(() => {
401
+ const items = [
402
+ {
403
+ label: t("builder.tabs.builder"),
404
+ icon: "i-lucide-layout-grid",
405
+ slot: "builder"
406
+ }
407
+ ];
408
+ if (!settingsHidden.value) {
409
+ items.push({
410
+ label: t("builder.tabs.settings"),
411
+ icon: "i-lucide-settings-2",
412
+ slot: "settings"
413
+ });
414
+ }
415
+ return items;
416
+ });
184
417
  const selectedPageKey = ref(safePages.value[0]?.page_key ?? null);
185
418
  const selectedFieldKey = ref(safePages.value[0]?.fields[0]?.field_key ?? null);
186
419
  const fieldTypeMeta = computed(() => ({
@@ -192,13 +425,13 @@ const fieldTypeMeta = computed(() => ({
192
425
  select_menu: { label: t("builder.fieldType.select_menu"), icon: "i-lucide-list-filter" },
193
426
  radio: { label: t("builder.fieldType.radio"), icon: "i-lucide-circle-dot" },
194
427
  checkbox: { label: t("builder.fieldType.checkbox"), icon: "i-lucide-check-square" },
428
+ consent: { label: t("builder.fieldType.consent"), icon: "i-lucide-badge-check" },
195
429
  checkbox_group: { label: t("builder.fieldType.checkbox_group"), icon: "i-lucide-list-checks" },
430
+ address: { label: t("builder.fieldType.address"), icon: "i-lucide-house" },
196
431
  switch: { label: t("builder.fieldType.switch"), icon: "i-lucide-toggle-left" },
432
+ temporal: { label: t("builder.fieldType.temporal"), icon: "i-lucide-calendar-clock" },
197
433
  date: { label: t("builder.fieldType.date"), icon: "i-lucide-calendar" },
198
434
  time: { label: t("builder.fieldType.time"), icon: "i-lucide-clock-3" },
199
- datetime: { label: t("builder.fieldType.datetime"), icon: "i-lucide-calendar-clock" },
200
- date_range: { label: t("builder.fieldType.date_range"), icon: "i-lucide-calendar-range" },
201
- datetime_range: { label: t("builder.fieldType.datetime_range"), icon: "i-lucide-calendar-range" },
202
435
  file: { label: t("builder.fieldType.file"), icon: "i-lucide-paperclip" }
203
436
  }));
204
437
  const fieldTypeItems = computed(() => FORM_FORGE_BUILDER_FIELD_TYPES.map((type) => ({
@@ -206,88 +439,25 @@ const fieldTypeItems = computed(() => FORM_FORGE_BUILDER_FIELD_TYPES.map((type)
206
439
  value: type,
207
440
  icon: fieldTypeMeta.value[type].icon
208
441
  })));
209
- function conditionActionLabel(action) {
210
- const labels = {
211
- show: t("builder.condition.action.show"),
212
- hide: t("builder.condition.action.hide"),
213
- skip: t("builder.condition.action.skip"),
214
- require: t("builder.condition.action.require"),
215
- disable: t("builder.condition.action.disable")
216
- };
217
- return labels[action];
442
+ function isChoiceFieldType(type) {
443
+ return type === "radio" || type === "checkbox_group";
218
444
  }
219
- function conditionMatchLabel(match) {
220
- const labels = {
221
- all: t("builder.condition.match.all"),
222
- any: t("builder.condition.match.any")
445
+ function createChoiceOption(index) {
446
+ return {
447
+ label: "",
448
+ value: `option_${index + 1}`
223
449
  };
224
- return labels[match];
225
450
  }
226
- function conditionOperatorLabel(operator) {
227
- const labels = {
228
- eq: t("builder.condition.operator.eq"),
229
- neq: t("builder.condition.operator.neq"),
230
- in: t("builder.condition.operator.in"),
231
- not_in: t("builder.condition.operator.not_in"),
232
- gt: t("builder.condition.operator.gt"),
233
- gte: t("builder.condition.operator.gte"),
234
- lt: t("builder.condition.operator.lt"),
235
- lte: t("builder.condition.operator.lte"),
236
- contains: t("builder.condition.operator.contains"),
237
- not_contains: t("builder.condition.operator.not_contains"),
238
- is_empty: t("builder.condition.operator.is_empty"),
239
- not_empty: t("builder.condition.operator.not_empty")
240
- };
241
- return labels[operator];
242
- }
243
- const conditionActionItems = computed(() => FORM_FORGE_BUILDER_CONDITION_ACTIONS.map((action) => ({
244
- label: conditionActionLabel(action),
245
- value: action
246
- })));
247
- const conditionMatchItems = computed(() => FORM_FORGE_BUILDER_CONDITION_MATCHES.map((match) => ({
248
- label: conditionMatchLabel(match),
249
- value: match
250
- })));
251
- const conditionOperatorItems = computed(() => FORM_FORGE_BUILDER_CONDITION_OPERATORS.map((operator) => ({
252
- label: conditionOperatorLabel(operator),
253
- value: operator
254
- })));
255
- const targetTypeItems = computed(() => [
256
- { label: t("builder.targetType.page"), value: "page" },
257
- { label: t("builder.targetType.field"), value: "field" }
258
- ]);
259
- const pageTargetItems = computed(() => {
260
- const items = [];
261
- for (const maybePage of safePages.value) {
262
- const title = typeof maybePage.title === "string" ? maybePage.title : "";
263
- items.push({
264
- label: title === "" ? maybePage.page_key : `${title} (${maybePage.page_key})`,
265
- value: maybePage.page_key
266
- });
451
+ function normalizeLoadedPageLogic(pages) {
452
+ if (!Array.isArray(pages)) {
453
+ return;
267
454
  }
268
- return items;
269
- });
270
- const fieldTargetItems = computed(() => {
271
- const items = [];
272
- for (const maybePage of safePages.value) {
273
- if (!Array.isArray(maybePage.fields)) {
455
+ for (const page of pages) {
456
+ if (page === void 0 || page === null) {
274
457
  continue;
275
458
  }
276
- for (const maybeField of maybePage.fields) {
277
- const label = maybeField.label === void 0 || maybeField.label === "" ? maybeField.field_key : `${maybeField.label} (${maybeField.field_key})`;
278
- items.push({
279
- label,
280
- value: maybeField.field_key
281
- });
282
- }
283
- }
284
- return items;
285
- });
286
- function selectedPage() {
287
- if (selectedPageKey.value === null) {
288
- return safePages.value[0];
459
+ ensurePageLogic(page);
289
460
  }
290
- return safePages.value.find((page) => page.page_key === selectedPageKey.value) ?? safePages.value[0];
291
461
  }
292
462
  function syncSelectionWithDraft(pages) {
293
463
  if (pages.length === 0) {
@@ -302,123 +472,29 @@ function syncSelectionWithDraft(pages) {
302
472
  selectedFieldKey.value = null;
303
473
  return;
304
474
  }
305
- const nextField = selectedFieldKey.value === null ? fields[0] : fields.find((field) => field.field_key === selectedFieldKey.value) ?? fields[0];
306
- selectedFieldKey.value = nextField.field_key;
307
- }
308
- function resolveHTMLElement(element) {
309
- if (element instanceof HTMLElement) {
310
- return element;
311
- }
312
- if (typeof element !== "object" || element === null || !("$el" in element)) {
313
- return null;
314
- }
315
- const componentElement = element.$el;
316
- return componentElement instanceof HTMLElement ? componentElement : null;
317
- }
318
- function registerFieldElement(fieldKey, element) {
319
- const htmlElement = resolveHTMLElement(element);
320
- if (htmlElement !== null) {
321
- fieldElements.set(fieldKey, htmlElement);
322
- if (fieldKey === selectedFieldKey.value) {
323
- nextTick(() => {
324
- updateRailPosition();
325
- });
326
- }
327
- return;
328
- }
329
- fieldElements.delete(fieldKey);
330
- }
331
- function updateRailPosition() {
332
- if (!isClientReady.value || typeof window === "undefined") {
333
- return;
334
- }
335
- const rootElement = builderRootElement.value;
336
- if (rootElement === null) {
475
+ if (selectedFieldKey.value === null) {
337
476
  return;
338
477
  }
339
- const rootRect = rootElement.getBoundingClientRect();
340
- const columnRect = builderColumnElement.value?.getBoundingClientRect() ?? rootRect;
341
- const selectedElement = selectedFieldKey.value === null ? void 0 : fieldElements.get(selectedFieldKey.value);
342
- const selectedRect = selectedElement?.getBoundingClientRect();
343
- if (window.innerWidth <= 1024) {
344
- railLeft.value = 0;
345
- return;
346
- }
347
- const railWidth = 68;
348
- const horizontalGap = 14;
349
- const minLeft = 8;
350
- const maxLeft = window.innerWidth - railWidth - 8;
351
- const horizontalAnchor = selectedRect?.right ?? columnRect.right;
352
- const desiredLeft = horizontalAnchor + horizontalGap;
353
- railLeft.value = Math.min(Math.max(desiredLeft, minLeft), maxLeft);
354
- const railHeight = 122;
355
- const minTop = Math.max(88, columnRect.top + 8);
356
- const maxTop = Math.max(minTop, Math.min(window.innerHeight - railHeight - 12, columnRect.bottom - railHeight - 8));
357
- if (selectedRect === void 0) {
358
- railTop.value = minTop;
359
- return;
360
- }
361
- const desiredTop = selectedRect.top + selectedRect.height / 2 - railHeight / 2;
362
- railTop.value = Math.min(Math.max(desiredTop, minTop), maxTop);
478
+ const nextField = fields.find((field) => field.field_key === selectedFieldKey.value) ?? fields[0];
479
+ selectedFieldKey.value = nextField.field_key;
363
480
  }
364
- onMounted(() => {
365
- isClientReady.value = true;
366
- window.addEventListener("scroll", updateRailPosition, { passive: true });
367
- window.addEventListener("resize", updateRailPosition);
368
- nextTick(() => {
369
- syncSelectionWithDraft(safePages.value);
370
- updateRailPosition();
371
- });
372
- });
373
- onBeforeUnmount(() => {
374
- window.removeEventListener("scroll", updateRailPosition);
375
- window.removeEventListener("resize", updateRailPosition);
376
- fieldElements.clear();
377
- });
378
481
  function selectField(page, fieldKey) {
379
482
  selectedPageKey.value = page.page_key;
380
483
  selectedFieldKey.value = fieldKey;
381
- nextTick(() => {
382
- updateRailPosition();
383
- });
384
484
  }
385
- function updatePageTitle(page, value) {
485
+ function findPage(pageKey) {
486
+ return safePages.value.find((page) => page.page_key === pageKey);
487
+ }
488
+ function selectFieldByKey(pageKey, fieldKey) {
489
+ const page = findPage(pageKey);
386
490
  if (page === void 0) {
387
491
  return;
388
492
  }
389
- page.title = value;
390
- }
391
- function readPageTitle(page) {
392
- if (page === void 0 || page === null) {
393
- return "";
493
+ if (selectedPageKey.value === pageKey && selectedFieldKey.value === fieldKey) {
494
+ selectedFieldKey.value = null;
495
+ return;
394
496
  }
395
- return typeof page.title === "string" ? page.title : "";
396
- }
397
- function pageOrder(page) {
398
- const index = safePages.value.findIndex((item) => item.page_key === page.page_key);
399
- return index < 0 ? 1 : index + 1;
400
- }
401
- function canMergeWithPreviousPage(page) {
402
- return safePages.value.length > 1 && pageOrder(page) > 1;
403
- }
404
- function mergePageWithPrevious(page) {
405
- builder.mergePageWithPrevious(page.page_key);
406
- nextTick(() => {
407
- syncSelectionWithDraft(safePages.value);
408
- updateRailPosition();
409
- });
410
- }
411
- function isFieldSelected(fieldKey) {
412
- return selectedFieldKey.value === fieldKey;
413
- }
414
- function showInlinePreview(field) {
415
- return field.type === "text" || field.type === "textarea" || field.type === "email";
416
- }
417
- function fieldTypeLabel(type) {
418
- return fieldTypeMeta.value[type]?.label ?? type;
419
- }
420
- function fieldTypeIcon(type) {
421
- return fieldTypeMeta.value[type]?.icon ?? "i-lucide-square";
497
+ selectField(page, fieldKey);
422
498
  }
423
499
  function isUuidLike(value) {
424
500
  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);
@@ -445,12 +521,21 @@ function applyLoadedForm(schema) {
445
521
  draft.value = {
446
522
  uuid: nextUuid,
447
523
  key: schema.key,
524
+ schema_version: schema.schema_version ?? 1,
448
525
  title: schema.title,
526
+ publish_at: schema.publish_at ?? null,
527
+ pause_at: schema.pause_at ?? null,
528
+ response_limit: schema.response_limit ?? null,
529
+ submission_code_required: schema.submission_code_required === true,
530
+ submission_code: null,
531
+ public_url: schema.public_url ?? null,
449
532
  category: schema.category ?? null,
450
533
  pages: cloneValue(schema.pages),
451
534
  conditions: cloneValue(schema.conditions),
452
- drafts: cloneValue(schema.drafts)
535
+ drafts: cloneValue(schema.drafts),
536
+ api: cloneValue(schema.api ?? {})
453
537
  };
538
+ normalizeLoadedPageLogic(draft.value.pages);
454
539
  isPublished.value = schema.is_published === true;
455
540
  }
456
541
  async function loadFormIntoBuilder(key, version) {
@@ -464,7 +549,6 @@ async function loadFormIntoBuilder(key, version) {
464
549
  applyLoadedForm(schema);
465
550
  nextTick(() => {
466
551
  syncSelectionWithDraft(safePages.value);
467
- updateRailPosition();
468
552
  });
469
553
  } catch (caughtError) {
470
554
  const message = caughtError instanceof Error ? caughtError.message : t("builder.error.loadForm");
@@ -486,22 +570,32 @@ async function loadFormRouteIntoBuilder(routeKey, version) {
486
570
  }
487
571
  await loadFormIntoBuilder(key, version);
488
572
  }
489
- function addField(page, type) {
490
- builder.addField(page.page_key, type);
573
+ function addField(pageKey, type) {
574
+ builder.addField(pageKey, type);
491
575
  builder.normalizeFieldLocations();
492
- const nextField = page.fields.at(-1);
493
- if (nextField !== void 0) {
576
+ const page = safePages.value.find((candidate) => candidate.page_key === pageKey);
577
+ const nextField = page?.fields.at(-1);
578
+ if (page !== void 0 && nextField !== void 0) {
494
579
  selectField(page, nextField.field_key);
495
580
  }
496
581
  }
497
582
  function onFieldTypeChange(field, nextType) {
498
583
  field.type = nextType;
499
- if (nextType === "select" || nextType === "select_menu" || nextType === "radio" || nextType === "checkbox_group") {
500
- if (field.options === void 0) {
501
- field.options = [];
584
+ if (isChoiceFieldType(nextType)) {
585
+ if (!Array.isArray(field.options) || field.options.length === 0) {
586
+ field.options = nextType === "checkbox_group" ? [createChoiceOption(0), createChoiceOption(1), createChoiceOption(2)] : [createChoiceOption(0), createChoiceOption(1)];
587
+ } else if (field.options.length < (nextType === "checkbox_group" ? 2 : 1)) {
588
+ const minimumOptionCount = nextType === "checkbox_group" ? 2 : 1;
589
+ const nextOptions = [...field.options];
590
+ for (let index = nextOptions.length; index < minimumOptionCount; index += 1) {
591
+ nextOptions.push(createChoiceOption(index));
592
+ }
593
+ field.options = nextOptions;
502
594
  }
595
+ field.display = nextType === "radio" || nextType === "checkbox_group" ? "list" : "menu";
503
596
  } else {
504
597
  field.options = void 0;
598
+ field.display = void 0;
505
599
  }
506
600
  if (nextType === "file") {
507
601
  field.multiple = false;
@@ -511,54 +605,26 @@ function onFieldTypeChange(field, nextType) {
511
605
  });
512
606
  }
513
607
  }
514
- }
515
- function addOption(field) {
516
- if (field.options === void 0) {
517
- field.options = [];
518
- }
519
- field.options.push({
520
- label: t("builder.optionDefaultLabel"),
521
- value: `option_${field.options.length + 1}`
522
- });
523
- }
524
- function removeOption(field, index) {
525
- if (field.options === void 0) {
526
- return;
527
- }
528
- field.options.splice(index, 1);
529
- }
530
- function optionLabel(option) {
531
- if (option === void 0 || option === null) {
532
- return "";
533
- }
534
- if (typeof option === "object" && "label" in option) {
535
- return typeof option.label === "string" ? option.label : "";
608
+ if (nextType === "consent" && typeof field.consent_label !== "string") {
609
+ field.consent_label = resolveDefaultConsentLabel(locale.value);
536
610
  }
537
- return String(option);
538
- }
539
- function setOptionLabel(field, optionIndex, value) {
540
- if (field.options === void 0) {
541
- return;
542
- }
543
- const option = field.options[optionIndex];
544
- if (option === void 0 || option === null) {
545
- return;
546
- }
547
- if (typeof option === "object" && "value" in option) {
548
- field.options[optionIndex] = {
549
- ...option,
550
- label: value
611
+ if (nextType === "address") {
612
+ if (!Array.isArray(field.address_fields) || field.address_fields.length === 0) {
613
+ field.address_fields = createDefaultAddressFields(locale.value);
614
+ }
615
+ field.default = {
616
+ line1: null,
617
+ line2: null,
618
+ city: null,
619
+ state: null,
620
+ zip: null,
621
+ country: null
551
622
  };
552
- return;
553
623
  }
554
- field.options[optionIndex] = {
555
- label: value,
556
- value: option
557
- };
558
624
  }
559
625
  async function save() {
560
626
  try {
561
- const shouldAutoPublish = props.defaultPublished;
627
+ const shouldAutoPublish = props.defaultPublished || props.autoPublishOnSave;
562
628
  await builder.save({
563
629
  autoPublish: shouldAutoPublish
564
630
  });
@@ -640,12 +706,6 @@ const canTogglePublish = computed(() => {
640
706
  }
641
707
  return true;
642
708
  });
643
- const toolbarActionsClass = computed(() => {
644
- if (showTopControls.value) {
645
- return ["builder-toolbar-actions"];
646
- }
647
- return ["builder-toolbar-actions", "builder-toolbar-actions--compact"];
648
- });
649
709
  async function togglePublishState() {
650
710
  if (!props.defaultPublished && isPublished.value) {
651
711
  await unpublish();
@@ -659,24 +719,41 @@ function removeField(page, fieldKey) {
659
719
  selectedFieldKey.value = null;
660
720
  }
661
721
  }
662
- function addQuestionFromRail() {
663
- const page = selectedPage();
722
+ function removeFieldByKey(pageKey, fieldKey) {
723
+ const page = findPage(pageKey);
664
724
  if (page === void 0) {
665
- builder.addPage();
666
- nextTick(() => {
667
- syncSelectionWithDraft(safePages.value);
668
- updateRailPosition();
669
- });
670
725
  return;
671
726
  }
672
- addField(page, "text");
727
+ removeField(page, fieldKey);
673
728
  }
674
- function addPageFromRail() {
675
- builder.addPage();
676
- nextTick(() => {
677
- syncSelectionWithDraft(safePages.value);
678
- updateRailPosition();
679
- });
729
+ function duplicateFieldByKey(pageKey, fieldKey) {
730
+ const page = findPage(pageKey);
731
+ if (page === void 0) {
732
+ return;
733
+ }
734
+ duplicateField(page, fieldKey);
735
+ }
736
+ function moveFieldByKey(pageKey, fieldKey, direction) {
737
+ builder.moveField(pageKey, fieldKey, direction);
738
+ }
739
+ function changeFieldTypeByKey(pageKey, fieldKey, nextType) {
740
+ const page = findPage(pageKey);
741
+ if (page === void 0) {
742
+ return;
743
+ }
744
+ const field = page.fields.find((item) => item.field_key === fieldKey);
745
+ if (field === void 0) {
746
+ return;
747
+ }
748
+ onFieldTypeChange(field, nextType);
749
+ }
750
+ function addFieldBelowByKey(pageKey, fieldKey, type) {
751
+ builder.insertFieldAfter(pageKey, fieldKey, type);
752
+ builder.normalizeFieldLocations();
753
+ }
754
+ function moveFieldToBlockByKey(pageKey, fieldKey, targetPageKey) {
755
+ builder.moveFieldToPage(pageKey, fieldKey, targetPageKey);
756
+ builder.normalizeFieldLocations();
680
757
  }
681
758
  async function openCategoryCreateModal() {
682
759
  if (props.readonly) {
@@ -748,6 +825,11 @@ watch(() => props.autosaveDelay, (value) => {
748
825
  }, {
749
826
  immediate: true
750
827
  });
828
+ watch(() => props.autoPublishOnSave, (value) => {
829
+ builder.autoPublishOnSave.value = value;
830
+ }, {
831
+ immediate: true
832
+ });
751
833
  watch(() => [props.loadFormKey, props.loadFormVersion], ([key, version]) => {
752
834
  if (typeof key !== "string" || key === "") {
753
835
  return;
@@ -769,523 +851,392 @@ watch(() => [props.formRouteKey, props.loadFormKey, props.loadFormVersion], ([ro
769
851
  });
770
852
  watch(() => safePages.value, (pages) => {
771
853
  syncSelectionWithDraft(pages);
772
- nextTick(() => {
773
- updateRailPosition();
774
- });
775
854
  }, {
776
855
  deep: true,
777
856
  immediate: true
778
857
  });
779
- watch(() => selectedFieldKey.value, () => {
780
- nextTick(() => {
781
- updateRailPosition();
782
- });
858
+ watch(
859
+ () => [selectedPageKey.value, selectedFieldKey.value],
860
+ ([pageKey, fieldKey]) => {
861
+ emit("selection-change", pageKey, fieldKey);
862
+ },
863
+ {
864
+ immediate: true
865
+ }
866
+ );
867
+ watch(settingsHidden, (value) => {
868
+ if (value) {
869
+ selectedTab.value = "builder";
870
+ }
871
+ }, {
872
+ immediate: true
783
873
  });
874
+ const builderExpose = {
875
+ save,
876
+ publish,
877
+ unpublish,
878
+ togglePublishState
879
+ };
880
+ defineExpose(builderExpose);
784
881
  </script>
785
882
 
786
883
  <template>
787
884
  <div
788
- v-if="isClientReady"
789
- ref="builderRootElement"
790
- class="builder-root"
885
+ class="w-full"
791
886
  >
792
- <div class="builder-layout">
887
+ <div :class="builderLayoutClass">
793
888
  <div
794
- ref="builderColumnElement"
795
- class="builder-column"
889
+ class="grid min-w-0 gap-6"
796
890
  >
797
- <UCard
798
- variant="subtle"
799
- class="builder-card"
800
- >
801
- <div class="builder-toolbar">
802
- <div
803
- v-if="showTopControls"
804
- class="builder-toolbar-grid"
805
- >
806
- <UInput
807
- v-if="!disableTitleInput"
808
- v-model="draftTitle"
809
- :disabled="readonly"
810
- :placeholder="t('builder.formTitlePlaceholder')"
811
- />
812
- <div
813
- v-if="!disableCategoryControl"
814
- class="builder-category-control"
815
- >
816
- <USelect
817
- v-model="draftCategorySelectValue"
818
- :items="categorySelectItems"
819
- :disabled="readonly"
820
- :placeholder="t('builder.categoryPlaceholder')"
821
- />
822
- <UTooltip :text="t('builder.tooltip.addCategory')">
823
- <UButton
824
- color="neutral"
825
- variant="soft"
826
- icon="i-lucide-folder-plus"
827
- :disabled="readonly"
828
- @click="openCategoryCreateModal"
829
- />
830
- </UTooltip>
831
- </div>
832
- </div>
833
-
834
- <div :class="toolbarActionsClass">
835
- <div class="builder-status">
836
- <span v-if="loadingRemoteForm">{{ t("builder.loadingForm") }}</span>
837
- <span v-if="formattedLastSavedAt !== null">{{ t("builder.lastSave", { value: formattedLastSavedAt }) }}</span>
838
- <span
839
- v-if="builderError !== null"
840
- class="builder-error"
841
- >{{ builderError }}</span>
842
- </div>
843
-
844
- <div class="builder-actions">
845
- <UButton
846
- :loading="saving"
847
- :disabled="readonly"
848
- @click="save"
849
- >
850
- {{ t("builder.save") }}
851
- </UButton>
852
- <UButton
853
- v-if="!disablePublishAction && !defaultPublished"
854
- :color="publishButtonColor"
855
- :variant="publishButtonVariant"
856
- :loading="publishing"
857
- :disabled="!canTogglePublish"
858
- @click="togglePublishState"
891
+ <template v-if="!settingsHidden">
892
+ <UTabs
893
+ v-model="selectedTab"
894
+ :items="builderTabs"
895
+ value-key="slot"
896
+ class="w-full"
897
+ :ui="{
898
+ list: 'w-full',
899
+ trigger: 'flex-1 justify-center'
900
+ }"
901
+ >
902
+ <template #builder>
903
+ <div class="grid gap-6">
904
+ <Draggable
905
+ v-model="draftPages"
906
+ item-key="page_key"
907
+ handle=".page-drag-handle"
908
+ group="formforge-pages"
909
+ class="grid gap-6"
859
910
  >
860
- {{ publishButtonLabel }}
861
- </UButton>
911
+ <template #item="{ element: page, index }">
912
+ <FormForgeBuilderBlockCard
913
+ :page="page"
914
+ :pages="safePages"
915
+ :page-index="index"
916
+ :total-pages="safePages.length"
917
+ :selected-field-key="selectedFieldKey"
918
+ :readonly="readonly"
919
+ :field-type-items="fieldTypeItems"
920
+ @select-field="selectFieldByKey"
921
+ @move-page="builder.movePage"
922
+ @duplicate-page="builder.duplicatePage"
923
+ @remove-page="builder.removePage"
924
+ @add-question="addField"
925
+ @move-field="moveFieldByKey"
926
+ @duplicate-field="duplicateFieldByKey"
927
+ @remove-field="removeFieldByKey"
928
+ @change-field-type="changeFieldTypeByKey"
929
+ @add-field-below="addFieldBelowByKey"
930
+ @move-field-to-block="moveFieldToBlockByKey"
931
+ />
932
+ </template>
933
+ </Draggable>
862
934
  </div>
863
- </div>
864
- </div>
865
- </UCard>
935
+ </template>
866
936
 
867
- <Draggable
868
- v-model="draftPages"
869
- item-key="page_key"
870
- handle=".page-drag-handle"
871
- group="formforge-pages"
872
- class="builder-stack pages-stack"
873
- >
874
- <template #item="{ element: page }">
875
- <div
876
- v-if="page !== void 0 && page !== null"
877
- class="page-block"
878
- >
879
- <UCard
880
- variant="soft"
881
- class="builder-card page-meta-card"
882
- >
883
- <div class="page-chip-row">
884
- <UBadge
885
- color="primary"
886
- variant="subtle"
887
- >
888
- {{ t("builder.pageCounter", { current: pageOrder(page), total: safePages.length }) }}
889
- </UBadge>
890
- </div>
937
+ <template #settings>
938
+ <UCard variant="subtle">
939
+ <template #header>
940
+ <div class="space-y-1">
941
+ <p class="text-sm font-semibold text-default">
942
+ {{ t("builder.settings.title") }}
943
+ </p>
944
+ <p class="text-sm text-muted">
945
+ {{ t("builder.settings.description") }}
946
+ </p>
947
+ </div>
948
+ </template>
891
949
 
892
- <div class="page-header">
893
- <span class="page-drag-handle drag-handle">⋮⋮</span>
894
- <div class="page-header-main">
950
+ <div class="space-y-6">
951
+ <div
952
+ v-if="!disableTitleInput || !disableCategoryControl"
953
+ class="grid gap-3 lg:grid-cols-[minmax(0,1fr)_minmax(18rem,22rem)]"
954
+ >
895
955
  <UInput
896
- :model-value="readPageTitle(page)"
897
- :disabled="readonly"
898
- class="grow"
899
- :placeholder="t('builder.pageTitlePlaceholder')"
900
- @update:model-value="(value) => updatePageTitle(page, value)"
901
- />
902
- <UTextarea
903
- v-model="page.description"
904
- :rows="2"
956
+ v-if="!disableTitleInput"
957
+ v-model="draftTitle"
905
958
  :disabled="readonly"
906
- :placeholder="t('builder.pageDescriptionPlaceholder')"
959
+ :placeholder="t('builder.settings.formTitlePlaceholder')"
960
+ :ui="{ base: 'w-full' }"
907
961
  />
908
- </div>
909
- <div class="page-actions">
910
- <UTooltip :text="t('builder.tooltip.mergePage')">
911
- <UButton
912
- color="neutral"
913
- variant="ghost"
914
- icon="i-lucide-arrow-up-to-line"
915
- :disabled="readonly || !canMergeWithPreviousPage(page)"
916
- @click="mergePageWithPrevious(page)"
962
+ <div
963
+ v-if="!disableCategoryControl"
964
+ class="flex flex-row items-center gap-2"
965
+ >
966
+ <USelect
967
+ v-model="draftCategorySelectValue"
968
+ :items="categorySelectItems"
969
+ :disabled="readonly"
970
+ :placeholder="t('builder.settings.categoryPlaceholder')"
971
+ :ui="{
972
+ base: 'w-full'
973
+ }"
917
974
  />
918
- </UTooltip>
919
- <UTooltip :text="t('builder.tooltip.deletePage')">
920
- <UButton
921
- color="neutral"
922
- variant="ghost"
923
- icon="i-lucide-trash-2"
924
- :disabled="readonly || safePages.length <= 1"
925
- @click="builder.removePage(page.page_key)"
975
+ <UTooltip :text="t('builder.tooltip.addCategory')">
976
+ <UButton
977
+ color="neutral"
978
+ variant="soft"
979
+ icon="i-lucide-folder-plus"
980
+ :disabled="readonly"
981
+ @click="openCategoryCreateModal"
982
+ />
983
+ </UTooltip>
984
+ </div>
985
+ </div>
986
+
987
+ <div class="grid gap-6">
988
+ <div class="grid gap-4 sm:grid-cols-[minmax(0,1fr)_auto] sm:items-start">
989
+ <div class="space-y-1">
990
+ <p class="text-sm font-semibold text-default">
991
+ {{ t("builder.settings.opening.title") }}
992
+ </p>
993
+ <p class="text-sm text-muted">
994
+ {{ t("builder.settings.opening.description") }}
995
+ </p>
996
+ </div>
997
+ <USwitch
998
+ v-model="openingEnabled"
999
+ :disabled="readonly"
926
1000
  />
927
- </UTooltip>
1001
+ </div>
1002
+
1003
+ <div
1004
+ v-if="openingEnabled"
1005
+ class="grid gap-4 sm:grid-cols-2"
1006
+ >
1007
+ <UFormField :label="t('builder.settings.opening.date')">
1008
+ <UInputDate
1009
+ v-model="publishAtDate"
1010
+ :disabled="readonly"
1011
+ :ui="{ base: 'w-full' }"
1012
+ />
1013
+ </UFormField>
1014
+ <UFormField :label="t('builder.settings.opening.time')">
1015
+ <UInputTime
1016
+ v-model="publishAtTime"
1017
+ :disabled="readonly"
1018
+ :hour-cycle="24"
1019
+ :ui="{ base: 'w-full' }"
1020
+ />
1021
+ </UFormField>
1022
+ </div>
928
1023
  </div>
929
- </div>
930
- </UCard>
931
1024
 
932
- <Draggable
933
- v-model="page.fields"
934
- item-key="field_key"
935
- handle=".field-drag-handle"
936
- :group="{ name: 'formforge-fields', pull: true, put: true }"
937
- class="builder-stack page-questions-stack"
938
- >
939
- <template #item="{ element: field }">
940
- <UCard
941
- v-if="field !== void 0 && field !== null"
942
- :ref="(element) => registerFieldElement(field.field_key, element)"
943
- variant="subtle"
944
- :class="[
945
- 'field-card',
946
- isFieldSelected(field.field_key) ? 'field-card--active' : ''
947
- ]"
948
- @click="selectField(page, field.field_key)"
949
- @focusin="selectField(page, field.field_key)"
950
- >
951
- <div class="field-shell">
952
- <div class="field-head">
953
- <span class="field-drag-handle drag-handle">⋮⋮</span>
954
- <UInput
955
- v-model="field.label"
1025
+ <div class="grid gap-6">
1026
+ <div class="grid gap-4 sm:grid-cols-[minmax(0,1fr)_auto] sm:items-start">
1027
+ <div class="space-y-1">
1028
+ <p class="text-sm font-semibold text-default">
1029
+ {{ t("builder.settings.closing.title") }}
1030
+ </p>
1031
+ <p class="text-sm text-muted">
1032
+ {{ t("builder.settings.closing.description") }}
1033
+ </p>
1034
+ </div>
1035
+ <USwitch
1036
+ v-model="closingEnabled"
1037
+ :disabled="readonly"
1038
+ />
1039
+ </div>
1040
+
1041
+ <div
1042
+ v-if="closingEnabled"
1043
+ class="grid gap-4 sm:grid-cols-2"
1044
+ >
1045
+ <UFormField :label="t('builder.settings.closing.date')">
1046
+ <UInputDate
1047
+ v-model="pauseAtDate"
956
1048
  :disabled="readonly"
957
- :placeholder="t('builder.questionPlaceholder')"
1049
+ :ui="{ base: 'w-full' }"
958
1050
  />
959
- <USelectMenu
960
- :model-value="field.type"
961
- :items="fieldTypeItems"
962
- value-key="value"
963
- label-key="label"
964
- :search-input="false"
965
- :leading-icon="fieldTypeIcon(field.type)"
1051
+ </UFormField>
1052
+ <UFormField :label="t('builder.settings.closing.time')">
1053
+ <UInputTime
1054
+ v-model="pauseAtTime"
1055
+ :disabled="readonly"
1056
+ :hour-cycle="24"
1057
+ :ui="{ base: 'w-full' }"
1058
+ />
1059
+ </UFormField>
1060
+ </div>
1061
+ </div>
1062
+
1063
+ <div class="grid gap-6">
1064
+ <div class="grid gap-4 sm:grid-cols-[minmax(0,1fr)_auto] sm:items-start">
1065
+ <div class="space-y-1">
1066
+ <p class="text-sm font-semibold text-default">
1067
+ {{ t("builder.settings.submissionLimit.title") }}
1068
+ </p>
1069
+ <p class="text-sm text-muted">
1070
+ {{ t("builder.settings.submissionLimit.description") }}
1071
+ </p>
1072
+ </div>
1073
+ <USwitch
1074
+ v-model="responseLimitEnabled"
1075
+ :disabled="readonly"
1076
+ />
1077
+ </div>
1078
+
1079
+ <UFormField
1080
+ v-if="responseLimitEnabled"
1081
+ :label="t('builder.settings.submissionLimit.maximumResponses')"
1082
+ >
1083
+ <UInputNumber
1084
+ v-model="responseLimitValue"
1085
+ :disabled="readonly"
1086
+ :min="1"
1087
+ :ui="{ base: 'w-full' }"
1088
+ />
1089
+ </UFormField>
1090
+ </div>
1091
+
1092
+ <div class="grid gap-6">
1093
+ <div class="space-y-1">
1094
+ <p class="text-sm font-semibold text-default">
1095
+ {{ t("builder.settings.access.title") }}
1096
+ </p>
1097
+ <p class="text-sm text-muted">
1098
+ {{ t("builder.settings.access.description") }}
1099
+ </p>
1100
+ </div>
1101
+
1102
+ <div class="grid gap-4">
1103
+ <div class="flex items-start justify-between gap-4">
1104
+ <div class="space-y-1">
1105
+ <p class="text-sm font-medium text-default">
1106
+ {{ t("builder.settings.access.publicForm.title") }}
1107
+ </p>
1108
+ <p class="text-sm text-muted">
1109
+ {{ t("builder.settings.access.publicForm.description") }}
1110
+ </p>
1111
+ </div>
1112
+ <USwitch
1113
+ v-model="submissionIsPublic"
966
1114
  :disabled="readonly"
967
- @update:model-value="(value) => onFieldTypeChange(field, value)"
968
1115
  />
969
1116
  </div>
970
1117
 
971
- <div
972
- v-if="isFieldSelected(field.field_key)"
973
- class="field-controls"
974
- >
975
- <UTooltip :text="t('builder.tooltip.duplicateQuestion')">
976
- <UButton
977
- color="neutral"
978
- variant="ghost"
979
- icon="i-lucide-copy"
980
- :disabled="readonly"
981
- @click.stop="duplicateField(page, field.field_key)"
982
- />
983
- </UTooltip>
984
- <UTooltip :text="t('builder.tooltip.deleteQuestion')">
985
- <UButton
986
- color="error"
987
- variant="ghost"
988
- icon="i-lucide-trash-2"
989
- :disabled="readonly || page.fields.length <= 1"
990
- @click.stop="removeField(page, field.field_key)"
991
- />
992
- </UTooltip>
993
- <div class="field-required-switch">
994
- <span>{{ t("builder.required") }}</span>
995
- <USwitch
996
- v-model="field.required"
997
- :disabled="readonly"
1118
+ <div v-if="submissionIsPublic">
1119
+ <UFormField :label="t('builder.settings.access.publicLink')">
1120
+ <UInput
1121
+ :model-value="publicUrlValue"
1122
+ disabled
1123
+ :placeholder="t('builder.settings.access.publicLinkPlaceholder')"
1124
+ :ui="{ base: 'w-full' }"
998
1125
  />
999
- </div>
1126
+ </UFormField>
1000
1127
  </div>
1001
1128
 
1002
- <div
1003
- v-if="isFieldSelected(field.field_key) && showInlinePreview(field)"
1004
- class="field-inline-preview"
1005
- >
1006
- <p class="field-inline-preview-label">
1007
- {{ fieldTypeLabel(field.type) }}
1008
- </p>
1009
- <div
1010
- v-if="field.type === 'textarea'"
1011
- class="field-inline-preview-textarea"
1012
- >
1013
- <span />
1014
- <span />
1129
+ <div class="flex items-start justify-between gap-4 border-t border-muted pt-4">
1130
+ <div class="space-y-1">
1131
+ <p class="text-sm font-medium text-default">
1132
+ {{ t("builder.settings.access.pinProtection.title") }}
1133
+ </p>
1134
+ <p class="text-sm text-muted">
1135
+ {{ t("builder.settings.access.pinProtection.description") }}
1136
+ </p>
1015
1137
  </div>
1016
- <div
1017
- v-else
1018
- class="field-inline-preview-line"
1138
+ <USwitch
1139
+ v-model="submissionPinEnabled"
1140
+ :disabled="readonly"
1019
1141
  />
1020
1142
  </div>
1021
1143
 
1022
- <div
1023
- v-if="isFieldSelected(field.field_key) && (field.type === 'select' || field.type === 'select_menu' || field.type === 'radio' || field.type === 'checkbox_group')"
1024
- class="field-options"
1025
- >
1026
- <div
1027
- v-for="(option, optionIndex) in field.options"
1028
- :key="optionIndex"
1029
- class="field-option-row"
1030
- >
1144
+ <div v-if="submissionPinEnabled">
1145
+ <UFormField :label="t('builder.settings.access.pin')">
1031
1146
  <UInput
1032
- :model-value="optionLabel(option)"
1033
- :disabled="readonly"
1034
- :placeholder="t('builder.optionLabelPlaceholder')"
1035
- @update:model-value="(value) => setOptionLabel(field, optionIndex, value)"
1036
- />
1037
- <UButton
1038
- color="neutral"
1039
- variant="ghost"
1040
- icon="i-lucide-x"
1147
+ v-model="submissionPinValue"
1041
1148
  :disabled="readonly"
1042
- @click="removeOption(field, optionIndex)"
1149
+ type="password"
1150
+ :placeholder="t('builder.settings.access.pinPlaceholder')"
1151
+ :ui="{ base: 'w-full' }"
1043
1152
  />
1044
- </div>
1045
- <UButton
1046
- color="neutral"
1047
- variant="soft"
1048
- icon="i-lucide-plus"
1049
- :disabled="readonly"
1050
- @click="addOption(field)"
1051
- >
1052
- {{ t("builder.addOption") }}
1053
- </UButton>
1153
+ </UFormField>
1054
1154
  </div>
1155
+ </div>
1156
+ </div>
1055
1157
 
1056
- <p
1057
- v-if="!isFieldSelected(field.field_key)"
1058
- class="field-preview"
1059
- >
1060
- {{ field.placeholder || field.help_text || (field.options?.length ? t("builder.optionsCount", { count: field.options.length }) : fieldTypeLabel(field.type)) }}
1061
- </p>
1158
+ <div class="flex flex-wrap items-center justify-between gap-3 border-t border-muted pt-4">
1159
+ <div class="flex flex-wrap gap-2 text-sm text-muted">
1160
+ <span v-if="loadingRemoteForm">{{ t("builder.loadingForm") }}</span>
1161
+ <span v-if="formattedLastSavedAt !== null">{{ t("builder.lastSave", { value: formattedLastSavedAt }) }}</span>
1162
+ <span
1163
+ v-if="builderError !== null"
1164
+ class="text-error"
1165
+ >{{ builderError }}</span>
1166
+ </div>
1062
1167
 
1063
- <details
1064
- v-if="isFieldSelected(field.field_key)"
1065
- class="field-advanced"
1168
+ <div class="flex flex-wrap items-center gap-2">
1169
+ <UButton
1170
+ :loading="saving"
1171
+ :disabled="readonly"
1172
+ @click="save"
1066
1173
  >
1067
- <summary class="field-advanced-summary">
1068
- {{ t("builder.advancedSettings") }}
1069
- </summary>
1070
-
1071
- <div class="field-advanced-body">
1072
- <div class="field-advanced-grid">
1073
- <UInput
1074
- v-model="field.placeholder"
1075
- :disabled="readonly"
1076
- :placeholder="t('builder.placeholderPlaceholder')"
1077
- />
1078
- <UTextarea
1079
- v-model="field.help_text"
1080
- :disabled="readonly"
1081
- :rows="2"
1082
- :placeholder="t('builder.helpTextPlaceholder')"
1083
- />
1084
- </div>
1085
-
1086
- <div
1087
- v-if="field.type === 'number'"
1088
- class="field-numbers"
1089
- >
1090
- <UInput
1091
- v-model="field.min"
1092
- :disabled="readonly"
1093
- :placeholder="t('builder.minPlaceholder')"
1094
- />
1095
- <UInput
1096
- v-model="field.max"
1097
- :disabled="readonly"
1098
- :placeholder="t('builder.maxPlaceholder')"
1099
- />
1100
- <UInput
1101
- v-model="field.step"
1102
- :disabled="readonly"
1103
- :placeholder="t('builder.stepPlaceholder')"
1104
- />
1105
- </div>
1106
-
1107
- <div
1108
- v-if="field.type === 'file'"
1109
- class="field-file"
1110
- >
1111
- <UCheckbox
1112
- v-model="field.multiple"
1113
- :disabled="readonly"
1114
- :label="t('builder.multiple')"
1115
- />
1116
- <UInput
1117
- :model-value="field.accept?.join(',') ?? ''"
1118
- :disabled="readonly"
1119
- :placeholder="t('builder.acceptedExtensionsPlaceholder')"
1120
- @update:model-value="(value) => {
1121
- field.accept = value.split(',').map((item) => item.trim()).filter((item) => item !== '');
1122
- }"
1123
- />
1124
- </div>
1125
- </div>
1126
- </details>
1174
+ {{ t("builder.save") }}
1175
+ </UButton>
1176
+ <UButton
1177
+ v-if="!disablePublishAction && !defaultPublished"
1178
+ :color="publishButtonColor"
1179
+ :variant="publishButtonVariant"
1180
+ :loading="publishing"
1181
+ :disabled="!canTogglePublish"
1182
+ @click="togglePublishState"
1183
+ >
1184
+ {{ publishButtonLabel }}
1185
+ </UButton>
1127
1186
  </div>
1128
- </UCard>
1129
- </template>
1130
- </Draggable>
1131
- </div>
1132
- </template>
1133
- </Draggable>
1187
+ </div>
1188
+ </div>
1189
+ </UCard>
1190
+ </template>
1191
+ </UTabs>
1192
+ </template>
1134
1193
 
1135
- <UCard
1136
- variant="subtle"
1137
- class="builder-card conditions-card"
1194
+ <div
1195
+ v-else
1196
+ class="grid gap-6"
1138
1197
  >
1139
- <div class="builder-row">
1140
- <h3 class="conditions-title">
1141
- {{ t("builder.conditions") }}
1142
- </h3>
1143
- <UButton
1144
- color="neutral"
1145
- variant="soft"
1146
- icon="i-lucide-plus"
1147
- :disabled="readonly"
1148
- @click="builder.addCondition()"
1149
- >
1150
- {{ t("builder.addCondition") }}
1151
- </UButton>
1152
- </div>
1153
-
1154
- <div
1155
- v-for="condition in safeConditions"
1156
- :key="condition.condition_key"
1157
- class="condition-card"
1198
+ <Draggable
1199
+ v-model="draftPages"
1200
+ item-key="page_key"
1201
+ handle=".page-drag-handle"
1202
+ group="formforge-pages"
1203
+ class="grid gap-6"
1158
1204
  >
1159
- <div class="grid grid-cols-1 gap-2 md:grid-cols-4">
1160
- <USelect
1161
- v-model="condition.target_type"
1162
- :items="targetTypeItems"
1163
- :disabled="readonly"
1205
+ <template #item="{ element: page, index }">
1206
+ <FormForgeBuilderBlockCard
1207
+ :page="page"
1208
+ :pages="safePages"
1209
+ :page-index="index"
1210
+ :total-pages="safePages.length"
1211
+ :selected-field-key="selectedFieldKey"
1212
+ :readonly="readonly"
1213
+ :field-type-items="fieldTypeItems"
1214
+ @select-field="selectFieldByKey"
1215
+ @move-page="builder.movePage"
1216
+ @duplicate-page="builder.duplicatePage"
1217
+ @remove-page="builder.removePage"
1218
+ @add-question="addField"
1219
+ @move-field="moveFieldByKey"
1220
+ @duplicate-field="duplicateFieldByKey"
1221
+ @remove-field="removeFieldByKey"
1222
+ @change-field-type="changeFieldTypeByKey"
1223
+ @add-field-below="addFieldBelowByKey"
1224
+ @move-field-to-block="moveFieldToBlockByKey"
1164
1225
  />
1165
- <USelect
1166
- v-if="condition.target_type === 'page'"
1167
- v-model="condition.target_key"
1168
- :items="pageTargetItems"
1169
- :disabled="readonly"
1170
- />
1171
- <USelect
1172
- v-else
1173
- v-model="condition.target_key"
1174
- :items="fieldTargetItems"
1175
- :disabled="readonly"
1176
- />
1177
- <USelect
1178
- v-model="condition.action"
1179
- :items="conditionActionItems"
1180
- :disabled="readonly"
1181
- />
1182
- <USelect
1183
- v-model="condition.match"
1184
- :items="conditionMatchItems"
1185
- :disabled="readonly"
1186
- />
1187
- </div>
1188
-
1189
- <div class="space-y-2">
1190
- <div
1191
- v-for="(clause, clauseIndex) in condition.when"
1192
- :key="`${condition.condition_key}-${clauseIndex}`"
1193
- class="grid grid-cols-1 gap-2 md:grid-cols-[1fr_180px_1fr_auto]"
1194
- >
1195
- <USelect
1196
- v-model="clause.field_key"
1197
- :items="fieldTargetItems"
1198
- :disabled="readonly"
1199
- />
1200
- <USelect
1201
- v-model="clause.operator"
1202
- :items="conditionOperatorItems"
1203
- :disabled="readonly"
1204
- />
1205
- <UInput
1206
- v-if="clause.operator !== 'is_empty' && clause.operator !== 'not_empty'"
1207
- :model-value="typeof clause.value === 'string' ? clause.value : String(clause.value ?? '')"
1208
- :disabled="readonly"
1209
- :placeholder="t('builder.valuePlaceholder')"
1210
- @update:model-value="(value) => {
1211
- clause.value = value;
1212
- }"
1213
- />
1214
- <div v-else />
1215
- <UButton
1216
- color="neutral"
1217
- variant="soft"
1218
- :disabled="readonly || condition.when.length <= 1"
1219
- @click="builder.removeConditionClause(condition.condition_key, clauseIndex)"
1220
- >
1221
- {{ t("builder.remove") }}
1222
- </UButton>
1223
- </div>
1224
- </div>
1225
-
1226
- <div class="condition-actions">
1227
- <UButton
1228
- color="neutral"
1229
- variant="soft"
1230
- :disabled="readonly"
1231
- @click="builder.addConditionClause(condition.condition_key)"
1232
- >
1233
- {{ t("builder.addClause") }}
1234
- </UButton>
1235
- <UButton
1236
- color="error"
1237
- variant="soft"
1238
- :disabled="readonly"
1239
- @click="builder.removeCondition(condition.condition_key)"
1240
- >
1241
- {{ t("builder.deleteCondition") }}
1242
- </UButton>
1243
- </div>
1244
- </div>
1245
- </UCard>
1246
- </div>
1247
-
1248
- <aside class="builder-rail-column">
1249
- <div
1250
- class="builder-rail"
1251
- :style="{ top: `${railTop}px`, left: `${railLeft}px` }"
1252
- >
1253
- <UTooltip :text="t('builder.rail.addQuestion')">
1254
- <UButton
1255
- color="neutral"
1256
- variant="soft"
1257
- icon="i-lucide-circle-plus"
1258
- class="rail-action"
1259
- :disabled="readonly"
1260
- @click="addQuestionFromRail"
1261
- />
1262
- </UTooltip>
1263
- <UTooltip :text="t('builder.rail.addPage')">
1264
- <UButton
1265
- color="neutral"
1266
- variant="soft"
1267
- icon="i-lucide-file-plus-2"
1268
- class="rail-action"
1269
- :disabled="readonly"
1270
- @click="addPageFromRail"
1271
- />
1272
- </UTooltip>
1226
+ </template>
1227
+ </Draggable>
1273
1228
  </div>
1274
- </aside>
1229
+ </div>
1275
1230
  </div>
1276
1231
  </div>
1277
1232
  <div
1278
- v-else
1233
+ v-if="loadingRemoteForm"
1279
1234
  class="space-y-6"
1280
1235
  >
1281
1236
  <UCard>
1282
- <div class="text-sm text-muted">
1237
+ <div class="text-sm text-gray-500">
1283
1238
  {{ t("builder.loadingBuilder") }}
1284
1239
  </div>
1285
1240
  </UCard>
1286
1241
  </div>
1287
1242
  </template>
1288
-
1289
- <style scoped>
1290
- .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-toolbar-actions--compact{border-top:none;padding-top:0}.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}}
1291
- </style>