nuxt-ui-tools 1.7.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) 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/utils/search-query-input.d.vue.ts +5 -1
  158. package/dist/runtime/table/components/utils/search-query-input.vue +17 -3
  159. package/dist/runtime/table/components/utils/search-query-input.vue.d.ts +5 -1
  160. package/package.json +56 -29
  161. package/dist/runtime/form/components/renderer/form-field-error.vue +0 -16
@@ -0,0 +1,247 @@
1
+ import { defineAsyncComponent } from "vue";
2
+ import { resolveFilePreviewText } from "./format.js";
3
+ export function defineFilePreviewRenderer(renderer) {
4
+ return renderer;
5
+ }
6
+ export function matchFilePreview(rule) {
7
+ return (file) => {
8
+ const { mime, extension } = file;
9
+ if (mime && rule.mimes?.some((entry) => entry.endsWith("/") ? mime.startsWith(entry) : mime === entry))
10
+ return true;
11
+ return Boolean(extension && rule.extensions?.includes(extension));
12
+ };
13
+ }
14
+ const MEDIA_MODE = "fullscreen md:modal";
15
+ const DOCUMENT_MODE = "fullscreen md:drawer";
16
+ export const fallbackFilePreviewRenderer = defineFilePreviewRenderer({
17
+ compact: true,
18
+ component: () => import("../components/renderers/fallback-renderer.vue"),
19
+ icon: "i-lucide-file",
20
+ kind: "other",
21
+ mode: MEDIA_MODE
22
+ });
23
+ export const builtInFilePreviewRenderers = [
24
+ defineFilePreviewRenderer({
25
+ component: () => import("../components/renderers/pdf-renderer.vue"),
26
+ icon: "i-lucide-file-text",
27
+ kind: "pdf",
28
+ match: matchFilePreview({ extensions: ["pdf"], mimes: ["application/pdf"] }),
29
+ mode: DOCUMENT_MODE
30
+ }),
31
+ defineFilePreviewRenderer({
32
+ component: () => import("../components/renderers/image-renderer.vue"),
33
+ icon: "i-lucide-file-image",
34
+ kind: "image",
35
+ match: matchFilePreview({
36
+ extensions: [
37
+ "apng",
38
+ "avif",
39
+ "bmp",
40
+ "gif",
41
+ "heic",
42
+ "heif",
43
+ "ico",
44
+ "jpeg",
45
+ "jpg",
46
+ "png",
47
+ "svg",
48
+ "webp"
49
+ ],
50
+ mimes: ["image/"]
51
+ }),
52
+ mode: MEDIA_MODE
53
+ }),
54
+ defineFilePreviewRenderer({
55
+ component: () => import("../components/renderers/video-renderer.vue"),
56
+ icon: "i-lucide-file-video",
57
+ kind: "video",
58
+ match: matchFilePreview({
59
+ extensions: ["m4v", "mov", "mp4", "ogv", "webm"],
60
+ mimes: ["video/"]
61
+ }),
62
+ mode: MEDIA_MODE
63
+ }),
64
+ defineFilePreviewRenderer({
65
+ compact: true,
66
+ component: () => import("../components/renderers/audio-renderer.vue"),
67
+ icon: "i-lucide-file-audio",
68
+ kind: "audio",
69
+ match: matchFilePreview({
70
+ extensions: ["aac", "flac", "m4a", "mp3", "oga", "ogg", "opus", "wav", "weba"],
71
+ mimes: ["audio/"]
72
+ }),
73
+ mode: MEDIA_MODE
74
+ }),
75
+ defineFilePreviewRenderer({
76
+ component: () => import("../components/renderers/csv-renderer.vue"),
77
+ icon: "i-lucide-file-spreadsheet",
78
+ kind: "csv",
79
+ match: matchFilePreview({
80
+ extensions: ["csv", "tsv"],
81
+ mimes: ["text/csv", "text/tab-separated-values"]
82
+ }),
83
+ mode: DOCUMENT_MODE
84
+ }),
85
+ defineFilePreviewRenderer({
86
+ component: () => import("../components/renderers/markdown-renderer.vue"),
87
+ icon: "i-lucide-file-text",
88
+ kind: "markdown",
89
+ match: matchFilePreview({
90
+ extensions: ["markdown", "md", "mdx"],
91
+ mimes: ["text/markdown", "text/x-markdown"]
92
+ }),
93
+ mode: DOCUMENT_MODE
94
+ }),
95
+ defineFilePreviewRenderer({
96
+ component: () => import("../components/renderers/text-renderer.vue"),
97
+ icon: "i-lucide-file-code",
98
+ kind: "text",
99
+ match: matchFilePreview({
100
+ extensions: [
101
+ "c",
102
+ "cjs",
103
+ "conf",
104
+ "cpp",
105
+ "cs",
106
+ "css",
107
+ "env",
108
+ "go",
109
+ "graphql",
110
+ "h",
111
+ "htm",
112
+ "html",
113
+ "ini",
114
+ "java",
115
+ "js",
116
+ "json",
117
+ "jsonc",
118
+ "jsx",
119
+ "kt",
120
+ "log",
121
+ "mjs",
122
+ "php",
123
+ "py",
124
+ "rb",
125
+ "rs",
126
+ "scss",
127
+ "sh",
128
+ "sql",
129
+ "srt",
130
+ "swift",
131
+ "toml",
132
+ "ts",
133
+ "tsx",
134
+ "txt",
135
+ "vtt",
136
+ "vue",
137
+ "xml",
138
+ "yaml",
139
+ "yml"
140
+ ],
141
+ mimes: [
142
+ "text/",
143
+ "application/json",
144
+ "application/ld+json",
145
+ "application/xml",
146
+ "application/javascript",
147
+ "application/x-yaml",
148
+ "application/yaml",
149
+ "application/sql"
150
+ ]
151
+ }),
152
+ mode: DOCUMENT_MODE
153
+ }),
154
+ defineFilePreviewRenderer({
155
+ compact: true,
156
+ component: () => import("../components/renderers/fallback-renderer.vue"),
157
+ icon: "i-lucide-file-type",
158
+ kind: "office",
159
+ match: matchFilePreview({
160
+ extensions: [
161
+ "doc",
162
+ "docx",
163
+ "key",
164
+ "numbers",
165
+ "odp",
166
+ "ods",
167
+ "odt",
168
+ "pages",
169
+ "ppt",
170
+ "pptx",
171
+ "rtf",
172
+ "xls",
173
+ "xlsx"
174
+ ],
175
+ mimes: [
176
+ "application/msword",
177
+ "application/rtf",
178
+ "application/vnd.ms-excel",
179
+ "application/vnd.ms-powerpoint",
180
+ "application/vnd.oasis.opendocument.presentation",
181
+ "application/vnd.oasis.opendocument.spreadsheet",
182
+ "application/vnd.oasis.opendocument.text",
183
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation",
184
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
185
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
186
+ ]
187
+ }),
188
+ mode: MEDIA_MODE
189
+ }),
190
+ defineFilePreviewRenderer({
191
+ compact: true,
192
+ component: () => import("../components/renderers/fallback-renderer.vue"),
193
+ icon: "i-lucide-file-archive",
194
+ kind: "archive",
195
+ match: matchFilePreview({
196
+ extensions: ["7z", "bz2", "gz", "rar", "tar", "tgz", "xz", "zip"],
197
+ mimes: [
198
+ "application/gzip",
199
+ "application/x-7z-compressed",
200
+ "application/x-bzip2",
201
+ "application/x-rar-compressed",
202
+ "application/x-tar",
203
+ "application/zip"
204
+ ]
205
+ }),
206
+ mode: MEDIA_MODE
207
+ }),
208
+ fallbackFilePreviewRenderer
209
+ ];
210
+ export function mergeFilePreviewRenderers(custom) {
211
+ const replaced = new Set(custom.map((renderer) => renderer.kind));
212
+ return [
213
+ ...custom,
214
+ ...builtInFilePreviewRenderers.filter((renderer) => !replaced.has(renderer.kind))
215
+ ];
216
+ }
217
+ export function detectFilePreviewKind(file, renderers) {
218
+ if (file.kind) return file.kind;
219
+ return renderers.find((renderer) => renderer.match?.(file))?.kind ?? "other";
220
+ }
221
+ export function findFilePreviewRenderer(kind, renderers) {
222
+ return renderers.find((renderer) => renderer.kind === kind) ?? renderers.find((renderer) => renderer.kind === "other") ?? fallbackFilePreviewRenderer;
223
+ }
224
+ const components = /* @__PURE__ */ new WeakMap();
225
+ export function filePreviewRendererComponent(renderer) {
226
+ const known = components.get(renderer);
227
+ if (known) return known;
228
+ const component = defineAsyncComponent(renderer.component);
229
+ components.set(renderer, component);
230
+ return component;
231
+ }
232
+ const BUILT_IN_KINDS = /* @__PURE__ */ new Set([
233
+ "archive",
234
+ "audio",
235
+ "csv",
236
+ "image",
237
+ "markdown",
238
+ "office",
239
+ "other",
240
+ "pdf",
241
+ "text",
242
+ "video"
243
+ ]);
244
+ export function filePreviewKindLabel(renderer, t) {
245
+ if (renderer.label) return resolveFilePreviewText(renderer.label);
246
+ return BUILT_IN_KINDS.has(renderer.kind) ? t(`filePreview.kinds.${renderer.kind}`) : renderer.kind;
247
+ }
@@ -1,7 +1,9 @@
1
1
  /**
2
2
  * Header of a form page: the schema `header` (eyebrow, title, description), an unsaved-changes
3
3
  * badge while a form with `controls.dirtyCheck` is modified, and the page actions. It stays
4
- * pinned while the page scrolls on wide layouts, and sections scroll to just below it.
4
+ * pinned while the page scrolls on wide layouts, and sections scroll to just below it. The badge
5
+ * keeps its place while the form is unmodified, hidden, and does not grow its line, so the first
6
+ * edit neither shifts the page nor resizes the pinned header.
5
7
  */
6
8
  type __VLS_Slots = {
7
9
  /** Before the heading, e.g. an avatar of the record. */
@@ -70,7 +70,7 @@ function headerText(config, key) {
70
70
  <slot name="title">{{ title }}</slot>
71
71
  </h1>
72
72
  <div
73
- v-if="description || unsaved || $slots.description"
73
+ v-if="description || page.dirtyCheck.value || $slots.description"
74
74
  :class="
75
75
  mergeFormUiClass(
76
76
  'flex flex-wrap items-center gap-2.5 text-[13.5px] text-muted',
@@ -82,12 +82,12 @@ function headerText(config, key) {
82
82
  <span v-if="description">{{ description }}</span>
83
83
  </slot>
84
84
  <UBadge
85
- v-if="unsaved"
85
+ v-if="page.dirtyCheck.value"
86
86
  color="warning"
87
87
  variant="soft"
88
88
  :label="t('form.page.unsavedChanges')"
89
- :class="pageUi?.unsaved"
90
- data-form-page-unsaved
89
+ :class="mergeFormUiClass(unsaved ? '-my-1' : '-my-1 invisible', pageUi?.unsaved)"
90
+ :data-form-page-unsaved="unsaved ? '' : void 0"
91
91
  />
92
92
  </div>
93
93
  </div>
@@ -1,7 +1,9 @@
1
1
  /**
2
2
  * Header of a form page: the schema `header` (eyebrow, title, description), an unsaved-changes
3
3
  * badge while a form with `controls.dirtyCheck` is modified, and the page actions. It stays
4
- * pinned while the page scrolls on wide layouts, and sections scroll to just below it.
4
+ * pinned while the page scrolls on wide layouts, and sections scroll to just below it. The badge
5
+ * keeps its place while the form is unmodified, hidden, and does not grow its line, so the first
6
+ * edit neither shifts the page nor resizes the pinned header.
5
7
  */
6
8
  type __VLS_Slots = {
7
9
  /** Before the heading, e.g. an avatar of the record. */
@@ -75,6 +75,7 @@ function select(event, key) {
75
75
  <nav
76
76
  :aria-labelledby="page.navigationTitle.value || $slots.title ? titleId : void 0"
77
77
  :aria-label="page.navigationTitle.value || $slots.title ? void 0 : t('form.page.navigation')"
78
+ :style="{ '--nut-form-page-header': `${page.stickyOffset.value}px` }"
78
79
  :class="
79
80
  mergeFormUiClass(
80
81
  'flex min-w-0 flex-col gap-3.5 @3xl/form-page:sticky @3xl/form-page:top-[calc(var(--nut-form-page-header,0px)+var(--nut-form-page-gap,24px))]',
@@ -126,7 +127,7 @@ function select(event, key) {
126
127
  :data-dirty="dirtyShown(section) || void 0"
127
128
  :class="
128
129
  mergeFormUiClass(
129
- 'group flex h-8 min-w-0 items-center gap-2 rounded-md border border-default bg-default px-2.5 text-[12.5px] font-medium text-muted no-underline transition-colors hover:text-highlighted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary data-[active]:border-primary data-[active]:bg-primary/10 data-[active]:text-highlighted @3xl/form-page:h-9 @3xl/form-page:gap-2.5 @3xl/form-page:rounded-[7px] @3xl/form-page:border-transparent @3xl/form-page:bg-transparent @3xl/form-page:text-[13.5px] @3xl/form-page:hover:bg-elevated @3xl/form-page:data-[active]:border-transparent @3xl/form-page:data-[active]:bg-default @3xl/form-page:data-[active]:font-semibold @3xl/form-page:data-[active]:shadow-[inset_2px_0_0_var(--ui-primary)]',
130
+ 'group relative flex h-8 min-w-0 items-center gap-2 rounded-md border border-default bg-default px-2.5 text-[12.5px] font-medium text-muted no-underline transition-colors hover:text-highlighted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary data-[active]:border-primary data-[active]:bg-primary/10 data-[active]:text-highlighted @3xl/form-page:h-9 @3xl/form-page:gap-2.5 @3xl/form-page:rounded-[7px] @3xl/form-page:border-transparent @3xl/form-page:bg-transparent @3xl/form-page:text-[13.5px] @3xl/form-page:hover:bg-elevated @3xl/form-page:data-[active]:border-transparent @3xl/form-page:data-[active]:bg-default @3xl/form-page:data-[active]:font-semibold @3xl/form-page:data-[active]:shadow-[inset_2px_0_0_var(--ui-primary)]',
130
131
  pageUi?.navigationItem
131
132
  )
132
133
  "
@@ -45,9 +45,12 @@ onBeforeUnmount(() => unregister?.());
45
45
  :data-form-page-section="section.key"
46
46
  :data-state="section.status"
47
47
  :data-dirty="dirty || void 0"
48
+ :style="{
49
+ scrollMarginTop: `calc(${page.stickyOffset.value}px + var(--nut-form-page-gap, 24px))`
50
+ }"
48
51
  :class="
49
52
  mergeFormUiClass(
50
- 'flex min-w-0 scroll-mt-[calc(var(--nut-form-page-header,0px)+var(--nut-form-page-gap,24px))] flex-col gap-[18px] rounded-xl border border-default bg-default px-5 pt-5 pb-6 transition-[border-color,box-shadow] duration-200 @3xl/form-page:px-6 @3xl/form-page:pt-[22px] data-[dirty]:border-primary data-[dirty]:ring-4 data-[dirty]:ring-primary/20',
53
+ 'flex min-w-0 flex-col gap-[18px] rounded-xl border border-default bg-default px-5 pt-5 pb-6 transition-[border-color,box-shadow] duration-200 @3xl/form-page:px-6 @3xl/form-page:pt-[22px] data-[dirty]:border-primary data-[dirty]:ring-4 data-[dirty]:ring-primary/20 data-[dirty]:ring-inset',
51
54
  pageUi?.section
52
55
  )
53
56
  "
@@ -72,7 +75,12 @@ onBeforeUnmount(() => unregister?.());
72
75
  </h2>
73
76
  <p
74
77
  v-if="section.description"
75
- :class="mergeFormUiClass('m-0 text-[13px] text-muted', pageUi?.sectionDescription)"
78
+ :class="
79
+ mergeFormUiClass(
80
+ 'order-last m-0 basis-full text-[13px] text-muted @3xl/form-page:order-none @3xl/form-page:basis-auto',
81
+ pageUi?.sectionDescription
82
+ )
83
+ "
76
84
  >
77
85
  {{ section.description }}
78
86
  </p>
@@ -84,20 +92,20 @@ onBeforeUnmount(() => unregister?.());
84
92
  :class="mergeFormUiClass('capitalize', pageUi?.sectionOptional)"
85
93
  />
86
94
  <div
87
- v-if="dirty || $slots.actions"
88
- :class="mergeFormUiClass('ms-auto flex items-center gap-2', pageUi?.sectionActions)"
95
+ v-if="page.dirtyCheck.value || $slots.actions"
96
+ :class="mergeFormUiClass('ms-auto -my-1.5 flex items-center gap-2', pageUi?.sectionActions)"
89
97
  >
90
98
  <slot name="actions" :section />
91
99
  <UButton
92
- v-if="dirty"
100
+ v-if="page.dirtyCheck.value"
93
101
  type="button"
94
102
  color="neutral"
95
103
  variant="ghost"
96
104
  size="sm"
97
105
  icon="i-lucide-rotate-ccw"
98
106
  :label="t('form.page.resetSection')"
99
- :class="pageUi?.sectionReset"
100
- data-form-page-reset
107
+ :class="mergeFormUiClass(dirty ? void 0 : 'invisible', pageUi?.sectionReset)"
108
+ :data-form-page-reset="dirty ? '' : void 0"
101
109
  @click="page.resetSection(section.key)"
102
110
  />
103
111
  </div>
@@ -39,7 +39,6 @@ const pageUi = computed(() => root.formUi.ui.value.page?.ui);
39
39
  :class="
40
40
  mergeFormUiClass('@container/form-page h-full min-w-0 overflow-y-auto bg-muted', pageUi?.root)
41
41
  "
42
- :style="{ '--nut-form-page-header': `${page.stickyOffset.value}px` }"
43
42
  data-form-page
44
43
  @submit.prevent="root.submit"
45
44
  >
@@ -3,6 +3,7 @@ import UButton from "@nuxt/ui/components/Button.vue";
3
3
  import UIcon from "@nuxt/ui/components/Icon.vue";
4
4
  import { computed, onScopeDispose, watch } from "vue";
5
5
  import { useUiToolsLocale } from "../../../i18n/use-locale";
6
+ import { fileExtensionLabel, fileIconName, formatFileSize } from "../../../shared/utils/file";
6
7
  const props = defineProps({
7
8
  file: { type: null, required: true },
8
9
  index: { type: Number, required: true },
@@ -23,8 +24,10 @@ const preview = computed(() => {
23
24
  previewUrl = URL.createObjectURL(props.file);
24
25
  return previewUrl;
25
26
  });
26
- const icon = computed(() => fileIcon(props.file.type));
27
- const meta = computed(() => `${extension(props.file)} \xB7 ${formatSize(props.file.size)}`);
27
+ const icon = computed(() => fileIconName(props.file.type));
28
+ const meta = computed(
29
+ () => `${fileExtensionLabel(props.file)} \xB7 ${formatFileSize(props.file.size, code.value)}`
30
+ );
28
31
  watch(
29
32
  () => props.file,
30
33
  () => {
@@ -39,42 +42,6 @@ onScopeDispose(() => {
39
42
  URL.revokeObjectURL(previewUrl);
40
43
  }
41
44
  });
42
- function fileIcon(type) {
43
- if (type === "application/pdf") {
44
- return "i-lucide-file-text";
45
- }
46
- if (type.includes("spreadsheet") || type.includes("excel") || type === "text/csv") {
47
- return "i-lucide-file-spreadsheet";
48
- }
49
- if (type.includes("word") || type.includes("document")) {
50
- return "i-lucide-file-type";
51
- }
52
- if (type.includes("zip") || type.includes("compressed")) {
53
- return "i-lucide-file-archive";
54
- }
55
- return "i-lucide-file";
56
- }
57
- function extension(file) {
58
- const dot = file.name.lastIndexOf(".");
59
- return dot === -1 ? file.type || "\u2014" : file.name.slice(dot + 1).toUpperCase();
60
- }
61
- function formatSize(bytes) {
62
- const units = ["o", "Ko", "Mo", "Go"];
63
- let size = bytes;
64
- let unit = 0;
65
- while (size >= 1024 && unit < units.length - 1) {
66
- size /= 1024;
67
- unit += 1;
68
- }
69
- let digits = 0;
70
- if (unit > 0 && size < 10) {
71
- digits = 1;
72
- }
73
- const formatted = new Intl.NumberFormat(code.value, { maximumFractionDigits: digits }).format(
74
- size
75
- );
76
- return `${formatted} ${units[unit]}`;
77
- }
78
45
  </script>
79
46
 
80
47
  <template>
@@ -54,7 +54,7 @@ export declare function useFieldControl<TField extends FormField>(field: () => T
54
54
  selectedItems: import("vue").ComputedRef<readonly import("../utils/options.js").ResolvedFormOption[]>;
55
55
  setSearch: (term: string) => void;
56
56
  };
57
- params: import("vue").ComputedRef<import("../types/index.js").FormFieldCallbackParams<{}, {}, unknown, unknown>>;
57
+ params: import("vue").ComputedRef<import("../types/index.js").FormFieldCallbackParams<{}, Readonly<import("../../shared/index.js").GenericObject>, unknown, unknown>>;
58
58
  placeholder: import("vue").ComputedRef<string>;
59
59
  validationPending: import("vue").ComputedRef<boolean>;
60
60
  };
@@ -37,7 +37,9 @@ export function useFormPage(params) {
37
37
  const dirtyCheck = computed(() => getSchemaDirtyCheck(root.schema.value));
38
38
  const navigationTitle = computed(() => getFormPageNavigationTitle(root.schema.value));
39
39
  const remaining = computed(
40
- () => sections.value.filter((section) => !section.optional && section.status !== "complete").length
40
+ () => sections.value.filter(
41
+ (section) => section.optional ? section.missing > 0 || section.status === "invalid" : section.status !== "complete"
42
+ ).length
41
43
  );
42
44
  const modified = computed(() => sections.value.filter((section) => section.dirty).length);
43
45
  const stickyOffset = ref(0);
@@ -147,6 +147,9 @@ export function useFormRuntime(params) {
147
147
  validate
148
148
  });
149
149
  async function submitHandler(externalSubmitHandler) {
150
+ await uploadRegistry.settle();
151
+ for (const path of uploadRegistry.pendingPaths())
152
+ validation.setError(path, t("form.fields.upload.pending"), { blocking: true });
150
153
  const result = await submission.submitHandler(externalSubmitHandler);
151
154
  if (result.success) {
152
155
  state.rebaseline();
@@ -1,5 +1,7 @@
1
1
  import type { FormUploadRuntimeState } from '../types/index.js';
2
2
  export declare function useFormUploadRegistry(): {
3
3
  get: (path: readonly string[]) => FormUploadRuntimeState;
4
+ pendingPaths: () => (readonly string[])[];
4
5
  register: (path: readonly string[], state: FormUploadRuntimeState) => () => void;
6
+ settle: () => Promise<void>;
5
7
  };
@@ -3,15 +3,20 @@ export function useFormUploadRegistry() {
3
3
  const states = shallowReactive({});
4
4
  function register(path, state) {
5
5
  const key = path.join(".");
6
- states[key] = state;
6
+ const entry = { path, state };
7
+ states[key] = entry;
7
8
  return () => {
8
- if (states[key] === state) {
9
- delete states[key];
10
- }
9
+ if (states[key] === entry) delete states[key];
11
10
  };
12
11
  }
13
12
  function get(path) {
14
- return states[path.join(".")];
13
+ return states[path.join(".")]?.state;
15
14
  }
16
- return { get, register };
15
+ async function settle() {
16
+ await Promise.all(Object.values(states).map((entry) => entry.state.settle()));
17
+ }
18
+ function pendingPaths() {
19
+ return Object.values(states).filter((entry) => entry.state.pending()).map((entry) => entry.path);
20
+ }
21
+ return { get, pendingPaths, register, settle };
17
22
  }
@@ -527,7 +527,11 @@ function isAsyncValidator(rule) {
527
527
  }
528
528
  function resolveRegleStatusErrors(status) {
529
529
  const errors = readReactiveProperty(status, "$errors");
530
- return Array.isArray(errors) ? errors.map(String) : [];
530
+ if (Array.isArray(errors)) {
531
+ return errors.map(String);
532
+ }
533
+ const own = isRecord(errors) ? errors.$self : void 0;
534
+ return Array.isArray(own) ? own.map(String) : [];
531
535
  }
532
536
  function toReglePath(path, state) {
533
537
  const reglePath = [];
@@ -7,7 +7,7 @@ export declare function isArrayActionConfig(action: FormArrayAction | undefined)
7
7
  export declare function useFormArrayItems(field: () => FormArrayItemsField, path: () => readonly string[]): {
8
8
  actionParams: (index: number) => {
9
9
  ctx: {};
10
- deps: {};
10
+ deps: Readonly<import("../../../shared/index.js").GenericObject>;
11
11
  getOptions: (key: string) => readonly unknown[];
12
12
  getValue: (key: string) => unknown;
13
13
  index: number;
@@ -0,0 +1,15 @@
1
+ import type { FormField } from '../../types/index.js';
2
+ /**
3
+ * One editable cell of an array table. An invalid cell highlights its control through the Nuxt UI
4
+ * form field context and shows its message in a tooltip on hover, kept open while the cell has
5
+ * focus, instead of pushing text under the control. Only invalid cells mount a tooltip, so large
6
+ * tables stay cheap to render. The cell positions the visually hidden message, which would
7
+ * otherwise escape the table's scroll containers and stretch the document.
8
+ */
9
+ type __VLS_Props = {
10
+ field: FormField;
11
+ itemPath: readonly string[];
12
+ };
13
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
14
+ declare const _default: typeof __VLS_export;
15
+ export default _default;
@@ -0,0 +1,58 @@
1
+ <script setup>
2
+ import UFormField from "@nuxt/ui/components/FormField.vue";
3
+ import UIcon from "@nuxt/ui/components/Icon.vue";
4
+ import UTooltip from "@nuxt/ui/components/Tooltip.vue";
5
+ import { useElementHover, useFocusWithin } from "@vueuse/core";
6
+ import { computed, useTemplateRef } from "vue";
7
+ import FormFieldRenderer from "../../components/renderer/form-field-renderer.vue";
8
+ import { useFormRuntimeContext } from "../../composables/use-form-runtime";
9
+ import { useFormUi } from "../../composables/use-form-ui";
10
+ import { mergeFormUiClass } from "../../utils/ui";
11
+ const props = defineProps({
12
+ field: { type: null, required: true },
13
+ itemPath: { type: Array, required: true }
14
+ });
15
+ const form = useFormRuntimeContext();
16
+ const formUi = useFormUi();
17
+ const ui = computed(() => formUi.ui.value.arrayTable?.ui);
18
+ const control = useTemplateRef("control");
19
+ const hovered = useElementHover(control);
20
+ const { focused } = useFocusWithin(control);
21
+ const path = computed(() => [...props.itemPath, props.field.key]);
22
+ const error = computed(() => form.getFieldError(path.value));
23
+ </script>
24
+
25
+ <template>
26
+ <div
27
+ ref="control"
28
+ :class="mergeFormUiClass('relative flex w-full items-center [&>*]:w-full', ui?.control)"
29
+ :data-form-cell-invalid="error ? path.join('.') : void 0"
30
+ >
31
+ <UFormField
32
+ :name="path.join('.')"
33
+ :error="error"
34
+ :ui="{ root: 'w-full', container: 'mt-0', error: 'sr-only' }"
35
+ >
36
+ <FormFieldRenderer :field="field" :parent-path="itemPath" bare />
37
+ </UFormField>
38
+ </div>
39
+ <UTooltip
40
+ v-if="error"
41
+ :reference="control ?? void 0"
42
+ :open="hovered || focused"
43
+ :content="{ align: 'start', side: 'top', sideOffset: 6 }"
44
+ :ui="{
45
+ content: mergeFormUiClass(
46
+ 'h-auto max-w-80 items-start gap-1.5 bg-error px-2.5 py-1.5 text-xs text-inverted ring-error',
47
+ ui?.error
48
+ ),
49
+ arrow: 'fill-error'
50
+ }"
51
+ arrow
52
+ >
53
+ <template #content>
54
+ <UIcon name="i-lucide-circle-alert" class="mt-px size-3.5 shrink-0" aria-hidden="true" />
55
+ <span data-form-cell-error="">{{ error }}</span>
56
+ </template>
57
+ </UTooltip>
58
+ </template>
@@ -0,0 +1,15 @@
1
+ import type { FormField } from '../../types/index.js';
2
+ /**
3
+ * One editable cell of an array table. An invalid cell highlights its control through the Nuxt UI
4
+ * form field context and shows its message in a tooltip on hover, kept open while the cell has
5
+ * focus, instead of pushing text under the control. Only invalid cells mount a tooltip, so large
6
+ * tables stay cheap to render. The cell positions the visually hidden message, which would
7
+ * otherwise escape the table's scroll containers and stretch the document.
8
+ */
9
+ type __VLS_Props = {
10
+ field: FormField;
11
+ itemPath: readonly string[];
12
+ };
13
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
14
+ declare const _default: typeof __VLS_export;
15
+ export default _default;