nuxt-ui-tools 1.3.0 → 1.5.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/module.mjs +10 -2
- package/dist/runtime/dashboard/composables/use-dashboard-remote-options.js +8 -3
- package/dist/runtime/dashboard/types/filters.d.ts +7 -1
- package/dist/runtime/dashboard/utils/builders/dashboard-filters.js +19 -2
- package/dist/runtime/form/components/actions/form-actions.vue +13 -127
- package/dist/runtime/form/components/page/form-page-actions.d.vue.ts +3 -0
- package/dist/runtime/form/components/page/form-page-actions.vue +40 -0
- package/dist/runtime/form/components/page/form-page-actions.vue.d.ts +3 -0
- package/dist/runtime/form/components/page/form-page-header.d.vue.ts +24 -0
- package/dist/runtime/form/components/page/form-page-header.vue +99 -0
- package/dist/runtime/form/components/page/form-page-header.vue.d.ts +24 -0
- package/dist/runtime/form/components/page/form-page-navigation.d.vue.ts +34 -0
- package/dist/runtime/form/components/page/form-page-navigation.vue +229 -0
- package/dist/runtime/form/components/page/form-page-navigation.vue.d.ts +34 -0
- package/dist/runtime/form/components/page/form-page-section.d.vue.ts +19 -0
- package/dist/runtime/form/components/page/form-page-section.vue +114 -0
- package/dist/runtime/form/components/page/form-page-section.vue.d.ts +19 -0
- package/dist/runtime/form/components/page/form-page-sections.d.vue.ts +21 -0
- package/dist/runtime/form/components/page/form-page-sections.vue +75 -0
- package/dist/runtime/form/components/page/form-page-sections.vue.d.ts +21 -0
- package/dist/runtime/form/components/page/form-page.d.vue.ts +80 -0
- package/dist/runtime/form/components/page/form-page.vue +91 -0
- package/dist/runtime/form/components/page/form-page.vue.d.ts +80 -0
- package/dist/runtime/form/components/renderer/form-field-shell.vue +7 -11
- package/dist/runtime/form/components/root/form.vue +25 -194
- package/dist/runtime/form/composables/use-field-options.js +19 -0
- package/dist/runtime/form/composables/use-form-action-buttons.d.ts +32 -0
- package/dist/runtime/form/composables/use-form-action-buttons.js +89 -0
- package/dist/runtime/form/composables/use-form-actions.d.ts +8 -5
- package/dist/runtime/form/composables/use-form-actions.js +1 -1
- package/dist/runtime/form/composables/use-form-page-scroll.d.ts +22 -0
- package/dist/runtime/form/composables/use-form-page-scroll.js +178 -0
- package/dist/runtime/form/composables/use-form-page.d.ts +114 -0
- package/dist/runtime/form/composables/use-form-page.js +83 -0
- package/dist/runtime/form/composables/use-form-root.d.ts +34 -0
- package/dist/runtime/form/composables/use-form-root.js +171 -0
- package/dist/runtime/form/composables/use-form-runtime.js +13 -4
- package/dist/runtime/form/composables/use-form.js +2 -22
- package/dist/runtime/form/composables/use-remote-field-options.js +21 -2
- package/dist/runtime/form/fields/checkbox-card/component.vue +28 -19
- package/dist/runtime/form/fields/checkbox-card/types.d.ts +2 -5
- package/dist/runtime/form/fields/choice-card/choice-card-label.d.vue.ts +17 -0
- package/dist/runtime/form/fields/choice-card/choice-card-label.vue +50 -0
- package/dist/runtime/form/fields/choice-card/choice-card-label.vue.d.ts +17 -0
- package/dist/runtime/form/fields/choice-card/types.d.ts +31 -0
- package/dist/runtime/form/fields/choice-card/types.js +0 -0
- package/dist/runtime/form/fields/choice-card/use-choice-card.d.ts +22 -0
- package/dist/runtime/form/fields/choice-card/use-choice-card.js +52 -0
- package/dist/runtime/form/fields/radio-card/component.vue +17 -18
- package/dist/runtime/form/fields/radio-card/types.d.ts +2 -4
- package/dist/runtime/form/index.d.ts +1 -1
- package/dist/runtime/form/index.js +7 -1
- package/dist/runtime/form/schema/index.d.ts +1 -1
- package/dist/runtime/form/schema/index.js +1 -0
- package/dist/runtime/form/schema/page.d.ts +50 -0
- package/dist/runtime/form/schema/page.js +25 -0
- package/dist/runtime/form/types/index.d.ts +1 -0
- package/dist/runtime/form/types/options.d.ts +22 -2
- package/dist/runtime/form/types/page.d.ts +172 -0
- package/dist/runtime/form/types/page.js +0 -0
- package/dist/runtime/form/types/ui.d.ts +60 -0
- package/dist/runtime/form/utils/controls.d.ts +15 -0
- package/dist/runtime/form/utils/controls.js +38 -0
- package/dist/runtime/form/utils/layout.js +3 -1
- package/dist/runtime/form/utils/page.d.ts +20 -0
- package/dist/runtime/form/utils/page.js +107 -0
- package/dist/runtime/form/utils/state.d.ts +2 -0
- package/dist/runtime/form/utils/state.js +1 -1
- package/dist/runtime/form/utils/ui.js +1 -0
- package/dist/runtime/i18n/locales/en.js +18 -0
- package/dist/runtime/i18n/locales/fr.js +18 -0
- package/dist/runtime/i18n/types.d.ts +29 -0
- package/dist/runtime/shared/index.d.ts +1 -0
- package/dist/runtime/shared/index.js +1 -0
- package/dist/runtime/shared/types/query.d.ts +5 -1
- package/dist/runtime/shared/types/remote-options.d.ts +56 -0
- package/dist/runtime/shared/utils/define-remote-options.d.ts +23 -0
- package/dist/runtime/shared/utils/define-remote-options.js +24 -0
- package/dist/runtime/shared/utils/remote-options-query.d.ts +6 -0
- package/dist/runtime/shared/utils/remote-options-query.js +18 -0
- package/dist/runtime/table/components/data-list/data-list-filter-tags.d.vue.ts +1 -1
- package/dist/runtime/table/components/data-list/data-list-filter-tags.vue +1 -1
- package/dist/runtime/table/components/data-list/data-list-filter-tags.vue.d.ts +1 -1
- package/dist/runtime/table/components/filters/panel/filter-panel.vue +14 -2
- package/dist/runtime/table/components/filters/shared/filter-option-multiple-list.vue +2 -2
- package/dist/runtime/table/components/filters/shared/filter-option-single-list.vue +2 -2
- package/dist/runtime/table/components/filters/shared/filter-option-tree-content.vue +5 -5
- package/dist/runtime/table/components/filters/shared/filter-trigger-tag.vue +7 -1
- package/dist/runtime/table/composables/use-table-filter-selected-options.js +16 -1
- package/dist/runtime/table/types/filters.d.ts +7 -0
- package/dist/runtime/table/types/remote-options.d.ts +3 -16
- package/dist/runtime/table/utils/remote-table-options.js +37 -37
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useQueryClient } from "@tanstack/vue-query";
|
|
1
|
+
import { hashKey, useQueryClient } from "@tanstack/vue-query";
|
|
2
2
|
import { debounceFilter, watchWithFilter } from "@vueuse/core";
|
|
3
3
|
import { computed, ref, shallowRef, watch } from "vue";
|
|
4
4
|
import { formOptionKey, mergeResolvedOptions, normalizeOptionItems } from "../utils/options.js";
|
|
@@ -384,7 +384,7 @@ export function useRemoteFieldOptions(params) {
|
|
|
384
384
|
selectedError.value = null;
|
|
385
385
|
return;
|
|
386
386
|
}
|
|
387
|
-
retainSelected(
|
|
387
|
+
retainSelected(loaded.value);
|
|
388
388
|
if (missingSelected.value.length) {
|
|
389
389
|
void hydrateSelected({ reconcile: false });
|
|
390
390
|
}
|
|
@@ -410,6 +410,25 @@ export function useRemoteFieldOptions(params) {
|
|
|
410
410
|
},
|
|
411
411
|
{ deep: true }
|
|
412
412
|
);
|
|
413
|
+
const pageIdentity = computed(() => {
|
|
414
|
+
const config = params.config.value;
|
|
415
|
+
return config?.queryKeyFor ? hashKey(
|
|
416
|
+
config.queryKeyFor({
|
|
417
|
+
page: { cursor: null, index: 1, size: pageSize.value },
|
|
418
|
+
search: ""
|
|
419
|
+
})
|
|
420
|
+
) : null;
|
|
421
|
+
});
|
|
422
|
+
const selectedIdentity = computed(() => {
|
|
423
|
+
const config = params.config.value;
|
|
424
|
+
return config?.selectedQueryKeyFor ? hashKey(config.selectedQueryKeyFor({ values: [] })) : null;
|
|
425
|
+
});
|
|
426
|
+
watch([pageIdentity, selectedIdentity], ([page, selected], [previousPage, previousSelected]) => {
|
|
427
|
+
if (page === previousPage && selected === previousSelected) return;
|
|
428
|
+
selectionGeneration.value += 1;
|
|
429
|
+
if (page !== previousPage) void reload({ clearSearch: false });
|
|
430
|
+
if (selectedValues.value.length) void hydrateSelected({ reconcile: true });
|
|
431
|
+
});
|
|
413
432
|
return {
|
|
414
433
|
activate,
|
|
415
434
|
error: errorValue,
|
|
@@ -5,18 +5,20 @@ import FormFieldShell from "../../components/renderer/form-field-shell.vue";
|
|
|
5
5
|
import { useFieldControl } from "../../composables/use-field-control";
|
|
6
6
|
import { formOptionKey } from "../../utils/options";
|
|
7
7
|
import { isBoolean, isNumber, isString, isUndefined } from "../../utils/predicate";
|
|
8
|
-
import
|
|
9
|
-
import {
|
|
8
|
+
import ChoiceCardLabel from "../choice-card/choice-card-label.vue";
|
|
9
|
+
import { CHOICE_CARD_PROPS, useChoiceCard } from "../choice-card/use-choice-card";
|
|
10
10
|
const props = defineProps({
|
|
11
11
|
field: { type: Object, required: true },
|
|
12
12
|
path: { type: Array, required: true }
|
|
13
13
|
});
|
|
14
14
|
const { fieldProps, form, controlProps, disabled, handleBlur, options } = useFieldControl(
|
|
15
15
|
() => props.field,
|
|
16
|
-
() => props.path
|
|
16
|
+
() => props.path,
|
|
17
|
+
{ omit: CHOICE_CARD_PROPS }
|
|
17
18
|
);
|
|
18
|
-
const
|
|
19
|
-
|
|
19
|
+
const cards = useChoiceCard({ props: () => fieldProps.value, ui: () => controlProps.value.ui });
|
|
20
|
+
const optionByKey = computed(
|
|
21
|
+
() => new Map(options.items.value.map((item) => [formOptionKey(item.value), item]))
|
|
20
22
|
);
|
|
21
23
|
const model = computed({
|
|
22
24
|
get: () => {
|
|
@@ -26,22 +28,17 @@ const model = computed({
|
|
|
26
28
|
set: (value) => form.setValue(
|
|
27
29
|
props.path,
|
|
28
30
|
value.flatMap((key) => {
|
|
29
|
-
const optionValue =
|
|
31
|
+
const optionValue = optionByKey.value.get(key)?.value;
|
|
30
32
|
return isUndefined(optionValue) ? [] : [optionValue];
|
|
31
33
|
})
|
|
32
34
|
)
|
|
33
35
|
});
|
|
34
36
|
const items = computed(
|
|
35
|
-
() => options.items.value.map((
|
|
37
|
+
() => options.items.value.map(({ icon: _icon, ...item }) => ({
|
|
38
|
+
...item,
|
|
39
|
+
value: formOptionKey(item.value)
|
|
40
|
+
}))
|
|
36
41
|
);
|
|
37
|
-
const groupUi = computed(() => ({
|
|
38
|
-
...controlProps.value.ui,
|
|
39
|
-
fieldset: mergeFormUiClass(
|
|
40
|
-
controlProps.value.ui?.fieldset,
|
|
41
|
-
fieldProps.value.orientation === "horizontal" ? "flex-wrap" : void 0
|
|
42
|
-
),
|
|
43
|
-
item: mergeFormUiClass(CARD_SELECTED_RING, controlProps.value.ui?.item)
|
|
44
|
-
}));
|
|
45
42
|
function isOptionValue(value) {
|
|
46
43
|
return isString(value) || isNumber(value) || isBoolean(value);
|
|
47
44
|
}
|
|
@@ -56,12 +53,24 @@ function isOptionValue(value) {
|
|
|
56
53
|
value-key="value"
|
|
57
54
|
label-key="label"
|
|
58
55
|
description-key="description"
|
|
59
|
-
:orientation="fieldProps.orientation"
|
|
60
|
-
:ui="groupUi"
|
|
61
56
|
variant="card"
|
|
62
|
-
:
|
|
57
|
+
:orientation="cards.orientation.value"
|
|
58
|
+
:indicator="cards.indicator.value"
|
|
59
|
+
:ui="cards.ui.value"
|
|
60
|
+
:style="cards.style.value"
|
|
63
61
|
:disabled="disabled"
|
|
64
62
|
@blur="handleBlur"
|
|
65
|
-
|
|
63
|
+
>
|
|
64
|
+
<template #label="{ item }">
|
|
65
|
+
<ChoiceCardLabel
|
|
66
|
+
:label="optionByKey.get(item.value)?.label ?? item.label"
|
|
67
|
+
:icon="optionByKey.get(item.value)?.icon"
|
|
68
|
+
:tile="cards.tile.value"
|
|
69
|
+
:corner="cards.corner.value"
|
|
70
|
+
:selected="model.includes(item.value)"
|
|
71
|
+
:ui="cards.ui.value"
|
|
72
|
+
/>
|
|
73
|
+
</template>
|
|
74
|
+
</UCheckboxGroup>
|
|
66
75
|
</FormFieldShell>
|
|
67
76
|
</template>
|
|
@@ -1,11 +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
3
|
import type { FormOptionConfig, FormOptionItem, FormOptionValue, FormOptionsSource } from '../../types/options.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
indicator?: 'start' | 'end' | 'hidden';
|
|
7
|
-
}
|
|
8
|
-
export interface FormCheckboxCardField<TContext = NonNullable<unknown>, TDeps = NonNullable<unknown>, TValue extends FormOptionValue = FormOptionValue, TOption extends FormOptionItem<TValue> = FormOptionItem<TValue>> extends FormStatefulFieldBase<'checkbox-card', readonly TValue[] | null, TContext, TDeps, FormCheckboxCardProps> {
|
|
4
|
+
import type { FormChoiceCardProps } from '../choice-card/types.js';
|
|
5
|
+
export interface FormCheckboxCardField<TContext = NonNullable<unknown>, TDeps = NonNullable<unknown>, TValue extends FormOptionValue = FormOptionValue, TOption extends FormOptionItem<TValue> = FormOptionItem<TValue>> extends FormStatefulFieldBase<'checkbox-card', readonly TValue[] | null, TContext, TDeps, FormChoiceCardProps> {
|
|
9
6
|
options: FormOptionConfig<TOption, TContext, TDeps, readonly TValue[] | null> | FormOptionsSource<TOption, TContext, TDeps, readonly TValue[] | null>;
|
|
10
7
|
}
|
|
11
8
|
export type CheckboxCardFieldOutput<TField> = readonly FieldOptionValue<TField>[] | NullableValue;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { FormControlUi } from '../../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Content of a card's label: the option icon, inline or in a tile above the label, and the check
|
|
4
|
+
* a selected card shows in its corner. Styled through `ui.tile`, `ui.tileIcon`, `ui.optionIcon`,
|
|
5
|
+
* `ui.check`, and `ui.checkIcon`.
|
|
6
|
+
*/
|
|
7
|
+
type __VLS_Props = {
|
|
8
|
+
label?: string;
|
|
9
|
+
icon?: string;
|
|
10
|
+
tile: boolean;
|
|
11
|
+
corner: boolean;
|
|
12
|
+
selected: boolean;
|
|
13
|
+
ui?: FormControlUi;
|
|
14
|
+
};
|
|
15
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
16
|
+
declare const _default: typeof __VLS_export;
|
|
17
|
+
export default _default;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import UIcon from "@nuxt/ui/components/Icon.vue";
|
|
3
|
+
import { mergeFormUiClass } from "../../utils/ui";
|
|
4
|
+
defineProps({
|
|
5
|
+
label: { type: String, required: false },
|
|
6
|
+
icon: { type: String, required: false },
|
|
7
|
+
tile: { type: Boolean, required: true },
|
|
8
|
+
corner: { type: Boolean, required: true },
|
|
9
|
+
selected: { type: Boolean, required: true },
|
|
10
|
+
ui: { type: Object, required: false }
|
|
11
|
+
});
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<span
|
|
16
|
+
v-if="tile && icon"
|
|
17
|
+
:data-selected="selected || void 0"
|
|
18
|
+
:class="
|
|
19
|
+
mergeFormUiClass(
|
|
20
|
+
'grid size-8 place-items-center rounded-lg bg-elevated text-muted transition-colors data-[selected]:bg-primary/10 data-[selected]:text-primary',
|
|
21
|
+
ui?.tile
|
|
22
|
+
)
|
|
23
|
+
"
|
|
24
|
+
data-choice-tile
|
|
25
|
+
>
|
|
26
|
+
<UIcon :name="icon" :class="mergeFormUiClass('size-4', ui?.tileIcon)" aria-hidden="true" />
|
|
27
|
+
</span>
|
|
28
|
+
<span v-if="!tile && icon" class="inline-flex items-center gap-2">
|
|
29
|
+
<UIcon
|
|
30
|
+
:name="icon"
|
|
31
|
+
:class="mergeFormUiClass('size-4 shrink-0', ui?.optionIcon)"
|
|
32
|
+
aria-hidden="true"
|
|
33
|
+
/>
|
|
34
|
+
<span>{{ label }}</span>
|
|
35
|
+
</span>
|
|
36
|
+
<span v-else class="block">{{ label }}</span>
|
|
37
|
+
<span
|
|
38
|
+
v-if="corner && selected"
|
|
39
|
+
aria-hidden="true"
|
|
40
|
+
:class="
|
|
41
|
+
mergeFormUiClass(
|
|
42
|
+
'absolute end-3 top-3 grid size-4 place-items-center rounded-[4px] bg-primary text-inverted',
|
|
43
|
+
ui?.check
|
|
44
|
+
)
|
|
45
|
+
"
|
|
46
|
+
data-choice-check
|
|
47
|
+
>
|
|
48
|
+
<UIcon name="i-lucide-check" :class="mergeFormUiClass('size-2.5', ui?.checkIcon)" />
|
|
49
|
+
</span>
|
|
50
|
+
</template>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { FormControlUi } from '../../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Content of a card's label: the option icon, inline or in a tile above the label, and the check
|
|
4
|
+
* a selected card shows in its corner. Styled through `ui.tile`, `ui.tileIcon`, `ui.optionIcon`,
|
|
5
|
+
* `ui.check`, and `ui.checkIcon`.
|
|
6
|
+
*/
|
|
7
|
+
type __VLS_Props = {
|
|
8
|
+
label?: string;
|
|
9
|
+
icon?: string;
|
|
10
|
+
tile: boolean;
|
|
11
|
+
corner: boolean;
|
|
12
|
+
selected: boolean;
|
|
13
|
+
ui?: FormControlUi;
|
|
14
|
+
};
|
|
15
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
16
|
+
declare const _default: typeof __VLS_export;
|
|
17
|
+
export default _default;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presentation of the radio and checkbox cards.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```ts
|
|
6
|
+
* // A fixed grid of cards, each with its icon in a tile above the label and a check in the
|
|
7
|
+
* // corner once selected.
|
|
8
|
+
* props: { columns: '1 md:2 lg:3', icon: 'tile', indicator: 'corner' }
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
export interface FormChoiceCardProps {
|
|
12
|
+
/**
|
|
13
|
+
* Lays the cards out in a fixed grid of this many equal columns. Accepts breakpoints:
|
|
14
|
+
* `3` or `'1 md:2 lg:3'`. Without it, cards flow along `orientation`.
|
|
15
|
+
*/
|
|
16
|
+
columns?: number | string;
|
|
17
|
+
/** Direction of the cards when they are not in a `columns` grid. */
|
|
18
|
+
orientation?: 'horizontal' | 'vertical';
|
|
19
|
+
/**
|
|
20
|
+
* Selection mark: the radio or checkbox before the content (`start`, the default), after it
|
|
21
|
+
* (`end`), none (`hidden`), or a check in the top corner of the selected cards (`corner`).
|
|
22
|
+
*/
|
|
23
|
+
indicator?: 'start' | 'end' | 'hidden' | 'corner';
|
|
24
|
+
/**
|
|
25
|
+
* Where an option's `icon` shows: before its label (`inline`, the default), or in a tile above
|
|
26
|
+
* it (`tile`) that takes the selection color. Style the parts the engine renders with the
|
|
27
|
+
* `ui` slots `tile`, `tileIcon`, `optionIcon`, `check`, and `checkIcon`, next to the Nuxt UI
|
|
28
|
+
* slots of the group.
|
|
29
|
+
*/
|
|
30
|
+
icon?: 'inline' | 'tile';
|
|
31
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { FormControlUi } from '../../types/index.js';
|
|
2
|
+
import type { FormChoiceCardProps } from './types.js';
|
|
3
|
+
/** Props the card fields render themselves, kept away from the Nuxt UI group. */
|
|
4
|
+
export declare const CHOICE_CARD_PROPS: string[];
|
|
5
|
+
/**
|
|
6
|
+
* Presentation of a radio or checkbox card group: the flowing row or fixed grid of cards, the
|
|
7
|
+
* selection mark, and where option icons go. Returns the props and `ui` slots for the Nuxt UI
|
|
8
|
+
* group, and what the card content renders (`tile`, `corner`).
|
|
9
|
+
*/
|
|
10
|
+
export declare function useChoiceCard(params: {
|
|
11
|
+
props: () => FormChoiceCardProps;
|
|
12
|
+
ui: () => FormControlUi | undefined;
|
|
13
|
+
}): {
|
|
14
|
+
corner: import("vue").ComputedRef<boolean>;
|
|
15
|
+
indicator: import("vue").ComputedRef<"end" | "start" | "hidden" | undefined>;
|
|
16
|
+
orientation: import("vue").ComputedRef<"horizontal" | "vertical" | undefined>;
|
|
17
|
+
style: import("vue").ComputedRef<{
|
|
18
|
+
'--nut-choice-columns': string;
|
|
19
|
+
} | undefined>;
|
|
20
|
+
tile: import("vue").ComputedRef<boolean>;
|
|
21
|
+
ui: import("vue").ComputedRef<FormControlUi>;
|
|
22
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { computed } from "vue";
|
|
2
|
+
import { useResponsiveValue } from "../../../shared/composables/use-responsive-value.js";
|
|
3
|
+
import { mergeFormUiClass } from "../../utils/ui.js";
|
|
4
|
+
import { CARD_SELECTED_RING } from "../card-selection.js";
|
|
5
|
+
export const CHOICE_CARD_PROPS = ["columns", "icon", "indicator", "orientation"];
|
|
6
|
+
export function useChoiceCard(params) {
|
|
7
|
+
const columnsValue = useResponsiveValue(() => String(params.props().columns ?? ""));
|
|
8
|
+
const columns = computed(() => {
|
|
9
|
+
const count = Math.trunc(Number(columnsValue.value));
|
|
10
|
+
return count > 0 ? count : void 0;
|
|
11
|
+
});
|
|
12
|
+
const corner = computed(() => params.props().indicator === "corner");
|
|
13
|
+
const tile = computed(() => params.props().icon === "tile");
|
|
14
|
+
const indicator = computed(() => {
|
|
15
|
+
const value = params.props().indicator;
|
|
16
|
+
return value === "corner" ? "hidden" : value;
|
|
17
|
+
});
|
|
18
|
+
const orientation = computed(() => params.props().orientation);
|
|
19
|
+
const style = computed(
|
|
20
|
+
() => columns.value ? { "--nut-choice-columns": String(columns.value) } : void 0
|
|
21
|
+
);
|
|
22
|
+
const ui = computed(() => {
|
|
23
|
+
const current = params.ui();
|
|
24
|
+
return {
|
|
25
|
+
...current,
|
|
26
|
+
fieldset: mergeFormUiClass(
|
|
27
|
+
columns.value ? "grid grid-cols-[repeat(var(--nut-choice-columns),minmax(0,1fr))] gap-2.5" : `gap-2.5${orientation.value === "horizontal" ? " flex-wrap" : ""}`,
|
|
28
|
+
current?.fieldset
|
|
29
|
+
),
|
|
30
|
+
item: mergeFormUiClass(
|
|
31
|
+
`border-default ${CARD_SELECTED_RING}`,
|
|
32
|
+
corner.value ? "relative" : void 0,
|
|
33
|
+
current?.item
|
|
34
|
+
),
|
|
35
|
+
// A tile stacks above the title, and the description below it, with gaps.
|
|
36
|
+
label: mergeFormUiClass(
|
|
37
|
+
tile.value ? "flex flex-col items-start gap-2.5 font-semibold" : void 0,
|
|
38
|
+
current?.label
|
|
39
|
+
),
|
|
40
|
+
// A hidden indicator centers the text: keep it aligned, and clear of an inline corner check.
|
|
41
|
+
wrapper: mergeFormUiClass(
|
|
42
|
+
tile.value ? "flex flex-col gap-1.5" : void 0,
|
|
43
|
+
[
|
|
44
|
+
indicator.value === "hidden" ? "ms-0 text-start" : "",
|
|
45
|
+
corner.value && !tile.value ? "pe-7" : ""
|
|
46
|
+
].join(" "),
|
|
47
|
+
current?.wrapper
|
|
48
|
+
)
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
return { corner, indicator, orientation, style, tile, ui };
|
|
52
|
+
}
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import UIcon from "@nuxt/ui/components/Icon.vue";
|
|
3
2
|
import URadioGroup from "@nuxt/ui/components/RadioGroup.vue";
|
|
4
3
|
import { computed } from "vue";
|
|
5
4
|
import FormFieldShell from "../../components/renderer/form-field-shell.vue";
|
|
6
5
|
import { useFieldControl } from "../../composables/use-field-control";
|
|
7
6
|
import { isBoolean, isNumber, isString } from "../../utils/predicate";
|
|
8
|
-
import
|
|
9
|
-
import {
|
|
7
|
+
import ChoiceCardLabel from "../choice-card/choice-card-label.vue";
|
|
8
|
+
import { CHOICE_CARD_PROPS, useChoiceCard } from "../choice-card/use-choice-card";
|
|
10
9
|
const props = defineProps({
|
|
11
10
|
field: { type: Object, required: true },
|
|
12
11
|
path: { type: Array, required: true }
|
|
13
12
|
});
|
|
14
13
|
const { fieldProps, form, controlProps, disabled, handleBlur, options } = useFieldControl(
|
|
15
14
|
() => props.field,
|
|
16
|
-
() => props.path
|
|
15
|
+
() => props.path,
|
|
16
|
+
{ omit: CHOICE_CARD_PROPS }
|
|
17
17
|
);
|
|
18
|
+
const cards = useChoiceCard({ props: () => fieldProps.value, ui: () => controlProps.value.ui });
|
|
18
19
|
const model = computed({
|
|
19
20
|
get: () => {
|
|
20
21
|
const value = form.getValue(props.path);
|
|
@@ -26,14 +27,6 @@ const model = computed({
|
|
|
26
27
|
set: (value) => form.setValue(props.path, value)
|
|
27
28
|
});
|
|
28
29
|
const items = computed(() => [...options.items.value]);
|
|
29
|
-
const groupUi = computed(() => ({
|
|
30
|
-
...controlProps.value.ui,
|
|
31
|
-
fieldset: mergeFormUiClass(
|
|
32
|
-
controlProps.value.ui?.fieldset,
|
|
33
|
-
fieldProps.value.orientation === "horizontal" ? "flex-wrap" : void 0
|
|
34
|
-
),
|
|
35
|
-
item: mergeFormUiClass(CARD_SELECTED_RING, controlProps.value.ui?.item)
|
|
36
|
-
}));
|
|
37
30
|
</script>
|
|
38
31
|
|
|
39
32
|
<template>
|
|
@@ -45,16 +38,22 @@ const groupUi = computed(() => ({
|
|
|
45
38
|
label-key="label"
|
|
46
39
|
variant="card"
|
|
47
40
|
:items="items"
|
|
48
|
-
:orientation="
|
|
49
|
-
:
|
|
41
|
+
:orientation="cards.orientation.value"
|
|
42
|
+
:indicator="cards.indicator.value"
|
|
43
|
+
:ui="cards.ui.value"
|
|
44
|
+
:style="cards.style.value"
|
|
50
45
|
:disabled="disabled"
|
|
51
46
|
@blur="handleBlur"
|
|
52
47
|
>
|
|
53
48
|
<template #label="{ item }">
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
49
|
+
<ChoiceCardLabel
|
|
50
|
+
:label="item.label"
|
|
51
|
+
:icon="item.icon"
|
|
52
|
+
:tile="cards.tile.value"
|
|
53
|
+
:corner="cards.corner.value"
|
|
54
|
+
:selected="model === item.value"
|
|
55
|
+
:ui="cards.ui.value"
|
|
56
|
+
/>
|
|
58
57
|
</template>
|
|
59
58
|
</URadioGroup>
|
|
60
59
|
</FormFieldShell>
|
|
@@ -1,10 +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
3
|
import type { FormOptionItem, FormOptionValue, FormOptionsSource } from '../../types/options.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
7
|
-
export interface FormRadioCardField<TContext = NonNullable<unknown>, TDeps = NonNullable<unknown>, TValue extends FormOptionValue = FormOptionValue, TOption extends FormOptionItem<TValue> = FormOptionItem<TValue>> extends FormStatefulFieldBase<'radio-card', TValue | null, TContext, TDeps, FormRadioCardProps> {
|
|
4
|
+
import type { FormChoiceCardProps } from '../choice-card/types.js';
|
|
5
|
+
export interface FormRadioCardField<TContext = NonNullable<unknown>, TDeps = NonNullable<unknown>, TValue extends FormOptionValue = FormOptionValue, TOption extends FormOptionItem<TValue> = FormOptionItem<TValue>> extends FormStatefulFieldBase<'radio-card', TValue | null, TContext, TDeps, FormChoiceCardProps> {
|
|
8
6
|
options: FormOptionsSource<TOption, TContext, TDeps, TValue | null>;
|
|
9
7
|
}
|
|
10
8
|
export type RadioCardFieldOutput<TField> = FieldOptionValue<TField> | NullableValue;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { defineFormField, defineFormFields, defineFormSchema } from './schema/index.js';
|
|
1
|
+
export { defineFormField, defineFormFields, defineFormPageSchema, defineFormPageSection, defineFormSchema, } from './schema/index.js';
|
|
2
2
|
export { useForm } from './composables/use-form.js';
|
|
3
3
|
export { useFormUi } from './composables/use-form-ui.js';
|
|
4
4
|
export { provideFormApi, useFormApi } from './composables/use-form-api.js';
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {
|
|
2
|
+
defineFormField,
|
|
3
|
+
defineFormFields,
|
|
4
|
+
defineFormPageSchema,
|
|
5
|
+
defineFormPageSection,
|
|
6
|
+
defineFormSchema
|
|
7
|
+
} from "./schema/index.js";
|
|
2
8
|
export { useForm } from "./composables/use-form.js";
|
|
3
9
|
export { useFormUi } from "./composables/use-form-ui.js";
|
|
4
10
|
export { provideFormApi, useFormApi } from "./composables/use-form-api.js";
|
|
@@ -81,4 +81,4 @@ export declare function defineFormField<const TField extends FormField>(field: T
|
|
|
81
81
|
* Defines a reusable field group while preserving literal inference.
|
|
82
82
|
*/
|
|
83
83
|
export declare function defineFormFields<const TFields extends readonly FormField[]>(fields: TFields): TFields;
|
|
84
|
-
export {};
|
|
84
|
+
export { defineFormPageSchema, defineFormPageSection } from './page.js';
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { FormContextData, FormContextDefinition, FormPageSchema, FormPageSchemaInput, FormPageSection } from '../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Defines one section of a form page while preserving literal field inference, so each section
|
|
4
|
+
* can live in its own file as a plain typed function.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* export function billingSection({ hasActiveContract }: { hasActiveContract: boolean }) {
|
|
9
|
+
* return defineFormPageSection({
|
|
10
|
+
* key: 'billing',
|
|
11
|
+
* label: () => t('account.sections.billing'),
|
|
12
|
+
* fields: [
|
|
13
|
+
* {
|
|
14
|
+
* key: 'vtestId',
|
|
15
|
+
* type: 'text',
|
|
16
|
+
* label: 'VTEST ID',
|
|
17
|
+
* // `accountType` belongs to another section: a page is one form, one state.
|
|
18
|
+
* dependencies: ['accountType'],
|
|
19
|
+
* required: ({ deps }) => hasActiveContract || 'accountType' in deps,
|
|
20
|
+
* },
|
|
21
|
+
* ],
|
|
22
|
+
* })
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare function defineFormPageSection<const TSection extends FormPageSection>(section: TSection): TSection;
|
|
27
|
+
/**
|
|
28
|
+
* Defines a form page: one form whose fields are grouped in `sections`. Each section becomes a
|
|
29
|
+
* card of the page and an entry of its navigation; render it with `<UiFormPage :form />`.
|
|
30
|
+
*
|
|
31
|
+
* The result is a normal form schema. Its `fields` are the cards generated from the sections,
|
|
32
|
+
* so `useForm`, validation, cross-section dependencies, and the typed `formData` work as for any
|
|
33
|
+
* schema, and the same schema opens in a modal or drawer as a stack of cards.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* export function accountFormSchema({ account }: { account?: Account } = {}) {
|
|
38
|
+
* return defineFormPageSchema({
|
|
39
|
+
* header: { title: () => t(account ? 'account.edit' : 'account.create') },
|
|
40
|
+
* controls: { dirtyCheck: Boolean(account) },
|
|
41
|
+
* navigation: { title: () => t('account.sections.title') },
|
|
42
|
+
* sections: [accountTypeSection(), identitySection(), billingSection({ hasActiveContract: false })],
|
|
43
|
+
* })
|
|
44
|
+
* }
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export declare function defineFormPageSchema<const TContext extends FormContextDefinition, const TSections extends readonly FormPageSection<FormContextData<NoInfer<TContext>>>[]>(schema: FormPageSchemaInput<TContext, TSections> & {
|
|
48
|
+
context: TContext;
|
|
49
|
+
}): FormPageSchema<FormPageSchemaInput<TContext, TSections>>;
|
|
50
|
+
export declare function defineFormPageSchema<const TSections extends readonly FormPageSection<FormContextData<undefined>>[]>(schema: FormPageSchemaInput<undefined, TSections>): FormPageSchema<FormPageSchemaInput<undefined, TSections>>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { isRecord } from "../utils/path.js";
|
|
2
|
+
export function defineFormPageSection(section) {
|
|
3
|
+
return section;
|
|
4
|
+
}
|
|
5
|
+
export function defineFormPageSchema(schema) {
|
|
6
|
+
if (!isRecord(schema) || !Array.isArray(schema.sections)) {
|
|
7
|
+
return schema;
|
|
8
|
+
}
|
|
9
|
+
return { ...schema, fields: schema.sections.filter(isRecord).map(toSectionCard) };
|
|
10
|
+
}
|
|
11
|
+
const CARD_PROPERTIES = ["condition", "dependencies", "description"];
|
|
12
|
+
function toSectionCard(section) {
|
|
13
|
+
const card = {
|
|
14
|
+
fields: section.fields,
|
|
15
|
+
key: section.key,
|
|
16
|
+
label: section.label,
|
|
17
|
+
type: "card"
|
|
18
|
+
};
|
|
19
|
+
for (const property of CARD_PROPERTIES) {
|
|
20
|
+
if (section[property] !== void 0) {
|
|
21
|
+
card[property] = section[property];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return card;
|
|
25
|
+
}
|
|
@@ -15,6 +15,7 @@ export type * from './layout';
|
|
|
15
15
|
export type * from './options';
|
|
16
16
|
export type * from './options-runtime';
|
|
17
17
|
export type * from './overlay';
|
|
18
|
+
export type * from './page';
|
|
18
19
|
export type * from './output';
|
|
19
20
|
export type * from './runtime';
|
|
20
21
|
export type * from './schema';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { QueryKey } from '@tanstack/vue-query';
|
|
2
|
+
import type { RemoteOptionsLoader, RemoteOptionsPageRequest, RemoteOptionsPagination, RemoteOptionsResult, RemoteOptionsSearch } from '../../shared/types/remote-options.js';
|
|
2
3
|
import type { FormValue } from './/index.js';
|
|
3
4
|
import type { FormFieldCallback, FormFieldCallbackParams } from './callbacks.js';
|
|
4
5
|
import type { FormQueryOptions } from './context.js';
|
|
@@ -92,6 +93,15 @@ export interface FormRemoteOptionConfig<TOption, TContext = NonNullable<unknown>
|
|
|
92
93
|
resolveSelected?: (request: FormRemoteSelectedRequest<TContext, TDeps, TValue, TOption>) => FormRemoteSource<readonly TOption[]>;
|
|
93
94
|
pagination: RemoteOptionsPagination;
|
|
94
95
|
search?: RemoteOptionsSearch;
|
|
96
|
+
/** Identity of the first page, so reusable loaders follow external scope changes. */
|
|
97
|
+
queryKeyFor?: (request: {
|
|
98
|
+
search: string;
|
|
99
|
+
page: RemoteOptionsPageRequest;
|
|
100
|
+
}) => QueryKey;
|
|
101
|
+
/** Identity of selected labels, including scope absent from the page endpoint. */
|
|
102
|
+
selectedQueryKeyFor?: (request: {
|
|
103
|
+
values: readonly FormOptionValue[];
|
|
104
|
+
}) => QueryKey;
|
|
95
105
|
/** Dependency aliases whose changes reset the loaded pages and re-run selected hydration. */
|
|
96
106
|
refreshOn?: readonly string[];
|
|
97
107
|
/** Clears selected values the latest successful selected hydration did not return. Defaults to `false`. */
|
|
@@ -103,4 +113,14 @@ export interface FormRemoteOptionConfig<TOption, TContext = NonNullable<unknown>
|
|
|
103
113
|
/** Disable the field while its option source has no usable data yet. */
|
|
104
114
|
disableOnLoading?: boolean;
|
|
105
115
|
}
|
|
106
|
-
|
|
116
|
+
/** Reuses one remote loader while keeping field-specific refresh and creation behavior local. */
|
|
117
|
+
export interface FormRemoteLoaderOptionConfig<TOption, TContext = NonNullable<unknown>, TDeps = NonNullable<unknown>, TValue = FormValue> extends Omit<FormRemoteOptionConfig<TOption, TContext, TDeps, TValue>, 'source' | 'resolveSelected' | 'pagination' | 'search' | 'queryKeyFor' | 'selectedQueryKeyFor'> {
|
|
118
|
+
loader: RemoteOptionsLoader<TOption>;
|
|
119
|
+
source?: never;
|
|
120
|
+
resolveSelected?: never;
|
|
121
|
+
pagination?: never;
|
|
122
|
+
search?: never;
|
|
123
|
+
queryKeyFor?: never;
|
|
124
|
+
selectedQueryKeyFor?: never;
|
|
125
|
+
}
|
|
126
|
+
export type FormAnyOptionConfig<TOption, TContext = NonNullable<unknown>, TDeps = NonNullable<unknown>, TValue = FormValue> = FormOptionConfig<TOption, TContext, TDeps, TValue> | FormRemoteOptionConfig<TOption, TContext, TDeps, TValue> | FormRemoteLoaderOptionConfig<TOption, TContext, TDeps, TValue> | FormOptionsSource<TOption, TContext, TDeps, TValue>;
|