nuxt-ui-tools 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/dist/module.d.mts +4 -0
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +30 -15
  4. package/dist/runtime/form/components/actions/FormActions.d.vue.ts +0 -2
  5. package/dist/runtime/form/components/actions/FormActions.vue +5 -8
  6. package/dist/runtime/form/components/actions/FormActions.vue.d.ts +0 -2
  7. package/dist/runtime/form/components/layout/DrawerLayout.vue +5 -1
  8. package/dist/runtime/form/components/layout/ModalLayout.vue +5 -1
  9. package/dist/runtime/form/components/renderer/FormCompositeControl.d.vue.ts +15 -0
  10. package/dist/runtime/form/components/renderer/FormCompositeControl.vue +13 -0
  11. package/dist/runtime/form/components/renderer/FormCompositeControl.vue.d.ts +15 -0
  12. package/dist/runtime/form/components/renderer/FormFieldRenderer.d.vue.ts +1 -0
  13. package/dist/runtime/form/components/renderer/FormFieldRenderer.vue +19 -3
  14. package/dist/runtime/form/components/renderer/FormFieldRenderer.vue.d.ts +1 -0
  15. package/dist/runtime/form/components/renderer/FormFieldShell.d.vue.ts +4 -4
  16. package/dist/runtime/form/components/renderer/FormFieldShell.vue +15 -1
  17. package/dist/runtime/form/components/renderer/FormFieldShell.vue.d.ts +4 -4
  18. package/dist/runtime/form/components/root/Form.vue +5 -3
  19. package/dist/runtime/form/composables/use-field-control.d.ts +1 -1
  20. package/dist/runtime/form/composables/use-field-control.js +4 -0
  21. package/dist/runtime/form/composables/use-field-options.d.ts +1 -1
  22. package/dist/runtime/form/composables/use-field-options.js +75 -31
  23. package/dist/runtime/form/composables/use-form-field-chrome.d.ts +3 -0
  24. package/dist/runtime/form/composables/use-form-field-chrome.js +9 -0
  25. package/dist/runtime/form/composables/use-form-runtime.js +17 -5
  26. package/dist/runtime/form/fields/array-list/component.vue +4 -1
  27. package/dist/runtime/form/fields/auto-complete/component.vue +23 -16
  28. package/dist/runtime/form/fields/checkbox-card/component.vue +28 -29
  29. package/dist/runtime/form/fields/checkbox-group/component.vue +28 -29
  30. package/dist/runtime/form/fields/color-picker/component.vue +4 -2
  31. package/dist/runtime/form/fields/date-family/component.vue +9 -4
  32. package/dist/runtime/form/fields/hierarchy/SelectionCheckbox.d.vue.ts +13 -0
  33. package/dist/runtime/form/fields/hierarchy/SelectionCheckbox.vue +24 -0
  34. package/dist/runtime/form/fields/hierarchy/SelectionCheckbox.vue.d.ts +13 -0
  35. package/dist/runtime/form/fields/hierarchy/SelectionRadio.d.vue.ts +14 -0
  36. package/dist/runtime/form/fields/hierarchy/SelectionRadio.vue +27 -0
  37. package/dist/runtime/form/fields/hierarchy/SelectionRadio.vue.d.ts +14 -0
  38. package/dist/runtime/form/fields/hierarchy/component.vue +257 -142
  39. package/dist/runtime/form/fields/hierarchy/utils.d.ts +9 -0
  40. package/dist/runtime/form/fields/hierarchy/utils.js +32 -0
  41. package/dist/runtime/form/fields/input-group/component.vue +7 -30
  42. package/dist/runtime/form/fields/matrix/component.vue +9 -3
  43. package/dist/runtime/form/fields/phone-number/component.vue +7 -2
  44. package/dist/runtime/form/fields/rating/component.vue +25 -2
  45. package/dist/runtime/form/fields/select/component.vue +29 -21
  46. package/dist/runtime/form/fields/switch-group/component.vue +8 -3
  47. package/dist/runtime/form/fields/time/component.vue +7 -5
  48. package/dist/runtime/form/fields/upload/component.vue +5 -3
  49. package/dist/runtime/form/types/api.d.ts +9 -3
  50. package/dist/runtime/form/types/field-base.d.ts +3 -1
  51. package/dist/runtime/form/types/runtime.d.ts +2 -0
  52. package/dist/runtime/form/types/ui.d.ts +1 -0
  53. package/dist/runtime/form/utils/api.js +3 -0
  54. package/dist/runtime/form/utils/options.d.ts +11 -2
  55. package/dist/runtime/form/utils/options.js +57 -9
  56. package/dist/runtime/form/utils/state.js +10 -2
  57. package/dist/runtime/i18n/locales/en.js +38 -0
  58. package/dist/runtime/i18n/locales/fr.js +38 -0
  59. package/dist/runtime/i18n/types.d.ts +38 -0
  60. package/dist/runtime/i18n/use-locale.d.ts +2 -2
  61. package/dist/runtime/i18n/use-locale.js +3 -3
  62. package/dist/runtime/table/components/DataList.d.vue.ts +2 -0
  63. package/dist/runtime/table/components/DataList.vue.d.ts +2 -0
  64. package/dist/runtime/table/components/actions/RowActions.d.vue.ts +4 -2
  65. package/dist/runtime/table/components/actions/RowActions.vue +2 -0
  66. package/dist/runtime/table/components/actions/RowActions.vue.d.ts +4 -2
  67. package/dist/runtime/table/components/data-list/DataListAddFilter.d.vue.ts +5 -3
  68. package/dist/runtime/table/components/data-list/DataListAddFilter.vue +6 -1
  69. package/dist/runtime/table/components/data-list/DataListAddFilter.vue.d.ts +5 -3
  70. package/dist/runtime/table/components/data-list/DataListContent.d.vue.ts +24 -17
  71. package/dist/runtime/table/components/data-list/DataListContent.vue +70 -17
  72. package/dist/runtime/table/components/data-list/DataListContent.vue.d.ts +24 -17
  73. package/dist/runtime/table/components/data-list/DataListDefault.d.vue.ts +2 -0
  74. package/dist/runtime/table/components/data-list/DataListDefault.vue.d.ts +2 -0
  75. package/dist/runtime/table/components/data-list/DataListFilterTags.d.vue.ts +2 -0
  76. package/dist/runtime/table/components/data-list/DataListFilterTags.vue.d.ts +2 -0
  77. package/dist/runtime/table/components/data-list/DataListLayoutSwitch.vue +17 -11
  78. package/dist/runtime/table/components/data-list/DataListRoot.vue +2 -46
  79. package/dist/runtime/table/components/data-list/DataListSortMenu.vue +6 -0
  80. package/dist/runtime/table/components/filters/shared/DynamicFilterPicker.d.vue.ts +5 -0
  81. package/dist/runtime/table/components/filters/shared/DynamicFilterPicker.vue +182 -59
  82. package/dist/runtime/table/components/filters/shared/DynamicFilterPicker.vue.d.ts +5 -0
  83. package/dist/runtime/table/components/filters/shared/FilterMatchModeButton.d.vue.ts +3 -1
  84. package/dist/runtime/table/components/filters/shared/FilterMatchModeButton.vue +29 -5
  85. package/dist/runtime/table/components/filters/shared/FilterMatchModeButton.vue.d.ts +3 -1
  86. package/dist/runtime/table/components/filters/shared/FilterMatchModePanel.d.vue.ts +17 -0
  87. package/dist/runtime/table/components/filters/shared/FilterMatchModePanel.vue +47 -0
  88. package/dist/runtime/table/components/filters/shared/FilterMatchModePanel.vue.d.ts +17 -0
  89. package/dist/runtime/table/components/filters/shared/FilterOptionMultipleList.d.vue.ts +3 -1
  90. package/dist/runtime/table/components/filters/shared/FilterOptionMultipleList.vue +12 -3
  91. package/dist/runtime/table/components/filters/shared/FilterOptionMultipleList.vue.d.ts +3 -1
  92. package/dist/runtime/table/components/filters/shared/FilterOptionPickerContent.d.vue.ts +3 -0
  93. package/dist/runtime/table/components/filters/shared/FilterOptionPickerContent.vue +11 -1
  94. package/dist/runtime/table/components/filters/shared/FilterOptionPickerContent.vue.d.ts +3 -0
  95. package/dist/runtime/table/components/filters/shared/FilterOptionRow.d.vue.ts +3 -0
  96. package/dist/runtime/table/components/filters/shared/FilterOptionRow.vue +48 -9
  97. package/dist/runtime/table/components/filters/shared/FilterOptionRow.vue.d.ts +3 -0
  98. package/dist/runtime/table/components/filters/shared/FilterOptionSingleList.d.vue.ts +3 -0
  99. package/dist/runtime/table/components/filters/shared/FilterOptionSingleList.vue +35 -9
  100. package/dist/runtime/table/components/filters/shared/FilterOptionSingleList.vue.d.ts +3 -0
  101. package/dist/runtime/table/components/filters/shared/FilterOptionTreeContent.d.vue.ts +3 -0
  102. package/dist/runtime/table/components/filters/shared/FilterOptionTreeContent.vue +77 -20
  103. package/dist/runtime/table/components/filters/shared/FilterOptionTreeContent.vue.d.ts +3 -0
  104. package/dist/runtime/table/components/filters/shared/FilterPopoverShell.d.vue.ts +27 -0
  105. package/dist/runtime/table/components/filters/shared/FilterPopoverShell.vue +36 -0
  106. package/dist/runtime/table/components/filters/shared/FilterPopoverShell.vue.d.ts +27 -0
  107. package/dist/runtime/table/components/filters/shared/FilterSearchablePanel.d.vue.ts +3 -0
  108. package/dist/runtime/table/components/filters/shared/FilterSearchablePanel.vue +40 -10
  109. package/dist/runtime/table/components/filters/shared/FilterSearchablePanel.vue.d.ts +3 -0
  110. package/dist/runtime/table/components/filters/shared/FilterStageTransition.d.vue.ts +23 -0
  111. package/dist/runtime/table/components/filters/shared/FilterStageTransition.vue +21 -0
  112. package/dist/runtime/table/components/filters/shared/FilterStageTransition.vue.d.ts +23 -0
  113. package/dist/runtime/table/components/filters/shared/FilterTriggerTag.d.vue.ts +3 -2
  114. package/dist/runtime/table/components/filters/shared/FilterTriggerTag.vue +26 -25
  115. package/dist/runtime/table/components/filters/shared/FilterTriggerTag.vue.d.ts +3 -2
  116. package/dist/runtime/table/components/filters/tags/BooleanFilterTag.d.vue.ts +1 -1
  117. package/dist/runtime/table/components/filters/tags/BooleanFilterTag.vue +75 -22
  118. package/dist/runtime/table/components/filters/tags/BooleanFilterTag.vue.d.ts +1 -1
  119. package/dist/runtime/table/components/filters/tags/DateFilterTag.d.vue.ts +3 -1
  120. package/dist/runtime/table/components/filters/tags/DateFilterTag.vue +254 -160
  121. package/dist/runtime/table/components/filters/tags/DateFilterTag.vue.d.ts +3 -1
  122. package/dist/runtime/table/components/filters/tags/FilterTagsBar.d.vue.ts +7 -6
  123. package/dist/runtime/table/components/filters/tags/FilterTagsBar.vue +12 -27
  124. package/dist/runtime/table/components/filters/tags/FilterTagsBar.vue.d.ts +7 -6
  125. package/dist/runtime/table/components/filters/tags/NumberFilterTag.d.vue.ts +3 -2
  126. package/dist/runtime/table/components/filters/tags/NumberFilterTag.vue +192 -106
  127. package/dist/runtime/table/components/filters/tags/NumberFilterTag.vue.d.ts +3 -2
  128. package/dist/runtime/table/components/filters/tags/OptionFilterTag.d.vue.ts +3 -2
  129. package/dist/runtime/table/components/filters/tags/OptionFilterTag.vue +110 -48
  130. package/dist/runtime/table/components/filters/tags/OptionFilterTag.vue.d.ts +3 -2
  131. package/dist/runtime/table/components/filters/tags/TextFilterTag.d.vue.ts +3 -2
  132. package/dist/runtime/table/components/filters/tags/TextFilterTag.vue +120 -57
  133. package/dist/runtime/table/components/filters/tags/TextFilterTag.vue.d.ts +3 -2
  134. package/dist/runtime/table/components/grid/GridRenderer.vue +1 -1
  135. package/dist/runtime/table/components/layout/TableFooter.vue +1 -0
  136. package/dist/runtime/table/components/table/TableRenderer.vue +18 -9
  137. package/dist/runtime/table/composables/use-filter-tag-session.d.ts +3 -4
  138. package/dist/runtime/table/composables/use-filter-tag-session.js +12 -32
  139. package/dist/runtime/table/composables/use-query-state.d.ts +3 -3
  140. package/dist/runtime/table/composables/use-query-state.js +7 -32
  141. package/dist/runtime/table/composables/use-table-api.js +2 -2
  142. package/dist/runtime/table/composables/use-table-columns.d.ts +1 -11
  143. package/dist/runtime/table/composables/use-table-filter-presentation.d.ts +0 -3
  144. package/dist/runtime/table/composables/use-table-filter-presentation.js +0 -9
  145. package/dist/runtime/table/composables/use-table-filters.js +18 -7
  146. package/dist/runtime/table/composables/use-table-internals.d.ts +3 -42
  147. package/dist/runtime/table/types/actions.d.ts +1 -1
  148. package/dist/runtime/table/types/data-list.d.ts +41 -8
  149. package/dist/runtime/table/types/filters.d.ts +4 -0
  150. package/dist/runtime/table/utils/actions.d.ts +1 -1
  151. package/dist/runtime/table/utils/columns/menu.d.ts +2 -11
  152. package/dist/runtime/table/utils/columns/render.d.ts +2 -9
  153. package/dist/runtime/table/utils/columns/render.js +4 -4
  154. package/dist/runtime/table/utils/data-list-ui.d.ts +3 -1
  155. package/dist/runtime/table/utils/data-list-ui.js +45 -0
  156. package/dist/runtime/table/utils/filters/editor-size.d.ts +11 -0
  157. package/dist/runtime/table/utils/filters/editor-size.js +45 -0
  158. package/dist/runtime/table/utils/filters/index.d.ts +1 -1
  159. package/dist/runtime/table/utils/filters/index.js +1 -1
  160. package/dist/runtime/table/utils/query-state.d.ts +18 -1
  161. package/dist/runtime/table/utils/query-state.js +109 -0
  162. package/package.json +4 -2
  163. package/dist/runtime/table/utils/filters/operators.d.ts +0 -4
  164. package/dist/runtime/table/utils/filters/operators.js +0 -6
@@ -1,35 +1,15 @@
1
1
  <script setup>
2
2
  import UFieldGroup from "@nuxt/ui/components/FieldGroup.vue";
3
- import { computed } from "vue";
4
3
  import FormFieldRenderer from "../../components/renderer/FormFieldRenderer.vue";
5
4
  import FormFieldShell from "../../components/renderer/FormFieldShell.vue";
6
- import { fieldPath } from "../../composables/use-form-runtime";
7
5
  import { useFormUi } from "../../composables/use-form-ui";
8
6
  import { mergeFormUiClass } from "../../utils/ui";
9
- import NumberField from "../number/component.vue";
10
- import PasswordField from "../password/component.vue";
11
- import SelectField from "../select/component.vue";
12
- import TextField from "../text/component.vue";
13
7
  const props = defineProps({
14
8
  field: { type: Object, required: true },
15
9
  path: { type: Array, required: true },
16
10
  parentPath: { type: Array, required: true }
17
11
  });
18
12
  const formUi = useFormUi();
19
- const groupChildren = computed(
20
- () => props.field.fields.map((child) => ({
21
- field: child,
22
- path: fieldPath(props.parentPath, child),
23
- renderer: getGroupedRenderer(child)
24
- }))
25
- );
26
- function getGroupedRenderer(field) {
27
- if (field.type === "text") return TextField;
28
- if (field.type === "password") return PasswordField;
29
- if (field.type === "number") return NumberField;
30
- if (field.type === "select") return SelectField;
31
- return null;
32
- }
33
13
  </script>
34
14
 
35
15
  <template>
@@ -40,16 +20,13 @@ function getGroupedRenderer(field) {
40
20
  mergeFormUiClass('w-full', formUi.ui.value.group?.ui?.root, formUi.ui.value.group?.ui?.base)
41
21
  "
42
22
  >
43
- <template v-for="child in groupChildren" :key="child.field.key">
44
- <component
45
- :is="child.renderer"
46
- v-if="child.renderer"
47
- :field="child.field"
48
- :path="child.path"
49
- bare
50
- />
51
- <FormFieldRenderer v-else :field="child.field" :parent-path="parentPath" />
52
- </template>
23
+ <FormFieldRenderer
24
+ v-for="child in field.fields"
25
+ :key="child.key"
26
+ :field="child"
27
+ :parent-path="parentPath"
28
+ bare
29
+ />
53
30
  </UFieldGroup>
54
31
  </FormFieldShell>
55
32
  </template>
@@ -1,5 +1,5 @@
1
1
  <script setup>
2
- import { computed } from "vue";
2
+ import { computed, useId } from "vue";
3
3
  import FormFieldRenderer from "../../components/renderer/FormFieldRenderer.vue";
4
4
  import FormFieldShell from "../../components/renderer/FormFieldShell.vue";
5
5
  import { useFormUi } from "../../composables/use-form-ui";
@@ -10,10 +10,11 @@ const props = defineProps({
10
10
  path: { type: Array, required: true }
11
11
  });
12
12
  const formUi = useFormUi();
13
+ const matrixInstanceId = useId();
13
14
  const visibleFields = computed(
14
15
  () => props.field.fields.filter((field) => field.type !== "hidden" && field.ignore !== true)
15
16
  );
16
- const matrixId = computed(() => props.path.join("-"));
17
+ const matrixId = computed(() => `${matrixInstanceId}-${props.path.join("-")}`);
17
18
  const minWidth = computed(
18
19
  () => typeof props.field.minWidth === "number" ? `${props.field.minWidth}px` : props.field.minWidth ?? "40rem"
19
20
  );
@@ -118,7 +119,12 @@ function controlClass(field) {
118
119
  "
119
120
  >
120
121
  <div :class="controlClass(column)">
121
- <FormFieldRenderer :field="column" :parent-path="rowPath(row)" bare />
122
+ <FormFieldRenderer
123
+ :field="column"
124
+ :parent-path="rowPath(row)"
125
+ :control-labelledby="`${rowLabelId(row)} ${columnLabelId(column)}`"
126
+ bare
127
+ />
122
128
  </div>
123
129
  </td>
124
130
  </tr>
@@ -4,7 +4,7 @@ import UIcon from "@nuxt/ui/components/Icon.vue";
4
4
  import UInput from "@nuxt/ui/components/Input.vue";
5
5
  import USelectMenu from "@nuxt/ui/components/SelectMenu.vue";
6
6
  import { getCountries, getCountryCallingCode, parsePhoneNumberFromString } from "libphonenumber-js";
7
- import { computed, onMounted, ref, watch } from "vue";
7
+ import { computed, onMounted, ref, useId, watch } from "vue";
8
8
  import { useUiToolsLocale } from "../../../i18n/use-locale";
9
9
  import FormFieldShell from "../../components/renderer/FormFieldShell.vue";
10
10
  import { useFieldControl } from "../../composables/use-field-control";
@@ -14,7 +14,7 @@ const props = defineProps({
14
14
  field: { type: Object, required: true },
15
15
  path: { type: Array, required: true }
16
16
  });
17
- const { locale } = useUiToolsLocale();
17
+ const { locale, t } = useUiToolsLocale();
18
18
  const { form, controlProps, disabled, handleBlur, placeholder } = useFieldControl(
19
19
  () => props.field,
20
20
  () => props.path
@@ -25,6 +25,7 @@ const phoneValue = ref("");
25
25
  const syncingFromExternal = ref(false);
26
26
  const syncingToForm = ref(false);
27
27
  const mounted = ref(false);
28
+ const controlId = useId();
28
29
  const countryOptions = computed(
29
30
  () => getCountries().map((code) => createCountryOption(code)).filter((option) => isCountryAllowed(option)).sort((left, right) => left.label.localeCompare(right.label))
30
31
  );
@@ -140,6 +141,8 @@ function toFlagEmoji(code) {
140
141
  >
141
142
  <USelectMenu
142
143
  v-model="countryCode"
144
+ :id="`${controlId}-country`"
145
+ :aria-label="t('form.fields.phone.country')"
143
146
  class="w-auto min-w-[5.75rem] shrink-0"
144
147
  value-key="value"
145
148
  label-key="label"
@@ -152,6 +155,8 @@ function toFlagEmoji(code) {
152
155
  <UInput
153
156
  v-model="phoneValue"
154
157
  v-bind="controlProps"
158
+ :id="`${controlId}-number`"
159
+ :aria-label="t('form.fields.phone.number')"
155
160
  class="min-w-0 flex-1"
156
161
  type="tel"
157
162
  :placeholder="placeholder"
@@ -1,12 +1,14 @@
1
1
  <script setup>
2
2
  import UIcon from "@nuxt/ui/components/Icon.vue";
3
3
  import { computed, ref } from "vue";
4
+ import { useUiToolsLocale } from "../../../i18n/use-locale";
4
5
  import FormFieldShell from "../../components/renderer/FormFieldShell.vue";
5
6
  import { useFieldControl } from "../../composables/use-field-control";
6
7
  const props = defineProps({
7
8
  field: { type: Object, required: true },
8
9
  path: { type: Array, required: true }
9
10
  });
11
+ const { t } = useUiToolsLocale();
10
12
  const { form, controlProps, disabled, handleBlur } = useFieldControl(
11
13
  () => props.field,
12
14
  () => props.path
@@ -30,17 +32,38 @@ function setRating(value) {
30
32
  }
31
33
  model.value = value;
32
34
  }
35
+ function moveRating(event) {
36
+ if (disabled.value) return;
37
+ const current = model.value ?? 0;
38
+ if (event.key === "Home") model.value = 1;
39
+ else if (event.key === "End") model.value = max.value;
40
+ else if (event.key === "ArrowRight" || event.key === "ArrowUp")
41
+ model.value = Math.min(max.value, current + 1);
42
+ else if (event.key === "ArrowLeft" || event.key === "ArrowDown")
43
+ model.value = Math.max(1, current - 1);
44
+ else return;
45
+ event.preventDefault();
46
+ }
33
47
  </script>
34
48
 
35
49
  <template>
36
50
  <FormFieldShell :field="field" :path="path">
37
- <div v-bind="controlProps" class="flex items-center gap-1" @blur="handleBlur">
51
+ <div
52
+ v-bind="controlProps"
53
+ role="radiogroup"
54
+ class="flex w-fit items-center gap-0.5 rounded-lg"
55
+ @blur="handleBlur"
56
+ @keydown="moveRating"
57
+ >
38
58
  <button
39
59
  v-for="value in max"
40
60
  :key="value"
41
61
  type="button"
42
62
  :disabled="disabled"
43
- :aria-pressed="model === value"
63
+ role="radio"
64
+ :aria-checked="model === value"
65
+ :aria-label="t('form.fields.rating.value', { value, max })"
66
+ :tabindex="model === value || !model && value === 1 ? 0 : -1"
44
67
  class="grid size-8 place-items-center rounded-md text-warning transition-colors hover:bg-elevated disabled:cursor-not-allowed disabled:opacity-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
45
68
  :class="value <= visualValue ? 'opacity-100' : 'opacity-40'"
46
69
  @mouseenter="hoverValue = value"
@@ -2,6 +2,7 @@
2
2
  import UButton from "@nuxt/ui/components/Button.vue";
3
3
  import USelectMenu from "@nuxt/ui/components/SelectMenu.vue";
4
4
  import { computed, ref } from "vue";
5
+ import { useUiToolsLocale } from "../../../i18n/use-locale";
5
6
  import FormFieldShell from "../../components/renderer/FormFieldShell.vue";
6
7
  import { useFieldControl } from "../../composables/use-field-control";
7
8
  const props = defineProps({
@@ -9,6 +10,7 @@ const props = defineProps({
9
10
  path: { type: Array, required: true },
10
11
  bare: { type: Boolean, required: false }
11
12
  });
13
+ const { t } = useUiToolsLocale();
12
14
  const { form, controlProps, disabled, handleBlur, options, placeholder } = useFieldControl(
13
15
  () => props.field,
14
16
  () => props.path
@@ -25,27 +27,29 @@ const model = computed({
25
27
  });
26
28
  const items = computed(() => [...options.items.value]);
27
29
  const createItem = computed(
28
- () => options.creatable.value ? props.field.createItem ?? { position: "bottom", when: "empty" } : false
30
+ () => options.creatable.value && props.field.createItem ? props.field.createItem : false
29
31
  );
30
- const showFooterActions = computed(() => options.refreshable.value || options.creatable.value);
32
+ const showExplicitCreate = computed(
33
+ () => options.creatable.value && !props.field.createItem
34
+ );
35
+ const showFooterActions = computed(() => options.refreshable.value || showExplicitCreate.value);
31
36
  const footerActionsClass = computed(
32
- () => options.refreshable.value && options.creatable.value ? "sm:grid-cols-2" : "grid-cols-1"
37
+ () => options.refreshable.value && showExplicitCreate.value ? "sm:grid-cols-2" : "grid-cols-1"
38
+ );
39
+ const createActionLabel = computed(
40
+ () => options.createLabel.value ?? t("form.fields.options.create")
33
41
  );
34
- const createActionLabel = computed(() => options.createLabel.value ?? "Create option");
35
42
  async function handleCreate(label) {
36
- const option = await options.create(label);
37
- if (!option) return;
38
- if (!options.selectCreatedOption.value) return;
39
- if (props.field.multiple) {
40
- const current = Array.isArray(model.value) ? model.value.filter(isOptionValue) : [];
41
- model.value = [...current, option.value];
42
- return;
43
- }
44
- model.value = option.value;
43
+ await options.create(label);
45
44
  }
46
45
  async function handleCreateAction() {
47
46
  await handleCreate(searchTerm.value);
48
47
  }
48
+ async function handleNativeCreate(label) {
49
+ const normalizedLabel = label.trim();
50
+ if (!normalizedLabel) return;
51
+ await handleCreate(normalizedLabel);
52
+ }
49
53
  async function refreshOptions() {
50
54
  await options.refresh();
51
55
  }
@@ -71,7 +75,7 @@ function isOptionValue(value) {
71
75
  :search-input="field.searchable ?? false"
72
76
  :clear="field.clearable === true"
73
77
  :create-item="createItem"
74
- @create="handleCreate"
78
+ @create="handleNativeCreate"
75
79
  @blur="handleBlur"
76
80
  >
77
81
  <template #content-bottom>
@@ -90,10 +94,10 @@ function isOptionValue(value) {
90
94
  :loading="options.pending.value || options.fetching.value"
91
95
  @click.stop="refreshOptions"
92
96
  >
93
- Refresh
97
+ {{ t("form.fields.options.refresh") }}
94
98
  </UButton>
95
99
  <UButton
96
- v-if="options.creatable.value"
100
+ v-if="showExplicitCreate"
97
101
  block
98
102
  size="xs"
99
103
  variant="ghost"
@@ -109,7 +113,9 @@ function isOptionValue(value) {
109
113
  </template>
110
114
 
111
115
  <template #create-item-label="{ item }">
112
- {{ options.creating.value ? "Creating..." : `Create "${item}"` }}
116
+ {{
117
+ options.creating.value ? t("form.fields.options.creating") : t("form.fields.options.createNamed", { label: item })
118
+ }}
113
119
  </template>
114
120
  </USelectMenu>
115
121
  <FormFieldShell v-else :field="field" :path="path">
@@ -128,7 +134,7 @@ function isOptionValue(value) {
128
134
  :search-input="field.searchable ?? false"
129
135
  :clear="field.clearable === true"
130
136
  :create-item="createItem"
131
- @create="handleCreate"
137
+ @create="handleNativeCreate"
132
138
  @blur="handleBlur"
133
139
  >
134
140
  <template #content-bottom>
@@ -147,10 +153,10 @@ function isOptionValue(value) {
147
153
  :loading="options.pending.value || options.fetching.value"
148
154
  @click.stop="refreshOptions"
149
155
  >
150
- Refresh
156
+ {{ t("form.fields.options.refresh") }}
151
157
  </UButton>
152
158
  <UButton
153
- v-if="options.creatable.value"
159
+ v-if="showExplicitCreate"
154
160
  block
155
161
  size="xs"
156
162
  variant="ghost"
@@ -166,7 +172,9 @@ function isOptionValue(value) {
166
172
  </template>
167
173
 
168
174
  <template #create-item-label="{ item }">
169
- {{ options.creating.value ? "Creating..." : `Create "${item}"` }}
175
+ {{
176
+ options.creating.value ? t("form.fields.options.creating") : t("form.fields.options.createNamed", { label: item })
177
+ }}
170
178
  </template>
171
179
  </USelectMenu>
172
180
  </FormFieldShell>
@@ -1,8 +1,9 @@
1
1
  <script setup>
2
2
  import USwitch from "@nuxt/ui/components/Switch.vue";
3
- import { computed } from "vue";
3
+ import { computed, useId } from "vue";
4
4
  import FormFieldShell from "../../components/renderer/FormFieldShell.vue";
5
5
  import { useFieldControl } from "../../composables/use-field-control";
6
+ import { formOptionKey } from "../../utils/options";
6
7
  const props = defineProps({
7
8
  field: { type: Object, required: true },
8
9
  path: { type: Array, required: true }
@@ -19,6 +20,7 @@ const model = computed({
19
20
  set: (value) => form.setValue(props.path, value)
20
21
  });
21
22
  const items = computed(() => [...options.items.value]);
23
+ const groupId = useId();
22
24
  function toggleOption(value, checked) {
23
25
  const current = model.value;
24
26
  if (checked) {
@@ -35,15 +37,18 @@ function isOptionValue(value) {
35
37
  <template>
36
38
  <FormFieldShell :field="field" :path="path">
37
39
  <div
38
- v-bind="controlProps"
40
+ role="group"
39
41
  class="grid gap-3"
40
42
  :class="field.orientation === 'horizontal' ? 'sm:flex sm:flex-wrap' : ''"
41
43
  >
42
44
  <USwitch
43
45
  v-for="item in items"
44
- :key="String(item.value)"
46
+ :id="`${groupId}:${formOptionKey(item.value)}`"
47
+ :key="formOptionKey(item.value)"
48
+ v-bind="controlProps"
45
49
  :model-value="model.includes(item.value)"
46
50
  :label="item.label"
51
+ :aria-label="item.label"
47
52
  :description="item.description"
48
53
  :disabled="disabled || item.disabled"
49
54
  :checked-icon="field.checkedIcon"
@@ -4,12 +4,14 @@ import UInput from "@nuxt/ui/components/Input.vue";
4
4
  import UPopover from "@nuxt/ui/components/Popover.vue";
5
5
  import USelect from "@nuxt/ui/components/Select.vue";
6
6
  import { computed, ref, watch } from "vue";
7
+ import { useUiToolsLocale } from "../../../i18n/use-locale";
7
8
  import FormFieldShell from "../../components/renderer/FormFieldShell.vue";
8
9
  import { useFieldControl } from "../../composables/use-field-control";
9
10
  const props = defineProps({
10
11
  field: { type: Object, required: true },
11
12
  path: { type: Array, required: true }
12
13
  });
14
+ const { t } = useUiToolsLocale();
13
15
  const { form, controlProps, disabled, handleBlur, placeholder } = useFieldControl(
14
16
  () => props.field,
15
17
  () => props.path
@@ -128,7 +130,7 @@ function padTimePart(value) {
128
130
  class="w-full"
129
131
  inputmode="numeric"
130
132
  leading-icon="i-lucide-clock"
131
- :placeholder="placeholder ?? 'HH:mm'"
133
+ :placeholder="placeholder ?? t('form.fields.time.format')"
132
134
  :disabled="disabled"
133
135
  @update:model-value="handleInput"
134
136
  @focus="handleFocus"
@@ -142,7 +144,7 @@ function padTimePart(value) {
142
144
  variant="ghost"
143
145
  size="xs"
144
146
  :disabled="disabled"
145
- aria-label="Clear time"
147
+ :aria-label="t('form.fields.time.clear')"
146
148
  @mousedown.prevent
147
149
  @click="clearTime"
148
150
  />
@@ -158,7 +160,7 @@ function padTimePart(value) {
158
160
  :items="hours"
159
161
  class="min-w-0"
160
162
  size="sm"
161
- placeholder="Hour"
163
+ :placeholder="t('form.fields.time.hour')"
162
164
  :portal="false"
163
165
  />
164
166
  <USelect
@@ -166,7 +168,7 @@ function padTimePart(value) {
166
168
  :items="minutes"
167
169
  class="min-w-0"
168
170
  size="sm"
169
- placeholder="Minute"
171
+ :placeholder="t('form.fields.time.minute')"
170
172
  :portal="false"
171
173
  />
172
174
  </div>
@@ -209,7 +211,7 @@ function padTimePart(value) {
209
211
  >
210
212
  </div>
211
213
  <p class="text-xs text-muted">
212
- {{ model ?? "HH:mm" }}
214
+ {{ model ?? t("form.fields.time.format") }}
213
215
  </p>
214
216
  </div>
215
217
  </template>
@@ -3,12 +3,14 @@ import UAlert from "@nuxt/ui/components/Alert.vue";
3
3
  import UButton from "@nuxt/ui/components/Button.vue";
4
4
  import UFileUpload from "@nuxt/ui/components/FileUpload.vue";
5
5
  import { computed, onScopeDispose, ref } from "vue";
6
+ import { useUiToolsLocale } from "../../../i18n/use-locale";
6
7
  import FormFieldShell from "../../components/renderer/FormFieldShell.vue";
7
8
  import { useFieldControl } from "../../composables/use-field-control";
8
9
  const props = defineProps({
9
10
  field: { type: Object, required: true },
10
11
  path: { type: Array, required: true }
11
12
  });
13
+ const { t } = useUiToolsLocale();
12
14
  const { form, controlProps, disabled, handleBlur, params } = useFieldControl(
13
15
  () => props.field,
14
16
  () => props.path
@@ -44,7 +46,7 @@ async function uploadFiles() {
44
46
  selectedFiles.value = null;
45
47
  } catch (error) {
46
48
  if (uploadRun.value !== run) return;
47
- uploadError.value = error instanceof Error ? error.message : "Upload failed.";
49
+ uploadError.value = error instanceof Error ? error.message : t("form.fields.upload.failed");
48
50
  } finally {
49
51
  if (uploadRun.value === run) uploadPending.value = false;
50
52
  }
@@ -105,7 +107,7 @@ function isFormObject(value) {
105
107
  :disabled="disabled || !files.length"
106
108
  @click="uploadFiles"
107
109
  >
108
- Upload
110
+ {{ t("form.fields.upload.upload") }}
109
111
  </UButton>
110
112
  <UButton
111
113
  v-if="uploadedValue"
@@ -116,7 +118,7 @@ function isFormObject(value) {
116
118
  :disabled="disabled"
117
119
  @click="removeUpload"
118
120
  >
119
- Remove
121
+ {{ t("form.fields.upload.remove") }}
120
122
  </UButton>
121
123
  </div>
122
124
 
@@ -1,4 +1,5 @@
1
1
  import type { ComputedRef } from 'vue';
2
+ import type { NestedPaths } from '../../shared/types/utils.js';
2
3
  import type { FormContextData } from './context.js';
3
4
  import type { FormMaybePromise, FormObject } from './utils.js';
4
5
  import type { FormValidationOptions } from './validation.js';
@@ -111,15 +112,20 @@ export interface FormFieldApi<TValue = unknown, TOption = unknown, TContext = Fo
111
112
  /**
112
113
  * Public form API available from form-level callbacks.
113
114
  */
114
- export interface FormApi {
115
+ export type FormFieldPath<TOutput = FormObject> = TOutput extends FormObject ? FormObject extends TOutput ? string : NestedPaths<TOutput> : string;
116
+ export interface FormApi<TOutput = FormObject> {
115
117
  /** Reads an internal form value by raw path. */
116
118
  get: (path: string) => unknown;
117
119
  /** Writes an internal form value by raw path. */
118
120
  set: (path: string, value: unknown) => void;
119
121
  /** Runs form validation. */
120
122
  validate: (options?: FormValidationOptions) => Promise<boolean>;
123
+ /** Sets an external error on a submitted output field. */
124
+ setError: (path: FormFieldPath<TOutput>, message: string) => void;
125
+ /** Clears one external field error, or every form error when no path is provided. */
126
+ clearError: (path?: FormFieldPath<TOutput>) => void;
121
127
  /** Focuses a mounted field by raw path. */
122
- focus: (path: string | readonly string[]) => Promise<boolean>;
128
+ focus: (path: FormFieldPath<TOutput> | readonly string[]) => Promise<boolean>;
123
129
  /** Submits the form through the configured submit lifecycle. */
124
130
  submit: () => Promise<void>;
125
131
  /** Resets the form to configured defaults. */
@@ -136,7 +142,7 @@ export interface FormSubmitHandlerParams<TOutput = FormObject> {
136
142
  /** Submitted output value after output transforms and omitted fields are applied. */
137
143
  formData: TOutput;
138
144
  /** Namespaced form API for state, validation, submit, and reset operations. */
139
- api: FormApi;
145
+ api: FormApi<TOutput>;
140
146
  }
141
147
  export type FormSubmitHandler<TOutput = FormObject, TSubmitData = unknown> = (params: FormSubmitHandlerParams<TOutput>) => FormMaybePromise<FormSubmitResult<TSubmitData>>;
142
148
  export interface FormSubmitHandlerResult<TSubmitData = unknown> {
@@ -2,7 +2,7 @@ import type { FormFieldCallback } from './callbacks.js';
2
2
  import type { FormField } from './field.js';
3
3
  import type { FormContainerLayout, FormItemLayout } from './layout.js';
4
4
  import type { FormTransformConfig } from './transform.js';
5
- import type { FormDynamic, FormMaybePromise, FormObject, FormText } from './utils.js';
5
+ import type { FormDynamic, FormMaybePromise, FormObject, FormRenderable, FormText } from './utils.js';
6
6
  import type { FormValidationConfig } from './validation.js';
7
7
  /**
8
8
  * Field kinds targeted by the current form runtime plan.
@@ -26,6 +26,8 @@ export interface FormStatefulFieldBase<TType extends FormFieldType, TValue, TCon
26
26
  description?: FormText;
27
27
  /** Optional right-side hint rendered by the field wrapper. */
28
28
  hint?: FormText;
29
+ /** Optional rich content rendered beside the label. Takes precedence over `hint`. */
30
+ labelExtra?: FormRenderable;
29
31
  /** Placeholder forwarded to controls that support placeholders. */
30
32
  placeholder?: FormText;
31
33
  /** Raw dependency paths read before evaluating callbacks. */
@@ -76,6 +76,8 @@ export interface FormRuntime {
76
76
  shouldRender: (field: FormField, path: readonly string[]) => boolean;
77
77
  validate: (options?: FormValidationOptions) => Promise<boolean>;
78
78
  validateCurrentStep: (options?: FormValidationOptions) => Promise<boolean>;
79
+ setError: (path: string | readonly string[], message: string) => void;
80
+ clearError: (path?: string | readonly string[]) => void;
79
81
  focusRequest: Ref<FormFocusRequest | null>;
80
82
  registerFieldElement: (path: string | readonly string[], element: HTMLElement) => () => void;
81
83
  focusField: (path: string | readonly string[]) => Promise<boolean>;
@@ -40,6 +40,7 @@ export interface FormFieldUi {
40
40
  description?: FormUiClass;
41
41
  error?: FormUiClass;
42
42
  hint?: FormUiClass;
43
+ labelExtra?: FormUiClass;
43
44
  help?: FormUiClass;
44
45
  body?: FormUiClass;
45
46
  content?: FormUiClass;
@@ -1,8 +1,11 @@
1
+ import { pathSegments } from "./path.js";
1
2
  export function createPublicFormApi(runtime) {
2
3
  return {
3
4
  get: runtime.getValue,
4
5
  set: runtime.setValue,
5
6
  validate: runtime.validate,
7
+ setError: (path, message) => runtime.setError(pathSegments(path), message),
8
+ clearError: (path) => runtime.clearError(path ? pathSegments(path) : void 0),
6
9
  focus: runtime.focusField,
7
10
  submit: async () => {
8
11
  await runtime.submit();
@@ -1,4 +1,9 @@
1
1
  import type { FormFieldCallbackParams, FormOptionItem, FormOptionValue } from '../types/index.js';
2
+ export interface FormOptionKeys {
3
+ value?: string;
4
+ label?: string;
5
+ children?: string;
6
+ }
2
7
  export interface ResolvedFormOption {
3
8
  value: FormOptionValue;
4
9
  label: string;
@@ -6,6 +11,10 @@ export interface ResolvedFormOption {
6
11
  disabled?: boolean;
7
12
  children?: readonly ResolvedFormOption[];
8
13
  }
9
- export declare function normalizeOptionItem(option: unknown): ResolvedFormOption;
10
- export declare function normalizeOptionItems(options: readonly unknown[] | undefined): ResolvedFormOption[];
14
+ export declare function normalizeOptionItem(option: unknown, keys?: FormOptionKeys): ResolvedFormOption;
15
+ export declare function normalizeOptionItems(options: readonly unknown[] | undefined, keys?: FormOptionKeys): ResolvedFormOption[];
16
+ export declare function formOptionKey(value: FormOptionValue): string;
17
+ export declare function flattenResolvedOptions(options: readonly ResolvedFormOption[]): readonly ResolvedFormOption[];
18
+ export declare function mergeResolvedOptions(...collections: readonly (readonly ResolvedFormOption[])[]): readonly ResolvedFormOption[];
19
+ export declare function normalizeOptionSelection(value: unknown, options: readonly ResolvedFormOption[]): any[] | FormOptionValue | null | undefined;
11
20
  export declare function resolveOptionSource(source: unknown, params: FormFieldCallbackParams): readonly FormOptionItem[];
@@ -1,14 +1,19 @@
1
1
  import { isRecord } from "./path.js";
2
- export function normalizeOptionItem(option) {
2
+ import { resolveFormText } from "./text.js";
3
+ export function normalizeOptionItem(option, keys = {}) {
3
4
  if (isRecord(option)) {
4
- const rawValue = typeof option.value === "undefined" ? option.key : option.value;
5
- const children = Array.isArray(option.children) ? option.children.map((child) => normalizeOptionItem(child)) : void 0;
5
+ const rawValue = readOptionProperty(option, keys.value ?? "value");
6
+ const fallbackValue = typeof rawValue === "undefined" ? option.key : rawValue;
7
+ const rawChildren = readOptionProperty(option, keys.children ?? "children");
8
+ const rawDescription = readOptionProperty(option, "description");
9
+ const children = Array.isArray(rawChildren) ? rawChildren.map((child) => normalizeOptionItem(child, keys)) : void 0;
10
+ const value = normalizeOptionValue(fallbackValue);
6
11
  return {
7
- value: normalizeOptionValue(rawValue),
8
- label: typeof option.label === "string" || typeof option.label === "number" ? String(option.label) : String(normalizeOptionValue(rawValue)),
9
- description: typeof option.description === "string" || typeof option.description === "number" ? String(option.description) : void 0,
12
+ value,
13
+ label: resolveFormText(readOptionProperty(option, keys.label ?? "label")) ?? String(value),
14
+ description: isFormText(rawDescription) ? resolveFormText(rawDescription) : void 0,
10
15
  disabled: option.disabled === true,
11
- children
16
+ ...children ? { children } : {}
12
17
  };
13
18
  }
14
19
  return {
@@ -16,8 +21,42 @@ export function normalizeOptionItem(option) {
16
21
  label: String(option)
17
22
  };
18
23
  }
19
- export function normalizeOptionItems(options) {
20
- return (options ?? []).map((option) => normalizeOptionItem(option));
24
+ export function normalizeOptionItems(options, keys = {}) {
25
+ return (options ?? []).map((option) => normalizeOptionItem(option, keys));
26
+ }
27
+ export function formOptionKey(value) {
28
+ return `${typeof value}:${String(value)}`;
29
+ }
30
+ export function flattenResolvedOptions(options) {
31
+ return options.flatMap((option) => [option, ...flattenResolvedOptions(option.children ?? [])]);
32
+ }
33
+ export function mergeResolvedOptions(...collections) {
34
+ const seen = /* @__PURE__ */ new Set();
35
+ return collections.flatMap(
36
+ (collection) => collection.filter((option) => {
37
+ const key = formOptionKey(option.value);
38
+ if (seen.has(key)) return false;
39
+ seen.add(key);
40
+ return true;
41
+ })
42
+ );
43
+ }
44
+ export function normalizeOptionSelection(value, options) {
45
+ const validKeys = new Set(
46
+ flattenResolvedOptions(options).map((option) => formOptionKey(option.value))
47
+ );
48
+ if (Array.isArray(value)) {
49
+ const selected = /* @__PURE__ */ new Set();
50
+ return value.filter((item) => {
51
+ if (!isOptionValue(item)) return false;
52
+ const key = formOptionKey(item);
53
+ if (!validKeys.has(key) || selected.has(key)) return false;
54
+ selected.add(key);
55
+ return true;
56
+ });
57
+ }
58
+ if (value === null || typeof value === "undefined") return value;
59
+ return isOptionValue(value) && validKeys.has(formOptionKey(value)) ? value : null;
21
60
  }
22
61
  export function resolveOptionSource(source, params) {
23
62
  if (!source) return [];
@@ -34,3 +73,12 @@ function normalizeOptionValue(value) {
34
73
  return value;
35
74
  return String(value);
36
75
  }
76
+ function readOptionProperty(option, key) {
77
+ return Object.getOwnPropertyDescriptor(option, key)?.value;
78
+ }
79
+ function isFormText(value) {
80
+ return typeof value === "string" || typeof value === "number" || typeof value === "function";
81
+ }
82
+ function isOptionValue(value) {
83
+ return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
84
+ }