nuxt-ui-tools 1.25.1 → 1.27.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 (49) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/file-preview/components/shell/file-preview-header.d.vue.ts +2 -0
  3. package/dist/runtime/file-preview/components/shell/file-preview-header.vue +13 -7
  4. package/dist/runtime/file-preview/components/shell/file-preview-header.vue.d.ts +2 -0
  5. package/dist/runtime/file-preview/components/shell/file-preview-shell.vue +6 -0
  6. package/dist/runtime/file-preview/components/shell/file-preview-stage.d.vue.ts +5 -3
  7. package/dist/runtime/file-preview/components/shell/file-preview-stage.vue +4 -10
  8. package/dist/runtime/file-preview/components/shell/file-preview-stage.vue.d.ts +5 -3
  9. package/dist/runtime/form/components/root/form.vue +1 -0
  10. package/dist/runtime/form/composables/use-field-options.js +3 -1
  11. package/dist/runtime/form/composables/use-form-overlay-controller.js +1 -1
  12. package/dist/runtime/form/composables/use-form-root.d.ts +3 -1
  13. package/dist/runtime/form/composables/use-form-root.js +26 -11
  14. package/dist/runtime/form/composables/use-form-runtime.js +14 -4
  15. package/dist/runtime/form/composables/use-form-validation.d.ts +5 -2
  16. package/dist/runtime/form/composables/use-form-validation.js +51 -5
  17. package/dist/runtime/form/fields/custom-component/component.vue +2 -3
  18. package/dist/runtime/form/fields/date-family/component.vue +7 -6
  19. package/dist/runtime/form/fields/date-family/utils.d.ts +5 -0
  20. package/dist/runtime/form/fields/date-family/utils.js +13 -0
  21. package/dist/runtime/form/fields/number/component.vue +11 -5
  22. package/dist/runtime/form/fields/phone-number/component.vue +20 -1
  23. package/dist/runtime/form/types/api.d.ts +9 -2
  24. package/dist/runtime/form/types/options.d.ts +4 -1
  25. package/dist/runtime/form/types/runtime.d.ts +10 -0
  26. package/dist/runtime/form/types/schema.d.ts +5 -1
  27. package/dist/runtime/form/utils/api.js +1 -0
  28. package/dist/runtime/i18n/locales/en.js +12 -3
  29. package/dist/runtime/i18n/locales/fr.js +12 -3
  30. package/dist/runtime/i18n/types.d.ts +16 -0
  31. package/dist/runtime/table/components/filters/tags/date-filter-tag.vue +4 -0
  32. package/dist/runtime/table/composables/use-filter-persistence.d.ts +14 -0
  33. package/dist/runtime/table/composables/use-filter-persistence.js +77 -0
  34. package/dist/runtime/table/composables/use-query-state.js +31 -26
  35. package/dist/runtime/table/composables/use-table-api.js +4 -0
  36. package/dist/runtime/table/composables/use-table-data.js +3 -2
  37. package/dist/runtime/table/composables/use-table-state.d.ts +1 -0
  38. package/dist/runtime/table/composables/use-table-state.js +13 -2
  39. package/dist/runtime/table/types/filters.d.ts +28 -0
  40. package/dist/runtime/table/types/table-api.d.ts +5 -0
  41. package/dist/runtime/table/utils/filter-persistence.d.ts +16 -0
  42. package/dist/runtime/table/utils/filter-persistence.js +42 -0
  43. package/dist/runtime/table/utils/filters/options.js +2 -1
  44. package/dist/runtime/table/utils/filters/preview/option-preview.js +4 -1
  45. package/dist/runtime/table/utils/index.d.ts +1 -0
  46. package/dist/runtime/table/utils/index.js +1 -0
  47. package/dist/runtime/table/utils/resolved-filters.d.ts +8 -0
  48. package/dist/runtime/table/utils/resolved-filters.js +6 -0
  49. package/package.json +1 -1
package/dist/module.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "configKey": "nuxtUiTools",
6
6
  "docs": "https://ui.nuxt.com/docs/getting-started/installation/nuxt",
7
7
  "name": "nuxt-ui-tools",
8
- "version": "1.25.1",
8
+ "version": "1.27.0",
9
9
  "builder": {
10
10
  "@nuxt/module-builder": "1.0.3",
11
11
  "unbuild": "unknown"
@@ -10,6 +10,8 @@ type __VLS_Props = {
10
10
  canExpand: boolean;
11
11
  expanded: boolean;
12
12
  details: boolean;
13
+ /** The file failed to load: its download and open-in-tab actions are hidden. */
14
+ unavailable: boolean;
13
15
  };
14
16
  declare var __VLS_20: {};
15
17
  type __VLS_Slots = {} & {
@@ -16,12 +16,13 @@ const props = defineProps({
16
16
  narrow: { type: Boolean, required: true },
17
17
  canExpand: { type: Boolean, required: true },
18
18
  expanded: { type: Boolean, required: true },
19
- details: { type: Boolean, required: true }
19
+ details: { type: Boolean, required: true },
20
+ unavailable: { type: Boolean, required: true }
20
21
  });
21
22
  const emit = defineEmits(["previous", "next", "close", "expand", "details", "download", "open", "action"]);
22
23
  const { t } = useUiToolsLocale();
23
24
  const name = computed(() => splitFilePreviewName(props.item.name || t("filePreview.untitled")));
24
- const downloadable = computed(() => props.item.file.download !== false);
25
+ const downloadable = computed(() => !props.unavailable && props.item.file.download !== false);
25
26
  const actions = computed(() => props.item.file.actions ?? []);
26
27
  const menu = computed(() => [
27
28
  [
@@ -33,11 +34,13 @@ const menu = computed(() => [
33
34
  onSelect: () => emit("download")
34
35
  }
35
36
  ] : [],
36
- {
37
- icon: "i-lucide-external-link",
38
- label: t("filePreview.openInNewTab"),
39
- onSelect: () => emit("open")
40
- }
37
+ ...props.unavailable ? [] : [
38
+ {
39
+ icon: "i-lucide-external-link",
40
+ label: t("filePreview.openInNewTab"),
41
+ onSelect: () => emit("open")
42
+ }
43
+ ]
41
44
  ],
42
45
  actions.value.map((action) => ({
43
46
  icon: action.icon,
@@ -124,11 +127,14 @@ const menu = computed(() => [
124
127
  v-if="downloadable"
125
128
  icon="i-lucide-download"
126
129
  :label="t('filePreview.download')"
130
+ data-file-preview-header-download=""
127
131
  @click="emit('download')"
128
132
  />
129
133
  <FilePreviewButton
134
+ v-if="!unavailable"
130
135
  icon="i-lucide-external-link"
131
136
  :label="t('filePreview.openInNewTab')"
137
+ data-file-preview-open-tab=""
132
138
  @click="emit('open')"
133
139
  />
134
140
  <span class="mx-1 h-5 w-px shrink-0 bg-(--ui-border)" aria-hidden="true" />
@@ -10,6 +10,8 @@ type __VLS_Props = {
10
10
  canExpand: boolean;
11
11
  expanded: boolean;
12
12
  details: boolean;
13
+ /** The file failed to load: its download and open-in-tab actions are hidden. */
14
+ unavailable: boolean;
13
15
  };
14
16
  declare var __VLS_20: {};
15
17
  type __VLS_Slots = {} & {
@@ -46,6 +46,7 @@ const stage = useTemplateRef("stage");
46
46
  const strip = useTemplateRef("strip");
47
47
  const showDetails = ref(false);
48
48
  const reported = shallowRef([]);
49
+ const failure = shallowRef(null);
49
50
  const files = computed(() => props.instance.files.value);
50
51
  const index = computed(() => props.instance.index.value);
51
52
  const loop = computed(() => props.instance.options.loop === true);
@@ -59,6 +60,9 @@ const itemRenderer = computed(
59
60
  );
60
61
  const kindLabel = computed(() => filePreviewKindLabel(itemRenderer.value, t));
61
62
  const source = useFilePreviewSource(() => display.value);
63
+ const unavailable = computed(
64
+ () => source.status.value === "error" || failure.value?.reason === "source"
65
+ );
62
66
  const many = computed(() => files.value.length > 1);
63
67
  const hasPrevious = computed(() => loop.value || index.value > 0);
64
68
  const hasNext = computed(() => loop.value || index.value < files.value.length - 1);
@@ -194,6 +198,7 @@ async function runAction(action) {
194
198
  :can-expand="canExpand"
195
199
  :expanded="expanded"
196
200
  :details="showDetails"
201
+ :unavailable="unavailable"
197
202
  @previous="instance.handle.previous"
198
203
  @next="instance.handle.next"
199
204
  @close="emit('close')"
@@ -227,6 +232,7 @@ async function runAction(action) {
227
232
  :kind-label="kindLabel"
228
233
  :arrows="arrows"
229
234
  @ready="reported = $event"
235
+ @failure="failure = $event"
230
236
  @previous="instance.handle.previous"
231
237
  @next="instance.handle.next"
232
238
  @download="download"
@@ -1,5 +1,5 @@
1
1
  import type { useFilePreviewSource } from '../../composables/use-file-preview-source.js';
2
- import type { FilePreviewContainer, FilePreviewDetail, FilePreviewItem, FilePreviewRendererDefinition } from '../../types/index.js';
2
+ import type { FilePreviewContainer, FilePreviewDetail, FilePreviewItem, FilePreviewRendererDefinition, FilePreviewRendererError } from '../../types/index.js';
3
3
  type __VLS_Props = {
4
4
  item: FilePreviewItem;
5
5
  display: FilePreviewItem;
@@ -12,20 +12,22 @@ type __VLS_Props = {
12
12
  next: boolean;
13
13
  } | null;
14
14
  };
15
- declare var __VLS_64: {};
15
+ declare var __VLS_57: {};
16
16
  type __VLS_Slots = {} & {
17
- default?: (props: typeof __VLS_64) => any;
17
+ default?: (props: typeof __VLS_57) => any;
18
18
  };
19
19
  declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
20
20
  previous: () => any;
21
21
  ready: (details: readonly FilePreviewDetail[]) => any;
22
22
  download: () => any;
23
23
  next: () => any;
24
+ failure: (error: FilePreviewRendererError | null) => any;
24
25
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
25
26
  onPrevious?: (() => any) | undefined;
26
27
  onReady?: ((details: readonly FilePreviewDetail[]) => any) | undefined;
27
28
  onDownload?: (() => any) | undefined;
28
29
  onNext?: (() => any) | undefined;
30
+ onFailure?: ((error: FilePreviewRendererError | null) => any) | undefined;
29
31
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
30
32
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
31
33
  declare const _default: typeof __VLS_export;
@@ -15,7 +15,7 @@ const props = defineProps({
15
15
  kindLabel: { type: String, required: true },
16
16
  arrows: { type: [Object, null], required: true }
17
17
  });
18
- const emit = defineEmits(["ready", "previous", "next", "download"]);
18
+ const emit = defineEmits(["ready", "failure", "previous", "next", "download"]);
19
19
  const { t } = useUiToolsLocale();
20
20
  const failure = shallowRef(null);
21
21
  const ready = ref(false);
@@ -28,6 +28,7 @@ const downloadable = computed(() => props.item.file.download !== false);
28
28
  const extension = computed(() => (props.display.extension ?? props.kindLabel).toUpperCase());
29
29
  function restart() {
30
30
  failure.value = null;
31
+ emit("failure", null);
31
32
  ready.value = false;
32
33
  slow.value = false;
33
34
  delay.start();
@@ -41,6 +42,7 @@ function handleReady(details) {
41
42
  function handleError(error) {
42
43
  failure.value = error;
43
44
  delay.stop();
45
+ emit("failure", error);
44
46
  }
45
47
  function retry() {
46
48
  attempt.value += 1;
@@ -101,14 +103,6 @@ function retry() {
101
103
  data-file-preview-retry=""
102
104
  @click="retry"
103
105
  />
104
- <UButton
105
- v-if="downloadable"
106
- icon="i-lucide-download"
107
- :label="t('filePreview.download')"
108
- color="neutral"
109
- variant="outline"
110
- @click="emit('download')"
111
- />
112
106
  </FilePreviewMessage>
113
107
 
114
108
  <FilePreviewMessage
@@ -121,7 +115,7 @@ function retry() {
121
115
  :detail="failure.message"
122
116
  >
123
117
  <UButton
124
- v-if="downloadable"
118
+ v-if="downloadable && failure.reason !== 'source'"
125
119
  icon="i-lucide-download"
126
120
  :label="t('filePreview.download')"
127
121
  @click="emit('download')"
@@ -1,5 +1,5 @@
1
1
  import type { useFilePreviewSource } from '../../composables/use-file-preview-source.js';
2
- import type { FilePreviewContainer, FilePreviewDetail, FilePreviewItem, FilePreviewRendererDefinition } from '../../types/index.js';
2
+ import type { FilePreviewContainer, FilePreviewDetail, FilePreviewItem, FilePreviewRendererDefinition, FilePreviewRendererError } from '../../types/index.js';
3
3
  type __VLS_Props = {
4
4
  item: FilePreviewItem;
5
5
  display: FilePreviewItem;
@@ -12,20 +12,22 @@ type __VLS_Props = {
12
12
  next: boolean;
13
13
  } | null;
14
14
  };
15
- declare var __VLS_64: {};
15
+ declare var __VLS_57: {};
16
16
  type __VLS_Slots = {} & {
17
- default?: (props: typeof __VLS_64) => any;
17
+ default?: (props: typeof __VLS_57) => any;
18
18
  };
19
19
  declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
20
20
  previous: () => any;
21
21
  ready: (details: readonly FilePreviewDetail[]) => any;
22
22
  download: () => any;
23
23
  next: () => any;
24
+ failure: (error: FilePreviewRendererError | null) => any;
24
25
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
25
26
  onPrevious?: (() => any) | undefined;
26
27
  onReady?: ((details: readonly FilePreviewDetail[]) => any) | undefined;
27
28
  onDownload?: (() => any) | undefined;
28
29
  onNext?: (() => any) | undefined;
30
+ onFailure?: ((error: FilePreviewRendererError | null) => any) | undefined;
29
31
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
30
32
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
31
33
  declare const _default: typeof __VLS_export;
@@ -49,6 +49,7 @@ const {
49
49
  onCancelled: (value) => emit("cancel", value),
50
50
  onSubmitted: (value, result) => emit("submit", value, result),
51
51
  schema: () => props.schema,
52
+ shell: () => props.shell,
52
53
  syncInput: () => props.syncInput,
53
54
  ui: () => props.ui,
54
55
  validate: () => props.validate
@@ -198,7 +198,9 @@ export function useFieldOptions(params) {
198
198
  const createLabel = computed(
199
199
  () => resolveFormText(optionConfig.value?.create?.label)
200
200
  );
201
- const disableOnLoading = computed(() => optionConfig.value?.disableOnLoading !== false);
201
+ const disableOnLoading = computed(
202
+ () => !remoteConfig.value && optionConfig.value?.disableOnLoading !== false
203
+ );
202
204
  const selectCreatedOption = computed(
203
205
  () => optionConfig.value?.create?.selectOnCreation !== false
204
206
  );
@@ -55,7 +55,7 @@ export function useFormOverlayController(instance) {
55
55
  open.value = true;
56
56
  return;
57
57
  }
58
- requestCancel();
58
+ void runtime.close();
59
59
  }
60
60
  function requestComplete(formData, submitData) {
61
61
  closeWithResolution({ formData, submitData, type: "complete" });
@@ -1,4 +1,4 @@
1
- import type { FormObject, FormRendererController, FormRuntime, FormSubmitHandlerResult, FormUiConfig, FormValidationMode, FormValue } from '../types/index.js';
1
+ import type { FormObject, FormRenderShell, FormRendererController, FormRuntime, FormSubmitHandlerResult, FormUiConfig, FormValidationMode, FormValue } from '../types/index.js';
2
2
  export interface UseFormRootParams {
3
3
  /** Controller from `useForm`. Its schema, input, and policies win over the direct ones. */
4
4
  form: () => FormRendererController | undefined;
@@ -9,6 +9,8 @@ export interface UseFormRootParams {
9
9
  syncInput: () => boolean | readonly string[] | undefined;
10
10
  validate: () => FormValidationMode | undefined;
11
11
  ui: () => FormUiConfig | undefined;
12
+ /** Shell the form renders in. Overlay shells ask before closing a modified form by default. */
13
+ shell?: () => FormRenderShell | undefined;
12
14
  onSubmitted: (value: FormObject, result: FormSubmitHandlerResult<FormValue>) => void;
13
15
  onCancelled: (value: FormObject) => void;
14
16
  }
@@ -42,6 +42,7 @@ export function useFormRoot(params) {
42
42
  onBeforeUnmount(() => runtime.bindCancel(null));
43
43
  const confirm = useFormConfirm();
44
44
  let leaveConfirmed = false;
45
+ let pendingLeave = null;
45
46
  watch(
46
47
  () => runtime.dirtyPaths.value,
47
48
  () => {
@@ -139,24 +140,35 @@ export function useFormRoot(params) {
139
140
  });
140
141
  }
141
142
  async function cancel() {
142
- if (!leaveConfirmed && shouldConfirmDirtyNavigation() && !await confirmLeave()) {
143
+ if (!leaveConfirmed && shouldConfirmDirtyClose() && !await confirmLeave()) {
143
144
  return;
144
145
  }
145
146
  params.onCancelled(runtime.output.value);
146
147
  }
147
- async function confirmLeave() {
148
- leaveConfirmed = await confirm({
149
- kind: "unsaved-changes",
150
- message: getDirtyNavigationMessage()
151
- });
152
- return leaveConfirmed;
148
+ function confirmLeave() {
149
+ pendingLeave ??= askLeave();
150
+ return pendingLeave;
153
151
  }
154
- function shouldConfirmDirtyNavigation() {
155
- if (!runtime.isDirty.value) {
156
- return false;
152
+ async function askLeave() {
153
+ try {
154
+ leaveConfirmed = await confirm({
155
+ kind: "unsaved-changes",
156
+ message: getDirtyNavigationMessage()
157
+ });
158
+ return leaveConfirmed;
159
+ } finally {
160
+ pendingLeave = null;
157
161
  }
162
+ }
163
+ function shouldConfirmDirtyNavigation() {
164
+ return confirmsDirtyChanges(getSchemaDirtyNavigation(schema.value));
165
+ }
166
+ function shouldConfirmDirtyClose() {
158
167
  const config = getSchemaDirtyNavigation(schema.value);
159
- if (!config) {
168
+ return confirmsDirtyChanges(isUndefined(config) ? isOverlayShell(params.shell?.()) : config);
169
+ }
170
+ function confirmsDirtyChanges(config) {
171
+ if (!runtime.isDirty.value || !config) {
160
172
  return false;
161
173
  }
162
174
  if (!isRecord(config)) {
@@ -197,6 +209,9 @@ export function useFormRoot(params) {
197
209
  validationMode
198
210
  };
199
211
  }
212
+ function isOverlayShell(shell) {
213
+ return shell === "modal" || shell === "drawer" || shell === "fullscreen";
214
+ }
200
215
  function isSameDocument(to, from) {
201
216
  return to.fullPath.split("#")[0] === from.fullPath.split("#")[0];
202
217
  }
@@ -1,5 +1,6 @@
1
1
  import { computed, inject, nextTick, provide, ref, shallowRef, watch } from "vue";
2
2
  import { useUiToolsLocale } from "../../i18n/use-locale.js";
3
+ import { dateFamilyCalendarType, formatDateFamilyBound } from "../fields/date-family/utils.js";
3
4
  import { createPublicFormApi } from "../utils/api.js";
4
5
  import { resolveFieldDependencies } from "../utils/dependencies.js";
5
6
  import { resolveFormLayoutConfig } from "../utils/layout.js";
@@ -45,7 +46,7 @@ export function useFormRuntime(params) {
45
46
  const navigationActionPending = ref(null);
46
47
  const effects = createEffectLifecycle();
47
48
  const cancelHandler = shallowRef(null);
48
- const { t } = useUiToolsLocale();
49
+ const { code, t } = useUiToolsLocale();
49
50
  setContext(getSchemaContext(params.schema.value));
50
51
  function createFormNamespace(path, read) {
51
52
  const parentPath = path.slice(0, -1);
@@ -66,7 +67,7 @@ export function useFormRuntime(params) {
66
67
  state: () => read.state(),
67
68
  submit: () => runtime.submit(),
68
69
  validate: () => runtime.validate(),
69
- close: () => cancelHandler.value?.() ?? Promise.resolve()
70
+ close: () => runtime.close()
70
71
  };
71
72
  }
72
73
  function createApiFactory(read) {
@@ -111,8 +112,15 @@ export function useFormRuntime(params) {
111
112
  const validation = useFormValidation({
112
113
  apiFactory,
113
114
  context,
114
- getDateMaxMessage: (max) => t("form.validation.dateMax", { max }),
115
- getDateMinMessage: (min) => t("form.validation.dateMin", { min }),
115
+ getDateMaxMessage: (max, type) => t(`form.validation.${dateFamilyCalendarType(type)}Max`, {
116
+ max: formatDateFamilyBound(max, code.value)
117
+ }),
118
+ getDateMinMessage: (min, type) => t(`form.validation.${dateFamilyCalendarType(type)}Min`, {
119
+ min: formatDateFamilyBound(min, code.value)
120
+ }),
121
+ getNumberBoundMessage: (edge, bound) => t(`form.validation.number${edge === "min" ? "Min" : "Max"}`, {
122
+ bound: new Intl.NumberFormat(code.value).format(bound)
123
+ }),
116
124
  getRequiredMessage: () => t("form.validation.required"),
117
125
  getUniqueMessage: () => t("form.fields.array.unique"),
118
126
  getValidationMode: () => params.validationMode?.value ?? true,
@@ -432,6 +440,7 @@ export function useFormRuntime(params) {
432
440
  },
433
441
  schema: params.schema,
434
442
  setError: (path, message, options) => validation.setError(pathSegments(path), message, options),
443
+ setInputError: (path, message) => validation.setInputError(pathSegments(path), message),
435
444
  setValue: state.setValue,
436
445
  settleEffects: settleEffectRounds,
437
446
  shouldRender: (field, path) => shouldRenderField(
@@ -454,6 +463,7 @@ export function useFormRuntime(params) {
454
463
  bindCancel: (cancel) => {
455
464
  cancelHandler.value = cancel;
456
465
  },
466
+ close: () => cancelHandler.value?.() ?? Promise.resolve(),
457
467
  trackEffect: effects.track,
458
468
  validate,
459
469
  validateCurrentStep
@@ -1,3 +1,4 @@
1
+ import type { FormDateFamilyType } from '../fields/date-family/utils.js';
1
2
  import type { FormValue, FormErrorOptions, FormField, FormObject, FormRuntimeContext, FormValidationError, FormValidationMode } from '../types/index.js';
2
3
  import type { FormFieldApiFactory } from './use-form-state.js';
3
4
  /**
@@ -14,8 +15,9 @@ export declare function useFormValidation(params: {
14
15
  getValidationMode: () => FormValidationMode;
15
16
  getRequiredMessage?: () => string;
16
17
  getUniqueMessage?: () => string;
17
- getDateMinMessage?: (bound: string) => string;
18
- getDateMaxMessage?: (bound: string) => string;
18
+ getDateMinMessage?: (bound: string, type: FormDateFamilyType) => string;
19
+ getDateMaxMessage?: (bound: string, type: FormDateFamilyType) => string;
20
+ getNumberBoundMessage?: (edge: 'min' | 'max', bound: number) => string;
19
21
  }): {
20
22
  clearError: (path?: readonly string[]) => void;
21
23
  clearValidationState: () => void;
@@ -25,6 +27,7 @@ export declare function useFormValidation(params: {
25
27
  isTouched: (path: readonly string[]) => boolean;
26
28
  markTouched: (path: readonly string[]) => void;
27
29
  setError: (path: readonly string[], message: string, options?: FormErrorOptions) => void;
30
+ setInputError: (path: readonly string[], message: string | null) => void;
28
31
  validate: () => Promise<boolean>;
29
32
  validateFields: (fields: readonly FormField[], parentPath: readonly string[]) => Promise<boolean>;
30
33
  validationErrors: import("vue").ComputedRef<readonly FormValidationError[]>;
@@ -23,6 +23,7 @@ import {
23
23
  import { resolveFormBoundaryText, resolveFormText } from "../utils/text.js";
24
24
  export function useFormValidation(params) {
25
25
  const customErrors = ref([]);
26
+ const inputErrors = ref(/* @__PURE__ */ new Map());
26
27
  const touchedPaths = ref([]);
27
28
  const pendingPaths = ref(/* @__PURE__ */ new Map());
28
29
  const validatedMessages = ref(/* @__PURE__ */ new Map());
@@ -34,6 +35,7 @@ export function useFormValidation(params) {
34
35
  context: params.context,
35
36
  getDateMaxMessage: params.getDateMaxMessage,
36
37
  getDateMinMessage: params.getDateMinMessage,
38
+ getNumberBoundMessage: params.getNumberBoundMessage,
37
39
  getRequiredMessage: params.getRequiredMessage,
38
40
  getUniqueMessage: params.getUniqueMessage,
39
41
  includeAsync: true,
@@ -50,6 +52,7 @@ export function useFormValidation(params) {
50
52
  context: params.context,
51
53
  getDateMaxMessage: params.getDateMaxMessage,
52
54
  getDateMinMessage: params.getDateMinMessage,
55
+ getNumberBoundMessage: params.getNumberBoundMessage,
53
56
  getRequiredMessage: params.getRequiredMessage,
54
57
  getUniqueMessage: params.getUniqueMessage,
55
58
  includeAsync: false,
@@ -67,6 +70,7 @@ export function useFormValidation(params) {
67
70
  context: params.context,
68
71
  getDateMaxMessage: params.getDateMaxMessage,
69
72
  getDateMinMessage: params.getDateMinMessage,
73
+ getNumberBoundMessage: params.getNumberBoundMessage,
70
74
  getRequiredMessage: params.getRequiredMessage,
71
75
  getUniqueMessage: params.getUniqueMessage,
72
76
  includeAsync: true,
@@ -83,6 +87,7 @@ export function useFormValidation(params) {
83
87
  context: params.context,
84
88
  getDateMaxMessage: params.getDateMaxMessage,
85
89
  getDateMinMessage: params.getDateMinMessage,
90
+ getNumberBoundMessage: params.getNumberBoundMessage,
86
91
  getRequiredMessage: params.getRequiredMessage,
87
92
  getUniqueMessage: params.getUniqueMessage,
88
93
  includeAsync: false,
@@ -185,7 +190,7 @@ export function useFormValidation(params) {
185
190
  if (validationRuns.get("$form") !== run) {
186
191
  return asyncValid;
187
192
  }
188
- return syncResult.valid && asyncValid && !hasBlockingErrors(paths);
193
+ return syncResult.valid && asyncValid && !hasBlockingErrors(paths) && !hasInputErrors(paths);
189
194
  } finally {
190
195
  removePendingPaths(pendingPaths, potentialAsyncPaths, pendingToken);
191
196
  }
@@ -236,7 +241,7 @@ export function useFormValidation(params) {
236
241
  (error) => paths.some((path) => isScopedPath(error.path, path))
237
242
  );
238
243
  }
239
- return syncResult.valid && asyncValid && !hasBlockingErrors(paths);
244
+ return syncResult.valid && asyncValid && !hasBlockingErrors(paths) && !hasInputErrors(paths);
240
245
  } finally {
241
246
  removePendingPaths(pendingPaths, potentialAsyncPaths, pendingToken);
242
247
  }
@@ -259,6 +264,21 @@ export function useFormValidation(params) {
259
264
  { blocking: options?.blocking === true, message, path: key }
260
265
  ];
261
266
  }
267
+ function setInputError(path, message) {
268
+ const key = path.join(".");
269
+ if ((inputErrors.value.get(key) ?? null) === message) {
270
+ return;
271
+ }
272
+ const next = new Map(inputErrors.value);
273
+ if (message === null) next.delete(key);
274
+ else next.set(key, message);
275
+ inputErrors.value = next;
276
+ }
277
+ function hasInputErrors(paths) {
278
+ return [...inputErrors.value.keys()].some(
279
+ (key) => paths.some((path) => isScopedPath(key, path))
280
+ );
281
+ }
262
282
  function hasBlockingErrors(paths) {
263
283
  return customErrors.value.some(
264
284
  (error) => error.blocking === true && paths.some((path) => isScopedPath(error.path, path))
@@ -307,7 +327,8 @@ export function useFormValidation(params) {
307
327
  validatedMessages.value = next;
308
328
  }
309
329
  function resolveFieldErrors(path) {
310
- return validatedMessages.value.get(path) ?? [];
330
+ const inputError = inputErrors.value.get(path);
331
+ return inputError ? [inputError] : validatedMessages.value.get(path) ?? [];
311
332
  }
312
333
  function markTouched(path) {
313
334
  touchPaths([path.join(".")]);
@@ -338,6 +359,7 @@ export function useFormValidation(params) {
338
359
  isTouched,
339
360
  markTouched,
340
361
  setError,
362
+ setInputError,
341
363
  validate,
342
364
  validateFields,
343
365
  validationErrors
@@ -488,6 +510,7 @@ function buildLeafRules(params) {
488
510
  return output;
489
511
  }
490
512
  applyDateBoundsRules(output, params);
513
+ applyNumberBoundsRules(output, params);
491
514
  applyPasswordRequirementRules(output, params);
492
515
  const authoredValidators = "validators" in params.field ? params.field.validators : void 0;
493
516
  let validators;
@@ -522,6 +545,29 @@ function applyPasswordRequirementRules(output, params) {
522
545
  );
523
546
  }
524
547
  }
548
+ function applyNumberBoundsRules(output, params) {
549
+ if (params.field.type !== "number") {
550
+ return;
551
+ }
552
+ const authoredProps = Object.getOwnPropertyDescriptor(params.field, "props")?.value;
553
+ const resolvedProps = isFunction(authoredProps) ? authoredProps(params.callbackParams) : authoredProps;
554
+ if (!isObject(resolvedProps)) {
555
+ return;
556
+ }
557
+ const { max, min } = resolvedProps;
558
+ if (isNumber(min)) {
559
+ output.numberMin = withMessage(
560
+ (value) => !isNumber(value) || value >= min,
561
+ params.getNumberBoundMessage?.("min", min) ?? `Must be at least ${min}`
562
+ );
563
+ }
564
+ if (isNumber(max)) {
565
+ output.numberMax = withMessage(
566
+ (value) => !isNumber(value) || value <= max,
567
+ params.getNumberBoundMessage?.("max", max) ?? `Must be at most ${max}`
568
+ );
569
+ }
570
+ }
525
571
  function isValidatorMap(value) {
526
572
  return isObject(value);
527
573
  }
@@ -779,13 +825,13 @@ function applyDateBoundsRules(output, params) {
779
825
  if (bounds.min !== void 0) {
780
826
  output.dateMin = withMessage(
781
827
  (value) => isWithinDateMin(value, bounds.min, dateField.type),
782
- params.getDateMinMessage?.(bounds.minLabel ?? "") ?? `Must be on or after ${bounds.minLabel}`
828
+ params.getDateMinMessage?.(bounds.minLabel ?? "", dateField.type) ?? `Must be on or after ${bounds.minLabel}`
783
829
  );
784
830
  }
785
831
  if (bounds.max !== void 0) {
786
832
  output.dateMax = withMessage(
787
833
  (value) => isWithinDateMax(value, bounds.max, dateField.type),
788
- params.getDateMaxMessage?.(bounds.maxLabel ?? "") ?? `Must be on or before ${bounds.maxLabel}`
834
+ params.getDateMaxMessage?.(bounds.maxLabel ?? "", dateField.type) ?? `Must be on or before ${bounds.maxLabel}`
789
835
  );
790
836
  }
791
837
  }
@@ -14,8 +14,7 @@ const model = computed({
14
14
  get: () => form.getValue(props.path),
15
15
  set: (value) => form.setValue(props.path, value)
16
16
  });
17
- const rendered = computed(() => props.field.render?.(params.value));
18
- const Rendered = () => rendered.value;
17
+ const Rendered = () => props.field.render?.(params.value);
19
18
  </script>
20
19
 
21
20
  <template>
@@ -28,6 +27,6 @@ const Rendered = () => rendered.value;
28
27
  :disabled="disabled"
29
28
  @blur="handleBlur"
30
29
  />
31
- <Rendered v-else-if="rendered" />
30
+ <Rendered v-else-if="field.render" />
32
31
  </FormFieldShell>
33
32
  </template>
@@ -11,6 +11,7 @@ import { isNullish } from "../../../shared/utils/predicate";
11
11
  import FormFieldShell from "../../components/renderer/form-field-shell.vue";
12
12
  import { useFieldControl } from "../../composables/use-field-control";
13
13
  import { isNumber, isString } from "../../utils/predicate";
14
+ import { resolveFieldText } from "../../utils/text";
14
15
  import {
15
16
  applyDateManualMask,
16
17
  calendarRangeFromCanonical,
@@ -43,6 +44,7 @@ const {
43
44
  disabled,
44
45
  handleBlur,
45
46
  interactionOwnerClass,
47
+ params,
46
48
  placeholder: fieldPlaceholder
47
49
  } = useFieldControl(
48
50
  () => props.field,
@@ -155,6 +157,9 @@ const minuteStep = computed(() => {
155
157
  });
156
158
  const timeStep = computed(() => ({ minute: minuteStep.value }));
157
159
  const calendarUi = computed(() => ({ heading: "min-w-0", root: "p-2" }));
160
+ const calendarLabel = computed(
161
+ () => resolveFieldText(Object.getOwnPropertyDescriptor(props.field, "label")?.value, params.value) ?? t("form.fields.date.calendar")
162
+ );
158
163
  watch(
159
164
  selectedValues,
160
165
  () => {
@@ -272,15 +277,9 @@ function commitManualInput(raw) {
272
277
  return false;
273
278
  }
274
279
  if (!isString(parsed)) {
275
- if (!isWithinBounds(parsed[0]) || !isWithinBounds(parsed[1])) {
276
- return false;
277
- }
278
280
  setStoredRange(parsed[0], parsed[1]);
279
281
  return true;
280
282
  }
281
- if (!isWithinBounds(parsed)) {
282
- return false;
283
- }
284
283
  setStoredPart(0, parsed);
285
284
  return true;
286
285
  }
@@ -453,6 +452,7 @@ function isCalendarRangeValue(candidate) {
453
452
  :week-numbers="fieldProps.calendar?.weekNumbers ?? false"
454
453
  :number-of-months="fieldProps.calendar?.numberOfMonths"
455
454
  :view-control="{ trailingIcon: 'i-lucide-chevron-down', class: 'font-medium' }"
455
+ :calendar-label="calendarLabel"
456
456
  :ui="calendarUi"
457
457
  @update:model-value="updateRangeCalendar"
458
458
  />
@@ -468,6 +468,7 @@ function isCalendarRangeValue(candidate) {
468
468
  :week-numbers="fieldProps.calendar?.weekNumbers ?? false"
469
469
  :number-of-months="fieldProps.calendar?.numberOfMonths"
470
470
  :view-control="{ trailingIcon: 'i-lucide-chevron-down', class: 'font-medium' }"
471
+ :calendar-label="calendarLabel"
471
472
  :ui="calendarUi"
472
473
  @update:model-value="updateSingleCalendar"
473
474
  />
@@ -37,4 +37,9 @@ export type FormDateSeedValue = Date | string | number | null | undefined;
37
37
  export declare function canonicalDateFromValue(value: FormDateSeedValue): string;
38
38
  export declare function canonicalDateToJsDate(value: string): Date | undefined;
39
39
  export declare function calendarSeedFromValue(value: FormDateSeedValue, type: FormDateFamilyType): string;
40
+ /**
41
+ * Writes a canonical bound (`2026`, `2026-10`, `2026-10-24`, `2026-10-24T09:30`) the way the
42
+ * locale reads it, such as « 24 oct. 2026 » or « octobre 2026 ».
43
+ */
44
+ export declare function formatDateFamilyBound(value: string, locale: string): string;
40
45
  export declare function calendarDateToJsDate(value: DateValue): Date;