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.
- package/dist/module.json +1 -1
- package/dist/runtime/form/components/page/form-page-header.d.vue.ts +3 -1
- package/dist/runtime/form/components/page/form-page-header.vue +44 -34
- package/dist/runtime/form/components/page/form-page-header.vue.d.ts +3 -1
- package/dist/runtime/form/components/page/form-page.vue +2 -1
- package/dist/runtime/form/components/utils/form-option-choice-label.d.vue.ts +16 -0
- package/dist/runtime/form/components/utils/form-option-choice-label.vue +37 -0
- package/dist/runtime/form/components/utils/form-option-choice-label.vue.d.ts +16 -0
- package/dist/runtime/form/components/utils/form-option-display-value.d.vue.ts +14 -0
- package/dist/runtime/form/components/utils/form-option-display-value.vue +20 -0
- package/dist/runtime/form/components/utils/form-option-display-value.vue.d.ts +14 -0
- package/dist/runtime/form/components/utils/form-option-item-label.d.vue.ts +9 -2
- package/dist/runtime/form/components/utils/form-option-item-label.vue +20 -13
- package/dist/runtime/form/components/utils/form-option-item-label.vue.d.ts +9 -2
- package/dist/runtime/form/components/utils/form-option-leading.d.vue.ts +23 -0
- package/dist/runtime/form/components/utils/form-option-leading.vue +41 -0
- package/dist/runtime/form/components/utils/form-option-leading.vue.d.ts +23 -0
- package/dist/runtime/form/components/utils/form-option-render.d.ts +11 -0
- package/dist/runtime/form/components/utils/form-option-render.js +5 -0
- package/dist/runtime/form/components/utils/form-option-suffix.d.vue.ts +8 -0
- package/dist/runtime/form/components/utils/form-option-suffix.vue +15 -0
- package/dist/runtime/form/components/utils/form-option-suffix.vue.d.ts +8 -0
- package/dist/runtime/form/components/utils/form-option-value-overlay.d.vue.ts +30 -0
- package/dist/runtime/form/components/utils/form-option-value-overlay.vue +44 -0
- package/dist/runtime/form/components/utils/form-option-value-overlay.vue.d.ts +30 -0
- package/dist/runtime/form/composables/use-choice-group-items.d.ts +27 -0
- package/dist/runtime/form/composables/use-choice-group-items.js +24 -0
- package/dist/runtime/form/composables/use-field-control.d.ts +5 -5
- package/dist/runtime/form/composables/use-form-layout.js +4 -3
- package/dist/runtime/form/composables/use-option-menu.d.ts +20 -0
- package/dist/runtime/form/composables/use-option-menu.js +41 -0
- package/dist/runtime/form/fields/auto-complete/component.vue +92 -51
- package/dist/runtime/form/fields/auto-complete/config.d.ts +1 -0
- package/dist/runtime/form/fields/auto-complete/config.js +1 -1
- package/dist/runtime/form/fields/auto-complete/types.d.ts +2 -2
- package/dist/runtime/form/fields/checkbox-card/component.vue +3 -3
- package/dist/runtime/form/fields/checkbox-card/types.d.ts +3 -3
- package/dist/runtime/form/fields/checkbox-group/component.vue +18 -4
- package/dist/runtime/form/fields/checkbox-group/types.d.ts +2 -2
- package/dist/runtime/form/fields/choice-card/types.d.ts +3 -22
- package/dist/runtime/form/fields/choice-card/utils.d.ts +0 -17
- package/dist/runtime/form/fields/choice-card/utils.js +0 -11
- package/dist/runtime/form/fields/index.d.ts +2 -0
- package/dist/runtime/form/fields/radio/component.vue +19 -2
- package/dist/runtime/form/fields/radio/types.d.ts +2 -2
- package/dist/runtime/form/fields/radio-card/component.vue +3 -3
- package/dist/runtime/form/fields/radio-card/types.d.ts +3 -3
- package/dist/runtime/form/fields/select/component.vue +56 -58
- package/dist/runtime/form/fields/select/config.d.ts +1 -0
- package/dist/runtime/form/fields/select/config.js +1 -1
- package/dist/runtime/form/fields/select/types.d.ts +2 -2
- package/dist/runtime/form/types/layout.d.ts +6 -0
- package/dist/runtime/form/types/options.d.ts +137 -0
- package/dist/runtime/form/types/ui.d.ts +5 -0
- package/dist/runtime/form/utils/options.d.ts +37 -13
- package/dist/runtime/form/utils/options.js +71 -0
- package/dist/runtime/form/utils/page-surface.d.ts +5 -0
- package/dist/runtime/form/utils/page-surface.js +1 -0
- package/dist/runtime/spreadsheet/components/internal/spreadsheet-import-column-row.d.vue.ts +2 -2
- package/dist/runtime/spreadsheet/components/internal/spreadsheet-import-column-row.vue.d.ts +2 -2
- package/dist/runtime/table/components/filters/tags/boolean-filter-tag.d.vue.ts +2 -2
- package/dist/runtime/table/components/filters/tags/boolean-filter-tag.vue.d.ts +2 -2
- package/dist/runtime/table/components/filters/tags/date-filter-tag.d.vue.ts +2 -2
- package/dist/runtime/table/components/filters/tags/date-filter-tag.vue.d.ts +2 -2
- package/dist/runtime/table/components/filters/tags/number-filter-tag.d.vue.ts +2 -2
- package/dist/runtime/table/components/filters/tags/number-filter-tag.vue.d.ts +2 -2
- package/dist/runtime/table/components/filters/tags/option-filter-tag.d.vue.ts +2 -2
- package/dist/runtime/table/components/filters/tags/option-filter-tag.vue.d.ts +2 -2
- package/dist/runtime/table/components/filters/tags/text-filter-tag.d.vue.ts +2 -2
- package/dist/runtime/table/components/filters/tags/text-filter-tag.vue.d.ts +2 -2
- package/dist/runtime/table/types/remote-options.d.ts +4 -1
- package/dist/runtime/table/utils/data-list-ui.d.ts +1 -1
- package/dist/runtime/table/utils/remote-table-options.js +1 -1
- package/package.json +1 -1
|
@@ -3,11 +3,11 @@ 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
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 } 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
|
|
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 }
|
|
@@ -40,7 +40,7 @@ const items = computed(
|
|
|
40
40
|
(item) => cards.compact.value ? { ...item, description: void 0 } : item
|
|
41
41
|
)
|
|
42
42
|
);
|
|
43
|
-
const groups = computed(() =>
|
|
43
|
+
const groups = computed(() => groupOptions(items.value));
|
|
44
44
|
function selectionOf(group) {
|
|
45
45
|
return group.items.some((item) => item.value === model.value) ? model.value : void 0;
|
|
46
46
|
}
|
|
@@ -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 { FormOptionValue, FormOptionsSource } from '../../types/options.js';
|
|
4
|
-
import type {
|
|
5
|
-
export interface FormRadioCardField<TContext = NonNullable<unknown>, TDeps = NonNullable<unknown>, TValue extends FormOptionValue = FormOptionValue, TOption extends FormChoiceCardOptionItem<TValue> = FormChoiceCardOptionItem<TValue>> extends FormStatefulFieldBase<'radio-card', TValue | null, TContext, TDeps, FormChoiceCardProps>,
|
|
3
|
+
import type { FormOptionGrouping, FormOptionValue, FormOptionsSource } from '../../types/options.js';
|
|
4
|
+
import type { FormChoiceCardOptionItem, FormChoiceCardProps } from '../choice-card/types.js';
|
|
5
|
+
export interface FormRadioCardField<TContext = NonNullable<unknown>, TDeps = NonNullable<unknown>, TValue extends FormOptionValue = FormOptionValue, TOption extends FormChoiceCardOptionItem<TValue> = FormChoiceCardOptionItem<TValue>> extends FormStatefulFieldBase<'radio-card', TValue | null, TContext, TDeps, FormChoiceCardProps>, FormOptionGrouping {
|
|
6
6
|
options: FormOptionsSource<TOption, TContext, TDeps, TValue | null>;
|
|
7
7
|
}
|
|
8
8
|
export type RadioCardFieldOutput<TField> = FieldOptionValue<TField> | NullableValue;
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import USelectMenu from "@nuxt/ui/components/SelectMenu.vue";
|
|
3
|
+
import { createReusableTemplate } from "@vueuse/core";
|
|
3
4
|
import { computed, ref, watch } from "vue";
|
|
4
5
|
import { useUiToolsLocale } from "../../../i18n/use-locale";
|
|
5
6
|
import FormFieldShell from "../../components/renderer/form-field-shell.vue";
|
|
7
|
+
import FormOptionDisplayValue from "../../components/utils/form-option-display-value.vue";
|
|
6
8
|
import FormOptionItemLabel from "../../components/utils/form-option-item-label.vue";
|
|
9
|
+
import FormOptionLeading from "../../components/utils/form-option-leading.vue";
|
|
7
10
|
import FormOptionMenuFooter from "../../components/utils/form-option-menu-footer.vue";
|
|
11
|
+
import FormOptionSuffix from "../../components/utils/form-option-suffix.vue";
|
|
8
12
|
import { useFieldControl } from "../../composables/use-field-control";
|
|
9
|
-
import {
|
|
13
|
+
import { useOptionMenu } from "../../composables/use-option-menu";
|
|
10
14
|
import { isBoolean, isNumber, isString } from "../../utils/predicate";
|
|
11
15
|
import { mergeFormUiClass } from "../../utils/ui";
|
|
12
16
|
const props = defineProps({
|
|
@@ -30,6 +34,7 @@ const {
|
|
|
30
34
|
() => props.path,
|
|
31
35
|
{ omit: ["createItem", "max", "searchable", "clearable"] }
|
|
32
36
|
);
|
|
37
|
+
const [DefineControl, ReuseControl] = createReusableTemplate();
|
|
33
38
|
const searchTerm = ref("");
|
|
34
39
|
const open = ref(false);
|
|
35
40
|
const model = computed({
|
|
@@ -51,16 +56,20 @@ const model = computed({
|
|
|
51
56
|
form.setValue(props.path, value);
|
|
52
57
|
}
|
|
53
58
|
});
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
const menu = useOptionMenu({
|
|
60
|
+
field: () => props.field,
|
|
61
|
+
loadMoreLabel: () => t("form.fields.options.loadingMore"),
|
|
62
|
+
model: () => model.value,
|
|
63
|
+
multiple: () => fieldProps.value.multiple === true,
|
|
64
|
+
options
|
|
65
|
+
});
|
|
61
66
|
const controlUi = computed(() => ({
|
|
62
67
|
...controlProps.value.ui,
|
|
63
|
-
content: mergeFormUiClass(controlProps.value.ui?.content, interactionOwnerClass.value)
|
|
68
|
+
content: mergeFormUiClass(controlProps.value.ui?.content, interactionOwnerClass.value),
|
|
69
|
+
itemDescription: mergeFormUiClass(
|
|
70
|
+
menu.renderOption.value ? "hidden" : "text-[12.5px]",
|
|
71
|
+
controlProps.value.ui?.itemDescription
|
|
72
|
+
)
|
|
64
73
|
}));
|
|
65
74
|
const createItem = computed(
|
|
66
75
|
() => options.creatable.value && fieldProps.value.createItem ? fieldProps.value.createItem : false
|
|
@@ -109,53 +118,7 @@ function isOptionValue(value) {
|
|
|
109
118
|
</script>
|
|
110
119
|
|
|
111
120
|
<template>
|
|
112
|
-
<
|
|
113
|
-
v-if="bare"
|
|
114
|
-
v-model="model"
|
|
115
|
-
v-model:search-term="searchTerm"
|
|
116
|
-
:open="open"
|
|
117
|
-
v-bind="controlProps"
|
|
118
|
-
class="w-full"
|
|
119
|
-
value-key="value"
|
|
120
|
-
label-key="label"
|
|
121
|
-
:items="items"
|
|
122
|
-
:multiple="fieldProps.multiple"
|
|
123
|
-
:placeholder="placeholder"
|
|
124
|
-
:disabled="disabled"
|
|
125
|
-
:loading="
|
|
126
|
-
validationPending || options.loading.value || options.fetching.value || options.creating.value
|
|
127
|
-
"
|
|
128
|
-
:trailing="true"
|
|
129
|
-
:search-input="searchable"
|
|
130
|
-
:ignore-filter="options.remote.value"
|
|
131
|
-
:clear="fieldProps.clearable === true"
|
|
132
|
-
:create-item="createItem"
|
|
133
|
-
:ui="controlUi"
|
|
134
|
-
@update:open="handleOpen"
|
|
135
|
-
@create="handleNativeCreate"
|
|
136
|
-
@blur="handleBlur"
|
|
137
|
-
>
|
|
138
|
-
<template #item-label="{ item }">
|
|
139
|
-
<FormOptionItemLabel :item="item" />
|
|
140
|
-
</template>
|
|
141
|
-
|
|
142
|
-
<template #content-bottom>
|
|
143
|
-
<FormOptionMenuFooter
|
|
144
|
-
:options="options"
|
|
145
|
-
:disabled="disabled"
|
|
146
|
-
:show-create="showExplicitCreate"
|
|
147
|
-
:create-label="createActionLabel"
|
|
148
|
-
@create="handleCreateAction"
|
|
149
|
-
/>
|
|
150
|
-
</template>
|
|
151
|
-
|
|
152
|
-
<template #create-item-label="{ item }">
|
|
153
|
-
{{
|
|
154
|
-
options.creating.value ? t("form.fields.options.creating") : t("form.fields.options.createNamed", { label: item })
|
|
155
|
-
}}
|
|
156
|
-
</template>
|
|
157
|
-
</USelectMenu>
|
|
158
|
-
<FormFieldShell v-else :field="field" :path="path">
|
|
121
|
+
<DefineControl>
|
|
159
122
|
<USelectMenu
|
|
160
123
|
v-model="model"
|
|
161
124
|
v-model:search-term="searchTerm"
|
|
@@ -164,7 +127,7 @@ function isOptionValue(value) {
|
|
|
164
127
|
class="w-full"
|
|
165
128
|
value-key="value"
|
|
166
129
|
label-key="label"
|
|
167
|
-
:items="items"
|
|
130
|
+
:items="menu.items.value"
|
|
168
131
|
:multiple="fieldProps.multiple"
|
|
169
132
|
:placeholder="placeholder"
|
|
170
133
|
:disabled="disabled"
|
|
@@ -181,8 +144,38 @@ function isOptionValue(value) {
|
|
|
181
144
|
@create="handleNativeCreate"
|
|
182
145
|
@blur="handleBlur"
|
|
183
146
|
>
|
|
147
|
+
<template v-if="menu.leadingOption.value" #leading="{ ui }">
|
|
148
|
+
<FormOptionLeading
|
|
149
|
+
:option="menu.leadingOption.value"
|
|
150
|
+
:icon-class="ui.leadingIcon()"
|
|
151
|
+
:avatar-size="ui.itemLeadingAvatarSize()"
|
|
152
|
+
chip-class="size-5"
|
|
153
|
+
/>
|
|
154
|
+
</template>
|
|
155
|
+
|
|
156
|
+
<template v-if="menu.customValueOption.value" #default="{ ui }">
|
|
157
|
+
<span :class="ui.value({ class: controlProps.ui?.value })" data-slot="value">
|
|
158
|
+
<FormOptionDisplayValue
|
|
159
|
+
:option="menu.customValueOption.value"
|
|
160
|
+
:render="menu.renderValue.value"
|
|
161
|
+
/>
|
|
162
|
+
</span>
|
|
163
|
+
</template>
|
|
164
|
+
|
|
165
|
+
<template #item-leading="{ item }">
|
|
166
|
+
<FormOptionLeading :option="menu.renderOption.value ? void 0 : item" />
|
|
167
|
+
</template>
|
|
168
|
+
|
|
184
169
|
<template #item-label="{ item }">
|
|
185
|
-
<FormOptionItemLabel
|
|
170
|
+
<FormOptionItemLabel
|
|
171
|
+
:option="item"
|
|
172
|
+
:selected="menu.isSelected(item)"
|
|
173
|
+
:render="menu.renderOption.value"
|
|
174
|
+
/>
|
|
175
|
+
</template>
|
|
176
|
+
|
|
177
|
+
<template #item-trailing="{ item }">
|
|
178
|
+
<FormOptionSuffix v-if="!menu.renderOption.value" :option="item" />
|
|
186
179
|
</template>
|
|
187
180
|
|
|
188
181
|
<template #content-bottom>
|
|
@@ -201,5 +194,10 @@ function isOptionValue(value) {
|
|
|
201
194
|
}}
|
|
202
195
|
</template>
|
|
203
196
|
</USelectMenu>
|
|
197
|
+
</DefineControl>
|
|
198
|
+
|
|
199
|
+
<ReuseControl v-if="bare" />
|
|
200
|
+
<FormFieldShell v-else :field="field" :path="path">
|
|
201
|
+
<ReuseControl />
|
|
204
202
|
</FormFieldShell>
|
|
205
203
|
</template>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { defineFormFieldKind } from "../../utils/field-kind.js";
|
|
2
2
|
export const selectFieldKind = 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: "select",
|
|
@@ -1,6 +1,6 @@
|
|
|
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
|
export type FormSelectCreateItem = boolean | 'always' | {
|
|
5
5
|
position?: 'top' | 'bottom';
|
|
6
6
|
when?: 'always' | 'empty';
|
|
@@ -12,7 +12,7 @@ export interface FormSelectProps {
|
|
|
12
12
|
createItem?: FormSelectCreateItem;
|
|
13
13
|
max?: number;
|
|
14
14
|
}
|
|
15
|
-
export interface FormSelectField<TContext = NonNullable<unknown>, TDeps = NonNullable<unknown>, TValue extends FormOptionValue = FormOptionValue, TOption extends FormOptionItem<TValue> = FormOptionItem<TValue>> extends FormStatefulFieldBase<'select', TValue | readonly TValue[] | null, TContext, TDeps, FormSelectProps> {
|
|
15
|
+
export interface FormSelectField<TContext = NonNullable<unknown>, TDeps = NonNullable<unknown>, TValue extends FormOptionValue = FormOptionValue, TOption extends FormOptionItem<TValue> = FormOptionItem<TValue>> extends FormStatefulFieldBase<'select', TValue | readonly TValue[] | null, TContext, TDeps, FormSelectProps>, FormOptionGrouping, FormOptionValueRenderers<TOption> {
|
|
16
16
|
options: FormAnyOptionConfig<TOption, TContext, TDeps, TValue | readonly TValue[] | null>;
|
|
17
17
|
}
|
|
18
18
|
type SelectFieldValue<TField> = FieldProps<TField> extends {
|
|
@@ -20,6 +20,12 @@ export interface FormLayoutConfig {
|
|
|
20
20
|
export interface FormItemLayout {
|
|
21
21
|
/** Column span for this field inside the owning layout grid. Replaces legacy `size`. */
|
|
22
22
|
span?: number | string;
|
|
23
|
+
/**
|
|
24
|
+
* Starts this field on a new row of the owning grid, at its first column, whatever the fields
|
|
25
|
+
* before it left free. Only the start of the column is set, so a responsive `span` still
|
|
26
|
+
* decides how wide the field is at each breakpoint.
|
|
27
|
+
*/
|
|
28
|
+
newRow?: boolean;
|
|
23
29
|
/** Fixed width used by renderers that lay fields out as columns, such as array tables. */
|
|
24
30
|
width?: number | string;
|
|
25
31
|
/** Label placement for this field. */
|
|
@@ -1,9 +1,29 @@
|
|
|
1
1
|
import type { QueryKey } from '@tanstack/vue-query';
|
|
2
|
+
import type { VNodeChild } from 'vue';
|
|
2
3
|
import type { RemoteOptionsLoader, RemoteOptionsPageRequest, RemoteOptionsPagination, RemoteOptionsResult, RemoteOptionsSearch } from '../../shared/types/remote-options.js';
|
|
3
4
|
import type { FormValue } from './/index.js';
|
|
4
5
|
import type { FormDependencies, FormFieldCallback, FormFieldCallbackParams } from './callbacks.js';
|
|
5
6
|
import type { FormQueryOptions } from './context.js';
|
|
6
7
|
import type { FormMaybePromise, FormText } from './utils.js';
|
|
8
|
+
/** Colors a `chip` option dot can take. */
|
|
9
|
+
export type FormOptionChipColor = 'primary' | 'secondary' | 'success' | 'info' | 'warning' | 'error' | 'neutral';
|
|
10
|
+
/**
|
|
11
|
+
* Picture drawn before an option label, passed to the Nuxt UI avatar. Use it for people and
|
|
12
|
+
* organisations: `text` is the fallback shown when `src` is missing or fails to load.
|
|
13
|
+
*/
|
|
14
|
+
export interface FormOptionAvatar {
|
|
15
|
+
/** Image URL. */
|
|
16
|
+
src?: string;
|
|
17
|
+
/** Accessible description of the image. Defaults to the option label. */
|
|
18
|
+
alt?: string;
|
|
19
|
+
/** Fallback text shown when there is no image, such as initials. */
|
|
20
|
+
text?: string;
|
|
21
|
+
}
|
|
22
|
+
/** Colored dot drawn before an option label, for statuses and categories. */
|
|
23
|
+
export interface FormOptionChip {
|
|
24
|
+
/** Color of the dot. Defaults to `primary`. */
|
|
25
|
+
color?: FormOptionChipColor;
|
|
26
|
+
}
|
|
7
27
|
/**
|
|
8
28
|
* Primitive value accepted by option-based fields.
|
|
9
29
|
*/
|
|
@@ -20,6 +40,123 @@ export interface FormOption<TValue extends FormOptionValue = FormOptionValue> {
|
|
|
20
40
|
description?: FormText;
|
|
21
41
|
/** Disables the option without removing it from the list. */
|
|
22
42
|
disabled?: boolean;
|
|
43
|
+
/** Iconify icon drawn before the label. */
|
|
44
|
+
icon?: string;
|
|
45
|
+
/**
|
|
46
|
+
* Picture drawn before the label, for people and organisations. Only one leading visual shows,
|
|
47
|
+
* in the order `icon`, `avatar`, `chip`.
|
|
48
|
+
*/
|
|
49
|
+
avatar?: FormOptionAvatar;
|
|
50
|
+
/**
|
|
51
|
+
* Colored dot drawn before the label, for statuses and categories. Only one leading visual
|
|
52
|
+
* shows, in the order `icon`, `avatar`, `chip`.
|
|
53
|
+
*/
|
|
54
|
+
chip?: FormOptionChip;
|
|
55
|
+
/**
|
|
56
|
+
* Secondary text at the end of the option, such as a code (`EUR`) or a count. It also follows
|
|
57
|
+
* the label in the closed control of a select.
|
|
58
|
+
*/
|
|
59
|
+
suffix?: FormText;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Option as the engine resolved it: the shared shape every option-based field renders from,
|
|
63
|
+
* whatever the authored option looked like (a primitive, a record, or a record with its own keys).
|
|
64
|
+
*/
|
|
65
|
+
export interface ResolvedFormOption {
|
|
66
|
+
value: FormOptionValue;
|
|
67
|
+
label: string;
|
|
68
|
+
description?: string;
|
|
69
|
+
disabled?: boolean;
|
|
70
|
+
icon?: string;
|
|
71
|
+
avatar?: FormOptionAvatar;
|
|
72
|
+
chip?: FormOptionChip;
|
|
73
|
+
/** Secondary text at the end of the option, already resolved. */
|
|
74
|
+
suffix?: string;
|
|
75
|
+
/**
|
|
76
|
+
* The option exactly as the schema or the source gave it, so render functions can read any
|
|
77
|
+
* data it carries. A primitive option is the primitive itself. The load-more row has none.
|
|
78
|
+
*/
|
|
79
|
+
item?: FormOptionItem;
|
|
80
|
+
/** Heading the option is listed under, for the fields that group their options. */
|
|
81
|
+
group?: string;
|
|
82
|
+
children?: readonly ResolvedFormOption[];
|
|
83
|
+
/** True when the option has children that load on demand. */
|
|
84
|
+
lazy?: boolean;
|
|
85
|
+
}
|
|
86
|
+
/** What a field hands `renderOption` for each option it lists. */
|
|
87
|
+
export interface FormOptionRenderContext<TOption = FormOptionItem> {
|
|
88
|
+
/** The resolved option: label, description, icon, avatar, chip and suffix as the field reads them. */
|
|
89
|
+
option: ResolvedFormOption;
|
|
90
|
+
/** The option exactly as authored or returned by the source, with any custom data it carries. */
|
|
91
|
+
item: TOption;
|
|
92
|
+
/** True when the option is part of the field value. */
|
|
93
|
+
selected: boolean;
|
|
94
|
+
/** True when the option cannot be chosen. */
|
|
95
|
+
disabled: boolean;
|
|
96
|
+
}
|
|
97
|
+
/** What a field hands `renderValue` for the option it shows in the closed control. */
|
|
98
|
+
export interface FormOptionValueRenderContext<TOption = FormOptionItem> {
|
|
99
|
+
/** The resolved option: label, description, icon, avatar, chip and suffix as the field reads them. */
|
|
100
|
+
option: ResolvedFormOption;
|
|
101
|
+
/** The option exactly as authored or returned by the source, with any custom data it carries. */
|
|
102
|
+
item: TOption;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Content render functions of the option-based fields, declared on the field next to its
|
|
106
|
+
* `options`. A field typed with its option type (`FormSelectField<Ctx, Deps, Value, Currency>`)
|
|
107
|
+
* hands them that type. In an inline schema the option type is not inferred into the callback, so
|
|
108
|
+
* annotate the parameter with the context of your option type to read its custom data.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```ts
|
|
112
|
+
* interface Currency extends FormOption<'EUR' | 'USD'> {
|
|
113
|
+
* symbol: string
|
|
114
|
+
* }
|
|
115
|
+
*
|
|
116
|
+
* // Each currency is listed as its symbol, then its name.
|
|
117
|
+
* {
|
|
118
|
+
* type: 'select',
|
|
119
|
+
* key: 'currency',
|
|
120
|
+
* options: [{ value: 'EUR', label: 'Euro', symbol: '€' }],
|
|
121
|
+
* renderOption: ({ item, option }: FormOptionRenderContext<Currency>) =>
|
|
122
|
+
* h('span', [item.symbol, ' ', option.label]),
|
|
123
|
+
* }
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
export interface FormOptionRenderers<TOption = FormOptionItem> {
|
|
127
|
+
/**
|
|
128
|
+
* Replaces the content of each option in the list: label, description, leading visual and
|
|
129
|
+
* suffix. The control keeps its own selection indicator and keyboard behavior.
|
|
130
|
+
*/
|
|
131
|
+
renderOption?(context: FormOptionRenderContext<TOption>): VNodeChild;
|
|
132
|
+
}
|
|
133
|
+
/** Renderers of the fields that show the chosen option in a closed control. */
|
|
134
|
+
export interface FormOptionValueRenderers<TOption = FormOptionItem> extends FormOptionRenderers<TOption> {
|
|
135
|
+
/**
|
|
136
|
+
* Replaces how the chosen option reads in the closed control, in single selection. Multiple
|
|
137
|
+
* selection keeps its comma-separated labels.
|
|
138
|
+
*/
|
|
139
|
+
renderValue?(context: FormOptionValueRenderContext<TOption>): VNodeChild;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Option grouping of the fields that list options under headings, declared on the field next to
|
|
143
|
+
* its `options`.
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* ```ts
|
|
147
|
+
* // Rows from the API, each with a `section` to list the options under.
|
|
148
|
+
* { type: 'select', options: () => queryOptions({ queryKey: ['plans'], queryFn: fetchPlans }), groupKey: 'section' }
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
export interface FormOptionGrouping {
|
|
152
|
+
/**
|
|
153
|
+
* Property of the options that holds the heading they are listed under. Options written inline
|
|
154
|
+
* in the schema group by their `group` without it. The records a function, a promise or a query
|
|
155
|
+
* returns group only when this names the property, so records that already carry a `group` for
|
|
156
|
+
* another purpose are never regrouped by surprise. `false` lists every option together, even
|
|
157
|
+
* inline options that carry a `group`.
|
|
158
|
+
*/
|
|
159
|
+
groupKey?: string | false;
|
|
23
160
|
}
|
|
24
161
|
/**
|
|
25
162
|
* Authored option item accepted by option-based fields.
|
|
@@ -227,6 +227,11 @@ export interface FormPageUi {
|
|
|
227
227
|
/** Grid holding the navigation and the sections. */
|
|
228
228
|
body?: FormUiClass;
|
|
229
229
|
header?: FormUiClass;
|
|
230
|
+
/**
|
|
231
|
+
* Row holding the heading and the actions. Beside a navigation it adds no box of its own; on a
|
|
232
|
+
* page without one it is the centered column the body shares, and carries the header padding.
|
|
233
|
+
*/
|
|
234
|
+
headerRow?: FormUiClass;
|
|
230
235
|
headerContent?: FormUiClass;
|
|
231
236
|
heading?: FormUiClass;
|
|
232
237
|
eyebrow?: FormUiClass;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FormValue, FormField, FormFieldCallbackParams, FormOptionItem, FormOptionValue } from '../types/index.js';
|
|
1
|
+
import type { FormValue, FormField, FormFieldCallbackParams, FormOptionItem, FormOptionValue, ResolvedFormOption } from '../types/index.js';
|
|
2
2
|
export interface FormOptionKeys {
|
|
3
3
|
value?: string;
|
|
4
4
|
label?: string;
|
|
@@ -9,18 +9,11 @@ export interface FormOptionKeys {
|
|
|
9
9
|
*/
|
|
10
10
|
group?: string;
|
|
11
11
|
}
|
|
12
|
-
export
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
icon?: string;
|
|
18
|
-
/** Heading the option is listed under, for the fields that group their options. */
|
|
19
|
-
group?: string;
|
|
20
|
-
children?: readonly ResolvedFormOption[];
|
|
21
|
-
/** True when the option has children that load on demand. */
|
|
22
|
-
lazy?: boolean;
|
|
23
|
-
}
|
|
12
|
+
export type { ResolvedFormOption } from '../types/index.js';
|
|
13
|
+
/** The one visual an option draws before its label: its icon, else its avatar, else its chip. */
|
|
14
|
+
export type OptionLeading = 'icon' | 'avatar' | 'chip';
|
|
15
|
+
export declare function resolveOptionLeading(option: ResolvedFormOption): OptionLeading | undefined;
|
|
16
|
+
export declare function resolveOptionDescriptionIndent(option: ResolvedFormOption): string | undefined;
|
|
24
17
|
export declare function normalizeOptionItem(option: FormValue, keys?: FormOptionKeys): ResolvedFormOption;
|
|
25
18
|
export declare function normalizeOptionItems(options: readonly FormValue[] | undefined, keys?: FormOptionKeys): ResolvedFormOption[];
|
|
26
19
|
/**
|
|
@@ -37,9 +30,40 @@ export declare function readOptionSource(field: FormField): FormValue;
|
|
|
37
30
|
* `groupKey` maps it. `groupKey: false` opts out.
|
|
38
31
|
*/
|
|
39
32
|
export declare function resolveOptionGroupKey(field: FormField): string | undefined;
|
|
33
|
+
export interface OptionGroup<TItem> {
|
|
34
|
+
/** Stable key of the group: its heading, or an empty string for the options without one. */
|
|
35
|
+
key: string;
|
|
36
|
+
/** Heading shown above the options of the group; none for options that declare no group. */
|
|
37
|
+
heading?: string;
|
|
38
|
+
/** The group's options, as the mutable list the Nuxt UI groups take. */
|
|
39
|
+
items: TItem[];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Lists options under their group headings. Headings follow the order in which they first appear
|
|
43
|
+
* in `items`, and each keeps its options in their authored order. The options without a group form
|
|
44
|
+
* one more group with no heading, placed where its first option appears. Without any group the
|
|
45
|
+
* result is a single headingless group, so an ungrouped list renders as it always did.
|
|
46
|
+
*/
|
|
47
|
+
export declare function groupOptions<TItem extends {
|
|
48
|
+
group?: string;
|
|
49
|
+
}>(items: readonly TItem[]): readonly OptionGroup<TItem>[];
|
|
40
50
|
export declare const LOAD_MORE_OPTION_VALUE = "__nut:load-more__";
|
|
41
51
|
export declare function isLoadMoreOption(item: FormValue): boolean;
|
|
42
52
|
export declare function appendLoadMoreOption(items: readonly ResolvedFormOption[], show: boolean, label: string): readonly ResolvedFormOption[];
|
|
53
|
+
/** Item of a Nuxt UI menu: an option, or the heading (`type: 'label'`) the menu draws above a group. */
|
|
54
|
+
export type OptionMenuItem = ResolvedFormOption & {
|
|
55
|
+
type?: 'label';
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Items for the Nuxt UI menu of a select or auto-complete: the options as one flat list, or, when
|
|
59
|
+
* some options belong to a group, one array per group with a heading item first. Nuxt UI draws
|
|
60
|
+
* each array as a group and keeps a heading only while its group still has a match for the search.
|
|
61
|
+
* The load-more row, when shown, is last and belongs to no group.
|
|
62
|
+
*/
|
|
63
|
+
export declare function toMenuItems(options: readonly ResolvedFormOption[], loadMore: {
|
|
64
|
+
show: boolean;
|
|
65
|
+
label: string;
|
|
66
|
+
}): OptionMenuItem[] | OptionMenuItem[][];
|
|
43
67
|
export declare function formOptionKey(value: FormOptionValue): string;
|
|
44
68
|
export declare function flattenResolvedOptions(options: readonly ResolvedFormOption[]): readonly ResolvedFormOption[];
|
|
45
69
|
export declare function mergeResolvedOptions(...collections: readonly (readonly ResolvedFormOption[])[]): readonly ResolvedFormOption[];
|
|
@@ -3,7 +3,32 @@ import { isRecord } from "./path.js";
|
|
|
3
3
|
import { isBoolean, isFunction, isNumber, isString, isUndefined } from "./predicate.js";
|
|
4
4
|
import { resolveFormText } from "./text.js";
|
|
5
5
|
const DEFAULT_GROUP_KEY = "group";
|
|
6
|
+
const CHIP_COLORS = [
|
|
7
|
+
"primary",
|
|
8
|
+
"secondary",
|
|
9
|
+
"success",
|
|
10
|
+
"info",
|
|
11
|
+
"warning",
|
|
12
|
+
"error",
|
|
13
|
+
"neutral"
|
|
14
|
+
];
|
|
15
|
+
export function resolveOptionLeading(option) {
|
|
16
|
+
if (option.icon) return "icon";
|
|
17
|
+
if (option.avatar) return "avatar";
|
|
18
|
+
if (option.chip) return "chip";
|
|
19
|
+
return void 0;
|
|
20
|
+
}
|
|
21
|
+
const DESCRIPTION_INDENT = {
|
|
22
|
+
avatar: "ps-8",
|
|
23
|
+
chip: "ps-6",
|
|
24
|
+
icon: "ps-6"
|
|
25
|
+
};
|
|
26
|
+
export function resolveOptionDescriptionIndent(option) {
|
|
27
|
+
const leading = resolveOptionLeading(option);
|
|
28
|
+
return leading ? DESCRIPTION_INDENT[leading] : void 0;
|
|
29
|
+
}
|
|
6
30
|
export function normalizeOptionItem(option, keys = {}) {
|
|
31
|
+
const item = isFormOptionItem(option) ? option : void 0;
|
|
7
32
|
if (isRecord(option)) {
|
|
8
33
|
const rawValue = readOptionProperty(option, keys.value ?? "value");
|
|
9
34
|
const fallbackValue = isUndefined(rawValue) ? option.key : rawValue;
|
|
@@ -14,12 +39,17 @@ export function normalizeOptionItem(option, keys = {}) {
|
|
|
14
39
|
const lazy = option.isLeaf === false || option.hasChildren === true;
|
|
15
40
|
const value = normalizeOptionValue(fallbackValue);
|
|
16
41
|
const rawIcon = readOptionProperty(option, "icon");
|
|
42
|
+
const rawSuffix = readOptionProperty(option, "suffix");
|
|
17
43
|
const normalized = {
|
|
44
|
+
avatar: normalizeOptionAvatar(readOptionProperty(option, "avatar")),
|
|
45
|
+
chip: normalizeOptionChip(readOptionProperty(option, "chip")),
|
|
18
46
|
description: isFormText(rawDescription) ? resolveFormText(rawDescription) : void 0,
|
|
19
47
|
disabled: option.disabled === true,
|
|
20
48
|
group: isFormText(rawGroup) ? resolveFormText(rawGroup) : void 0,
|
|
21
49
|
icon: isString(rawIcon) ? rawIcon : void 0,
|
|
50
|
+
item,
|
|
22
51
|
label: resolveFormText(readOptionProperty(option, keys.label ?? "label")) ?? String(value),
|
|
52
|
+
suffix: isFormText(rawSuffix) ? resolveFormText(rawSuffix) : void 0,
|
|
23
53
|
value
|
|
24
54
|
};
|
|
25
55
|
if (children) {
|
|
@@ -33,10 +63,27 @@ export function normalizeOptionItem(option, keys = {}) {
|
|
|
33
63
|
return normalized;
|
|
34
64
|
}
|
|
35
65
|
return {
|
|
66
|
+
item,
|
|
36
67
|
label: String(option),
|
|
37
68
|
value: normalizeOptionValue(option)
|
|
38
69
|
};
|
|
39
70
|
}
|
|
71
|
+
function isFormOptionItem(value) {
|
|
72
|
+
return isOptionValue(value) || isRecord(value);
|
|
73
|
+
}
|
|
74
|
+
function normalizeOptionAvatar(raw) {
|
|
75
|
+
if (!isRecord(raw)) return void 0;
|
|
76
|
+
const { alt, src, text } = raw;
|
|
77
|
+
return {
|
|
78
|
+
alt: isString(alt) ? alt : void 0,
|
|
79
|
+
src: isString(src) ? src : void 0,
|
|
80
|
+
text: isString(text) ? text : void 0
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function normalizeOptionChip(raw) {
|
|
84
|
+
if (!isRecord(raw)) return void 0;
|
|
85
|
+
return { color: CHIP_COLORS.find((color) => color === raw.color) };
|
|
86
|
+
}
|
|
40
87
|
export function normalizeOptionItems(options, keys = {}) {
|
|
41
88
|
return (options ?? []).map((option) => normalizeOptionItem(option, keys));
|
|
42
89
|
}
|
|
@@ -51,6 +98,17 @@ export function resolveOptionGroupKey(field) {
|
|
|
51
98
|
if (isString(mapped) && mapped) return mapped;
|
|
52
99
|
return Array.isArray(readOptionSource(field)) ? DEFAULT_GROUP_KEY : void 0;
|
|
53
100
|
}
|
|
101
|
+
export function groupOptions(items) {
|
|
102
|
+
const groups = /* @__PURE__ */ new Map();
|
|
103
|
+
for (const item of items) {
|
|
104
|
+
const key = item.group?.trim() ?? "";
|
|
105
|
+
const members = groups.get(key);
|
|
106
|
+
if (members) members.push(item);
|
|
107
|
+
else groups.set(key, [item]);
|
|
108
|
+
}
|
|
109
|
+
if (groups.size === 0) return [{ items: [], key: "" }];
|
|
110
|
+
return [...groups].map(([key, members]) => ({ heading: key || void 0, items: members, key }));
|
|
111
|
+
}
|
|
54
112
|
export const LOAD_MORE_OPTION_VALUE = "__nut:load-more__";
|
|
55
113
|
export function isLoadMoreOption(item) {
|
|
56
114
|
return isRecord(item) && item.value === LOAD_MORE_OPTION_VALUE;
|
|
@@ -61,6 +119,19 @@ export function appendLoadMoreOption(items, show, label) {
|
|
|
61
119
|
}
|
|
62
120
|
return [...items, { disabled: true, label, value: LOAD_MORE_OPTION_VALUE }];
|
|
63
121
|
}
|
|
122
|
+
function menuHeading(heading) {
|
|
123
|
+
return { label: heading, type: "label", value: "" };
|
|
124
|
+
}
|
|
125
|
+
export function toMenuItems(options, loadMore) {
|
|
126
|
+
const groups = groupOptions(options);
|
|
127
|
+
if (!groups.some((group) => group.heading)) {
|
|
128
|
+
return [...appendLoadMoreOption(options, loadMore.show, loadMore.label)];
|
|
129
|
+
}
|
|
130
|
+
const nested = groups.map(
|
|
131
|
+
(group) => group.heading ? [menuHeading(group.heading), ...group.items] : group.items
|
|
132
|
+
);
|
|
133
|
+
return loadMore.show ? [...nested, [...appendLoadMoreOption([], true, loadMore.label)]] : nested;
|
|
134
|
+
}
|
|
64
135
|
export function formOptionKey(value) {
|
|
65
136
|
const type = isString(value) ? "string" : isNumber(value) ? "number" : isBoolean(value) ? "boolean" : "unknown";
|
|
66
137
|
return `${type}:${String(value)}`;
|
|
@@ -15,6 +15,11 @@ export interface FormPageSurfaceClasses {
|
|
|
15
15
|
/** Placeholder shown while the schema context loads. */
|
|
16
16
|
sectionSkeleton: string;
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* The centered column of a page without a navigation. The body and the row of the header share
|
|
20
|
+
* it, so both start and end on the same edges, padding included.
|
|
21
|
+
*/
|
|
22
|
+
export declare const FORM_PAGE_COLUMN = "mx-auto w-full max-w-[816px]";
|
|
18
23
|
export declare const DEFAULT_FORM_PAGE_SURFACE: FormPageSurface;
|
|
19
24
|
/** The surface a page draws its sections with, `card` unless the config names a known one. */
|
|
20
25
|
export declare function resolveFormPageSurface(value: FormValue): FormPageSurface;
|
|
@@ -6,12 +6,12 @@ type __VLS_Props = {
|
|
|
6
6
|
};
|
|
7
7
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
8
8
|
reset: () => any;
|
|
9
|
-
assign: (header: number) => any;
|
|
10
9
|
ignore: () => any;
|
|
10
|
+
assign: (header: number) => any;
|
|
11
11
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
12
12
|
onReset?: (() => any) | undefined;
|
|
13
|
-
onAssign?: ((header: number) => any) | undefined;
|
|
14
13
|
onIgnore?: (() => any) | undefined;
|
|
14
|
+
onAssign?: ((header: number) => any) | undefined;
|
|
15
15
|
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
16
16
|
declare const _default: typeof __VLS_export;
|
|
17
17
|
export default _default;
|
|
@@ -6,12 +6,12 @@ type __VLS_Props = {
|
|
|
6
6
|
};
|
|
7
7
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
8
8
|
reset: () => any;
|
|
9
|
-
assign: (header: number) => any;
|
|
10
9
|
ignore: () => any;
|
|
10
|
+
assign: (header: number) => any;
|
|
11
11
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
12
12
|
onReset?: (() => any) | undefined;
|
|
13
|
-
onAssign?: ((header: number) => any) | undefined;
|
|
14
13
|
onIgnore?: (() => any) | undefined;
|
|
14
|
+
onAssign?: ((header: number) => any) | undefined;
|
|
15
15
|
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
16
16
|
declare const _default: typeof __VLS_export;
|
|
17
17
|
export default _default;
|