nuxt-ui-tools 1.18.0 → 1.19.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 (74) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/form/components/page/form-page-header.d.vue.ts +3 -1
  3. package/dist/runtime/form/components/page/form-page-header.vue +44 -34
  4. package/dist/runtime/form/components/page/form-page-header.vue.d.ts +3 -1
  5. package/dist/runtime/form/components/page/form-page.vue +2 -1
  6. package/dist/runtime/form/components/utils/form-option-choice-label.d.vue.ts +16 -0
  7. package/dist/runtime/form/components/utils/form-option-choice-label.vue +37 -0
  8. package/dist/runtime/form/components/utils/form-option-choice-label.vue.d.ts +16 -0
  9. package/dist/runtime/form/components/utils/form-option-display-value.d.vue.ts +14 -0
  10. package/dist/runtime/form/components/utils/form-option-display-value.vue +20 -0
  11. package/dist/runtime/form/components/utils/form-option-display-value.vue.d.ts +14 -0
  12. package/dist/runtime/form/components/utils/form-option-item-label.d.vue.ts +9 -2
  13. package/dist/runtime/form/components/utils/form-option-item-label.vue +20 -13
  14. package/dist/runtime/form/components/utils/form-option-item-label.vue.d.ts +9 -2
  15. package/dist/runtime/form/components/utils/form-option-leading.d.vue.ts +23 -0
  16. package/dist/runtime/form/components/utils/form-option-leading.vue +41 -0
  17. package/dist/runtime/form/components/utils/form-option-leading.vue.d.ts +23 -0
  18. package/dist/runtime/form/components/utils/form-option-render.d.ts +11 -0
  19. package/dist/runtime/form/components/utils/form-option-render.js +5 -0
  20. package/dist/runtime/form/components/utils/form-option-suffix.d.vue.ts +8 -0
  21. package/dist/runtime/form/components/utils/form-option-suffix.vue +15 -0
  22. package/dist/runtime/form/components/utils/form-option-suffix.vue.d.ts +8 -0
  23. package/dist/runtime/form/components/utils/form-option-value-overlay.d.vue.ts +30 -0
  24. package/dist/runtime/form/components/utils/form-option-value-overlay.vue +44 -0
  25. package/dist/runtime/form/components/utils/form-option-value-overlay.vue.d.ts +30 -0
  26. package/dist/runtime/form/composables/use-choice-group-items.d.ts +27 -0
  27. package/dist/runtime/form/composables/use-choice-group-items.js +24 -0
  28. package/dist/runtime/form/composables/use-field-control.d.ts +5 -5
  29. package/dist/runtime/form/composables/use-form-layout.js +4 -3
  30. package/dist/runtime/form/composables/use-option-menu.d.ts +20 -0
  31. package/dist/runtime/form/composables/use-option-menu.js +41 -0
  32. package/dist/runtime/form/fields/auto-complete/component.vue +92 -51
  33. package/dist/runtime/form/fields/auto-complete/config.d.ts +1 -0
  34. package/dist/runtime/form/fields/auto-complete/config.js +1 -1
  35. package/dist/runtime/form/fields/auto-complete/types.d.ts +2 -2
  36. package/dist/runtime/form/fields/checkbox-card/component.vue +3 -3
  37. package/dist/runtime/form/fields/checkbox-card/types.d.ts +3 -3
  38. package/dist/runtime/form/fields/checkbox-group/component.vue +18 -4
  39. package/dist/runtime/form/fields/checkbox-group/types.d.ts +2 -2
  40. package/dist/runtime/form/fields/choice-card/types.d.ts +3 -22
  41. package/dist/runtime/form/fields/choice-card/utils.d.ts +0 -17
  42. package/dist/runtime/form/fields/choice-card/utils.js +0 -11
  43. package/dist/runtime/form/fields/index.d.ts +2 -0
  44. package/dist/runtime/form/fields/radio/component.vue +19 -2
  45. package/dist/runtime/form/fields/radio/types.d.ts +2 -2
  46. package/dist/runtime/form/fields/radio-card/component.vue +3 -3
  47. package/dist/runtime/form/fields/radio-card/types.d.ts +3 -3
  48. package/dist/runtime/form/fields/select/component.vue +56 -58
  49. package/dist/runtime/form/fields/select/config.d.ts +1 -0
  50. package/dist/runtime/form/fields/select/config.js +1 -1
  51. package/dist/runtime/form/fields/select/types.d.ts +2 -2
  52. package/dist/runtime/form/types/layout.d.ts +6 -0
  53. package/dist/runtime/form/types/options.d.ts +137 -0
  54. package/dist/runtime/form/types/ui.d.ts +5 -0
  55. package/dist/runtime/form/utils/options.d.ts +37 -13
  56. package/dist/runtime/form/utils/options.js +71 -0
  57. package/dist/runtime/form/utils/page-surface.d.ts +5 -0
  58. package/dist/runtime/form/utils/page-surface.js +1 -0
  59. package/dist/runtime/spreadsheet/components/internal/spreadsheet-import-column-row.d.vue.ts +2 -2
  60. package/dist/runtime/spreadsheet/components/internal/spreadsheet-import-column-row.vue.d.ts +2 -2
  61. package/dist/runtime/table/components/filters/tags/boolean-filter-tag.d.vue.ts +2 -2
  62. package/dist/runtime/table/components/filters/tags/boolean-filter-tag.vue.d.ts +2 -2
  63. package/dist/runtime/table/components/filters/tags/date-filter-tag.d.vue.ts +2 -2
  64. package/dist/runtime/table/components/filters/tags/date-filter-tag.vue.d.ts +2 -2
  65. package/dist/runtime/table/components/filters/tags/number-filter-tag.d.vue.ts +2 -2
  66. package/dist/runtime/table/components/filters/tags/number-filter-tag.vue.d.ts +2 -2
  67. package/dist/runtime/table/components/filters/tags/option-filter-tag.d.vue.ts +2 -2
  68. package/dist/runtime/table/components/filters/tags/option-filter-tag.vue.d.ts +2 -2
  69. package/dist/runtime/table/components/filters/tags/text-filter-tag.d.vue.ts +2 -2
  70. package/dist/runtime/table/components/filters/tags/text-filter-tag.vue.d.ts +2 -2
  71. package/dist/runtime/table/types/remote-options.d.ts +4 -1
  72. package/dist/runtime/table/utils/data-list-ui.d.ts +1 -1
  73. package/dist/runtime/table/utils/remote-table-options.js +1 -1
  74. package/package.json +1 -1
@@ -0,0 +1,27 @@
1
+ import type { FormControlUi, FormOptionRuntimeState, ResolvedFormOption } from '../types/index.js';
2
+ /**
3
+ * Items for the Nuxt UI radio and checkbox groups of a field whose options carry more than a
4
+ * label. The group gets the plain parts of each option only: the leading visual, the suffix and a
5
+ * custom render are drawn in its label slot (`optionOf` finds the resolved option behind a slot
6
+ * item), and the description sits under the label, lined up with it. Nothing else of the option
7
+ * reaches the group, whose own `icon` is the check mark of a checkbox.
8
+ */
9
+ export declare function useChoiceGroupItems<TValue extends string | ResolvedFormOption['value']>(params: {
10
+ /** True when a `renderOption` function owns the content of the options. */
11
+ custom: () => boolean;
12
+ options: FormOptionRuntimeState;
13
+ ui: () => FormControlUi | undefined;
14
+ /** The value the group knows an option by. */
15
+ valueOf: (option: ResolvedFormOption) => TValue;
16
+ }): {
17
+ items: import("vue").ComputedRef<{
18
+ description: string | undefined;
19
+ disabled: boolean | undefined;
20
+ label: string;
21
+ ui: {
22
+ description: string;
23
+ } | undefined;
24
+ value: TValue;
25
+ }[]>;
26
+ optionOf: (key: string) => ResolvedFormOption;
27
+ };
@@ -0,0 +1,24 @@
1
+ import { computed } from "vue";
2
+ import { formOptionKey, resolveOptionDescriptionIndent } from "../utils/options.js";
3
+ import { mergeFormUiClass } from "../utils/ui.js";
4
+ export function useChoiceGroupItems(params) {
5
+ const optionByKey = computed(
6
+ () => new Map(params.options.items.value.map((option) => [formOptionKey(option.value), option]))
7
+ );
8
+ const items = computed(
9
+ () => params.options.items.value.map((option) => {
10
+ const indent = params.custom() ? void 0 : resolveOptionDescriptionIndent(option);
11
+ return {
12
+ description: params.custom() ? void 0 : option.description,
13
+ disabled: option.disabled,
14
+ label: option.label,
15
+ ui: indent ? { description: mergeFormUiClass(params.ui()?.description, indent) } : void 0,
16
+ value: params.valueOf(option)
17
+ };
18
+ })
19
+ );
20
+ function optionOf(key) {
21
+ return optionByKey.value.get(key) ?? { label: key, value: key };
22
+ }
23
+ return { items, optionOf };
24
+ }
@@ -28,17 +28,17 @@ export declare function useFieldControl<TField extends FormField>(field: () => T
28
28
  interactionOwnerClass: import("vue").ComputedRef<string>;
29
29
  options: {
30
30
  activate: () => void;
31
- add: (option: FormValue) => import("../utils/options.js").ResolvedFormOption;
31
+ add: (option: FormValue) => import("../types/index.js").ResolvedFormOption;
32
32
  creatable: import("vue").ComputedRef<boolean>;
33
- create: (label?: string) => Promise<import("../utils/options.js").ResolvedFormOption | null>;
33
+ create: (label?: string) => Promise<import("../types/index.js").ResolvedFormOption | null>;
34
34
  createLabel: import("vue").ComputedRef<string | undefined>;
35
35
  creating: import("vue").ComputedRef<boolean>;
36
36
  disableOnLoading: import("vue").ComputedRef<boolean>;
37
37
  error: import("vue").ComputedRef<unknown>;
38
38
  fetching: import("vue").ComputedRef<boolean>;
39
39
  hasMore: import("vue").ComputedRef<boolean>;
40
- items: import("vue").ComputedRef<readonly import("../utils/options.js").ResolvedFormOption[]>;
41
- loadChildren: (option: import("../utils/options.js").ResolvedFormOption) => Promise<void>;
40
+ items: import("vue").ComputedRef<readonly import("../types/index.js").ResolvedFormOption[]>;
41
+ loadChildren: (option: import("../types/index.js").ResolvedFormOption) => Promise<void>;
42
42
  loadMore: () => Promise<void>;
43
43
  loading: import("vue").ComputedRef<boolean>;
44
44
  loadingMore: import("vue").ComputedRef<boolean>;
@@ -51,7 +51,7 @@ export declare function useFieldControl<TField extends FormField>(field: () => T
51
51
  retryable: import("vue").ComputedRef<boolean>;
52
52
  search: import("vue").ComputedRef<string>;
53
53
  selectCreatedOption: import("vue").ComputedRef<boolean>;
54
- selectedItems: import("vue").ComputedRef<readonly import("../utils/options.js").ResolvedFormOption[]>;
54
+ selectedItems: import("vue").ComputedRef<readonly import("../types/index.js").ResolvedFormOption[]>;
55
55
  setSearch: (term: string) => void;
56
56
  };
57
57
  params: import("vue").ComputedRef<import("../types/index.js").FormFieldCallbackParams<{}, import("../types/index.js").FormDependencies, unknown, unknown>>;
@@ -30,9 +30,10 @@ export function useFormItemLayout(params) {
30
30
  ),
31
31
  normalizeFormGridColumnSpanStyle
32
32
  );
33
- const style = computed(
34
- () => span.value ?? normalizeFormGridColumnSpanStyle(String(FORM_LAYOUT_DEFAULTS.fieldSpan))
35
- );
33
+ const style = computed(() => {
34
+ const spanStyle = span.value ?? normalizeFormGridColumnSpanStyle(String(FORM_LAYOUT_DEFAULTS.fieldSpan));
35
+ return params.layout()?.newRow ? `${spanStyle}; grid-column-start: 1` : spanStyle;
36
+ });
36
37
  return {
37
38
  span,
38
39
  style
@@ -0,0 +1,20 @@
1
+ import type { FormAutoCompleteField, FormOptionRuntimeState, FormOptionValue, FormSelectField, ResolvedFormOption } from '../types/index.js';
2
+ /**
3
+ * What the select and the auto-complete share around the options they list in a Nuxt UI menu: the
4
+ * menu items (grouped when the options carry headings), which option is selected, and the option
5
+ * the closed control shows with its leading visual and its custom value.
6
+ */
7
+ export declare function useOptionMenu(params: {
8
+ field: () => FormSelectField | FormAutoCompleteField;
9
+ model: () => FormOptionValue | readonly FormOptionValue[] | null | undefined;
10
+ multiple: () => boolean;
11
+ options: FormOptionRuntimeState;
12
+ loadMoreLabel: () => string;
13
+ }): {
14
+ customValueOption: import("vue").ComputedRef<ResolvedFormOption | undefined>;
15
+ isSelected: (option: ResolvedFormOption) => boolean;
16
+ items: import("vue").ComputedRef<import("../utils/options.js").OptionMenuItem[] | import("../utils/options.js").OptionMenuItem[][]>;
17
+ leadingOption: import("vue").ComputedRef<ResolvedFormOption | undefined>;
18
+ renderOption: import("vue").ComputedRef<((context: import("../types/index.js").FormOptionRenderContext<import("../types/index.js").FormOptionItem<FormOptionValue>>) => import("vue").VNodeChild) | undefined>;
19
+ renderValue: import("vue").ComputedRef<((context: import("../types/index.js").FormOptionValueRenderContext<import("../types/index.js").FormOptionItem<FormOptionValue>>) => import("vue").VNodeChild) | undefined>;
20
+ };
@@ -0,0 +1,41 @@
1
+ import { computed } from "vue";
2
+ import { formOptionKey, resolveOptionLeading, toMenuItems } from "../utils/options.js";
3
+ export function useOptionMenu(params) {
4
+ const renderOption = computed(() => params.field().renderOption);
5
+ const renderValue = computed(() => params.field().renderValue);
6
+ const items = computed(
7
+ () => toMenuItems(params.options.items.value, {
8
+ label: params.loadMoreLabel(),
9
+ show: params.options.remote.value && (params.options.hasMore.value || params.options.loadingMore.value)
10
+ })
11
+ );
12
+ const selectedKeys = computed(() => {
13
+ const model = params.model();
14
+ const values = Array.isArray(model) ? model : [model];
15
+ return new Set(
16
+ values.filter((value) => value !== null && value !== void 0).map(formOptionKey)
17
+ );
18
+ });
19
+ const valueOption = computed(
20
+ () => params.multiple() ? void 0 : params.options.selectedItems.value[0]
21
+ );
22
+ const leadingOption = computed(() => {
23
+ const option = valueOption.value;
24
+ return option && !renderValue.value && resolveOptionLeading(option) ? option : void 0;
25
+ });
26
+ const customValueOption = computed(() => {
27
+ const option = valueOption.value;
28
+ return option && (renderValue.value || option.suffix) ? option : void 0;
29
+ });
30
+ function isSelected(option) {
31
+ return selectedKeys.value.has(formOptionKey(option.value));
32
+ }
33
+ return {
34
+ customValueOption,
35
+ isSelected,
36
+ items,
37
+ leadingOption,
38
+ renderOption,
39
+ renderValue
40
+ };
41
+ }
@@ -4,9 +4,12 @@ import { computed, ref, watch } from "vue";
4
4
  import { useUiToolsLocale } from "../../../i18n/use-locale";
5
5
  import FormFieldShell from "../../components/renderer/form-field-shell.vue";
6
6
  import FormOptionItemLabel from "../../components/utils/form-option-item-label.vue";
7
+ import FormOptionLeading from "../../components/utils/form-option-leading.vue";
7
8
  import FormOptionMenuFooter from "../../components/utils/form-option-menu-footer.vue";
9
+ import FormOptionSuffix from "../../components/utils/form-option-suffix.vue";
10
+ import FormOptionValueOverlay from "../../components/utils/form-option-value-overlay.vue";
8
11
  import { useFieldControl } from "../../composables/use-field-control";
9
- import { appendLoadMoreOption } from "../../utils/options";
12
+ import { useOptionMenu } from "../../composables/use-option-menu";
10
13
  import { isBoolean, isNumber, isString } from "../../utils/predicate";
11
14
  import { mergeFormUiClass } from "../../utils/ui";
12
15
  const props = defineProps({
@@ -30,6 +33,7 @@ const {
30
33
  { omit: ["createItem", "clearable"] }
31
34
  );
32
35
  const searchTerm = ref("");
36
+ const open = ref(false);
33
37
  const model = computed({
34
38
  get: () => {
35
39
  const value = form.getValue(props.path);
@@ -43,16 +47,27 @@ const model = computed({
43
47
  },
44
48
  set: (value) => form.setValue(props.path, value)
45
49
  });
46
- const items = computed(() => [
47
- ...appendLoadMoreOption(
48
- options.items.value,
49
- options.remote.value && (options.hasMore.value || options.loadingMore.value),
50
- t("form.fields.options.loadingMore")
51
- )
52
- ]);
50
+ const menu = useOptionMenu({
51
+ field: () => props.field,
52
+ loadMoreLabel: () => t("form.fields.options.loadingMore"),
53
+ model: () => model.value,
54
+ multiple: () => fieldProps.value.multiple === true,
55
+ options
56
+ });
57
+ const overlayActive = computed(
58
+ () => menu.customValueOption.value !== void 0 && !open.value && searchTerm.value === ""
59
+ );
53
60
  const controlUi = computed(() => ({
54
61
  ...controlProps.value.ui,
55
- content: mergeFormUiClass(controlProps.value.ui?.content, interactionOwnerClass.value)
62
+ base: mergeFormUiClass(
63
+ controlProps.value.ui?.base,
64
+ overlayActive.value ? "text-transparent" : void 0
65
+ ),
66
+ content: mergeFormUiClass(controlProps.value.ui?.content, interactionOwnerClass.value),
67
+ itemDescription: mergeFormUiClass(
68
+ menu.renderOption.value ? "hidden" : "text-[12.5px]",
69
+ controlProps.value.ui?.itemDescription
70
+ )
56
71
  }));
57
72
  const createItem = computed(
58
73
  () => options.creatable.value && fieldProps.value.createItem ? fieldProps.value.createItem : false
@@ -68,10 +83,9 @@ watch(searchTerm, (term) => {
68
83
  options.setSearch(term);
69
84
  }
70
85
  });
71
- function handleOpen(open) {
72
- if (open) {
73
- options.activate();
74
- }
86
+ function handleOpen(next) {
87
+ open.value = next;
88
+ if (next) options.activate();
75
89
  }
76
90
  async function handleCreate(label) {
77
91
  await options.create(label);
@@ -93,47 +107,74 @@ function isOptionValue(value) {
93
107
 
94
108
  <template>
95
109
  <FormFieldShell :field="field" :path="path">
96
- <UInputMenu
97
- v-model="model"
98
- v-model:search-term="searchTerm"
99
- v-bind="controlProps"
100
- class="w-full"
101
- value-key="value"
102
- label-key="label"
103
- :items="items"
104
- :multiple="fieldProps.multiple"
105
- :placeholder="placeholder"
106
- :disabled="disabled"
107
- :loading="
110
+ <FormOptionValueOverlay
111
+ :option="menu.customValueOption.value"
112
+ :render="menu.renderValue.value"
113
+ :active="overlayActive"
114
+ >
115
+ <UInputMenu
116
+ v-model="model"
117
+ v-model:search-term="searchTerm"
118
+ v-bind="controlProps"
119
+ class="w-full"
120
+ value-key="value"
121
+ label-key="label"
122
+ :items="menu.items.value"
123
+ :multiple="fieldProps.multiple"
124
+ :placeholder="placeholder"
125
+ :disabled="disabled"
126
+ :loading="
108
127
  validationPending || options.loading.value || options.fetching.value || options.creating.value
109
128
  "
110
- :clear="fieldProps.clearable === true"
111
- :ignore-filter="options.remote.value"
112
- :create-item="createItem"
113
- :ui="controlUi"
114
- @update:open="handleOpen"
115
- @create="handleNativeCreate"
116
- @blur="handleBlur"
117
- >
118
- <template #create-item-label="{ item }">
119
- {{
120
- options.creating.value ? t("form.fields.options.creating") : t("form.fields.options.createNamed", { label: item })
121
- }}
122
- </template>
129
+ :clear="fieldProps.clearable === true"
130
+ :ignore-filter="options.remote.value"
131
+ :create-item="createItem"
132
+ :ui="controlUi"
133
+ @update:open="handleOpen"
134
+ @create="handleNativeCreate"
135
+ @blur="handleBlur"
136
+ >
137
+ <template #create-item-label="{ item }">
138
+ {{
139
+ options.creating.value ? t("form.fields.options.creating") : t("form.fields.options.createNamed", { label: item })
140
+ }}
141
+ </template>
142
+
143
+ <template v-if="menu.leadingOption.value" #leading="{ ui }">
144
+ <FormOptionLeading
145
+ :option="menu.leadingOption.value"
146
+ :icon-class="ui.leadingIcon()"
147
+ :avatar-size="ui.itemLeadingAvatarSize()"
148
+ chip-class="size-5"
149
+ />
150
+ </template>
151
+
152
+ <template #item-leading="{ item }">
153
+ <FormOptionLeading :option="menu.renderOption.value ? void 0 : item" />
154
+ </template>
155
+
156
+ <template #item-label="{ item }">
157
+ <FormOptionItemLabel
158
+ :option="item"
159
+ :selected="menu.isSelected(item)"
160
+ :render="menu.renderOption.value"
161
+ />
162
+ </template>
123
163
 
124
- <template #item-label="{ item }">
125
- <FormOptionItemLabel :item="item" />
126
- </template>
164
+ <template #item-trailing="{ item }">
165
+ <FormOptionSuffix v-if="!menu.renderOption.value" :option="item" />
166
+ </template>
127
167
 
128
- <template #content-bottom>
129
- <FormOptionMenuFooter
130
- :options="options"
131
- :disabled="disabled"
132
- :show-create="showExplicitCreate"
133
- :create-label="createActionLabel"
134
- @create="handleCreateAction"
135
- />
136
- </template>
137
- </UInputMenu>
168
+ <template #content-bottom>
169
+ <FormOptionMenuFooter
170
+ :options="options"
171
+ :disabled="disabled"
172
+ :show-create="showExplicitCreate"
173
+ :create-label="createActionLabel"
174
+ @create="handleCreateAction"
175
+ />
176
+ </template>
177
+ </UInputMenu>
178
+ </FormOptionValueOverlay>
138
179
  </FormFieldShell>
139
180
  </template>
@@ -4,6 +4,7 @@ export declare const autoCompleteFieldKind: {
4
4
  };
5
5
  readonly options: {
6
6
  readonly enabled: true;
7
+ readonly groups: true;
7
8
  };
8
9
  readonly state: "stateful";
9
10
  readonly transform: true;
@@ -1,7 +1,7 @@
1
1
  import { defineFormFieldKind } from "../../utils/field-kind.js";
2
2
  export const autoCompleteFieldKind = defineFormFieldKind({
3
3
  layout: { item: true },
4
- options: { enabled: true },
4
+ options: { enabled: true, groups: true },
5
5
  state: "stateful",
6
6
  transform: true,
7
7
  type: "auto-complete",
@@ -1,13 +1,13 @@
1
1
  import type { FormStatefulFieldBase } from '../../types/field-base.js';
2
2
  import type { FieldOptionValue, FieldProps, FallbackNever, NullableValue } from '../../types/field-output-utils.js';
3
- import type { FormAnyOptionConfig, FormOptionItem, FormOptionValue } from '../../types/options.js';
3
+ import type { FormAnyOptionConfig, FormOptionGrouping, FormOptionItem, FormOptionValue, FormOptionValueRenderers } from '../../types/options.js';
4
4
  import type { FormSelectCreateItem } from '../select/types.js';
5
5
  export interface FormAutoCompleteProps {
6
6
  multiple?: boolean;
7
7
  clearable?: boolean;
8
8
  createItem?: FormSelectCreateItem;
9
9
  }
10
- export interface FormAutoCompleteField<TContext = NonNullable<unknown>, TDeps = NonNullable<unknown>, TValue extends FormOptionValue = FormOptionValue, TOption extends FormOptionItem<TValue> = FormOptionItem<TValue>> extends FormStatefulFieldBase<'auto-complete', TValue | readonly TValue[] | null, TContext, TDeps, FormAutoCompleteProps> {
10
+ export interface FormAutoCompleteField<TContext = NonNullable<unknown>, TDeps = NonNullable<unknown>, TValue extends FormOptionValue = FormOptionValue, TOption extends FormOptionItem<TValue> = FormOptionItem<TValue>> extends FormStatefulFieldBase<'auto-complete', TValue | readonly TValue[] | null, TContext, TDeps, FormAutoCompleteProps>, FormOptionGrouping, FormOptionValueRenderers<TOption> {
11
11
  options: FormAnyOptionConfig<TOption, TContext, TDeps, TValue | readonly TValue[] | null>;
12
12
  }
13
13
  type AutoCompleteFieldValue<TField> = FieldProps<TField> extends {
@@ -3,11 +3,11 @@ import UCheckboxGroup from "@nuxt/ui/components/CheckboxGroup.vue";
3
3
  import { computed } from "vue";
4
4
  import FormFieldShell from "../../components/renderer/form-field-shell.vue";
5
5
  import { useFieldControl } from "../../composables/use-field-control";
6
- import { formOptionKey } from "../../utils/options";
6
+ import { formOptionKey, groupOptions } from "../../utils/options";
7
7
  import { isBoolean, isNumber, isString, isUndefined } from "../../utils/predicate";
8
8
  import ChoiceCardLabel from "../choice-card/choice-card-label.vue";
9
9
  import { CHOICE_CARD_PROPS, useChoiceCard } from "../choice-card/use-choice-card";
10
- import { choiceCardGroupAttrs, groupChoiceCardItems } from "../choice-card/utils";
10
+ import { choiceCardGroupAttrs } from "../choice-card/utils";
11
11
  const props = defineProps({
12
12
  field: { type: Object, required: true },
13
13
  path: { type: Array, required: true }
@@ -45,7 +45,7 @@ const items = computed(
45
45
  value: formOptionKey(item.value)
46
46
  }))
47
47
  );
48
- const groups = computed(() => groupChoiceCardItems(items.value));
48
+ const groups = computed(() => groupOptions(items.value));
49
49
  function selectionOf(group) {
50
50
  return model.value.filter((key) => group.items.some((item) => item.value === key));
51
51
  }
@@ -1,8 +1,8 @@
1
1
  import type { FormStatefulFieldBase } from '../../types/field-base.js';
2
2
  import type { FieldOptionValue, NullableValue } from '../../types/field-output-utils.js';
3
- import type { FormOptionConfig, FormOptionValue, FormOptionsSource } from '../../types/options.js';
4
- import type { FormChoiceCardGrouping, FormChoiceCardOptionItem, FormChoiceCardProps } from '../choice-card/types.js';
5
- export interface FormCheckboxCardField<TContext = NonNullable<unknown>, TDeps = NonNullable<unknown>, TValue extends FormOptionValue = FormOptionValue, TOption extends FormChoiceCardOptionItem<TValue> = FormChoiceCardOptionItem<TValue>> extends FormStatefulFieldBase<'checkbox-card', readonly TValue[] | null, TContext, TDeps, FormChoiceCardProps>, FormChoiceCardGrouping {
3
+ import type { FormOptionConfig, FormOptionGrouping, FormOptionValue, FormOptionsSource } from '../../types/options.js';
4
+ import type { FormChoiceCardOptionItem, FormChoiceCardProps } from '../choice-card/types.js';
5
+ export interface FormCheckboxCardField<TContext = NonNullable<unknown>, TDeps = NonNullable<unknown>, TValue extends FormOptionValue = FormOptionValue, TOption extends FormChoiceCardOptionItem<TValue> = FormChoiceCardOptionItem<TValue>> extends FormStatefulFieldBase<'checkbox-card', readonly TValue[] | null, TContext, TDeps, FormChoiceCardProps>, FormOptionGrouping {
6
6
  options: FormOptionConfig<TOption, TContext, TDeps, readonly TValue[] | null> | FormOptionsSource<TOption, TContext, TDeps, readonly TValue[] | null>;
7
7
  }
8
8
  export type CheckboxCardFieldOutput<TField> = readonly FieldOptionValue<TField>[] | NullableValue;
@@ -2,6 +2,8 @@
2
2
  import UCheckboxGroup from "@nuxt/ui/components/CheckboxGroup.vue";
3
3
  import { computed } from "vue";
4
4
  import FormFieldShell from "../../components/renderer/form-field-shell.vue";
5
+ import FormOptionChoiceLabel from "../../components/utils/form-option-choice-label.vue";
6
+ import { useChoiceGroupItems } from "../../composables/use-choice-group-items";
5
7
  import { useFieldControl } from "../../composables/use-field-control";
6
8
  import { formOptionKey } from "../../utils/options";
7
9
  import { isBoolean, isNumber, isString, isUndefined } from "../../utils/predicate";
@@ -31,9 +33,13 @@ const model = computed({
31
33
  })
32
34
  )
33
35
  });
34
- const items = computed(
35
- () => options.items.value.map((item) => ({ ...item, value: formOptionKey(item.value) }))
36
- );
36
+ const renderOption = computed(() => props.field.renderOption);
37
+ const { items, optionOf } = useChoiceGroupItems({
38
+ custom: () => renderOption.value !== void 0,
39
+ options,
40
+ ui: () => controlProps.value.ui,
41
+ valueOf: (option) => formOptionKey(option.value)
42
+ });
37
43
  const groupUi = computed(() => ({
38
44
  ...controlProps.value.ui,
39
45
  fieldset: mergeFormUiClass(
@@ -65,6 +71,14 @@ function isOptionValue(value) {
65
71
  :indicator="fieldProps.indicator"
66
72
  :disabled="disabled"
67
73
  @blur="handleBlur"
68
- />
74
+ >
75
+ <template #label="{ item }">
76
+ <FormOptionChoiceLabel
77
+ :option="optionOf(item.value)"
78
+ :selected="model.includes(item.value)"
79
+ :render="renderOption"
80
+ />
81
+ </template>
82
+ </UCheckboxGroup>
69
83
  </FormFieldShell>
70
84
  </template>
@@ -1,6 +1,6 @@
1
1
  import type { FormStatefulFieldBase } from '../../types/field-base.js';
2
2
  import type { FieldOptionValue, NullableValue } from '../../types/field-output-utils.js';
3
- import type { FormOptionConfig, FormOptionItem, FormOptionValue, FormOptionsSource } from '../../types/options.js';
3
+ import type { FormOptionConfig, FormOptionItem, FormOptionRenderers, FormOptionValue, FormOptionsSource } from '../../types/options.js';
4
4
  export interface FormCheckboxGroupProps {
5
5
  /**
6
6
  * `list` (the default) lines up bare checkboxes, `card` frames each option on its own, and
@@ -10,7 +10,7 @@ export interface FormCheckboxGroupProps {
10
10
  orientation?: 'horizontal' | 'vertical';
11
11
  indicator?: 'start' | 'end' | 'hidden';
12
12
  }
13
- export interface FormCheckboxGroupField<TContext = NonNullable<unknown>, TDeps = NonNullable<unknown>, TValue extends FormOptionValue = FormOptionValue, TOption extends FormOptionItem<TValue> = FormOptionItem<TValue>> extends FormStatefulFieldBase<'checkbox-group', readonly TValue[] | null, TContext, TDeps, FormCheckboxGroupProps> {
13
+ export interface FormCheckboxGroupField<TContext = NonNullable<unknown>, TDeps = NonNullable<unknown>, TValue extends FormOptionValue = FormOptionValue, TOption extends FormOptionItem<TValue> = FormOptionItem<TValue>> extends FormStatefulFieldBase<'checkbox-group', readonly TValue[] | null, TContext, TDeps, FormCheckboxGroupProps>, FormOptionRenderers<TOption> {
14
14
  options: FormOptionConfig<TOption, TContext, TDeps, readonly TValue[] | null> | FormOptionsSource<TOption, TContext, TDeps, readonly TValue[] | null>;
15
15
  }
16
16
  export type CheckboxGroupFieldOutput<TField> = readonly FieldOptionValue<TField>[] | NullableValue;
@@ -46,29 +46,10 @@ export interface FormChoiceCardProps {
46
46
  density?: FormChoiceCardDensity;
47
47
  }
48
48
  /**
49
- * Option grouping of the radio and checkbox cards, declared on the field next to its `options`.
50
- *
51
- * @example
52
- * ```ts
53
- * // Rows from the API, each with a `section` to list the cards under.
54
- * { type: 'radio-card', options: () => queryOptions({ queryKey: ['plans'], queryFn: fetchPlans }), groupKey: 'section' }
55
- * ```
56
- */
57
- export interface FormChoiceCardGrouping {
58
- /**
59
- * Property of the options that holds the heading they are listed under. Options written inline
60
- * in the schema group by their `group` without it. The records a function, a promise or a query
61
- * returns group only when this names the property, so records that already carry a `group` for
62
- * another purpose are never regrouped by surprise. `false` lists every option together, even
63
- * inline options that carry a `group`.
64
- */
65
- groupKey?: string | false;
66
- }
67
- /**
68
- * Option of a radio or checkbox card: the shared option shape plus the icon and the heading the
69
- * cards can show.
49
+ * Option of a radio or checkbox card: the shared option shape plus the heading the cards can show.
50
+ * A card draws its `icon` only, so the avatar, chip and suffix of other option fields are left out.
70
51
  */
71
- export interface FormChoiceCardOption<TValue extends FormOptionValue = FormOptionValue> extends FormOption<TValue> {
52
+ export interface FormChoiceCardOption<TValue extends FormOptionValue = FormOptionValue> extends Omit<FormOption<TValue>, 'avatar' | 'chip' | 'suffix'> {
72
53
  /** Icon shown with the label, inline or in a tile (see `props.icon`). */
73
54
  icon?: string;
74
55
  /**
@@ -1,21 +1,4 @@
1
1
  import type { FormObject } from '../../types/index.js';
2
- export interface ChoiceCardGroup<TItem> {
3
- /** Stable key of the group: its heading, or an empty string for the options without one. */
4
- key: string;
5
- /** Heading shown above the cards of the group; none for options that declare no group. */
6
- heading?: string;
7
- /** The group's options, as the mutable list the Nuxt UI groups take. */
8
- items: TItem[];
9
- }
10
- /**
11
- * Lists options under their group headings. Headings follow the order in which they first appear
12
- * in `items`, and each keeps its options in their authored order. The options without a group form
13
- * one more group with no heading, placed where its first option appears. Without any group the
14
- * result is a single headingless group, so an ungrouped list renders as it always did.
15
- */
16
- export declare function groupChoiceCardItems<TItem extends {
17
- group?: string;
18
- }>(items: readonly TItem[]): readonly ChoiceCardGroup<TItem>[];
19
2
  /**
20
3
  * Attributes for the Nuxt UI group at `index` among the groups of one field. Every group carries
21
4
  * the control attributes of the field, but an `id` names a single element: the first group keeps
@@ -1,15 +1,4 @@
1
1
  import { isString } from "../../utils/predicate.js";
2
- export function groupChoiceCardItems(items) {
3
- const groups = /* @__PURE__ */ new Map();
4
- for (const item of items) {
5
- const key = item.group?.trim() ?? "";
6
- const members = groups.get(key);
7
- if (members) members.push(item);
8
- else groups.set(key, [item]);
9
- }
10
- if (groups.size === 0) return [{ items: [], key: "" }];
11
- return [...groups].map(([key, members]) => ({ heading: key || void 0, items: members, key }));
12
- }
13
2
  export function choiceCardGroupAttrs(attrs, index) {
14
3
  const id = attrs.id;
15
4
  if (index === 0 || !isString(id)) return attrs;
@@ -86,6 +86,7 @@ export declare const formFieldKinds: readonly [{
86
86
  };
87
87
  readonly options: {
88
88
  readonly enabled: true;
89
+ readonly groups: true;
89
90
  };
90
91
  readonly state: "stateful";
91
92
  readonly transform: true;
@@ -529,6 +530,7 @@ export declare const formFieldKinds: readonly [{
529
530
  };
530
531
  readonly options: {
531
532
  readonly enabled: true;
533
+ readonly groups: true;
532
534
  };
533
535
  readonly state: "stateful";
534
536
  readonly transform: true;
@@ -2,7 +2,10 @@
2
2
  import URadioGroup from "@nuxt/ui/components/RadioGroup.vue";
3
3
  import { computed } from "vue";
4
4
  import FormFieldShell from "../../components/renderer/form-field-shell.vue";
5
+ import FormOptionChoiceLabel from "../../components/utils/form-option-choice-label.vue";
6
+ import { useChoiceGroupItems } from "../../composables/use-choice-group-items";
5
7
  import { useFieldControl } from "../../composables/use-field-control";
8
+ import { formOptionKey } from "../../utils/options";
6
9
  import { isBoolean, isNumber, isString } from "../../utils/predicate";
7
10
  import { mergeFormUiClass } from "../../utils/ui";
8
11
  import { CARD_SELECTED_RING } from "../card-selection";
@@ -24,7 +27,13 @@ const model = computed({
24
27
  },
25
28
  set: (value) => form.setValue(props.path, value)
26
29
  });
27
- const items = computed(() => [...options.items.value]);
30
+ const renderOption = computed(() => props.field.renderOption);
31
+ const { items, optionOf } = useChoiceGroupItems({
32
+ custom: () => renderOption.value !== void 0,
33
+ options,
34
+ ui: () => controlProps.value.ui,
35
+ valueOf: (option) => option.value
36
+ });
28
37
  const groupUi = computed(() => ({
29
38
  ...controlProps.value.ui,
30
39
  item: mergeFormUiClass(
@@ -49,6 +58,14 @@ const groupUi = computed(() => ({
49
58
  :indicator="fieldProps.indicator"
50
59
  :disabled="disabled"
51
60
  @blur="handleBlur"
52
- />
61
+ >
62
+ <template #label="{ item }">
63
+ <FormOptionChoiceLabel
64
+ :option="optionOf(formOptionKey(item.value))"
65
+ :selected="model === item.value"
66
+ :render="renderOption"
67
+ />
68
+ </template>
69
+ </URadioGroup>
53
70
  </FormFieldShell>
54
71
  </template>
@@ -1,6 +1,6 @@
1
1
  import type { FormStatefulFieldBase } from '../../types/field-base.js';
2
2
  import type { FieldOptionValue, NullableValue } from '../../types/field-output-utils.js';
3
- import type { FormOptionItem, FormOptionValue, FormOptionsSource } from '../../types/options.js';
3
+ import type { FormOptionItem, FormOptionRenderers, FormOptionValue, FormOptionsSource } from '../../types/options.js';
4
4
  export interface FormRadioProps {
5
5
  /**
6
6
  * `list` (the default) lines up bare radios, `card` frames each option on its own, and `table`
@@ -11,7 +11,7 @@ export interface FormRadioProps {
11
11
  orientation?: 'horizontal' | 'vertical';
12
12
  indicator?: 'start' | 'end' | 'hidden';
13
13
  }
14
- export interface FormRadioField<TContext = NonNullable<unknown>, TDeps = NonNullable<unknown>, TValue extends FormOptionValue = FormOptionValue, TOption extends FormOptionItem<TValue> = FormOptionItem<TValue>> extends FormStatefulFieldBase<'radio', TValue | null, TContext, TDeps, FormRadioProps> {
14
+ export interface FormRadioField<TContext = NonNullable<unknown>, TDeps = NonNullable<unknown>, TValue extends FormOptionValue = FormOptionValue, TOption extends FormOptionItem<TValue> = FormOptionItem<TValue>> extends FormStatefulFieldBase<'radio', TValue | null, TContext, TDeps, FormRadioProps>, FormOptionRenderers<TOption> {
15
15
  options: FormOptionsSource<TOption, TContext, TDeps, TValue | null>;
16
16
  }
17
17
  export type RadioFieldOutput<TField> = FieldOptionValue<TField> | NullableValue;