nuxt-ui-tools 1.7.0 → 1.8.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 (162) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/module.mjs +18 -0
  3. package/dist/runtime/file-preview/components/provider/file-preview-host.d.vue.ts +7 -0
  4. package/dist/runtime/file-preview/components/provider/file-preview-host.vue +147 -0
  5. package/dist/runtime/file-preview/components/provider/file-preview-host.vue.d.ts +7 -0
  6. package/dist/runtime/file-preview/components/provider/file-preview-provider.d.vue.ts +21 -0
  7. package/dist/runtime/file-preview/components/provider/file-preview-provider.vue +23 -0
  8. package/dist/runtime/file-preview/components/provider/file-preview-provider.vue.d.ts +21 -0
  9. package/dist/runtime/file-preview/components/renderers/audio-renderer.d.vue.ts +10 -0
  10. package/dist/runtime/file-preview/components/renderers/audio-renderer.vue +83 -0
  11. package/dist/runtime/file-preview/components/renderers/audio-renderer.vue.d.ts +10 -0
  12. package/dist/runtime/file-preview/components/renderers/csv-renderer.d.vue.ts +10 -0
  13. package/dist/runtime/file-preview/components/renderers/csv-renderer.vue +116 -0
  14. package/dist/runtime/file-preview/components/renderers/csv-renderer.vue.d.ts +10 -0
  15. package/dist/runtime/file-preview/components/renderers/fallback-renderer.d.vue.ts +10 -0
  16. package/dist/runtime/file-preview/components/renderers/fallback-renderer.vue +41 -0
  17. package/dist/runtime/file-preview/components/renderers/fallback-renderer.vue.d.ts +10 -0
  18. package/dist/runtime/file-preview/components/renderers/file-preview-code.d.vue.ts +9 -0
  19. package/dist/runtime/file-preview/components/renderers/file-preview-code.vue +83 -0
  20. package/dist/runtime/file-preview/components/renderers/file-preview-code.vue.d.ts +9 -0
  21. package/dist/runtime/file-preview/components/renderers/file-preview-text-loading.d.vue.ts +3 -0
  22. package/dist/runtime/file-preview/components/renderers/file-preview-text-loading.vue +14 -0
  23. package/dist/runtime/file-preview/components/renderers/file-preview-text-loading.vue.d.ts +3 -0
  24. package/dist/runtime/file-preview/components/renderers/image-renderer.d.vue.ts +10 -0
  25. package/dist/runtime/file-preview/components/renderers/image-renderer.vue +182 -0
  26. package/dist/runtime/file-preview/components/renderers/image-renderer.vue.d.ts +10 -0
  27. package/dist/runtime/file-preview/components/renderers/markdown-renderer.d.vue.ts +10 -0
  28. package/dist/runtime/file-preview/components/renderers/markdown-renderer.vue +78 -0
  29. package/dist/runtime/file-preview/components/renderers/markdown-renderer.vue.d.ts +10 -0
  30. package/dist/runtime/file-preview/components/renderers/media-controls.d.vue.ts +10 -0
  31. package/dist/runtime/file-preview/components/renderers/media-controls.vue +145 -0
  32. package/dist/runtime/file-preview/components/renderers/media-controls.vue.d.ts +10 -0
  33. package/dist/runtime/file-preview/components/renderers/pdf-renderer.d.vue.ts +10 -0
  34. package/dist/runtime/file-preview/components/renderers/pdf-renderer.vue +58 -0
  35. package/dist/runtime/file-preview/components/renderers/pdf-renderer.vue.d.ts +10 -0
  36. package/dist/runtime/file-preview/components/renderers/text-renderer.d.vue.ts +10 -0
  37. package/dist/runtime/file-preview/components/renderers/text-renderer.vue +41 -0
  38. package/dist/runtime/file-preview/components/renderers/text-renderer.vue.d.ts +10 -0
  39. package/dist/runtime/file-preview/components/renderers/video-renderer.d.vue.ts +10 -0
  40. package/dist/runtime/file-preview/components/renderers/video-renderer.vue +110 -0
  41. package/dist/runtime/file-preview/components/renderers/video-renderer.vue.d.ts +10 -0
  42. package/dist/runtime/file-preview/components/shell/file-preview-button.d.vue.ts +16 -0
  43. package/dist/runtime/file-preview/components/shell/file-preview-button.vue +40 -0
  44. package/dist/runtime/file-preview/components/shell/file-preview-button.vue.d.ts +16 -0
  45. package/dist/runtime/{form/components/renderer/form-field-error.d.vue.ts → file-preview/components/shell/file-preview-details.d.vue.ts} +4 -2
  46. package/dist/runtime/file-preview/components/shell/file-preview-details.vue +25 -0
  47. package/dist/runtime/{form/components/renderer/form-field-error.vue.d.ts → file-preview/components/shell/file-preview-details.vue.d.ts} +4 -2
  48. package/dist/runtime/file-preview/components/shell/file-preview-header.d.vue.ts +44 -0
  49. package/dist/runtime/file-preview/components/shell/file-preview-header.vue +163 -0
  50. package/dist/runtime/file-preview/components/shell/file-preview-header.vue.d.ts +44 -0
  51. package/dist/runtime/file-preview/components/shell/file-preview-message.d.vue.ts +21 -0
  52. package/dist/runtime/file-preview/components/shell/file-preview-message.vue +49 -0
  53. package/dist/runtime/file-preview/components/shell/file-preview-message.vue.d.ts +21 -0
  54. package/dist/runtime/file-preview/components/shell/file-preview-shell.d.vue.ts +17 -0
  55. package/dist/runtime/file-preview/components/shell/file-preview-shell.vue +280 -0
  56. package/dist/runtime/file-preview/components/shell/file-preview-shell.vue.d.ts +17 -0
  57. package/dist/runtime/file-preview/components/shell/file-preview-stage.d.vue.ts +37 -0
  58. package/dist/runtime/file-preview/components/shell/file-preview-stage.vue +173 -0
  59. package/dist/runtime/file-preview/components/shell/file-preview-stage.vue.d.ts +37 -0
  60. package/dist/runtime/file-preview/components/shell/file-preview-thumb.d.vue.ts +14 -0
  61. package/dist/runtime/file-preview/components/shell/file-preview-thumb.vue +69 -0
  62. package/dist/runtime/file-preview/components/shell/file-preview-thumb.vue.d.ts +14 -0
  63. package/dist/runtime/file-preview/components/shell/file-preview-tools.d.vue.ts +13 -0
  64. package/dist/runtime/file-preview/components/shell/file-preview-tools.vue +10 -0
  65. package/dist/runtime/file-preview/components/shell/file-preview-tools.vue.d.ts +13 -0
  66. package/dist/runtime/file-preview/composables/use-file-preview-api.d.ts +21 -0
  67. package/dist/runtime/file-preview/composables/use-file-preview-api.js +83 -0
  68. package/dist/runtime/file-preview/composables/use-file-preview-shell.d.ts +13 -0
  69. package/dist/runtime/file-preview/composables/use-file-preview-shell.js +30 -0
  70. package/dist/runtime/file-preview/composables/use-file-preview-source.d.ts +17 -0
  71. package/dist/runtime/file-preview/composables/use-file-preview-source.js +51 -0
  72. package/dist/runtime/file-preview/composables/use-file-preview-text.d.ts +14 -0
  73. package/dist/runtime/file-preview/composables/use-file-preview-text.js +79 -0
  74. package/dist/runtime/file-preview/index.d.ts +5 -0
  75. package/dist/runtime/file-preview/index.js +14 -0
  76. package/dist/runtime/file-preview/plugins/file-preview-api.d.ts +11 -0
  77. package/dist/runtime/file-preview/plugins/file-preview-api.js +10 -0
  78. package/dist/runtime/file-preview/types/api.d.ts +74 -0
  79. package/dist/runtime/file-preview/types/api.js +0 -0
  80. package/dist/runtime/file-preview/types/file.d.ts +77 -0
  81. package/dist/runtime/file-preview/types/file.js +0 -0
  82. package/dist/runtime/file-preview/types/index.d.ts +4 -0
  83. package/dist/runtime/file-preview/types/index.js +0 -0
  84. package/dist/runtime/file-preview/types/renderer.d.ts +49 -0
  85. package/dist/runtime/file-preview/types/renderer.js +0 -0
  86. package/dist/runtime/file-preview/types/shell.d.ts +12 -0
  87. package/dist/runtime/file-preview/types/shell.js +0 -0
  88. package/dist/runtime/file-preview/utils/container.d.ts +4 -0
  89. package/dist/runtime/file-preview/utils/container.js +10 -0
  90. package/dist/runtime/file-preview/utils/csv.d.ts +17 -0
  91. package/dist/runtime/file-preview/utils/csv.js +53 -0
  92. package/dist/runtime/file-preview/utils/download.d.ts +8 -0
  93. package/dist/runtime/file-preview/utils/download.js +40 -0
  94. package/dist/runtime/file-preview/utils/files.d.ts +7 -0
  95. package/dist/runtime/file-preview/utils/files.js +53 -0
  96. package/dist/runtime/file-preview/utils/format.d.ts +13 -0
  97. package/dist/runtime/file-preview/utils/format.js +40 -0
  98. package/dist/runtime/file-preview/utils/instance.d.ts +9 -0
  99. package/dist/runtime/file-preview/utils/instance.js +57 -0
  100. package/dist/runtime/file-preview/utils/renderers.d.ts +36 -0
  101. package/dist/runtime/file-preview/utils/renderers.js +247 -0
  102. package/dist/runtime/form/components/page/form-page-header.d.vue.ts +3 -1
  103. package/dist/runtime/form/components/page/form-page-header.vue +4 -4
  104. package/dist/runtime/form/components/page/form-page-header.vue.d.ts +3 -1
  105. package/dist/runtime/form/components/page/form-page-navigation.vue +2 -1
  106. package/dist/runtime/form/components/page/form-page-section.vue +15 -7
  107. package/dist/runtime/form/components/page/form-page.vue +0 -1
  108. package/dist/runtime/form/components/utils/form-file-preview.vue +5 -38
  109. package/dist/runtime/form/composables/use-field-control.d.ts +1 -1
  110. package/dist/runtime/form/composables/use-form-page.js +3 -1
  111. package/dist/runtime/form/composables/use-form-runtime.js +3 -0
  112. package/dist/runtime/form/composables/use-form-upload-registry.d.ts +2 -0
  113. package/dist/runtime/form/composables/use-form-upload-registry.js +11 -6
  114. package/dist/runtime/form/composables/use-form-validation.js +5 -1
  115. package/dist/runtime/form/fields/array-list/use-array-items.d.ts +1 -1
  116. package/dist/runtime/form/fields/array-table/array-table-cell.d.vue.ts +15 -0
  117. package/dist/runtime/form/fields/array-table/array-table-cell.vue +58 -0
  118. package/dist/runtime/form/fields/array-table/array-table-cell.vue.d.ts +15 -0
  119. package/dist/runtime/form/fields/array-table/component.vue +42 -26
  120. package/dist/runtime/form/fields/date/types.d.ts +6 -1
  121. package/dist/runtime/form/fields/date-family/component.vue +7 -1
  122. package/dist/runtime/form/fields/date-family/utils.d.ts +8 -1
  123. package/dist/runtime/form/fields/date-family/utils.js +10 -2
  124. package/dist/runtime/form/fields/hidden/types.d.ts +8 -1
  125. package/dist/runtime/form/fields/upload/component.vue +55 -176
  126. package/dist/runtime/form/fields/upload/types.d.ts +68 -3
  127. package/dist/runtime/form/fields/upload/upload-item.d.vue.ts +24 -0
  128. package/dist/runtime/form/fields/upload/upload-item.vue +158 -0
  129. package/dist/runtime/form/fields/upload/upload-item.vue.d.ts +24 -0
  130. package/dist/runtime/form/fields/upload/use-upload-items.d.ts +44 -0
  131. package/dist/runtime/form/fields/upload/use-upload-items.js +317 -0
  132. package/dist/runtime/form/types/callbacks.d.ts +8 -2
  133. package/dist/runtime/form/types/controller.d.ts +6 -3
  134. package/dist/runtime/form/types/field-base.d.ts +5 -5
  135. package/dist/runtime/form/types/field-output-utils.d.ts +12 -1
  136. package/dist/runtime/form/types/field-output.d.ts +1 -1
  137. package/dist/runtime/form/types/field.d.ts +3 -2
  138. package/dist/runtime/form/types/form-api.d.ts +4 -2
  139. package/dist/runtime/form/types/options-runtime.d.ts +8 -4
  140. package/dist/runtime/form/types/options.d.ts +11 -11
  141. package/dist/runtime/form/types/output.d.ts +33 -10
  142. package/dist/runtime/form/types/page.d.ts +7 -3
  143. package/dist/runtime/form/types/transform.d.ts +5 -2
  144. package/dist/runtime/form/types/ui.d.ts +7 -3
  145. package/dist/runtime/form/types/upload-runtime.d.ts +4 -0
  146. package/dist/runtime/form/utils/page.js +24 -5
  147. package/dist/runtime/form/utils/state.js +14 -11
  148. package/dist/runtime/i18n/locales/en.js +104 -1
  149. package/dist/runtime/i18n/locales/fr.js +104 -1
  150. package/dist/runtime/i18n/types.d.ts +121 -0
  151. package/dist/runtime/query-state/composables.d.ts +2 -2
  152. package/dist/runtime/shared/styles/tokens.css +1 -1
  153. package/dist/runtime/shared/types/utils.d.ts +12 -4
  154. package/dist/runtime/shared/utils/file.d.ts +11 -0
  155. package/dist/runtime/shared/utils/file.js +50 -0
  156. package/dist/runtime/table/components/table/table-renderer.vue +23 -3
  157. package/dist/runtime/table/components/table/table-skeleton-cell.js +2 -2
  158. package/dist/runtime/table/components/utils/search-query-input.d.vue.ts +5 -1
  159. package/dist/runtime/table/components/utils/search-query-input.vue +17 -3
  160. package/dist/runtime/table/components/utils/search-query-input.vue.d.ts +5 -1
  161. package/package.json +56 -29
  162. package/dist/runtime/form/components/renderer/form-field-error.vue +0 -16
@@ -2,16 +2,18 @@
2
2
  import UButton from "@nuxt/ui/components/Button.vue";
3
3
  import UIcon from "@nuxt/ui/components/Icon.vue";
4
4
  import { useScrollShadow } from "@nuxt/ui/composables/useScrollShadow";
5
+ import { useElementSize } from "@vueuse/core";
5
6
  import { computed, defineAsyncComponent, ref } from "vue";
6
- import FormFieldError from "../../components/renderer/form-field-error.vue";
7
- import FormFieldRenderer from "../../components/renderer/form-field-renderer.vue";
8
7
  import { useResolvedFieldProps } from "../../composables/use-field-control";
9
8
  import { isRecord } from "../../utils/path";
10
9
  import { isNumber, isString } from "../../utils/predicate";
11
10
  import { resolveFormText } from "../../utils/text";
12
11
  import { mergeFormUiClass } from "../../utils/ui";
13
12
  import { useFormArrayItems } from "../array-list/use-array-items";
13
+ import ArrayTableCell from "./array-table-cell.vue";
14
14
  const DEFAULT_COLUMN_MIN_WIDTH = "140px";
15
+ const ACTION_BUTTON_WIDTH = 24;
16
+ const ACTIONS_CELL_INSET = 14;
15
17
  const props = defineProps({
16
18
  field: { type: Object, required: true },
17
19
  path: { type: Array, required: true }
@@ -49,6 +51,7 @@ const {
49
51
  );
50
52
  const viewportRef = ref(null);
51
53
  const viewportShadow = useScrollShadow(viewportRef, { orientation: "horizontal", size: 20 });
54
+ const { width: viewportWidth } = useElementSize(viewportRef);
52
55
  const ui = computed(() => formUi.ui.value.arrayTable?.ui);
53
56
  const columns = computed(
54
57
  () => props.field.fields.filter((field) => field.type !== "hidden" && field.ignore !== true)
@@ -65,6 +68,19 @@ const showActionsColumn = computed(
65
68
  (_item, index) => isDraggable.value || canDelete(index) || hasCustomActions(index)
66
69
  )
67
70
  );
71
+ const pinnedStyle = computed(
72
+ () => viewportWidth.value > 0 ? { width: `${viewportWidth.value}px` } : void 0
73
+ );
74
+ const actionsStyle = computed(() => {
75
+ const buttons = Math.max(0, ...items.value.map((_item, index) => rowActionCount(index)));
76
+ return buttons ? { width: `${buttons * ACTION_BUTTON_WIDTH + ACTIONS_CELL_INSET}px` } : void 0;
77
+ });
78
+ function rowActionCount(index) {
79
+ const custom = (props.field.actions?.custom ?? []).filter(
80
+ (_action, actionIndex) => customActionVisible(index, actionIndex)
81
+ );
82
+ return Number(isDraggable.value) + Number(canDelete(index)) + custom.length;
83
+ }
68
84
  function columnStyle(field) {
69
85
  const layout = Object.getOwnPropertyDescriptor(field, "layout")?.value;
70
86
  const width = isRecord(layout) ? layout.width : void 0;
@@ -127,6 +143,7 @@ function fieldLabel(field) {
127
143
  <th
128
144
  v-if="showActionsColumn"
129
145
  scope="col"
146
+ :style="actionsStyle"
130
147
  :class="
131
148
  mergeFormUiClass(
132
149
  'sticky right-0 w-px border-b border-l border-default bg-elevated px-1.5 py-2 shadow-[-8px_0_12px_-10px_rgba(0,0,0,0.45)]',
@@ -159,15 +176,7 @@ function fieldLabel(field) {
159
176
  :key="column.key"
160
177
  :class="mergeFormUiClass('border-r border-default p-1.5 last:border-r-0', ui?.cell)"
161
178
  >
162
- <div
163
- :class="mergeFormUiClass('flex w-full items-center [&>*]:w-full', ui?.control)"
164
- >
165
- <FormFieldRenderer :field="column" :parent-path="itemPath(index)" bare />
166
- </div>
167
- <FormFieldError
168
- :path="[...itemPath(index), column.key]"
169
- :class="mergeFormUiClass('mt-1 px-2.5 text-xs text-error', ui?.error)"
170
- />
179
+ <ArrayTableCell :field="column" :item-path="itemPath(index)" />
171
180
  </td>
172
181
  <td
173
182
  v-if="showActionsColumn"
@@ -218,11 +227,16 @@ function fieldLabel(field) {
218
227
  </td>
219
228
  </tr>
220
229
  <tr v-if="items.length === 0">
221
- <td
222
- :colspan="columns.length + (showActionsColumn ? 1 : 0)"
223
- :class="mergeFormUiClass('px-3 py-10 text-center text-muted', ui?.empty)"
224
- >
225
- {{ emptyLabel }}
230
+ <td :colspan="columns.length + (showActionsColumn ? 1 : 0)" class="p-0">
231
+ <div
232
+ :class="
233
+ mergeFormUiClass('sticky left-0 px-3 py-10 text-center text-muted', ui?.empty)
234
+ "
235
+ :style="pinnedStyle"
236
+ data-form-array-empty=""
237
+ >
238
+ {{ emptyLabel }}
239
+ </div>
226
240
  </td>
227
241
  </tr>
228
242
  </component>
@@ -230,22 +244,24 @@ function fieldLabel(field) {
230
244
  <tr>
231
245
  <td
232
246
  :colspan="columns.length + (showActionsColumn ? 1 : 0)"
233
- :class="mergeFormUiClass('border-t border-default p-1.5', ui?.addCell)"
247
+ :class="mergeFormUiClass('border-t border-default p-0', ui?.addCell)"
234
248
  >
235
- <button
236
- type="button"
237
- :class="
249
+ <div class="sticky left-0 p-1.5" :style="pinnedStyle">
250
+ <button
251
+ type="button"
252
+ :class="
238
253
  mergeFormUiClass(
239
254
  'flex w-full items-center justify-center gap-2 rounded-md border border-dashed border-default px-3 py-2 text-sm text-muted transition-colors hover:border-inverted/40 hover:bg-elevated/50 hover:text-default focus-visible:outline-2 focus-visible:outline-primary',
240
255
  ui?.add
241
256
  )
242
257
  "
243
- :data-form-array-add="path.join('.')"
244
- @click="addItem()"
245
- >
246
- <UIcon name="i-lucide-plus" class="size-4" aria-hidden="true" />
247
- <span>{{ addItemLabel }}</span>
248
- </button>
258
+ :data-form-array-add="path.join('.')"
259
+ @click="addItem()"
260
+ >
261
+ <UIcon name="i-lucide-plus" class="size-4" aria-hidden="true" />
262
+ <span>{{ addItemLabel }}</span>
263
+ </button>
264
+ </div>
249
265
  </td>
250
266
  </tr>
251
267
  </tfoot>
@@ -13,4 +13,9 @@ export interface FormDateProps {
13
13
  calendar?: FormDateFamilyCalendarOptions;
14
14
  }
15
15
  export type FormDateField<TContext = NonNullable<unknown>, TDeps = NonNullable<unknown>> = FormStatefulFieldBase<'date', Date | string | null, TContext, TDeps, FormDateProps>;
16
- export type DateFieldOutput = Date | string | NullableValue;
16
+ /** A date field submits its canonical `YYYY-MM-DD` value, or a `Date` with `outputFormat: 'date'`. */
17
+ export type DateFieldOutput<TField> = TField extends {
18
+ props: {
19
+ outputFormat: 'date';
20
+ };
21
+ } ? Date | NullableValue : string | NullableValue;
@@ -102,7 +102,13 @@ const placeholder = computed(() => {
102
102
  if (explicitlyConfigured !== void 0) {
103
103
  return fieldPlaceholder.value;
104
104
  }
105
- return manualInput.value.placeholder ?? dateManualPlaceholder(manualFormat.value, isRange.value);
105
+ return manualInput.value.placeholder ?? dateManualPlaceholder(manualFormat.value, isRange.value, {
106
+ day: t("form.fields.date.tokens.day"),
107
+ hour: t("form.fields.date.tokens.hour"),
108
+ minute: t("form.fields.date.tokens.minute"),
109
+ month: t("form.fields.date.tokens.month"),
110
+ year: t("form.fields.date.tokens.year")
111
+ });
106
112
  });
107
113
  const previewValue = computed(() => {
108
114
  if (!isRange.value) {
@@ -13,7 +13,14 @@ export declare function isDateFamilyRange(type: FormDateFamilyType): type is "da
13
13
  export declare function hasDateFamilyTime(type: FormDateFamilyType): type is "datetime" | "datetimerange";
14
14
  export declare function dateFamilyCalendarType(type: FormDateFamilyType): 'date' | 'month' | 'year';
15
15
  export declare function defaultDateManualFormat(type: FormDateFamilyType, locale: string): string;
16
- export declare function dateManualPlaceholder(format: string, range: boolean): string;
16
+ export interface DateManualPlaceholderTokens {
17
+ day: string;
18
+ month: string;
19
+ year: string;
20
+ hour: string;
21
+ minute: string;
22
+ }
23
+ export declare function dateManualPlaceholder(format: string, range: boolean, tokens?: DateManualPlaceholderTokens): string;
17
24
  export declare function applyDateManualMask(value: string, format: string, range: boolean): string;
18
25
  export declare function parseDateManualValue(value: string, format: string, type: FormDateFamilyType): string | readonly [string, string] | undefined;
19
26
  export declare function formatDateManualValue(values: readonly [string, string], format: string, type: FormDateFamilyType): string;
@@ -25,8 +25,16 @@ export function defaultDateManualFormat(type, locale) {
25
25
  const dateFormat = locale.toLowerCase().startsWith("fr") ? "dd/MM/yyyy" : "MM/dd/yyyy";
26
26
  return hasDateFamilyTime(type) ? `${dateFormat} HH:mm` : dateFormat;
27
27
  }
28
- export function dateManualPlaceholder(format, range) {
29
- const placeholder = format.toLowerCase();
28
+ const DATE_PLACEHOLDER_TOKEN = /yyyy|MM|dd|HH|mm/gu;
29
+ export function dateManualPlaceholder(format, range, tokens) {
30
+ const names = {
31
+ HH: tokens?.hour,
32
+ MM: tokens?.month,
33
+ dd: tokens?.day,
34
+ mm: tokens?.minute,
35
+ yyyy: tokens?.year
36
+ };
37
+ const placeholder = format.replaceAll(DATE_PLACEHOLDER_TOKEN, (token) => names[token] ?? token.toLowerCase()).toLowerCase();
30
38
  return range ? `${placeholder} \u2013 ${placeholder}` : placeholder;
31
39
  }
32
40
  export function applyDateManualMask(value, format, range) {
@@ -2,4 +2,11 @@ import type { FormValue } from '../../types/index.js';
2
2
  import type { FormStatefulFieldBase } from '../../types/field-base.js';
3
3
  import type { FieldDefaultValue, FallbackNever } from '../../types/field-output-utils.js';
4
4
  export type FormHiddenField<TContext = NonNullable<unknown>, TDeps = NonNullable<unknown>> = Pick<FormStatefulFieldBase<'hidden', FormValue, TContext, TDeps>, 'key' | 'type' | 'default' | 'dependencies' | 'condition' | 'validation' | 'transform' | 'submit' | 'ignore' | 'watch' | 'watchOptions' | 'onDependencyChange' | 'onRendered' | 'stateEffect'>;
5
- export type HiddenFieldOutput<TField> = FallbackNever<FieldDefaultValue<TField>, FormValue>;
5
+ type TransformInputResult<TField> = TField extends {
6
+ transform: {
7
+ input: (...params: never[]) => infer TResult;
8
+ };
9
+ } ? TResult : never;
10
+ /** A hidden field holds its default, or what its `transform.input` makes of the input. */
11
+ export type HiddenFieldOutput<TField> = FallbackNever<FieldDefaultValue<TField>, FallbackNever<TransformInputResult<TField>, FormValue>>;
12
+ export {};
@@ -1,209 +1,88 @@
1
1
  <script setup>
2
- import UAlert from "@nuxt/ui/components/Alert.vue";
3
- import UButton from "@nuxt/ui/components/Button.vue";
4
2
  import UFileUpload from "@nuxt/ui/components/FileUpload.vue";
5
- import UProgress from "@nuxt/ui/components/Progress.vue";
6
- import { computed, onScopeDispose, ref } from "vue";
3
+ import { computed, onScopeDispose, shallowRef, useTemplateRef, watch } from "vue";
7
4
  import { useUiToolsLocale } from "../../../i18n/use-locale";
8
5
  import FormFieldShell from "../../components/renderer/form-field-shell.vue";
9
- import FormFilePreview from "../../components/utils/form-file-preview.vue";
10
6
  import { useFieldControl } from "../../composables/use-field-control";
11
- import { isObject, isString, isUndefined } from "../../utils/predicate";
12
7
  import { resolveFormText } from "../../utils/text";
8
+ import UploadItem from "./upload-item.vue";
9
+ import { useUploadItems } from "./use-upload-items";
13
10
  const props = defineProps({
14
11
  field: { type: Object, required: true },
15
12
  path: { type: Array, required: true }
16
13
  });
17
14
  const { t } = useUiToolsLocale();
18
- const { fieldProps, form, controlProps, disabled, handleBlur, params, validationPending } = useFieldControl(
15
+ const { api, fieldProps, form, controlProps, disabled, handleBlur, validationPending } = useFieldControl(
19
16
  () => props.field,
20
17
  () => props.path,
21
- { omit: ["dropzoneLabel", "dropzoneDescription", "autoUpload"] }
18
+ { omit: ["dropzoneLabel", "dropzoneDescription", "autoUpload", "max", "preview"] }
22
19
  );
23
- const selectedFiles = ref(null);
24
- const uploadPending = ref(false);
25
- const uploadError = ref(null);
26
- const uploadProgress = ref(null);
27
- const uploadRun = ref(0);
28
- const uploadedValue = computed(() => {
29
- const value = form.getValue(props.path);
30
- if (isString(value) || value === null) {
31
- return value;
32
- }
33
- if (Array.isArray(value) && value.every((item) => isString(item))) {
34
- return value;
35
- }
36
- if (Array.isArray(value) && value.every(isFormObject)) {
37
- return value;
38
- }
39
- return isFormObject(value) ? value : null;
40
- });
41
- const files = computed(() => {
42
- if (Array.isArray(selectedFiles.value)) {
43
- return selectedFiles.value;
44
- }
45
- return selectedFiles.value ? [selectedFiles.value] : [];
46
- });
47
- const openDialog = ref(null);
48
- const hasSingleFile = computed(() => {
49
- const { value } = selectedFiles;
50
- return !fieldProps.value.multiple && value !== null && !Array.isArray(value);
51
- });
52
- const dropzoneUi = computed(() => ({
53
- ...controlProps.value.ui,
54
- base: hasSingleFile.value ? "hidden" : controlProps.value.ui?.base,
55
- file: hasSingleFile.value ? "relative inset-auto p-0" : controlProps.value.ui?.file,
56
- files: hasSingleFile.value ? "w-full" : "mb-2 gap-2"
57
- }));
58
- function captureOpen(open) {
59
- openDialog.value = open;
60
- return "";
61
- }
62
- function replaceFile() {
63
- openDialog.value?.();
64
- }
65
- async function uploadFiles() {
66
- const run = uploadRun.value + 1;
67
- uploadRun.value = run;
68
- uploadError.value = null;
69
- if (!files.value.length) {
70
- return;
71
- }
72
- uploadPending.value = true;
73
- try {
74
- uploadProgress.value = null;
75
- const value = await props.field.upload.handler({
76
- ...params.value,
77
- files: files.value,
78
- onProgress: (percent) => {
79
- if (uploadRun.value === run) {
80
- uploadProgress.value = Math.min(100, Math.max(0, percent));
81
- }
82
- }
83
- });
84
- if (uploadRun.value !== run) {
85
- return;
86
- }
87
- form.setValue(props.path, value);
88
- selectedFiles.value = null;
89
- } catch (error) {
90
- if (uploadRun.value !== run) {
91
- return;
92
- }
93
- uploadError.value = error instanceof Error ? error.message : t("form.fields.upload.failed");
94
- } finally {
95
- if (uploadRun.value === run) {
96
- uploadPending.value = false;
97
- uploadProgress.value = null;
98
- }
99
- }
100
- }
101
- async function removeUpload(value) {
102
- uploadPending.value = true;
103
- try {
104
- await props.field.upload.onDelete?.({
105
- ...params.value,
106
- value: isUndefined(value) ? uploadedValue.value : value
107
- });
108
- form.setValue(props.path, null);
109
- selectedFiles.value = null;
110
- } finally {
111
- uploadPending.value = false;
112
- }
113
- }
114
- async function cancelUpload() {
115
- uploadRun.value += 1;
116
- uploadPending.value = false;
117
- }
118
- async function retryUpload() {
119
- await uploadFiles();
120
- }
121
- const unregisterUpload = form.registerFieldUpload(props.path, {
122
- cancel: cancelUpload,
123
- remove: removeUpload,
124
- retry: retryUpload,
125
- start: uploadFiles
126
- });
20
+ const uploads = useUploadItems(
21
+ () => props.field,
22
+ () => props.path
23
+ );
24
+ const picker = useTemplateRef("picker");
25
+ const picked = shallowRef(null);
26
+ const compact = computed(
27
+ () => !uploads.multiple.value && fieldProps.value.variant === "button"
28
+ );
29
+ const unregisterUpload = form.registerFieldUpload(props.path, uploads.runtime);
127
30
  onScopeDispose(unregisterUpload);
128
- function handleFileChange() {
31
+ watch(picked, (value) => {
32
+ const files = Array.isArray(value) ? value : value ? [value] : [];
33
+ if (files.length === 0) return;
34
+ picked.value = uploads.multiple.value ? [] : null;
129
35
  void handleBlur();
130
- if (fieldProps.value.autoUpload ?? false) {
131
- void uploadFiles();
36
+ uploads.select(files);
37
+ });
38
+ watch(
39
+ () => uploads.runtime.pending(),
40
+ (pending) => {
41
+ if (!pending) api.value.validation.clearError();
132
42
  }
133
- }
134
- function isFormObject(value) {
135
- return isObject(value) && value !== null && !Array.isArray(value);
43
+ );
44
+ function replace() {
45
+ picker.value?.inputRef?.click();
136
46
  }
137
47
  </script>
138
48
 
139
49
  <template>
140
50
  <FormFieldShell :field="field" :path="path">
141
- <div class="grid gap-3">
51
+ <div class="grid gap-2" :data-form-upload-busy="uploads.busy.value || void 0">
52
+ <ul
53
+ v-if="uploads.items.value.length"
54
+ class="m-0 grid list-none gap-2 p-0"
55
+ data-form-upload-list=""
56
+ >
57
+ <UploadItem
58
+ v-for="item in uploads.items.value"
59
+ :key="item.key"
60
+ :item="item"
61
+ :compact="compact"
62
+ :disabled="disabled"
63
+ :replaceable="!uploads.multiple.value"
64
+ @open="uploads.open(item.key)"
65
+ @start="uploads.start(item.key)"
66
+ @cancel="uploads.cancel(item.key)"
67
+ @retry="uploads.retry(item.key)"
68
+ @remove="uploads.remove(item.key)"
69
+ @replace="replace"
70
+ />
71
+ </ul>
142
72
  <UFileUpload
143
- v-model="selectedFiles"
73
+ ref="picker"
74
+ v-model="picked"
144
75
  v-bind="controlProps"
145
- class="w-full"
76
+ :class="uploads.canSelect.value ? 'w-full' : 'hidden'"
146
77
  :accept="fieldProps.accept"
147
- :multiple="fieldProps.multiple"
148
- :disabled="disabled || uploadPending || validationPending"
78
+ :multiple="uploads.multiple.value"
79
+ :disabled="disabled || validationPending"
149
80
  :label="resolveFormText(fieldProps.dropzoneLabel) ?? t('form.fields.file.drop')"
150
81
  :description="resolveFormText(fieldProps.dropzoneDescription)"
151
82
  :icon="fieldProps.icon"
152
83
  :variant="fieldProps.variant"
153
84
  :layout="fieldProps.layout"
154
- :preview="fieldProps.preview"
155
- position="outside"
156
- :ui="dropzoneUi"
157
- @change="handleFileChange"
158
- >
159
- <template #files-top="{ open }">
160
- <span hidden>{{ captureOpen(open) }}</span>
161
- </template>
162
- <template #file="{ file, index, removeFile }">
163
- <FormFilePreview
164
- :file="file"
165
- :index="index"
166
- :disabled="disabled"
167
- :remove-file="removeFile"
168
- :replace="fieldProps.multiple ? void 0 : replaceFile"
169
- />
170
- </template>
171
- </UFileUpload>
172
- <UProgress
173
- v-if="uploadPending && uploadProgress !== null"
174
- :model-value="uploadProgress"
175
- size="sm"
176
- data-form-upload-progress=""
177
- />
178
-
179
- <div class="flex flex-wrap items-center gap-2">
180
- <UButton
181
- icon="i-lucide-upload"
182
- :loading="uploadPending"
183
- :disabled="disabled || !files.length"
184
- @click="uploadFiles"
185
- >
186
- {{ t("form.fields.upload.upload") }}
187
- </UButton>
188
- <UButton
189
- v-if="uploadedValue"
190
- icon="i-lucide-trash-2"
191
- color="neutral"
192
- variant="soft"
193
- :loading="uploadPending"
194
- :disabled="disabled"
195
- @click="removeUpload"
196
- >
197
- {{ t("form.fields.upload.remove") }}
198
- </UButton>
199
- </div>
200
-
201
- <UAlert
202
- v-if="uploadError"
203
- color="error"
204
- variant="soft"
205
- icon="i-lucide-circle-alert"
206
- :description="uploadError"
85
+ :preview="false"
207
86
  />
208
87
  </div>
209
88
  </FormFieldShell>
@@ -2,30 +2,95 @@ import type { FormValue } from '../../types/index.js';
2
2
  import type { FormFieldCallbackParams } from '../../types/callbacks.js';
3
3
  import type { FormStatefulFieldBase } from '../../types/field-base.js';
4
4
  import type { FieldProps, NullableValue } from '../../types/field-output-utils.js';
5
- import type { FormObject, FormText } from '../../types/utils.js';
5
+ import type { FormMaybePromise, FormObject, FormText } from '../../types/utils.js';
6
+ /**
7
+ * One stored upload value: the URL string or the object returned by the upload handler.
8
+ */
9
+ export type FormUploadValue = string | FormObject;
6
10
  export interface FormUploadCallbackParams<TContext = NonNullable<unknown>, TDeps = NonNullable<unknown>> extends FormFieldCallbackParams<TContext, TDeps> {
11
+ /** Files handled by this call. The field calls the handler once per selected file. */
7
12
  files: readonly File[];
13
+ /** Reports the progress of this upload, from `0` to `100`. */
8
14
  onProgress: (percent: number) => void;
15
+ /** Aborted when the user cancels the upload or removes the file while it uploads. */
16
+ signal: AbortSignal;
17
+ }
18
+ /**
19
+ * Uploads the selected file and returns the value stored in the form.
20
+ * When an array is returned for a single file, its first item is stored.
21
+ */
22
+ export type FormUploadHandler<TContext = NonNullable<unknown>, TDeps = NonNullable<unknown>> = (params: FormUploadCallbackParams<TContext, TDeps>) => Promise<FormUploadValue | readonly FormUploadValue[] | null>;
23
+ /**
24
+ * Display data for a stored upload value, used to render files that are already set.
25
+ */
26
+ export interface FormUploadResolvedFile {
27
+ /** File name shown in the list. */
28
+ name: string;
29
+ /** Size in bytes, shown next to the file type when provided. */
30
+ size?: number | null;
31
+ /** MIME type, used for the file icon and image previews. */
32
+ type?: string | null;
33
+ /** Link opened by the open action when no `upload.open` hook is configured. */
34
+ url?: string | null;
35
+ /** Image preview source. Defaults to `url` for image types. */
36
+ thumbnail?: string | null;
37
+ /** Replaces the file type and size line under the name, for example with an upload date. */
38
+ description?: string | null;
39
+ /**
40
+ * Shows the open action for this file. Defaults to `true` when an `upload.open` hook or a
41
+ * `url` is available.
42
+ */
43
+ openable?: boolean;
44
+ }
45
+ export interface FormUploadValueParams<TContext = NonNullable<unknown>, TDeps = NonNullable<unknown>> extends FormFieldCallbackParams<TContext, TDeps> {
46
+ /** The stored value being resolved or opened. */
47
+ value: FormUploadValue;
48
+ }
49
+ export interface FormUploadOpenParams<TContext = NonNullable<unknown>, TDeps = NonNullable<unknown>> extends FormUploadValueParams<TContext, TDeps> {
50
+ /** Display data resolved for the stored value. */
51
+ file: FormUploadResolvedFile;
9
52
  }
10
- export type FormUploadHandler<TContext = NonNullable<unknown>, TDeps = NonNullable<unknown>> = (params: FormUploadCallbackParams<TContext, TDeps>) => Promise<string | FormObject | readonly string[] | readonly FormObject[] | null>;
11
53
  export interface FormUploadDeleteParams<TContext = NonNullable<unknown>, TDeps = NonNullable<unknown>> extends FormFieldCallbackParams<TContext, TDeps> {
12
54
  value: FormValue;
13
55
  }
14
56
  export interface FormUploadProps {
15
57
  multiple?: boolean;
58
+ /** Maximum number of stored and pending files when `multiple` is enabled. */
59
+ max?: number;
16
60
  accept?: string;
61
+ /** Uploads files as soon as they are selected. Defaults to `true`. */
17
62
  autoUpload?: boolean;
18
63
  dropzoneLabel?: FormText;
19
64
  dropzoneDescription?: FormText;
20
65
  icon?: string | false;
66
+ /** `button` renders a compact picker, suited to table cells. Ignored when `multiple` is set. */
21
67
  variant?: 'area' | 'button';
22
68
  layout?: 'list' | 'grid';
69
+ /** Shows image thumbnails for image files. Defaults to `true`. */
23
70
  preview?: boolean;
24
71
  }
25
- export interface FormUploadField<TContext = NonNullable<unknown>, TDeps = NonNullable<unknown>> extends FormStatefulFieldBase<'upload', string | FormObject | readonly string[] | readonly FormObject[] | null, TContext, TDeps, FormUploadProps> {
72
+ export interface FormUploadField<TContext = NonNullable<unknown>, TDeps = NonNullable<unknown>> extends FormStatefulFieldBase<'upload', FormUploadValue | readonly FormUploadValue[] | null, TContext, TDeps, FormUploadProps> {
26
73
  output: 'url' | 'object';
27
74
  upload: {
28
75
  handler: FormUploadHandler<TContext, TDeps>;
76
+ /**
77
+ * Resolves display data for a stored value so files that are already set render with
78
+ * their name, size, and preview. Without it, string values use their last URL segment as
79
+ * the name and the value as the link; object values read their `name`, `size`, `type`,
80
+ * `url`, and `thumbnail` keys.
81
+ *
82
+ * @example
83
+ * resolve: async ({ value }) => {
84
+ * const file = await api.files.show(String(value))
85
+ * return { name: file.name, size: file.size, type: file.mimeType }
86
+ * }
87
+ */
88
+ resolve?: (params: FormUploadValueParams<TContext, TDeps>) => FormMaybePromise<FormUploadResolvedFile | null>;
89
+ /**
90
+ * Opens a stored file, for example through a short-lived download link.
91
+ * Defaults to opening the resolved `url` in a new tab.
92
+ */
93
+ open?: (params: FormUploadOpenParams<TContext, TDeps>) => FormMaybePromise<void>;
29
94
  onDelete?: (params: FormUploadDeleteParams<TContext, TDeps>) => Promise<void> | void;
30
95
  };
31
96
  }
@@ -0,0 +1,24 @@
1
+ import type { FormUploadItem } from './use-upload-items.js';
2
+ type __VLS_Props = {
3
+ item: FormUploadItem;
4
+ disabled?: boolean;
5
+ compact?: boolean;
6
+ replaceable?: boolean;
7
+ };
8
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
9
+ replace: () => any;
10
+ retry: () => any;
11
+ start: () => any;
12
+ open: () => any;
13
+ remove: () => any;
14
+ cancel: () => any;
15
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
16
+ onReplace?: (() => any) | undefined;
17
+ onRetry?: (() => any) | undefined;
18
+ onStart?: (() => any) | undefined;
19
+ onOpen?: (() => any) | undefined;
20
+ onRemove?: (() => any) | undefined;
21
+ onCancel?: (() => any) | undefined;
22
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
23
+ declare const _default: typeof __VLS_export;
24
+ export default _default;