nuxt-ui-tools 1.25.0 → 1.26.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 (72) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/display/components/blocks/display-accordion.vue +4 -1
  3. package/dist/runtime/display/components/blocks/display-alert.vue +5 -2
  4. package/dist/runtime/display/components/blocks/display-breakdown.vue +13 -7
  5. package/dist/runtime/display/components/blocks/display-chart.vue +6 -2
  6. package/dist/runtime/display/components/blocks/display-code.vue +2 -1
  7. package/dist/runtime/display/components/blocks/display-date.vue +21 -16
  8. package/dist/runtime/display/components/blocks/display-diff.vue +19 -7
  9. package/dist/runtime/display/components/blocks/display-empty.vue +3 -2
  10. package/dist/runtime/display/components/blocks/display-facts.vue +24 -9
  11. package/dist/runtime/display/components/blocks/display-heading.vue +10 -3
  12. package/dist/runtime/display/components/blocks/display-list.vue +10 -4
  13. package/dist/runtime/display/components/blocks/display-media.vue +19 -9
  14. package/dist/runtime/display/components/blocks/display-meter.vue +12 -11
  15. package/dist/runtime/display/components/blocks/display-person.vue +12 -5
  16. package/dist/runtime/display/components/blocks/display-record.vue +78 -18
  17. package/dist/runtime/display/components/blocks/display-stat.vue +16 -5
  18. package/dist/runtime/display/components/blocks/display-table.vue +4 -0
  19. package/dist/runtime/display/components/blocks/display-text.vue +8 -2
  20. package/dist/runtime/display/components/blocks/display-timeline.vue +7 -1
  21. package/dist/runtime/display/components/parts/display-actions.vue +4 -1
  22. package/dist/runtime/display/components/parts/display-chip.vue +27 -1
  23. package/dist/runtime/display/components/parts/display-fact-value.vue +2 -1
  24. package/dist/runtime/display/components/parts/display-record-media.d.vue.ts +2 -0
  25. package/dist/runtime/display/components/parts/display-record-media.vue +5 -3
  26. package/dist/runtime/display/components/parts/display-record-media.vue.d.ts +2 -0
  27. package/dist/runtime/display/directives/reveal-truncated.d.ts +10 -0
  28. package/dist/runtime/display/directives/reveal-truncated.js +31 -0
  29. package/dist/runtime/display/types/blocks/diff.d.ts +1 -1
  30. package/dist/runtime/display/types/blocks/facts.d.ts +3 -1
  31. package/dist/runtime/display/types/blocks/person.d.ts +3 -1
  32. package/dist/runtime/display/types/blocks/record.d.ts +4 -2
  33. package/dist/runtime/display/types/config.d.ts +9 -2
  34. package/dist/runtime/display/types/shared.d.ts +9 -0
  35. package/dist/runtime/display/utils/config.d.ts +7 -2
  36. package/dist/runtime/display/utils/config.js +11 -1
  37. package/dist/runtime/display/utils/links.d.ts +12 -0
  38. package/dist/runtime/display/utils/links.js +9 -0
  39. package/dist/runtime/file-preview/components/shell/file-preview-header.d.vue.ts +2 -0
  40. package/dist/runtime/file-preview/components/shell/file-preview-header.vue +13 -7
  41. package/dist/runtime/file-preview/components/shell/file-preview-header.vue.d.ts +2 -0
  42. package/dist/runtime/file-preview/components/shell/file-preview-shell.vue +6 -0
  43. package/dist/runtime/file-preview/components/shell/file-preview-stage.d.vue.ts +5 -3
  44. package/dist/runtime/file-preview/components/shell/file-preview-stage.vue +4 -10
  45. package/dist/runtime/file-preview/components/shell/file-preview-stage.vue.d.ts +5 -3
  46. package/dist/runtime/form/components/root/form.vue +1 -0
  47. package/dist/runtime/form/composables/use-field-options.js +3 -1
  48. package/dist/runtime/form/composables/use-form-overlay-controller.js +1 -1
  49. package/dist/runtime/form/composables/use-form-root.d.ts +3 -1
  50. package/dist/runtime/form/composables/use-form-root.js +41 -15
  51. package/dist/runtime/form/composables/use-form-runtime.js +14 -4
  52. package/dist/runtime/form/composables/use-form-validation.d.ts +5 -2
  53. package/dist/runtime/form/composables/use-form-validation.js +51 -5
  54. package/dist/runtime/form/fields/custom-component/component.vue +2 -3
  55. package/dist/runtime/form/fields/date-family/component.vue +7 -6
  56. package/dist/runtime/form/fields/date-family/utils.d.ts +5 -0
  57. package/dist/runtime/form/fields/date-family/utils.js +13 -0
  58. package/dist/runtime/form/fields/number/component.vue +11 -5
  59. package/dist/runtime/form/fields/phone-number/component.vue +20 -1
  60. package/dist/runtime/form/types/api.d.ts +9 -2
  61. package/dist/runtime/form/types/options.d.ts +4 -1
  62. package/dist/runtime/form/types/runtime.d.ts +10 -0
  63. package/dist/runtime/form/types/schema.d.ts +5 -1
  64. package/dist/runtime/form/utils/api.js +1 -0
  65. package/dist/runtime/i18n/locales/en.js +12 -3
  66. package/dist/runtime/i18n/locales/fr.js +12 -3
  67. package/dist/runtime/i18n/types.d.ts +16 -0
  68. package/dist/runtime/table/components/filters/tags/date-filter-tag.vue +4 -0
  69. package/dist/runtime/table/types/filters.d.ts +17 -0
  70. package/dist/runtime/table/utils/filters/options.js +2 -1
  71. package/dist/runtime/table/utils/filters/preview/option-preview.js +4 -1
  72. package/package.json +1 -1
@@ -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;
@@ -193,6 +193,19 @@ export function calendarSeedFromValue(value, type) {
193
193
  }
194
194
  return "";
195
195
  }
196
+ export function formatDateFamilyBound(value, locale) {
197
+ const date = /^\d{4}$/u.test(value) ? void 0 : canonicalDateToJsDate(value);
198
+ if (!date) {
199
+ return value;
200
+ }
201
+ if (/^\d{4}-\d{2}$/u.test(value)) {
202
+ return new Intl.DateTimeFormat(locale, { month: "long", year: "numeric" }).format(date);
203
+ }
204
+ return new Intl.DateTimeFormat(
205
+ locale,
206
+ value.includes("T") ? { dateStyle: "medium", timeStyle: "short" } : { dateStyle: "medium" }
207
+ ).format(date);
208
+ }
196
209
  export function calendarDateToJsDate(value) {
197
210
  return value.toDate(getLocalTimeZone());
198
211
  }
@@ -16,7 +16,7 @@ const { code } = useUiToolsLocale();
16
16
  const { fieldProps, form, controlProps, disabled, handleBlur, placeholder } = useFieldControl(
17
17
  () => props.field,
18
18
  () => props.path,
19
- { omit: ["prefix", "suffix", "format", "controls", "mono"] }
19
+ { omit: ["prefix", "suffix", "format", "controls", "mono", "min", "max"] }
20
20
  );
21
21
  const model = computed({
22
22
  get: () => {
@@ -25,6 +25,12 @@ const model = computed({
25
25
  },
26
26
  set: (value) => form.setValue(props.path, value ?? null)
27
27
  });
28
+ const atMax = computed(
29
+ () => isNumber(model.value) && isNumber(fieldProps.value.max) && model.value >= fieldProps.value.max
30
+ );
31
+ const atMin = computed(
32
+ () => isNumber(model.value) && isNumber(fieldProps.value.min) && model.value <= fieldProps.value.min
33
+ );
28
34
  const prefix = computed(() => resolveFormText(fieldProps.value.prefix));
29
35
  const suffix = computed(() => resolveFormText(fieldProps.value.suffix));
30
36
  const controls = computed(() => fieldProps.value.controls !== false);
@@ -76,13 +82,13 @@ const affixStyle = computed(() => ({
76
82
  :ui="controlUi"
77
83
  :placeholder="placeholder"
78
84
  :disabled="disabled"
79
- :min="fieldProps.min"
80
- :max="fieldProps.max"
81
85
  :step="fieldProps.step"
82
86
  :locale="code"
83
87
  :format-options="fieldProps.format"
84
88
  :increment="controls"
85
89
  :decrement="controls"
90
+ :increment-disabled="atMax"
91
+ :decrement-disabled="atMin"
86
92
  @blur="handleBlur"
87
93
  />
88
94
  <span
@@ -121,13 +127,13 @@ const affixStyle = computed(() => ({
121
127
  :ui="controlUi"
122
128
  :placeholder="placeholder"
123
129
  :disabled="disabled"
124
- :min="fieldProps.min"
125
- :max="fieldProps.max"
126
130
  :step="fieldProps.step"
127
131
  :locale="code"
128
132
  :format-options="fieldProps.format"
129
133
  :increment="controls"
130
134
  :decrement="controls"
135
+ :increment-disabled="atMax"
136
+ :decrement-disabled="atMin"
131
137
  @blur="handleBlur"
132
138
  />
133
139
  <span
@@ -4,7 +4,12 @@ import UFieldGroup from "@nuxt/ui/components/FieldGroup.vue";
4
4
  import UIcon from "@nuxt/ui/components/Icon.vue";
5
5
  import UInput from "@nuxt/ui/components/Input.vue";
6
6
  import USelectMenu from "@nuxt/ui/components/SelectMenu.vue";
7
- import { getCountries, getCountryCallingCode, parsePhoneNumberFromString } from "libphonenumber-js";
7
+ import {
8
+ getCountries,
9
+ getCountryCallingCode,
10
+ parsePhoneNumberFromString,
11
+ validatePhoneNumberLength
12
+ } from "libphonenumber-js";
8
13
  import { computed, onMounted, ref, useId, watch } from "vue";
9
14
  import { useUiToolsLocale } from "../../../i18n/use-locale";
10
15
  import FormFieldShell from "../../components/renderer/form-field-shell.vue";
@@ -47,6 +52,20 @@ const processedValue = computed(() => {
47
52
  }
48
53
  return { valid: true, value: formatPhoneNumber(parsed) };
49
54
  });
55
+ const inputError = computed(() => {
56
+ if (!phoneValue.value || !countryCode.value || processedValue.value.valid) {
57
+ return null;
58
+ }
59
+ return validatePhoneNumberLength(phoneValue.value, countryCode.value) === "TOO_SHORT" ? t("form.validation.phoneIncomplete") : t("form.validation.phoneInvalid");
60
+ });
61
+ watch(
62
+ [() => props.path, inputError],
63
+ ([path, message], _previous, onCleanup) => {
64
+ form.setInputError(path, message);
65
+ onCleanup(() => form.setInputError(path, null));
66
+ },
67
+ { immediate: true }
68
+ );
50
69
  onMounted(() => {
51
70
  syncFromExternalValue(form.getValue(props.path));
52
71
  if (!countryCode.value) {
@@ -61,8 +61,8 @@ export interface FormFieldFormApi {
61
61
  submit: () => Promise<boolean>;
62
62
  /**
63
63
  * Closes the form the way its Cancel action does: asks to discard unsaved changes when the
64
- * schema asks for it, then cancels (an overlay closes). Does nothing for a form rendered without
65
- * a cancel handler.
64
+ * schema asks for it (an overlay form does by default), then cancels (an overlay closes). Does
65
+ * nothing for a form rendered without a cancel handler.
66
66
  */
67
67
  close: () => Promise<void>;
68
68
  /** Advances to the next step on stepped forms. */
@@ -206,6 +206,13 @@ export interface FormApi<TOutput = FormObject> {
206
206
  submit: () => Promise<void>;
207
207
  /** Resets the form and waits for dependency effects triggered by the reset. */
208
208
  reset: () => Promise<void>;
209
+ /**
210
+ * Closes the form the way its Cancel action does: asks to discard unsaved changes when the
211
+ * schema asks for it (an overlay form does by default), then cancels, so an overlay closes and
212
+ * its `createForm` promise resolves with `isCompleted: false`. Does nothing for a form rendered
213
+ * without a cancel handler.
214
+ */
215
+ close: () => Promise<void>;
209
216
  }
210
217
  export type FormSubmitAction = 'next' | 'previous' | 'submit' | 'reset';
211
218
  export type FormSubmitResult<TSubmitData = FormValue> = boolean | void | {
@@ -257,7 +257,10 @@ export interface FormRemoteOptionConfig<TOption, TContext = NonNullable<unknown>
257
257
  create?: FormCreateOption<TOption, TContext, TDeps, TValue>;
258
258
  /** Enables the field-level refresh affordance. */
259
259
  allowOptionsRefresh?: boolean;
260
- /** Disable the field while its option source has no usable data yet. */
260
+ /**
261
+ * Ignored: remote options load once the menu opens and the menu shows the loading state, so the
262
+ * field and its search input stay usable while a page loads.
263
+ */
261
264
  disableOnLoading?: boolean;
262
265
  }
263
266
  /** Reuses one remote loader while keeping field-specific refresh and creation behavior local. */
@@ -80,6 +80,11 @@ export interface FormRuntime {
80
80
  validate: (options?: FormValidationOptions) => Promise<boolean>;
81
81
  validateCurrentStep: (options?: FormValidationOptions) => Promise<boolean>;
82
82
  setError: (path: string | readonly string[], message: string, options?: FormErrorOptions) => void;
83
+ /**
84
+ * Reports input a control holds but cannot store as a value, such as an incomplete phone number,
85
+ * or clears it with `null`. It shows like a rule error once the field is touched and blocks submit.
86
+ */
87
+ setInputError: (path: string | readonly string[], message: string | null) => void;
83
88
  clearError: (path?: string | readonly string[]) => void;
84
89
  focusRequest: Ref<FormFocusRequest | null>;
85
90
  registerFieldElement: (path: string | readonly string[], element: HTMLElement) => () => void;
@@ -94,4 +99,9 @@ export interface FormRuntime {
94
99
  goToStep: (index: number) => Promise<boolean>;
95
100
  /** Cancels the rendered form, as its Cancel action does. Set by the form root that renders it. */
96
101
  bindCancel: (cancel: (() => Promise<void>) | null) => void;
102
+ /**
103
+ * Closes the form the way its Cancel action does, asking first when it holds unsaved changes the
104
+ * schema protects. Resolves at once when no form root renders the runtime.
105
+ */
106
+ close: () => Promise<void>;
97
107
  }
@@ -18,7 +18,11 @@ export interface FormControlsConfig {
18
18
  ignoreDirtyPaths?: readonly string[];
19
19
  /** Focuses the first field after mount, or a specific raw field path. */
20
20
  autoFocus?: boolean | string;
21
- /** Warns before an inline dirty form is closed or navigated away from. */
21
+ /**
22
+ * Asks before a modified form is cancelled, closed, or left for another page. A form opened as a
23
+ * modal, drawer, or fullscreen overlay asks before closing (Escape, outside click, close button,
24
+ * Cancel, `api.close()`) unless this is `false`; leaving the page asks only when it is set.
25
+ */
22
26
  confirmNavOnDirty?: boolean | FormDirtyNavigationConfig;
23
27
  /** Keeps external input changes synchronized after the initial mount. */
24
28
  syncInput?: boolean | readonly string[];
@@ -2,6 +2,7 @@ import { pathSegments } from "./path.js";
2
2
  export function createPublicFormApi(runtime) {
3
3
  return {
4
4
  clearError: (path) => runtime.clearError(path ? pathSegments(path) : void 0),
5
+ close: runtime.close,
5
6
  focus: runtime.focusField,
6
7
  get: runtime.getValue,
7
8
  initial: runtime.getInitialValue,
@@ -269,6 +269,7 @@ export default defineUiToolsLocale({
269
269
  open: "Open color picker"
270
270
  },
271
271
  date: {
272
+ calendar: "Calendar",
272
273
  clear: "Clear value",
273
274
  confirm: "Confirm",
274
275
  end: "End",
@@ -355,9 +356,17 @@ export default defineUiToolsLocale({
355
356
  fallback: "Step {number}"
356
357
  },
357
358
  validation: {
358
- dateMax: "The date must be on or before {max}",
359
- dateMin: "The date must be on or after {min}",
360
- required: "This field is required"
359
+ dateMax: "Date after {max}",
360
+ dateMin: "Date before {min}",
361
+ monthMax: "Month after {max}",
362
+ monthMin: "Month before {min}",
363
+ numberMax: "Maximum value: {bound}",
364
+ numberMin: "Minimum value: {bound}",
365
+ phoneIncomplete: "Incomplete phone number",
366
+ phoneInvalid: "Invalid phone number",
367
+ required: "This field is required",
368
+ yearMax: "Year after {max}",
369
+ yearMin: "Year before {min}"
361
370
  }
362
371
  },
363
372
  spreadsheet: {
@@ -269,6 +269,7 @@ export default defineUiToolsLocale({
269
269
  open: "Ouvrir le s\xE9lecteur de couleur"
270
270
  },
271
271
  date: {
272
+ calendar: "Calendrier",
272
273
  clear: "Effacer la valeur",
273
274
  confirm: "Confirmer",
274
275
  end: "Fin",
@@ -355,9 +356,17 @@ export default defineUiToolsLocale({
355
356
  fallback: "\xC9tape {number}"
356
357
  },
357
358
  validation: {
358
- dateMax: "La date doit \xEAtre au plus tard le {max}",
359
- dateMin: "La date doit \xEAtre au plus t\xF4t le {min}",
360
- required: "Ce champ est requis"
359
+ dateMax: "Date post\xE9rieure au {max}",
360
+ dateMin: "Date ant\xE9rieure au {min}",
361
+ monthMax: "Mois post\xE9rieur \xE0 {max}",
362
+ monthMin: "Mois ant\xE9rieur \xE0 {min}",
363
+ numberMax: "Valeur maximale : {bound}",
364
+ numberMin: "Valeur minimale : {bound}",
365
+ phoneIncomplete: "Num\xE9ro de t\xE9l\xE9phone incomplet",
366
+ phoneInvalid: "Num\xE9ro de t\xE9l\xE9phone invalide",
367
+ required: "Ce champ est requis",
368
+ yearMax: "Ann\xE9e post\xE9rieure \xE0 {max}",
369
+ yearMin: "Ann\xE9e ant\xE9rieure \xE0 {min}"
361
370
  }
362
371
  },
363
372
  spreadsheet: {
@@ -504,8 +504,22 @@ export interface UiToolsFormMessages {
504
504
  };
505
505
  validation: {
506
506
  required: string;
507
+ /** A date before the field's `min`: "Date before {min}". */
507
508
  dateMin: string;
509
+ /** A date after the field's `max`: "Date after {max}". */
508
510
  dateMax: string;
511
+ monthMin: string;
512
+ monthMax: string;
513
+ yearMin: string;
514
+ yearMax: string;
515
+ /** A number below the field's `min`: "Minimum value: {bound}". */
516
+ numberMin: string;
517
+ /** A number above the field's `max`: "Maximum value: {bound}". */
518
+ numberMax: string;
519
+ /** A phone number too short for its country. */
520
+ phoneIncomplete: string;
521
+ /** A phone number that is not valid for its country or allowed types. */
522
+ phoneInvalid: string;
509
523
  };
510
524
  fields: {
511
525
  text: {
@@ -562,6 +576,8 @@ export interface UiToolsFormMessages {
562
576
  end: string;
563
577
  clear: string;
564
578
  confirm: string;
579
+ /** Accessible name of the calendar when the field has no label, read before the month. */
580
+ calendar: string;
565
581
  tokens: {
566
582
  day: string;
567
583
  month: string;
@@ -464,6 +464,9 @@ function areSameCalendarDay(left, right) {
464
464
  <UCalendar
465
465
  :model-value="calendarRange"
466
466
  :size="size"
467
+ :calendar-label="
468
+ internals.filters.getFilterLabelText({ label: definition.label })
469
+ "
467
470
  range
468
471
  :number-of-months="rangeCalendarPanels"
469
472
  :paged-navigation="
@@ -602,6 +605,7 @@ function areSameCalendarDay(left, right) {
602
605
  "
603
606
  :model-value="localDate"
604
607
  :size="size"
608
+ :calendar-label="internals.filters.getFilterLabelText({ label: definition.label })"
605
609
  :fixed-weeks="filterUi.scalar.calendar.fixedWeeks"
606
610
  :ui="{
607
611
  root: mergeDataListUiClass(
@@ -647,6 +647,23 @@ export interface TableOptionFilterDefinition<TRow extends GenericObject = Generi
647
647
  */
648
648
  remote?: TableFilterRemoteOptions<TValue, TPresentation>;
649
649
  facet?: TableFilterFacetSpec<TRow, TContext, TKey>;
650
+ /**
651
+ * Labels the values a facet lists when `options` is not set, such as provider statuses stored
652
+ * as `Approved` or `Security compromised`. The facet still decides which values show, with their
653
+ * counts; a value it returns `undefined` for shows as stored. Tags and previews of a committed
654
+ * value read the same label.
655
+ *
656
+ * @example
657
+ * ```ts
658
+ * source: {
659
+ * facet: true,
660
+ * label: (value) => ({ Approved: 'Validé', Done: 'Terminé' })[String(value)],
661
+ * }
662
+ * ```
663
+ */
664
+ label?: {
665
+ bivarianceHack(value: TValue): TableTextValue | undefined;
666
+ }['bivarianceHack'];
650
667
  sort?: 'alpha' | 'count';
651
668
  };
652
669
  editor?: TableOptionFilterEditorConfig<TPresentation>;
@@ -21,9 +21,10 @@ export function resolveFilterOptionEntries(options) {
21
21
  return [];
22
22
  }
23
23
  const configuredOptions = options.options ?? options.definition.source?.options ?? [];
24
+ const facetLabel = options.definition.source?.label;
24
25
  const sourceOptions = configuredOptions.length || options.authoritative ? configuredOptions : (options.facetCounts ?? []).map((entry) => ({
25
26
  count: entry.count,
26
- label: String(entry.value),
27
+ label: facetLabel?.(entry.value) ?? String(entry.value),
27
28
  value: entry.value
28
29
  }));
29
30
  const countByValue = new Map(
@@ -1,3 +1,5 @@
1
+ import { isBoolean, isNullish, isNumber, isString } from "../../../../shared/utils/predicate.js";
2
+ import { getFilterLabelText } from "../common.js";
1
3
  import { resolveBooleanFilterUi, resolveOptionFilterUi } from "../ui.js";
2
4
  export function buildOptionFilterPreview(options) {
3
5
  const values = Array.isArray(options.rule.value) ? options.rule.value : [options.rule.value];
@@ -48,7 +50,8 @@ function resolveOptionPreviewLabel(options) {
48
50
  );
49
51
  return options.value ? filterUi.labels.true : filterUi.labels.false;
50
52
  }
51
- return String(options.value ?? "");
53
+ const label = isString(options.value) || isNumber(options.value) || isBoolean(options.value) ? options.definition.source?.label?.(options.value) : void 0;
54
+ return isNullish(label) ? String(options.value ?? "") : getFilterLabelText({ label });
52
55
  }
53
56
  function resolveOptionPreview(options) {
54
57
  if (options.definition.kind === "boolean") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-ui-tools",
3
- "version": "1.25.0",
3
+ "version": "1.26.0",
4
4
  "description": "Nuxt module and runtime toolkit for UI-heavy Nuxt applications",
5
5
  "homepage": "https://github.com/ChronicStone/nuxt-ui-tools#readme",
6
6
  "bugs": "https://github.com/ChronicStone/nuxt-ui-tools/issues",