@shwfed/nuxt 0.11.46 → 0.11.48

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.
@@ -6,21 +6,15 @@ import { CalendarDate, getLocalTimeZone } from "@internationalized/date";
6
6
  import { Icon } from "@iconify/vue";
7
7
  import { Effect } from "effect";
8
8
  import { format, parse } from "date-fns";
9
- import { deleteProperty, getProperty, hasProperty, setProperty } from "dot-prop";
9
+ import { deleteProperty, getProperty, setProperty } from "dot-prop";
10
10
  import { computed, nextTick, readonly, ref, toRaw, useId, watch, watchEffect } from "vue";
11
11
  import { useI18n } from "vue-i18n";
12
12
  import { useCheating } from "#imports";
13
- import { RadioGroupIndicator, RadioGroupItem, RadioGroupRoot } from "reka-ui";
14
13
  import { mergeDslContexts, useCELContext } from "../../../plugins/cel/context";
15
- import { Calendar } from "../calendar";
16
14
  import { Button } from "../button";
17
- import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "../command";
18
- import { Field, FieldContent, FieldError, FieldLabel, FieldSet } from "../field";
15
+ import FieldsBody from "./FieldsBody.vue";
19
16
  import FieldsConfiguratorDialog from "../fields-configurator/FieldsConfiguratorDialog.vue";
20
- import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, InputGroupNumberField, InputGroupTextarea } from "../input-group";
21
- import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from "../popover";
22
17
  import { Skeleton } from "../skeleton";
23
- import { Tooltip, TooltipContent, TooltipTrigger } from "../tooltip";
24
18
  import { getLocalizedText } from "../../../utils/coders";
25
19
  import { FieldsConfigC, createFieldsConfig } from "./schema";
26
20
  const id = useId();
@@ -31,7 +25,7 @@ const defaultConfig = createFieldsConfig({
31
25
  const props = defineProps({
32
26
  config: { type: null, required: true }
33
27
  });
34
- defineSlots();
28
+ const slots = defineSlots();
35
29
  const emit = defineEmits(["update:config", "initial-value-ready"]);
36
30
  const config = computedAsync(async () => FieldsConfigC.parse(await props.config.pipe(Effect.runPromise) ?? defaultConfig));
37
31
  const { t, locale } = useI18n();
@@ -50,35 +44,23 @@ const isReady = ref(false);
50
44
  const hasInitializedFieldValues = ref(false);
51
45
  const hasEmittedInitialValueReady = ref(false);
52
46
  const readyResolvers = [];
53
- function cloneConfig(config2) {
54
- const nextConfig = {
55
- kind: config2.kind,
56
- compatibilityDate: config2.compatibilityDate,
57
- fields: config2.fields.slice(),
58
- groups: config2.groups.map((group) => ({
59
- ...group,
60
- fields: group.fields.slice()
61
- }))
62
- };
63
- if (config2.orientation) {
64
- nextConfig.orientation = config2.orientation;
65
- }
66
- if (config2.bordered) {
67
- nextConfig.bordered = config2.bordered;
47
+ function requireSlotProps(slotProps) {
48
+ if (!slotProps) {
49
+ throw new TypeError("missing slot props");
68
50
  }
69
- if (config2.style) {
70
- nextConfig.style = config2.style;
71
- }
72
- return nextConfig;
51
+ return slotProps;
52
+ }
53
+ function cloneConfig(config2) {
54
+ return structuredClone(toRaw(config2));
73
55
  }
74
- function getConfigOrientation(config2) {
75
- return config2.orientation ?? "horizontal";
56
+ function getBodyOrientation(body) {
57
+ return body.orientation ?? "horizontal";
76
58
  }
77
- function usesContentsOrientation(config2) {
78
- return getConfigOrientation(config2) === "contents";
59
+ function usesContentsOrientation(body) {
60
+ return getBodyOrientation(body) === "contents";
79
61
  }
80
- function isConfigBordered(config2) {
81
- return usesContentsOrientation(config2) && config2.bordered === true;
62
+ function isBodyBordered(body) {
63
+ return usesContentsOrientation(body) && body.bordered === true;
82
64
  }
83
65
  function tryEvaluateExpression(source, context) {
84
66
  try {
@@ -99,12 +81,12 @@ function evaluateExpression(source, context, fallback) {
99
81
  }
100
82
  return result.value;
101
83
  }
102
- function getConfigStyle(config2) {
84
+ function getBodyStyle(body) {
103
85
  const style = {};
104
- if (!config2.style) {
86
+ if (!body.style) {
105
87
  return style;
106
88
  }
107
- const styleMap = evaluateExpression(config2.style, { form: modelValue.value }, {});
89
+ const styleMap = evaluateExpression(body.style, { form: modelValue.value }, {});
108
90
  if (!styleMap || typeof styleMap !== "object" || Array.isArray(styleMap)) {
109
91
  return style;
110
92
  }
@@ -134,8 +116,8 @@ function getFieldPartStyle(styleExpression) {
134
116
  function getFieldStyle(field) {
135
117
  return getFieldPartStyle(field.style);
136
118
  }
137
- function getGroupStyle(group, config2) {
138
- const style = usesContentsOrientation(config2) ? getConfigStyle(config2) : {};
119
+ function getGroupStyle(group, body) {
120
+ const style = usesContentsOrientation(body) ? getBodyStyle(body) : {};
139
121
  const groupStyle = group.style ? evaluateExpression(group.style, { form: modelValue.value, id: group.id }, {}) : {};
140
122
  if (!groupStyle || typeof groupStyle !== "object" || Array.isArray(groupStyle)) {
141
123
  return style;
@@ -147,48 +129,69 @@ function getGroupStyle(group, config2) {
147
129
  }
148
130
  return style;
149
131
  }
150
- function getConfigEntries(config2) {
132
+ function getBodyEntries(body) {
151
133
  return [
152
- ...config2.fields.map((field) => ({
134
+ ...body.fields.map((field) => ({
153
135
  key: `field:${field.id}`,
154
136
  field
155
137
  })),
156
- ...config2.groups.map((group) => ({
138
+ ...(body.groups ?? []).map((group) => ({
157
139
  key: `group:${group.id}`,
158
140
  group
159
141
  }))
160
142
  ];
161
143
  }
162
- function getConfigFields(config2) {
163
- return [...config2.fields, ...config2.groups.flatMap((group) => group.fields)];
144
+ function visitFields(fields, visitor, visible) {
145
+ for (const field of fields) {
146
+ const fieldVisible = field.type === "empty" ? visible : visible && !isFieldHidden(field);
147
+ visitor(field, fieldVisible);
148
+ if (field.type === "container") {
149
+ visitFields(field.fields, visitor, fieldVisible);
150
+ }
151
+ }
152
+ }
153
+ function visitBodyFields(body, visitor) {
154
+ visitFields(body.fields, visitor, true);
155
+ for (const group of body.groups ?? []) {
156
+ visitFields(group.fields, visitor, true);
157
+ }
164
158
  }
165
- function getFieldContainerStyle(field, config2) {
166
- if (!isPassiveField(field) && usesContentsOrientation(config2)) {
159
+ function getFieldContainerStyle(field, body) {
160
+ if (field.type === "slot" || field.type === "container") {
161
+ return {};
162
+ }
163
+ if (!isPassiveField(field) && usesContentsOrientation(body)) {
167
164
  return {};
168
165
  }
169
166
  return getFieldStyle(field);
170
167
  }
171
- function getFieldLabelStyle(field, config2) {
172
- if (!usesContentsOrientation(config2)) {
168
+ function getFieldLabelStyle(field, body) {
169
+ if (!usesContentsOrientation(body)) {
173
170
  return {};
174
171
  }
175
172
  return getFieldPartStyle(field.labelStyle);
176
173
  }
177
- function getFieldContentStyle(field, config2) {
178
- const style = usesContentsOrientation(config2) ? getFieldPartStyle(field.contentStyle) : {};
179
- if (usesContentsOrientation(config2) && isFieldLabelHidden(field) && style.gridColumn === void 0) {
174
+ function getFieldContentStyle(field, body) {
175
+ const style = usesContentsOrientation(body) ? getFieldPartStyle(field.contentStyle) : {};
176
+ if (usesContentsOrientation(body) && isFieldLabelHidden(field) && style.gridColumn === void 0) {
180
177
  style.gridColumn = "1 / -1";
181
178
  }
182
179
  return style;
183
180
  }
181
+ function getMarkdownBodyContentStyle(field, body) {
182
+ if (!usesContentsOrientation(body)) {
183
+ return {};
184
+ }
185
+ return {
186
+ ...getFieldStyle(field),
187
+ gridColumn: "1 / -1"
188
+ };
189
+ }
184
190
  function isPassiveField(field) {
185
191
  return field.type === "slot" || field.type === "empty";
186
192
  }
187
- function isLabeledDisplayField(field) {
188
- return field.type === "markdown";
189
- }
190
193
  function isInteractiveField(field) {
191
- return !isLabeledDisplayField(field);
194
+ return field.type !== "slot" && field.type !== "empty" && field.type !== "markdown" && field.type !== "markdown-body" && field.type !== "container";
192
195
  }
193
196
  function isFieldLabelHidden(field) {
194
197
  return field.hideLabel === true;
@@ -216,13 +219,13 @@ function getFieldValue(field) {
216
219
  return getProperty(modelValue.value, field.path);
217
220
  }
218
221
  function initializeFieldValues(config2) {
219
- for (const field of getConfigFields(config2)) {
220
- if (isPassiveField(field) || isLabeledDisplayField(field) || !field.initialValue) {
221
- continue;
222
+ visitBodyFields(config2, (field) => {
223
+ if (!isInteractiveField(field) || !field.initialValue) {
224
+ return;
222
225
  }
223
226
  const initialValueResult = tryEvaluateExpression(field.initialValue, { form: modelValue.value });
224
227
  if (!initialValueResult.ok) {
225
- continue;
228
+ return;
226
229
  }
227
230
  const initialValue = initialValueResult.value;
228
231
  setProperty(
@@ -230,7 +233,7 @@ function initializeFieldValues(config2) {
230
233
  field.path,
231
234
  field.type === "number" && typeof initialValue === "bigint" ? Number(initialValue) : initialValue
232
235
  );
233
- }
236
+ });
234
237
  }
235
238
  const MIME_LABELS = {
236
239
  "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": "Excel",
@@ -535,15 +538,15 @@ function validateField(field) {
535
538
  }
536
539
  function validateFields() {
537
540
  const nextValidationErrors = {};
538
- for (const field of getConfigFields(displayConfig.value)) {
539
- if (isPassiveField(field) || isLabeledDisplayField(field)) {
540
- continue;
541
+ visitBodyFields(displayConfig.value, (field, visible) => {
542
+ if (!visible || !isInteractiveField(field)) {
543
+ return;
541
544
  }
542
545
  const failure = getValidationFailure(field);
543
546
  if (failure) {
544
547
  nextValidationErrors[field.path] = failure;
545
548
  }
546
- }
549
+ });
547
550
  validationErrors.value = nextValidationErrors;
548
551
  return Object.keys(nextValidationErrors).length === 0;
549
552
  }
@@ -554,7 +557,10 @@ function getFieldLabel(field) {
554
557
  return getLocalizedText(field.title, locale.value) ?? field.path;
555
558
  }
556
559
  function getDisplayFieldLabel(field) {
557
- return getLocalizedText(field.title, locale.value) ?? field.id;
560
+ if ("title" in field && field.title) {
561
+ return getLocalizedText(field.title, locale.value) ?? field.id;
562
+ }
563
+ return field.id;
558
564
  }
559
565
  function isFieldRequired(field) {
560
566
  return field.required === true;
@@ -566,9 +572,19 @@ function renderValidationMessage(field) {
566
572
  }
567
573
  function renderMarkdownField(field) {
568
574
  const source = getLocalizedText(field.locale, locale.value) ?? "";
575
+ return renderMarkdownSource(source, false);
576
+ }
577
+ function renderMarkdownBodyField(field) {
578
+ const source = getLocalizedText(field.locale, locale.value) ?? "";
579
+ return renderMarkdownSource(source, field.inline === true);
580
+ }
581
+ function renderMarkdownSource(source, inline) {
569
582
  if (source.trim().length === 0) {
570
583
  return "";
571
584
  }
585
+ if (inline) {
586
+ return $md.inline`${source}`(mergeDslContexts({ form: modelValue.value }, dslContext));
587
+ }
572
588
  return $md.block`${source}`(mergeDslContexts({ form: modelValue.value }, dslContext));
573
589
  }
574
590
  function isCalendarDateDisabled(field, date) {
@@ -596,6 +612,30 @@ function handleCalendarBlur(field) {
596
612
  }
597
613
  }, 0);
598
614
  }
615
+ function getFieldMaxLength(field) {
616
+ if (!field.maxLength) {
617
+ return void 0;
618
+ }
619
+ return evaluateExpression(field.maxLength, void 0, void 0);
620
+ }
621
+ function getNumberFieldMin(field) {
622
+ if (!field.min) {
623
+ return void 0;
624
+ }
625
+ return evaluateExpression(field.min, void 0, void 0);
626
+ }
627
+ function getNumberFieldMax(field) {
628
+ if (!field.max) {
629
+ return void 0;
630
+ }
631
+ return evaluateExpression(field.max, void 0, void 0);
632
+ }
633
+ function getNumberFieldStep(field) {
634
+ if (!field.step) {
635
+ return void 0;
636
+ }
637
+ return evaluateExpression(field.step, void 0, void 0);
638
+ }
599
639
  function handleConfiguratorConfirm(nextConfig) {
600
640
  displayConfig.value = cloneConfig(nextConfig);
601
641
  emit("update:config", nextConfig);
@@ -627,11 +667,11 @@ watch(config, (value) => {
627
667
  }, { immediate: true });
628
668
  watchEffect(() => {
629
669
  const activePaths = /* @__PURE__ */ new Set();
630
- for (const field of getConfigFields(displayConfig.value)) {
631
- if (!isPassiveField(field) && !isFieldHidden(field) && !isLabeledDisplayField(field) && !isFieldDisabled(field)) {
670
+ visitBodyFields(displayConfig.value, (field, visible) => {
671
+ if (visible && isInteractiveField(field) && !isFieldDisabled(field)) {
632
672
  activePaths.add(field.path);
633
673
  }
634
- }
674
+ });
635
675
  for (const path of Object.keys(validationErrors.value)) {
636
676
  if (!activePaths.has(path)) {
637
677
  clearFieldValidation(path);
@@ -648,11 +688,71 @@ watchEffect(() => {
648
688
  }
649
689
  }
650
690
  });
691
+ const renderContext = computed(() => ({
692
+ id,
693
+ isCheating: isCheating.value,
694
+ modelValue: modelValue.value,
695
+ slotForm: slotForm.value,
696
+ valid,
697
+ calendarOpen: calendarOpen.value,
698
+ selectOpen: selectOpen.value,
699
+ templateDownloading: templateDownloading.value,
700
+ getBodyEntries,
701
+ getBodyOrientation,
702
+ isBodyBordered,
703
+ getBodyStyle,
704
+ getGroupStyle,
705
+ getFieldStyle,
706
+ getFieldContainerStyle,
707
+ getFieldLabelStyle,
708
+ getFieldContentStyle,
709
+ getMarkdownBodyContentStyle,
710
+ isInteractiveField,
711
+ isFieldLabelHidden,
712
+ isFieldHidden,
713
+ isFieldDisabled,
714
+ isFieldInvalid,
715
+ getFieldLabel,
716
+ getDisplayFieldLabel,
717
+ isFieldRequired,
718
+ renderValidationMessage,
719
+ renderMarkdownField,
720
+ renderMarkdownBodyField,
721
+ toCalendarDateValue,
722
+ displayCalendarValue,
723
+ isCalendarDateDisabled,
724
+ handleCalendarOpenChange,
725
+ handleCalendarBlur,
726
+ validateField,
727
+ getFieldMaxLength,
728
+ getNumberFieldMin,
729
+ getNumberFieldMax,
730
+ getNumberFieldStep,
731
+ getSelectFieldState,
732
+ getSelectDisplayValue,
733
+ handleSelectValueChange,
734
+ handleSelectOpenChange,
735
+ handleSelectBlur,
736
+ handleSelectCommandValueChange,
737
+ clearSelectField,
738
+ getUploadTemplateIcon,
739
+ getUploadAcceptString,
740
+ getUploadDescription,
741
+ getUploadMaxCount,
742
+ getUploadFiles,
743
+ handleUploadInputChange,
744
+ handleUploadDrop,
745
+ handleUploadDragOver,
746
+ handleTemplateDownload,
747
+ getFileIcon,
748
+ removeUploadFile
749
+ }));
651
750
  </script>
652
751
 
653
752
  <script>
654
753
  export {
655
754
  CalendarFieldC,
755
+ ContainerFieldC,
656
756
  CURRENT_COMPATIBILITY_DATE,
657
757
  EmptyFieldC,
658
758
  FieldC,
@@ -662,12 +762,14 @@ export {
662
762
  FieldsConfigC,
663
763
  FieldsConfigInputC,
664
764
  KIND,
765
+ MarkdownBodyFieldC,
665
766
  MarkdownFieldC,
666
767
  NumberFieldC,
667
768
  SelectFieldC,
668
769
  SlotFieldC,
669
770
  SUPPORTED_COMPATIBILITY_DATES,
670
771
  StringFieldC,
772
+ TextareaFieldC,
671
773
  UploadFieldC,
672
774
  ValidationRuleC,
673
775
  createFieldsConfig,
@@ -680,9 +782,9 @@ export {
680
782
  :class="[
681
783
  'relative p-1 -m-1 border border-dashed',
682
784
  isCheating ? 'border-(--primary)/20 rounded hover:border-(--primary)/40 transition-colors duration-150 group cursor-pointer' : 'border-transparent',
683
- isConfigBordered(displayConfig) ? '!p-0 !m-0 border-transparent' : ''
785
+ isBodyBordered(displayConfig) ? '!p-0 !m-0 border-transparent' : ''
684
786
  ]"
685
- :style="getConfigStyle(displayConfig)"
787
+ :style="getBodyStyle(displayConfig)"
686
788
  >
687
789
  <Button
688
790
  v-if="isCheating"
@@ -708,552 +810,20 @@ export {
708
810
  class="absolute inset-0 z-10 w-full h-full"
709
811
  />
710
812
 
711
- <component
712
- :is="entry.group ? FieldSet : 'div'"
713
- v-for="entry in getConfigEntries(displayConfig)"
714
- :key="entry.key"
715
- :data-slot="entry.group ? 'fields-group' : 'fields-root-entry'"
716
- :data-group-id="entry.group?.id"
717
- :class="entry.group ? [
718
- isConfigBordered(displayConfig) ? 'overflow-hidden border border-zinc-200 [[data-slot=fields-group]+&]:border-t-0 [&>[data-slot=field]:last-child>[data-slot=field-label]]:border-b-0 [&>[data-slot=field]:last-child>[data-slot=field-content]]:border-r-0 [&>[data-slot=field]:last-child>[data-slot=field-content]]:border-b-0 [&>div:last-child]:border-r-0 [&>div:last-child]:border-b-0' : ''
719
- ] : void 0"
720
- :style="entry.group ? getGroupStyle(entry.group, displayConfig) : { display: 'contents' }"
813
+ <FieldsBody
814
+ :body="displayConfig"
815
+ :renderer="renderContext"
721
816
  >
722
817
  <template
723
- v-for="field in entry.group ? entry.group.fields : entry.field ? [entry.field] : []"
724
- :key="field.id"
818
+ v-for="(_, slotName) in slots"
819
+ #[slotName]="slotProps"
725
820
  >
726
- <div
727
- v-if="field.type === 'slot' && isConfigBordered(displayConfig)"
728
- :class="'border-b border-r border-zinc-200'"
729
- :style="getFieldStyle(field)"
730
- >
731
- <slot
732
- :name="field.id"
733
- :form="slotForm"
734
- :style="{}"
735
- :valid="valid"
736
- />
737
- </div>
738
821
  <slot
739
- v-else-if="field.type === 'slot'"
740
- :name="field.id"
741
- :form="slotForm"
742
- :style="getFieldStyle(field)"
743
- :valid="valid"
822
+ :name="slotName"
823
+ v-bind="requireSlotProps(slotProps)"
744
824
  />
745
- <div
746
- v-else-if="field.type === 'empty'"
747
- :class="isConfigBordered(displayConfig) ? 'border-b border-r border-zinc-200' : void 0"
748
- :style="getFieldStyle(field)"
749
- />
750
- <Field
751
- v-else-if="field.type === 'markdown' && !isFieldHidden(field)"
752
- :orientation="getConfigOrientation(displayConfig)"
753
- :style="getFieldContainerStyle(field, displayConfig)"
754
- >
755
- <FieldLabel
756
- v-if="!isFieldLabelHidden(field)"
757
- :class="isConfigBordered(displayConfig) ? 'border-b border-r border-zinc-200' : void 0"
758
- :style="getFieldLabelStyle(field, displayConfig)"
759
- >
760
- <span>{{ getDisplayFieldLabel(field) }}</span>
761
- </FieldLabel>
762
- <FieldContent
763
- :class="isConfigBordered(displayConfig) ? 'border-b border-r border-zinc-200 p-2 items-center justify-center text-center' : void 0"
764
- :style="getFieldContentStyle(field, displayConfig)"
765
- >
766
- <div
767
- class="text-center"
768
- data-slot="fields-markdown"
769
- v-html="renderMarkdownField(field)"
770
- />
771
- </FieldContent>
772
- </Field>
773
- <Field
774
- v-else-if="field.type === 'upload' && !isFieldHidden(field)"
775
- :data-disabled="isFieldDisabled(field) ? 'true' : void 0"
776
- :data-invalid="isFieldInvalid(field) ? 'true' : void 0"
777
- :orientation="getConfigOrientation(displayConfig)"
778
- :style="getFieldContainerStyle(field, displayConfig)"
779
- >
780
- <FieldLabel
781
- v-if="!isFieldLabelHidden(field)"
782
- :class="isConfigBordered(displayConfig) ? 'border-b border-r border-zinc-200' : void 0"
783
- :style="getFieldLabelStyle(field, displayConfig)"
784
- >
785
- <span class="inline-flex items-start gap-0.5">
786
- <span>{{ getFieldLabel(field) }}</span>
787
- <sup
788
- v-if="isFieldRequired(field)"
789
- class="text-red-500 leading-none"
790
- >*</sup>
791
- </span>
792
- <span v-if="isCheating">
793
- <span class="font-mono">{{ field.path }}</span>
794
- </span>
795
- </FieldLabel>
796
- <FieldContent
797
- :class="isConfigBordered(displayConfig) ? 'border-b border-r border-zinc-200 p-2' : void 0"
798
- :style="getFieldContentStyle(field, displayConfig)"
799
- >
800
- <div
801
- :data-disabled="isFieldDisabled(field) ? 'true' : void 0"
802
- class="flex flex-col gap-2"
803
- >
804
- <label
805
- v-if="getUploadFiles(field).length < getUploadMaxCount(field)"
806
- :class="[
807
- 'flex cursor-pointer flex-col items-center justify-center gap-3 rounded-lg border-2 border-dashed px-4 py-8 transition-colors',
808
- isFieldDisabled(field) ? 'cursor-not-allowed border-zinc-200 opacity-50' : 'border-zinc-300 hover:border-zinc-400 hover:bg-zinc-50'
809
- ]"
810
- @drop="handleUploadDrop(field, $event)"
811
- @dragover="handleUploadDragOver"
812
- >
813
- <Icon
814
- :icon="field.icon ?? 'fluent:cloud-arrow-up-20-regular'"
815
- class="text-4xl text-zinc-400"
816
- />
817
- <button
818
- v-if="field.template"
819
- type="button"
820
- class="inline-flex items-center gap-1.5 rounded-md border border-zinc-300 bg-white px-3 py-1.5 text-sm text-zinc-600 transition-colors hover:border-[--el-color-primary] hover:text-[--el-color-primary] disabled:opacity-50"
821
- :disabled="templateDownloading[field.id]"
822
- @click.prevent.stop="handleTemplateDownload(field)"
823
- >
824
- <Icon
825
- :icon="templateDownloading[field.id] ? 'svg-spinners:ring-resize' : getUploadTemplateIcon(field)"
826
- class="text-base"
827
- />
828
- {{ getLocalizedText(field.templateName, locale) ?? t("upload-download-template") }}
829
- </button>
830
- <p
831
- v-if="field.description"
832
- class="text-sm text-zinc-600"
833
- v-html="$md.inline`${getLocalizedText(field.description, locale) ?? ''}`()"
834
- />
835
- <p
836
- v-else
837
- class="text-sm text-zinc-600"
838
- >
839
- {{ t("upload-click-or-drag") }}
840
- </p>
841
- <p class="text-xs text-zinc-400">
842
- {{ getUploadDescription(field) || t("upload-accept-all") }}
843
- </p>
844
- <input
845
- type="file"
846
- class="sr-only"
847
- :accept="getUploadAcceptString(field)"
848
- :disabled="isFieldDisabled(field)"
849
- :multiple="getUploadMaxCount(field) !== 1"
850
- @change="handleUploadInputChange(field, $event)"
851
- >
852
- </label>
853
- <ul
854
- v-if="getUploadFiles(field).length > 0"
855
- class="flex flex-col gap-1"
856
- >
857
- <li
858
- v-for="(file, fileIndex) in getUploadFiles(field)"
859
- :key="`${field.id}:${fileIndex}:${file.name}`"
860
- class="flex items-center gap-2 rounded-md border border-zinc-200 px-3 py-2"
861
- >
862
- <Icon
863
- :icon="getFileIcon(file.name)"
864
- class="shrink-0 text-lg"
865
- />
866
- <span class="flex-1 truncate text-sm text-zinc-700">{{ file.name }}</span>
867
- <button
868
- type="button"
869
- class="shrink-0 text-zinc-300 transition-colors hover:text-red-500"
870
- :disabled="isFieldDisabled(field)"
871
- @click="removeUploadFile(field, fileIndex)"
872
- >
873
- <Icon
874
- icon="fluent:delete-20-regular"
875
- class="text-lg"
876
- />
877
- </button>
878
- </li>
879
- </ul>
880
- </div>
881
- <FieldError
882
- v-if="isFieldInvalid(field)"
883
- :class="usesContentsOrientation(displayConfig) ? 'static pt-1' : void 0"
884
- >
885
- <span v-html="renderValidationMessage(field)" />
886
- </FieldError>
887
- </FieldContent>
888
- </Field>
889
- <Field
890
- v-else-if="isInteractiveField(field) && !isFieldHidden(field)"
891
- :data-disabled="isFieldDisabled(field) ? 'true' : void 0"
892
- :data-invalid="isFieldInvalid(field) ? 'true' : void 0"
893
- :orientation="getConfigOrientation(displayConfig)"
894
- :style="getFieldContainerStyle(field, displayConfig)"
895
- >
896
- <FieldLabel
897
- v-if="!isFieldLabelHidden(field)"
898
- :for="['string', 'textarea', 'number', 'select'].includes(field.type) ? `${id}:${field.path}` : void 0"
899
- :class="isConfigBordered(displayConfig) ? 'border-b border-r border-zinc-200' : void 0"
900
- :style="getFieldLabelStyle(field, displayConfig)"
901
- >
902
- <span class="inline-flex items-start gap-0.5">
903
- <span>{{ getFieldLabel(field) }}</span>
904
- <sup
905
- v-if="isFieldRequired(field)"
906
- class="text-red-500 leading-none"
907
- >*</sup>
908
- </span>
909
- <span v-if="isCheating">
910
- <span class="font-mono">{{ field.path }}</span>
911
- </span>
912
- </FieldLabel>
913
- <FieldContent
914
- :class="isConfigBordered(displayConfig) ? 'border-b border-r border-zinc-200 p-2' : void 0"
915
- :style="getFieldContentStyle(field, displayConfig)"
916
- >
917
- <Popover
918
- v-if="field.type === 'calendar'"
919
- @update:open="(open) => handleCalendarOpenChange(field, open)"
920
- >
921
- <PopoverAnchor as-child>
922
- <InputGroup :data-disabled="isFieldDisabled(field) ? 'true' : void 0">
923
- <PopoverTrigger as-child>
924
- <InputGroupInput
925
- :model-value="displayCalendarValue(getProperty(modelValue, field.path), field.display, field.value)"
926
- class="text-left"
927
- :disabled="isFieldDisabled(field)"
928
- :aria-invalid="isFieldInvalid(field) ? 'true' : void 0"
929
- readonly
930
- @blur="handleCalendarBlur(field)"
931
- />
932
- </PopoverTrigger>
933
- <InputGroupAddon v-if="field.icon">
934
- <Icon
935
- :icon="field.icon"
936
- />
937
- </InputGroupAddon>
938
- <InputGroupAddon
939
- v-if="hasProperty(modelValue, field.path)"
940
- align="inline-end"
941
- :class="getConfigOrientation(displayConfig) === 'floating' ? 'group-data-[disabled=true]/input-group:hidden' : void 0"
942
- >
943
- <Tooltip :delay-duration="800">
944
- <TooltipTrigger>
945
- <InputGroupButton as-child>
946
- <button
947
- type="button"
948
- class="text-zinc-300 hover:text-zinc-500 transition-colors"
949
- :disabled="isFieldDisabled(field)"
950
- @click="deleteProperty(modelValue, field.path)"
951
- >
952
- <Icon
953
- icon="fluent:dismiss-20-regular"
954
- />
955
- </button>
956
- </InputGroupButton>
957
- </TooltipTrigger>
958
- <TooltipContent>
959
- {{ t("clear") }}
960
- </TooltipContent>
961
- </Tooltip>
962
- </InputGroupAddon>
963
- </InputGroup>
964
- </PopoverAnchor>
965
- <PopoverContent class="w-72">
966
- <Calendar
967
- :locale="locale"
968
- :layout="field.mode"
969
- :model-value="toCalendarDateValue(getProperty(modelValue, field.path), field.value)"
970
- :disabled="isFieldDisabled(field)"
971
- :is-date-disabled="field.disableDate ? (date) => isCalendarDateDisabled(field, date) : void 0"
972
- @update:model-value="(value) => {
973
- if (value === void 0) {
974
- deleteProperty(modelValue, field.path);
975
- } else {
976
- setProperty(modelValue, field.path, format(value.toDate(getLocalTimeZone()), field.value));
977
- }
978
- }"
979
- />
980
- </PopoverContent>
981
- </Popover>
982
- <template v-else>
983
- <template v-if="field.type === 'select'">
984
- <Popover
985
- v-for="selectState in [getSelectFieldState(field)]"
986
- :key="`${field.id}:select:${selectState.selectedKey ?? 'empty'}`"
987
- :open="selectOpen[field.path] === true"
988
- @update:open="(open) => handleSelectOpenChange(field, open)"
989
- >
990
- <PopoverAnchor as-child>
991
- <InputGroup :data-disabled="isFieldDisabled(field) ? 'true' : void 0">
992
- <PopoverTrigger as-child>
993
- <InputGroupInput
994
- :id="`${id}:${field.path}`"
995
- :model-value="getSelectDisplayValue(selectState, selectState.selectedKey)"
996
- :disabled="isFieldDisabled(field)"
997
- :aria-invalid="isFieldInvalid(field) ? 'true' : void 0"
998
- :placeholder="t('select-placeholder')"
999
- class="text-left"
1000
- readonly
1001
- @blur="handleSelectBlur(field)"
1002
- />
1003
- </PopoverTrigger>
1004
- <InputGroupAddon v-if="field.icon">
1005
- <Icon
1006
- :icon="field.icon"
1007
- />
1008
- </InputGroupAddon>
1009
- <InputGroupAddon
1010
- v-if="hasProperty(modelValue, field.path)"
1011
- align="inline-end"
1012
- :class="getConfigOrientation(displayConfig) === 'floating' ? 'group-data-[disabled=true]/input-group:hidden' : void 0"
1013
- >
1014
- <Tooltip :delay-duration="800">
1015
- <TooltipTrigger>
1016
- <InputGroupButton as-child>
1017
- <button
1018
- type="button"
1019
- class="text-zinc-300 hover:text-zinc-500 transition-colors"
1020
- :disabled="isFieldDisabled(field)"
1021
- @click="clearSelectField(field)"
1022
- >
1023
- <Icon
1024
- icon="fluent:dismiss-20-regular"
1025
- />
1026
- </button>
1027
- </InputGroupButton>
1028
- </TooltipTrigger>
1029
- <TooltipContent>
1030
- {{ t("clear") }}
1031
- </TooltipContent>
1032
- </Tooltip>
1033
- </InputGroupAddon>
1034
- </InputGroup>
1035
- </PopoverAnchor>
1036
-
1037
- <PopoverContent
1038
- class="p-0"
1039
- :style="{ width: 'var(--reka-popover-trigger-width)' }"
1040
- >
1041
- <Command
1042
- :model-value="selectState.selectedKey"
1043
- :disabled="isFieldDisabled(field)"
1044
- selection-behavior="toggle"
1045
- @update:model-value="(value) => handleSelectCommandValueChange(field, selectState, value)"
1046
- >
1047
- <CommandInput :placeholder="t('select-search-placeholder')" />
1048
- <CommandList>
1049
- <CommandEmpty as-child>
1050
- <section class="h-32 flex flex-col text-lg items-center justify-center gap-2 select-none">
1051
- <Icon
1052
- icon="fluent:app-recent-20-regular"
1053
- class="text-zinc-400 text-2xl!"
1054
- />
1055
- <p class="text-zinc-500">
1056
- {{ t("select-empty") }}
1057
- </p>
1058
- </section>
1059
- </CommandEmpty>
1060
- <CommandGroup>
1061
- <CommandItem
1062
- v-for="option in selectState.options"
1063
- :key="option.key"
1064
- data-slot="select-option"
1065
- :value="option.key"
1066
- class="data-highlighted:bg-zinc-50 data-highlighted:text-zinc-700 data-[state=checked]:bg-zinc-100 data-[state=checked]:text-zinc-700 transition cursor-pointer relative flex items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none"
1067
- >
1068
- {{ option.label }}
1069
- </CommandItem>
1070
- </CommandGroup>
1071
- </CommandList>
1072
- </Command>
1073
- </PopoverContent>
1074
- </Popover>
1075
- </template>
1076
-
1077
- <template v-else-if="field.type === 'radio'">
1078
- <RadioGroupRoot
1079
- v-for="radioState in [getSelectFieldState(field)]"
1080
- :key="`${field.id}:radio:${radioState.selectedKey ?? 'empty'}`"
1081
- :model-value="radioState.selectedKey"
1082
- :disabled="isFieldDisabled(field)"
1083
- :aria-invalid="isFieldInvalid(field) ? 'true' : void 0"
1084
- class="flex flex-wrap gap-x-4 gap-y-1.5"
1085
- @update:model-value="(value) => handleSelectValueChange(field, radioState, value)"
1086
- @focusout="validateField(field)"
1087
- >
1088
- <label
1089
- v-for="option in radioState.options"
1090
- :key="option.key"
1091
- class="flex items-center gap-1.5 text-sm cursor-pointer data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50"
1092
- :data-disabled="isFieldDisabled(field) ? 'true' : void 0"
1093
- >
1094
- <RadioGroupItem
1095
- :value="option.key"
1096
- data-slot="radio-group-item"
1097
- class="size-4 rounded-full border border-zinc-300 data-[state=checked]:border-(--primary) data-[state=checked]:bg-(--primary) focus:outline-none focus-visible:ring-1 focus-visible:ring-(--primary) focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 transition-colors"
1098
- >
1099
- <RadioGroupIndicator class="flex items-center justify-center">
1100
- <span class="size-1.5 rounded-full bg-white" />
1101
- </RadioGroupIndicator>
1102
- </RadioGroupItem>
1103
- {{ option.label }}
1104
- </label>
1105
- </RadioGroupRoot>
1106
- </template>
1107
-
1108
- <InputGroup
1109
- v-else
1110
- :data-disabled="isFieldDisabled(field) ? 'true' : void 0"
1111
- :class="field.type === 'textarea' ? 'h-auto flex-col items-stretch' : void 0"
1112
- >
1113
- <div
1114
- v-if="field.type === 'textarea'"
1115
- class="flex min-w-0 w-full items-center"
1116
- >
1117
- <InputGroupTextarea
1118
- :id="`${id}:${field.path}`"
1119
- :model-value="getProperty(modelValue, field.path)"
1120
- :maxlength="field.maxLength ? $dsl.evaluate`${field.maxLength}`() : void 0"
1121
- :disabled="isFieldDisabled(field)"
1122
- :aria-invalid="isFieldInvalid(field) ? 'true' : void 0"
1123
- @update:model-value="(value) => {
1124
- if (!value && !field.discardEmptyString) {
1125
- deleteProperty(modelValue, field.path);
1126
- } else {
1127
- setProperty(modelValue, field.path, value);
1128
- }
1129
- }"
1130
- @blur="validateField(field)"
1131
- />
1132
- <InputGroupAddon v-if="field.icon">
1133
- <Icon
1134
- :icon="field.icon"
1135
- />
1136
- </InputGroupAddon>
1137
- </div>
1138
- <InputGroupInput
1139
- v-if="field.type === 'string'"
1140
- :id="`${id}:${field.path}`"
1141
- :treat-empty-as-different-state-from-null="!!field.discardEmptyString"
1142
- :model-value="getProperty(modelValue, field.path)"
1143
- :maxlength="field.maxLength ? $dsl.evaluate`${field.maxLength}`() : void 0"
1144
- :disabled="isFieldDisabled(field)"
1145
- :aria-invalid="isFieldInvalid(field) ? 'true' : void 0"
1146
- @update:model-value="(value) => {
1147
- if (!value && !field.discardEmptyString) {
1148
- deleteProperty(modelValue, field.path);
1149
- } else {
1150
- setProperty(modelValue, field.path, value);
1151
- }
1152
- }"
1153
- @blur="validateField(field)"
1154
- />
1155
- <InputGroupNumberField
1156
- v-if="field.type === 'number'"
1157
- :id="`${id}:${field.path}`"
1158
- :model-value="getProperty(modelValue, field.path) ?? null"
1159
- :min="field.min ? $dsl.evaluate`${field.min}`() : void 0"
1160
- :max="field.max ? $dsl.evaluate`${field.max}`() : void 0"
1161
- :step="field.step ? $dsl.evaluate`${field.step}`() : void 0"
1162
- :disabled="isFieldDisabled(field)"
1163
- :invalid="isFieldInvalid(field)"
1164
- @update:model-value="(value) => {
1165
- if (!value && value !== 0) {
1166
- deleteProperty(modelValue, field.path);
1167
- } else {
1168
- setProperty(modelValue, field.path, value);
1169
- }
1170
- }"
1171
- @blur="validateField(field)"
1172
- />
1173
- <InputGroupAddon v-if="field.type !== 'textarea' && field.icon">
1174
- <Icon
1175
- :icon="field.icon"
1176
- />
1177
- </InputGroupAddon>
1178
- <InputGroupAddon
1179
- v-if="field.type !== 'textarea' && hasProperty(modelValue, field.path)"
1180
- align="inline-end"
1181
- :class="getConfigOrientation(displayConfig) === 'floating' ? 'group-data-[disabled=true]/input-group:hidden' : void 0"
1182
- >
1183
- <Tooltip :delay-duration="800">
1184
- <TooltipTrigger>
1185
- <InputGroupButton as-child>
1186
- <button
1187
- type="button"
1188
- class="text-zinc-300 hover:text-zinc-500 transition-colors"
1189
- :disabled="isFieldDisabled(field)"
1190
- @click="deleteProperty(modelValue, field.path)"
1191
- >
1192
- <Icon
1193
- icon="fluent:dismiss-20-regular"
1194
- />
1195
- </button>
1196
- </InputGroupButton>
1197
- </TooltipTrigger>
1198
- <TooltipContent>
1199
- {{ t("clear") }}
1200
- </TooltipContent>
1201
- </Tooltip>
1202
- </InputGroupAddon>
1203
- <InputGroupAddon
1204
- v-if="field.type === 'string' && field.maxLength && getProperty(modelValue, field.path)"
1205
- align="inline-end"
1206
- >
1207
- <span class="text-xs text-zinc-400 font-mono">
1208
- <span class="inline-block text-right">{{ String(getProperty(modelValue, field.path) ?? "").length }}</span>/{{ field.maxLength }}
1209
- </span>
1210
- </InputGroupAddon>
1211
- <InputGroupAddon
1212
- v-if="field.type === 'textarea' && (hasProperty(modelValue, field.path) || field.maxLength && getProperty(modelValue, field.path))"
1213
- align="block-end"
1214
- >
1215
- <Tooltip
1216
- v-if="hasProperty(modelValue, field.path)"
1217
- :delay-duration="800"
1218
- >
1219
- <TooltipTrigger>
1220
- <InputGroupButton as-child>
1221
- <button
1222
- type="button"
1223
- class="text-zinc-300 hover:text-zinc-500 transition-colors"
1224
- :disabled="isFieldDisabled(field)"
1225
- @click="deleteProperty(modelValue, field.path)"
1226
- >
1227
- <Icon
1228
- icon="fluent:dismiss-20-regular"
1229
- />
1230
- </button>
1231
- </InputGroupButton>
1232
- </TooltipTrigger>
1233
- <TooltipContent>
1234
- {{ t("clear") }}
1235
- </TooltipContent>
1236
- </Tooltip>
1237
- <span
1238
- v-if="field.maxLength && getProperty(modelValue, field.path)"
1239
- class="text-xs text-zinc-400 font-mono"
1240
- >
1241
- <span class="inline-block text-right">{{ String(getProperty(modelValue, field.path) ?? "").length }}</span>/{{ field.maxLength }}
1242
- </span>
1243
- </InputGroupAddon>
1244
- </InputGroup>
1245
- </template>
1246
-
1247
- <FieldError
1248
- v-if="isFieldInvalid(field)"
1249
- :class="usesContentsOrientation(displayConfig) ? 'static pt-1' : void 0"
1250
- >
1251
- <span v-html="renderValidationMessage(field)" />
1252
- </FieldError>
1253
- </FieldContent>
1254
- </Field>
1255
825
  </template>
1256
- </component>
826
+ </FieldsBody>
1257
827
 
1258
828
  <slot />
1259
829
  </div>