nuxt-ui-tools 1.17.0 → 1.18.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 +1 -0
- package/dist/runtime/form/components/page/form-page-navigation.d.vue.ts +5 -3
- package/dist/runtime/form/components/page/form-page-navigation.vue +6 -3
- package/dist/runtime/form/components/page/form-page-navigation.vue.d.ts +5 -3
- package/dist/runtime/form/components/page/form-page-section.vue +6 -5
- package/dist/runtime/form/components/page/form-page-sections.d.vue.ts +4 -3
- package/dist/runtime/form/components/page/form-page-sections.vue +7 -2
- package/dist/runtime/form/components/page/form-page-sections.vue.d.ts +4 -3
- package/dist/runtime/form/components/page/form-page.d.vue.ts +2 -0
- package/dist/runtime/form/components/page/form-page.vue +10 -2
- package/dist/runtime/form/components/page/form-page.vue.d.ts +2 -0
- package/dist/runtime/form/components/renderer/form-field-renderer.vue +6 -4
- package/dist/runtime/form/components/renderer/form-field-shell.d.vue.ts +4 -4
- package/dist/runtime/form/components/renderer/form-field-shell.vue +28 -4
- package/dist/runtime/form/components/renderer/form-field-shell.vue.d.ts +4 -4
- package/dist/runtime/form/components/root/form.vue +2 -2
- package/dist/runtime/form/composables/use-field-control.d.ts +1 -1
- package/dist/runtime/form/composables/use-field-control.js +9 -21
- package/dist/runtime/form/composables/use-field-options.js +8 -4
- package/dist/runtime/form/composables/use-form-page.d.ts +11 -1
- package/dist/runtime/form/composables/use-form-page.js +12 -6
- package/dist/runtime/form/composables/use-form-runtime.js +1 -1
- package/dist/runtime/form/fields/array-primitive/types.d.ts +1 -1
- package/dist/runtime/form/fields/checkbox-card/component.vue +54 -28
- package/dist/runtime/form/fields/checkbox-card/config.d.ts +1 -0
- package/dist/runtime/form/fields/checkbox-card/config.js +1 -1
- package/dist/runtime/form/fields/checkbox-card/types.d.ts +3 -3
- package/dist/runtime/form/fields/choice-card/choice-card-label.d.vue.ts +7 -0
- package/dist/runtime/form/fields/choice-card/choice-card-label.vue +27 -1
- package/dist/runtime/form/fields/choice-card/choice-card-label.vue.d.ts +7 -0
- package/dist/runtime/form/fields/choice-card/types.d.ts +54 -1
- package/dist/runtime/form/fields/choice-card/use-choice-card.d.ts +7 -3
- package/dist/runtime/form/fields/choice-card/use-choice-card.js +37 -15
- package/dist/runtime/form/fields/choice-card/utils.d.ts +27 -0
- package/dist/runtime/form/fields/choice-card/utils.js +17 -0
- package/dist/runtime/form/fields/hierarchy/component.vue +3 -2
- package/dist/runtime/form/fields/index.d.ts +2 -0
- package/dist/runtime/form/fields/radio-card/component.vue +58 -28
- package/dist/runtime/form/fields/radio-card/config.d.ts +1 -0
- package/dist/runtime/form/fields/radio-card/config.js +1 -1
- package/dist/runtime/form/fields/radio-card/types.d.ts +3 -3
- package/dist/runtime/form/types/field-base.d.ts +6 -0
- package/dist/runtime/form/types/field-kind.d.ts +3 -1
- package/dist/runtime/form/types/field.d.ts +1 -0
- package/dist/runtime/form/types/page.d.ts +20 -8
- package/dist/runtime/form/types/ui.d.ts +24 -4
- package/dist/runtime/form/utils/disabled.d.ts +11 -0
- package/dist/runtime/form/utils/disabled.js +11 -0
- package/dist/runtime/form/utils/field-instance.js +3 -0
- package/dist/runtime/form/utils/options.d.ts +22 -1
- package/dist/runtime/form/utils/options.js +15 -0
- package/dist/runtime/form/utils/page-surface.d.ts +21 -0
- package/dist/runtime/form/utils/page-surface.js +27 -0
- package/dist/runtime/form/utils/page.d.ts +21 -1
- package/dist/runtime/form/utils/page.js +24 -2
- package/dist/runtime/i18n/locales/en.js +28 -8
- package/dist/runtime/i18n/locales/fr.js +28 -8
- package/dist/runtime/i18n/types.d.ts +46 -7
- package/dist/runtime/table/components/actions/row-actions.d.vue.ts +2 -0
- package/dist/runtime/table/components/actions/row-actions.vue +8 -3
- package/dist/runtime/table/components/actions/row-actions.vue.d.ts +2 -0
- package/dist/runtime/table/components/data-list/data-list-content.vue +7 -1
- package/dist/runtime/table/components/data-list/data-list-default.d.vue.ts +16 -16
- package/dist/runtime/table/components/data-list/data-list-default.vue +3 -0
- package/dist/runtime/table/components/data-list/data-list-default.vue.d.ts +16 -16
- package/dist/runtime/table/components/data-list/data-list-layout-switch.vue +7 -9
- package/dist/runtime/table/components/data-list/data-list-root.vue +5 -6
- package/dist/runtime/table/components/data-list/data-list-sort-menu.vue +3 -1
- package/dist/runtime/table/components/data-list/data-list-table.d.vue.ts +8 -1
- package/dist/runtime/table/components/data-list/data-list-table.vue +18 -1
- package/dist/runtime/table/components/data-list/data-list-table.vue.d.ts +8 -1
- package/dist/runtime/table/components/data-list/data-list-view-toggle.d.vue.ts +25 -0
- package/dist/runtime/table/components/data-list/data-list-view-toggle.vue +57 -0
- package/dist/runtime/table/components/data-list/data-list-view-toggle.vue.d.ts +25 -0
- package/dist/runtime/table/components/data-list/sort-menu-panel.vue +1 -1
- package/dist/runtime/table/components/layout/table-footer.d.vue.ts +6 -4
- package/dist/runtime/table/components/layout/table-footer.vue +80 -3
- package/dist/runtime/table/components/layout/table-footer.vue.d.ts +6 -4
- package/dist/runtime/table/components/summary/summary-content.d.ts +13 -0
- package/dist/runtime/table/components/summary/summary-content.js +5 -0
- package/dist/runtime/table/components/summary/summary-renderer.d.vue.ts +31 -0
- package/dist/runtime/table/components/summary/summary-renderer.vue +282 -0
- package/dist/runtime/table/components/summary/summary-renderer.vue.d.ts +31 -0
- package/dist/runtime/table/components/summary/summary-row.d.vue.ts +19 -0
- package/dist/runtime/table/components/summary/summary-row.vue +102 -0
- package/dist/runtime/table/components/summary/summary-row.vue.d.ts +19 -0
- package/dist/runtime/table/components/table/table-empty-state.vue +39 -9
- package/dist/runtime/table/components/table/table-renderer.vue +1 -1
- package/dist/runtime/table/components/table/table-row-actions-control.vue +10 -6
- package/dist/runtime/table/components/table/table-selection-control.d.vue.ts +5 -1
- package/dist/runtime/table/components/table/table-selection-control.vue +17 -4
- package/dist/runtime/table/components/table/table-selection-control.vue.d.ts +5 -1
- package/dist/runtime/table/components/table/table-tree-control.vue +2 -1
- package/dist/runtime/table/components/table/table-tree-header.vue +0 -1
- package/dist/runtime/table/composables/use-table-api.d.ts +2 -0
- package/dist/runtime/table/composables/use-table-api.js +8 -0
- package/dist/runtime/table/composables/use-table-columns.d.ts +2 -0
- package/dist/runtime/table/composables/use-table-controls.d.ts +6 -0
- package/dist/runtime/table/composables/use-table-controls.js +6 -3
- package/dist/runtime/table/composables/use-table-internals.d.ts +33 -0
- package/dist/runtime/table/composables/use-table-internals.js +9 -0
- package/dist/runtime/table/composables/use-table-row-labels.d.ts +16 -0
- package/dist/runtime/table/composables/use-table-row-labels.js +22 -0
- package/dist/runtime/table/composables/use-table-row-summary.d.ts +18 -0
- package/dist/runtime/table/composables/use-table-row-summary.js +41 -0
- package/dist/runtime/table/constants/mobile.d.ts +79 -0
- package/dist/runtime/table/constants/mobile.js +17 -0
- package/dist/runtime/table/constants/summary.d.ts +67 -0
- package/dist/runtime/table/constants/summary.js +31 -0
- package/dist/runtime/table/types/data-list.d.ts +58 -8
- package/dist/runtime/table/types/layout.d.ts +73 -0
- package/dist/runtime/table/types/schema.d.ts +9 -1
- package/dist/runtime/table/types/source.d.ts +6 -1
- package/dist/runtime/table/types/table-api.d.ts +12 -0
- package/dist/runtime/table/utils/columns/defs.js +3 -1
- package/dist/runtime/table/utils/columns/render.d.ts +2 -0
- package/dist/runtime/table/utils/columns/render.js +2 -2
- package/dist/runtime/table/utils/data-list-ui.js +3 -1
- package/dist/runtime/table/utils/mobile-defaults.d.ts +7 -0
- package/dist/runtime/table/utils/mobile-defaults.js +25 -0
- package/package.json +1 -1
|
@@ -3,9 +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
7
|
import { isBoolean, isNumber, isString } from "../../utils/predicate";
|
|
7
8
|
import ChoiceCardLabel from "../choice-card/choice-card-label.vue";
|
|
8
9
|
import { CHOICE_CARD_PROPS, useChoiceCard } from "../choice-card/use-choice-card";
|
|
10
|
+
import { choiceCardGroupAttrs, groupChoiceCardItems } from "../choice-card/utils";
|
|
9
11
|
const props = defineProps({
|
|
10
12
|
field: { type: Object, required: true },
|
|
11
13
|
path: { type: Array, required: true }
|
|
@@ -15,7 +17,11 @@ const { fieldProps, form, controlProps, disabled, handleBlur, options } = useFie
|
|
|
15
17
|
() => props.path,
|
|
16
18
|
{ omit: CHOICE_CARD_PROPS }
|
|
17
19
|
);
|
|
18
|
-
const cards = useChoiceCard({
|
|
20
|
+
const cards = useChoiceCard({
|
|
21
|
+
kind: "radio",
|
|
22
|
+
props: () => fieldProps.value,
|
|
23
|
+
ui: () => controlProps.value.ui
|
|
24
|
+
});
|
|
19
25
|
const model = computed({
|
|
20
26
|
get: () => {
|
|
21
27
|
const value = form.getValue(props.path);
|
|
@@ -26,36 +32,60 @@ const model = computed({
|
|
|
26
32
|
},
|
|
27
33
|
set: (value) => form.setValue(props.path, value)
|
|
28
34
|
});
|
|
29
|
-
const
|
|
35
|
+
const descriptionByKey = computed(
|
|
36
|
+
() => new Map(options.items.value.map((item) => [formOptionKey(item.value), item.description]))
|
|
37
|
+
);
|
|
38
|
+
const items = computed(
|
|
39
|
+
() => options.items.value.map(
|
|
40
|
+
(item) => cards.compact.value ? { ...item, description: void 0 } : item
|
|
41
|
+
)
|
|
42
|
+
);
|
|
43
|
+
const groups = computed(() => groupChoiceCardItems(items.value));
|
|
44
|
+
function selectionOf(group) {
|
|
45
|
+
return group.items.some((item) => item.value === model.value) ? model.value : void 0;
|
|
46
|
+
}
|
|
47
|
+
function select(value) {
|
|
48
|
+
if (isString(value) || isNumber(value) || isBoolean(value)) model.value = value;
|
|
49
|
+
}
|
|
30
50
|
</script>
|
|
31
51
|
|
|
32
52
|
<template>
|
|
33
53
|
<FormFieldShell :field="field" :path="path">
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
54
|
+
<div :class="cards.groupsClass.value">
|
|
55
|
+
<URadioGroup
|
|
56
|
+
v-for="(group, index) in groups"
|
|
57
|
+
:key="group.key"
|
|
58
|
+
:model-value="selectionOf(group)"
|
|
59
|
+
v-bind="choiceCardGroupAttrs(controlProps, index)"
|
|
60
|
+
value-key="value"
|
|
61
|
+
label-key="label"
|
|
62
|
+
variant="card"
|
|
63
|
+
:legend="group.heading"
|
|
64
|
+
:items="group.items"
|
|
65
|
+
:orientation="cards.orientation.value"
|
|
66
|
+
:indicator="cards.indicator.value"
|
|
67
|
+
:ui="cards.ui.value"
|
|
68
|
+
:style="cards.style.value"
|
|
69
|
+
:disabled="disabled"
|
|
70
|
+
@update:model-value="select"
|
|
71
|
+
@blur="handleBlur"
|
|
72
|
+
>
|
|
73
|
+
<template #label="{ item }">
|
|
74
|
+
<ChoiceCardLabel
|
|
75
|
+
:label="item.label"
|
|
76
|
+
:description="
|
|
77
|
+
cards.compact.value ? descriptionByKey.get(formOptionKey(item.value)) : void 0
|
|
78
|
+
"
|
|
79
|
+
:icon="item.icon"
|
|
80
|
+
:value="item.value"
|
|
81
|
+
:tile="cards.tile.value"
|
|
82
|
+
:compact="cards.compact.value"
|
|
83
|
+
:mark="cards.mark.value"
|
|
84
|
+
:selected="model === item.value"
|
|
85
|
+
:ui="cards.ui.value"
|
|
86
|
+
/>
|
|
87
|
+
</template>
|
|
88
|
+
</URadioGroup>
|
|
89
|
+
</div>
|
|
60
90
|
</FormFieldShell>
|
|
61
91
|
</template>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { defineFormFieldKind } from "../../utils/field-kind.js";
|
|
2
2
|
export const radioCardFieldKind = 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: "radio-card",
|
|
@@ -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 {
|
|
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
|
|
3
|
+
import type { FormOptionValue, FormOptionsSource } from '../../types/options.js';
|
|
4
|
+
import type { FormChoiceCardGrouping, 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>, FormChoiceCardGrouping {
|
|
6
6
|
options: FormOptionsSource<TOption, TContext, TDeps, TValue | null>;
|
|
7
7
|
}
|
|
8
8
|
export type RadioCardFieldOutput<TField> = FieldOptionValue<TField> | NullableValue;
|
|
@@ -53,6 +53,12 @@ export interface FormStatefulFieldBase<TType extends FormFieldType, TValue, TCon
|
|
|
53
53
|
props?: FormFieldProps<TProps, TContext, TDeps>;
|
|
54
54
|
/** Disables the field without removing it from form state. */
|
|
55
55
|
disabled?: FormFieldCallback<boolean, TContext, TDeps, TValue>;
|
|
56
|
+
/**
|
|
57
|
+
* Why the field is disabled, shown under the control with a lock icon only while the `disabled`
|
|
58
|
+
* callback holds. Static text, or a callback receiving `{ ctx, deps, api }` that returns nothing
|
|
59
|
+
* when no reason applies.
|
|
60
|
+
*/
|
|
61
|
+
disabledReason?: FormText | FormFieldCallback<FormText | undefined, TContext, TDeps, TValue>;
|
|
56
62
|
/** Controls whether the field is rendered. Hidden fields can still be part of form state. */
|
|
57
63
|
condition?: FormFieldCallback<boolean, TContext, TDeps, TValue>;
|
|
58
64
|
/** Validation behavior for this field. */
|
|
@@ -21,6 +21,8 @@ export interface FormFieldKindDefinition<TType extends FormFieldType = FormField
|
|
|
21
21
|
/** Whether the field owns option resolution. */
|
|
22
22
|
options?: {
|
|
23
23
|
enabled: boolean;
|
|
24
|
+
/** Whether the field lists its options under headings read from a property of the options. */
|
|
25
|
+
groups?: boolean;
|
|
24
26
|
};
|
|
25
27
|
/** Whether the field owns file upload work. */
|
|
26
28
|
upload?: {
|
|
@@ -33,4 +35,4 @@ export interface FormFieldKindDefinition<TType extends FormFieldType = FormField
|
|
|
33
35
|
}
|
|
34
36
|
export type FormFieldKindState = FormFieldKindDefinition['state'];
|
|
35
37
|
export type FormFieldState = FormFieldKindState;
|
|
36
|
-
export type FormFieldCapability = 'value' | 'children' | 'label' | 'description' | 'hint' | 'layout' | 'itemLayout' | 'containerLayout' | 'options' | 'upload' | 'validation' | 'transform' | 'submit';
|
|
38
|
+
export type FormFieldCapability = 'value' | 'children' | 'label' | 'description' | 'hint' | 'layout' | 'itemLayout' | 'containerLayout' | 'options' | 'optionGroups' | 'upload' | 'validation' | 'transform' | 'submit';
|
|
@@ -54,6 +54,7 @@ export type { FormAutoCompleteField } from '../fields/auto-complete/types.js';
|
|
|
54
54
|
export type { FormButtonField } from '../fields/button/types.js';
|
|
55
55
|
export type { FormCardField } from '../fields/card/types.js';
|
|
56
56
|
export type { FormCheckboxCardField } from '../fields/checkbox-card/types.js';
|
|
57
|
+
export type { FormChoiceCardDensity, FormChoiceCardOption, FormChoiceCardOptionItem, FormChoiceCardProps, } from '../fields/choice-card/types.js';
|
|
57
58
|
export type { FormCheckboxField } from '../fields/checkbox/types.js';
|
|
58
59
|
export type { FormCheckboxGroupField } from '../fields/checkbox-group/types.js';
|
|
59
60
|
export type { FormColorPickerField } from '../fields/color-picker/types.js';
|
|
@@ -41,10 +41,10 @@ export interface FormPageSection<TContext = NonNullable<unknown>, TFields extend
|
|
|
41
41
|
/** Supporting copy next to the title. */
|
|
42
42
|
description?: FormText;
|
|
43
43
|
/**
|
|
44
|
-
*
|
|
45
|
-
* without a required field is optional already; set this when its
|
|
46
|
-
* by their defaults and the user has nothing to do there. Set
|
|
47
|
-
* required when its only required fields are the cells of array rows.
|
|
44
|
+
* Marks the section optional while nothing is entered in it, and leaves it out of the sections
|
|
45
|
+
* left to complete. A section without a required field is optional already; set this when its
|
|
46
|
+
* required fields are filled by their defaults and the user has nothing to do there. Set
|
|
47
|
+
* `false` to keep a section required when its only required fields are the cells of array rows.
|
|
48
48
|
*/
|
|
49
49
|
optional?: boolean;
|
|
50
50
|
/** Grid of the section's fields, merged over the schema `layout`. */
|
|
@@ -62,6 +62,12 @@ export interface FormPageSection<TContext = NonNullable<unknown>, TFields extend
|
|
|
62
62
|
export interface FormPageNavigationConfig {
|
|
63
63
|
/** Heading above the entries, e.g. `'Création'`. Accepts a function to translate it. */
|
|
64
64
|
title?: FormText;
|
|
65
|
+
/**
|
|
66
|
+
* Shows the navigation only when the page has at least this many visible sections. Below it,
|
|
67
|
+
* the page is one focused column of content: two short sections gain nothing from a side
|
|
68
|
+
* navigation. Without it, the navigation always shows.
|
|
69
|
+
*/
|
|
70
|
+
minSections?: number;
|
|
65
71
|
}
|
|
66
72
|
/**
|
|
67
73
|
* Schema accepted by `defineFormPageSchema`: a form schema whose `sections` replace `fields`.
|
|
@@ -142,12 +148,18 @@ export interface FormPageScrollOptions {
|
|
|
142
148
|
/**
|
|
143
149
|
* Where a section stands:
|
|
144
150
|
*
|
|
145
|
-
* - `invalid`: it shows at least one validation error
|
|
151
|
+
* - `invalid`: it shows at least one validation error, which only appears once the user left the
|
|
152
|
+
* field or tried to submit, so a section nobody touched is never `invalid`
|
|
153
|
+
* - `pending`: a required field has no value yet, in a required section, or in an optional one
|
|
154
|
+
* the user started and left incomplete
|
|
146
155
|
* - `complete`: nothing required is missing and it shows no error; an optional section also
|
|
147
156
|
* needs a value, entered by the user or provided by the form input
|
|
148
|
-
* - `
|
|
157
|
+
* - `optional`: an optional section without any entered value, where there is nothing to do
|
|
158
|
+
*
|
|
159
|
+
* `invalid` and `pending` are the sections that keep the form from being submitted, and the only
|
|
160
|
+
* ones the navigation counts as left to complete.
|
|
149
161
|
*/
|
|
150
|
-
export type FormPageSectionStatus = 'invalid' | 'complete' | '
|
|
162
|
+
export type FormPageSectionStatus = 'invalid' | 'pending' | 'complete' | 'optional';
|
|
151
163
|
/**
|
|
152
164
|
* Live state of a visible section, as the page navigation and section slots receive it.
|
|
153
165
|
*/
|
|
@@ -167,7 +179,7 @@ export interface FormPageSectionState {
|
|
|
167
179
|
optional: boolean;
|
|
168
180
|
/** Number of required fields without a value, the fields of array items included. */
|
|
169
181
|
missing: number;
|
|
170
|
-
/**
|
|
182
|
+
/** Where the section stands, see `FormPageSectionStatus`. */
|
|
171
183
|
status: FormPageSectionStatus;
|
|
172
184
|
/** True when a value of the section differs from the form's baseline. */
|
|
173
185
|
dirty: boolean;
|
|
@@ -44,6 +44,8 @@ export interface FormFieldUi {
|
|
|
44
44
|
hint?: FormUiClass;
|
|
45
45
|
labelExtra?: FormUiClass;
|
|
46
46
|
help?: FormUiClass;
|
|
47
|
+
disabledReason?: FormUiClass;
|
|
48
|
+
disabledReasonIcon?: FormUiClass;
|
|
47
49
|
body?: FormUiClass;
|
|
48
50
|
content?: FormUiClass;
|
|
49
51
|
pending?: FormUiClass;
|
|
@@ -191,11 +193,27 @@ export interface FormOverlayUi {
|
|
|
191
193
|
overlay?: FormUiClass;
|
|
192
194
|
content?: FormUiClass;
|
|
193
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* How the sections of a form page are drawn:
|
|
198
|
+
*
|
|
199
|
+
* - `card`: each section is a bordered card on the gray canvas of the page (default)
|
|
200
|
+
* - `plain`: no box. A section is its heading, description, and fields on the page itself,
|
|
201
|
+
* separated from the next by spacing and a hairline
|
|
202
|
+
*/
|
|
203
|
+
export type FormPageSurface = 'card' | 'plain';
|
|
204
|
+
/**
|
|
205
|
+
* The `page` part of the form UI config: the surface of the sections and the `ui` slots of the
|
|
206
|
+
* page parts. Both follow the usual layering: app config, then schema `ui`, then the `ui` prop.
|
|
207
|
+
*/
|
|
208
|
+
export interface FormPageUiConfig extends FormUiPartConfig<FormPageUi> {
|
|
209
|
+
/** Defaults to `card`. */
|
|
210
|
+
surface?: FormPageSurface;
|
|
211
|
+
}
|
|
194
212
|
/**
|
|
195
213
|
* Slots of a form page and its parts. Data attributes carry the live state, so a slot can
|
|
196
214
|
* restyle one state: `data-active` on the current navigation entry, `data-state`
|
|
197
|
-
* (`complete`, `invalid`, `pending`) on entries
|
|
198
|
-
* sections and entries.
|
|
215
|
+
* (`complete`, `invalid`, `pending`, `optional`) on entries, indicators, and sections,
|
|
216
|
+
* `data-dirty` on modified sections and entries.
|
|
199
217
|
*
|
|
200
218
|
* Sections and the navigation stay below the pinned header: the page gives each section its
|
|
201
219
|
* `scroll-margin-top` and the navigation the `--nut-form-page-header` variable (the header's
|
|
@@ -228,9 +246,10 @@ export interface FormPageUi {
|
|
|
228
246
|
navigationIndicator?: FormUiClass;
|
|
229
247
|
/** Check of a complete section in the indicator. */
|
|
230
248
|
navigationIndicatorIcon?: FormUiClass;
|
|
231
|
-
/** Dot in the indicator of the current section while it is pending. */
|
|
249
|
+
/** Dot in the indicator of the current section while it is pending or optional. */
|
|
232
250
|
navigationIndicatorMarker?: FormUiClass;
|
|
233
251
|
navigationLabel?: FormUiClass;
|
|
252
|
+
/** The "Optional" tag of an entry whose optional section has nothing entered. */
|
|
234
253
|
navigationOptional?: FormUiClass;
|
|
235
254
|
navigationDirty?: FormUiClass;
|
|
236
255
|
/** Wrapper under the entries, whose padding insets the summary. */
|
|
@@ -246,6 +265,7 @@ export interface FormPageUi {
|
|
|
246
265
|
sectionHeader?: FormUiClass;
|
|
247
266
|
sectionTitle?: FormUiClass;
|
|
248
267
|
sectionDescription?: FormUiClass;
|
|
268
|
+
/** The "Optional" badge of an optional section with nothing entered. */
|
|
249
269
|
sectionOptional?: FormUiClass;
|
|
250
270
|
sectionActions?: FormUiClass;
|
|
251
271
|
sectionReset?: FormUiClass;
|
|
@@ -288,5 +308,5 @@ export interface FormUiConfig {
|
|
|
288
308
|
drawer?: FormUiPartConfig<FormOverlayUi>;
|
|
289
309
|
fullscreen?: FormUiPartConfig<FormOverlayUi>;
|
|
290
310
|
/** Form page rendered by `FormPage` and its parts. */
|
|
291
|
-
page?:
|
|
311
|
+
page?: FormPageUiConfig;
|
|
292
312
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { FormField, FormFieldCallbackParams } from '../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Whether the field's authored `disabled` callback holds. Engine-driven disabling (a pending form
|
|
4
|
+
* action, options still loading) is separate: it explains itself, so it never carries a reason.
|
|
5
|
+
*/
|
|
6
|
+
export declare function isFieldDisabledByCallback(field: FormField, params: FormFieldCallbackParams): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* The reason to show under a field while its `disabled` callback holds: the authored
|
|
9
|
+
* `disabledReason` text, or nothing when the field is enabled or the author gave no reason.
|
|
10
|
+
*/
|
|
11
|
+
export declare function resolveFieldDisabledReason(field: FormField, params: FormFieldCallbackParams): string | undefined;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { isFunction } from "./predicate.js";
|
|
2
|
+
import { resolveFieldText } from "./text.js";
|
|
3
|
+
export function isFieldDisabledByCallback(field, params) {
|
|
4
|
+
const disabled = Object.getOwnPropertyDescriptor(field, "disabled")?.value;
|
|
5
|
+
return isFunction(disabled) ? disabled(params) === true : false;
|
|
6
|
+
}
|
|
7
|
+
export function resolveFieldDisabledReason(field, params) {
|
|
8
|
+
if (!isFieldDisabledByCallback(field, params)) return void 0;
|
|
9
|
+
const reason = Object.getOwnPropertyDescriptor(field, "disabledReason")?.value;
|
|
10
|
+
return resolveFieldText(reason, params) || void 0;
|
|
11
|
+
}
|
|
@@ -67,6 +67,9 @@ export function fieldKindHas(kind, capability) {
|
|
|
67
67
|
if (capability === "options") {
|
|
68
68
|
return kind.options?.enabled === true;
|
|
69
69
|
}
|
|
70
|
+
if (capability === "optionGroups") {
|
|
71
|
+
return kind.options?.groups === true;
|
|
72
|
+
}
|
|
70
73
|
if (capability === "upload") {
|
|
71
74
|
return kind.upload?.enabled === true;
|
|
72
75
|
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import type { FormValue, FormFieldCallbackParams, FormOptionItem, FormOptionValue } from '../types/index.js';
|
|
1
|
+
import type { FormValue, FormField, FormFieldCallbackParams, FormOptionItem, FormOptionValue } from '../types/index.js';
|
|
2
2
|
export interface FormOptionKeys {
|
|
3
3
|
value?: string;
|
|
4
4
|
label?: string;
|
|
5
5
|
children?: string;
|
|
6
|
+
/**
|
|
7
|
+
* Property holding the heading an option is listed under. Options are not grouped without it:
|
|
8
|
+
* a `group` property is never read unless it is mapped here.
|
|
9
|
+
*/
|
|
10
|
+
group?: string;
|
|
6
11
|
}
|
|
7
12
|
export interface ResolvedFormOption {
|
|
8
13
|
value: FormOptionValue;
|
|
@@ -10,12 +15,28 @@ export interface ResolvedFormOption {
|
|
|
10
15
|
description?: string;
|
|
11
16
|
disabled?: boolean;
|
|
12
17
|
icon?: string;
|
|
18
|
+
/** Heading the option is listed under, for the fields that group their options. */
|
|
19
|
+
group?: string;
|
|
13
20
|
children?: readonly ResolvedFormOption[];
|
|
14
21
|
/** True when the option has children that load on demand. */
|
|
15
22
|
lazy?: boolean;
|
|
16
23
|
}
|
|
17
24
|
export declare function normalizeOptionItem(option: FormValue, keys?: FormOptionKeys): ResolvedFormOption;
|
|
18
25
|
export declare function normalizeOptionItems(options: readonly FormValue[] | undefined, keys?: FormOptionKeys): ResolvedFormOption[];
|
|
26
|
+
/**
|
|
27
|
+
* The `options` of a field as authored: its array, function or query source, whether given
|
|
28
|
+
* directly or inside a `source` property.
|
|
29
|
+
*/
|
|
30
|
+
export declare function readOptionSource(field: FormField): FormValue;
|
|
31
|
+
/**
|
|
32
|
+
* Property the options of a field hold their heading in, for the field kinds that list options
|
|
33
|
+
* under headings. The field's `groupKey` names it, read statically from the definition like
|
|
34
|
+
* `labelKey` and `valueKey`. Without it, options written inline in the schema group by their
|
|
35
|
+
* `group`, the way a literal option is authored. The records a function, a promise or a query
|
|
36
|
+
* returns belong to the author's data, so a `group` they happen to carry is not read until
|
|
37
|
+
* `groupKey` maps it. `groupKey: false` opts out.
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolveOptionGroupKey(field: FormField): string | undefined;
|
|
19
40
|
export declare const LOAD_MORE_OPTION_VALUE = "__nut:load-more__";
|
|
20
41
|
export declare function isLoadMoreOption(item: FormValue): boolean;
|
|
21
42
|
export declare function appendLoadMoreOption(items: readonly ResolvedFormOption[], show: boolean, label: string): readonly ResolvedFormOption[];
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
import { createFormFieldInstance } from "./field-instance.js";
|
|
1
2
|
import { isRecord } from "./path.js";
|
|
2
3
|
import { isBoolean, isFunction, isNumber, isString, isUndefined } from "./predicate.js";
|
|
3
4
|
import { resolveFormText } from "./text.js";
|
|
5
|
+
const DEFAULT_GROUP_KEY = "group";
|
|
4
6
|
export function normalizeOptionItem(option, keys = {}) {
|
|
5
7
|
if (isRecord(option)) {
|
|
6
8
|
const rawValue = readOptionProperty(option, keys.value ?? "value");
|
|
7
9
|
const fallbackValue = isUndefined(rawValue) ? option.key : rawValue;
|
|
8
10
|
const rawChildren = readOptionProperty(option, keys.children ?? "children");
|
|
9
11
|
const rawDescription = readOptionProperty(option, "description");
|
|
12
|
+
const rawGroup = keys.group ? readOptionProperty(option, keys.group) : void 0;
|
|
10
13
|
const children = Array.isArray(rawChildren) ? rawChildren.map((child) => normalizeOptionItem(child, keys)) : void 0;
|
|
11
14
|
const lazy = option.isLeaf === false || option.hasChildren === true;
|
|
12
15
|
const value = normalizeOptionValue(fallbackValue);
|
|
@@ -14,6 +17,7 @@ export function normalizeOptionItem(option, keys = {}) {
|
|
|
14
17
|
const normalized = {
|
|
15
18
|
description: isFormText(rawDescription) ? resolveFormText(rawDescription) : void 0,
|
|
16
19
|
disabled: option.disabled === true,
|
|
20
|
+
group: isFormText(rawGroup) ? resolveFormText(rawGroup) : void 0,
|
|
17
21
|
icon: isString(rawIcon) ? rawIcon : void 0,
|
|
18
22
|
label: resolveFormText(readOptionProperty(option, keys.label ?? "label")) ?? String(value),
|
|
19
23
|
value
|
|
@@ -36,6 +40,17 @@ export function normalizeOptionItem(option, keys = {}) {
|
|
|
36
40
|
export function normalizeOptionItems(options, keys = {}) {
|
|
37
41
|
return (options ?? []).map((option) => normalizeOptionItem(option, keys));
|
|
38
42
|
}
|
|
43
|
+
export function readOptionSource(field) {
|
|
44
|
+
const options = Object.getOwnPropertyDescriptor(field, "options")?.value;
|
|
45
|
+
return isRecord(options) && "source" in options ? options.source : options;
|
|
46
|
+
}
|
|
47
|
+
export function resolveOptionGroupKey(field) {
|
|
48
|
+
if (!createFormFieldInstance(field).capability.has("optionGroups")) return void 0;
|
|
49
|
+
const mapped = Object.getOwnPropertyDescriptor(field, "groupKey")?.value;
|
|
50
|
+
if (mapped === false) return void 0;
|
|
51
|
+
if (isString(mapped) && mapped) return mapped;
|
|
52
|
+
return Array.isArray(readOptionSource(field)) ? DEFAULT_GROUP_KEY : void 0;
|
|
53
|
+
}
|
|
39
54
|
export const LOAD_MORE_OPTION_VALUE = "__nut:load-more__";
|
|
40
55
|
export function isLoadMoreOption(item) {
|
|
41
56
|
return isRecord(item) && item.value === LOAD_MORE_OPTION_VALUE;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { FormPageSurface, FormValue } from '../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* What a surface sets on the parts of a form page it restyles. These are the default classes of
|
|
4
|
+
* each slot: the `FormPageUi` slots merge over them.
|
|
5
|
+
*/
|
|
6
|
+
export interface FormPageSurfaceClasses {
|
|
7
|
+
/** Background the sections sit on. */
|
|
8
|
+
root: string;
|
|
9
|
+
/** Gap between the sections. */
|
|
10
|
+
sections: string;
|
|
11
|
+
/** The box of a card, or the spacing, hairline, and modified marker of a plain section. */
|
|
12
|
+
section: string;
|
|
13
|
+
/** Where the description sits against the title. */
|
|
14
|
+
sectionDescription: string;
|
|
15
|
+
/** Placeholder shown while the schema context loads. */
|
|
16
|
+
sectionSkeleton: string;
|
|
17
|
+
}
|
|
18
|
+
export declare const DEFAULT_FORM_PAGE_SURFACE: FormPageSurface;
|
|
19
|
+
/** The surface a page draws its sections with, `card` unless the config names a known one. */
|
|
20
|
+
export declare function resolveFormPageSurface(value: FormValue): FormPageSurface;
|
|
21
|
+
export declare function getFormPageSurfaceClasses(surface: FormPageSurface): FormPageSurfaceClasses;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { isString } from "./predicate.js";
|
|
2
|
+
export const DEFAULT_FORM_PAGE_SURFACE = "card";
|
|
3
|
+
const FORM_PAGE_SURFACES = {
|
|
4
|
+
card: {
|
|
5
|
+
root: "bg-muted",
|
|
6
|
+
section: "gap-[18px] rounded-xl border border-default bg-default px-5 pt-5 pb-6 transition-[border-color,box-shadow] duration-200 @3xl/form-page:px-6 @3xl/form-page:pt-[22px] data-[dirty]:border-primary data-[dirty]:ring-4 data-[dirty]:ring-primary/20 data-[dirty]:ring-inset",
|
|
7
|
+
sectionDescription: "order-last basis-full @3xl/form-page:order-none @3xl/form-page:basis-auto",
|
|
8
|
+
sectionSkeleton: "rounded-xl border border-default bg-default px-6 pt-[22px] pb-6",
|
|
9
|
+
sections: "gap-[18px]"
|
|
10
|
+
},
|
|
11
|
+
plain: {
|
|
12
|
+
root: "bg-default",
|
|
13
|
+
section: "relative gap-[18px] border-t border-default py-8 first:border-t-0 first:pt-0 last:pb-0 before:pointer-events-none before:absolute before:inset-y-8 before:-start-3 before:w-0.5 before:rounded-full before:transition-colors first:before:top-0 last:before:bottom-0 data-[dirty]:before:bg-primary",
|
|
14
|
+
sectionDescription: "order-last basis-full",
|
|
15
|
+
sectionSkeleton: "border-t border-default py-8 first:border-t-0 first:pt-0",
|
|
16
|
+
sections: "gap-0"
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
export function resolveFormPageSurface(value) {
|
|
20
|
+
return isFormPageSurface(value) ? value : DEFAULT_FORM_PAGE_SURFACE;
|
|
21
|
+
}
|
|
22
|
+
export function getFormPageSurfaceClasses(surface) {
|
|
23
|
+
return FORM_PAGE_SURFACES[surface];
|
|
24
|
+
}
|
|
25
|
+
function isFormPageSurface(value) {
|
|
26
|
+
return isString(value) && Object.hasOwn(FORM_PAGE_SURFACES, value);
|
|
27
|
+
}
|
|
@@ -1,10 +1,30 @@
|
|
|
1
|
-
import type { FormObject, FormPageSectionEntry, FormPageSectionState, FormRuntime, FormValue } from '../types/index.js';
|
|
1
|
+
import type { FormObject, FormPageSectionEntry, FormPageSectionState, FormPageSectionStatus, FormRuntime, FormValue } from '../types/index.js';
|
|
2
2
|
/** Pairs each section of a page schema with the card `defineFormPageSchema` generated for it. */
|
|
3
3
|
export declare function getFormPageSections(schema: FormValue): readonly FormPageSectionEntry[];
|
|
4
4
|
/** Resolved heading of the page navigation. */
|
|
5
5
|
export declare function getFormPageNavigationTitle(schema: FormValue): string | undefined;
|
|
6
|
+
/**
|
|
7
|
+
* Number of visible sections the navigation needs to show. Zero, the default, shows it whatever
|
|
8
|
+
* the number of sections; anything that is not a count above one means the same.
|
|
9
|
+
*/
|
|
10
|
+
export declare function getFormPageNavigationMinSections(schema: FormValue): number;
|
|
6
11
|
/** True while the section's `condition` lets it render. */
|
|
7
12
|
export declare function isFormPageSectionVisible(entry: FormPageSectionEntry, runtime: FormRuntime): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Where a section stands, from what it shows and what it still misses. An optional section nothing
|
|
15
|
+
* was entered in is `optional`, not `pending`: the user has nothing to do there.
|
|
16
|
+
*/
|
|
17
|
+
export declare function resolveFormPageSectionStatus(params: {
|
|
18
|
+
/** The section shows a validation error, which needs the user to leave a field or submit. */
|
|
19
|
+
invalid: boolean;
|
|
20
|
+
/** Number of required fields without a value. */
|
|
21
|
+
missing: number;
|
|
22
|
+
optional: boolean;
|
|
23
|
+
/** The user entered a value, or the form input brought one. Only read for an optional section. */
|
|
24
|
+
provided: boolean;
|
|
25
|
+
}): FormPageSectionStatus;
|
|
26
|
+
/** True while the section keeps the form from being submitted: the sections left to complete. */
|
|
27
|
+
export declare function isFormPageSectionBlocking(section: Pick<FormPageSectionState, 'status'>): boolean;
|
|
8
28
|
/**
|
|
9
29
|
* Live state of one visible section: what it still misses, whether it shows errors, and what
|
|
10
30
|
* changed since the baseline.
|
|
@@ -13,6 +13,12 @@ import {
|
|
|
13
13
|
resolveRequired
|
|
14
14
|
} from "./state.js";
|
|
15
15
|
import { resolveFormText } from "./text.js";
|
|
16
|
+
const BLOCKS_SUBMIT = {
|
|
17
|
+
complete: false,
|
|
18
|
+
invalid: true,
|
|
19
|
+
optional: false,
|
|
20
|
+
pending: true
|
|
21
|
+
};
|
|
16
22
|
export function getFormPageSections(schema) {
|
|
17
23
|
if (!isRecord(schema) || !Array.isArray(schema.sections)) {
|
|
18
24
|
return [];
|
|
@@ -30,9 +36,25 @@ export function getFormPageNavigationTitle(schema) {
|
|
|
30
36
|
const { title } = schema.navigation;
|
|
31
37
|
return isFormText(title) ? resolveFormText(title) : void 0;
|
|
32
38
|
}
|
|
39
|
+
export function getFormPageNavigationMinSections(schema) {
|
|
40
|
+
if (!isRecord(schema) || !isRecord(schema.navigation)) {
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
const { minSections } = schema.navigation;
|
|
44
|
+
return isNumber(minSections) && minSections > 1 ? Math.trunc(minSections) : 0;
|
|
45
|
+
}
|
|
33
46
|
export function isFormPageSectionVisible(entry, runtime) {
|
|
34
47
|
return !hasCondition(entry.card) || runtime.shouldRender(entry.card, [entry.card.key]);
|
|
35
48
|
}
|
|
49
|
+
export function resolveFormPageSectionStatus(params) {
|
|
50
|
+
if (params.invalid) return "invalid";
|
|
51
|
+
if (params.missing > 0) return "pending";
|
|
52
|
+
if (params.optional && !params.provided) return "optional";
|
|
53
|
+
return "complete";
|
|
54
|
+
}
|
|
55
|
+
export function isFormPageSectionBlocking(section) {
|
|
56
|
+
return BLOCKS_SUBMIT[section.status];
|
|
57
|
+
}
|
|
36
58
|
export function resolveFormPageSectionState(params) {
|
|
37
59
|
const { entry, runtime } = params;
|
|
38
60
|
const leaves = collectVisibleLeaves(entry.section.fields, [], runtime);
|
|
@@ -43,7 +65,7 @@ export function resolveFormPageSectionState(params) {
|
|
|
43
65
|
const required = params.requiredEnforced ? leaves.filter((leaf) => isRequiredLeaf(leaf, runtime)) : [];
|
|
44
66
|
const missing = required.filter((leaf) => isEmptyValue(runtime.getValue(leaf.path))).length;
|
|
45
67
|
const optional = entry.section.optional ?? required.every((leaf) => leaf.item);
|
|
46
|
-
const
|
|
68
|
+
const provided = optional && leaves.some((leaf) => isProvided(leaf, runtime, dirtyPaths, params.input));
|
|
47
69
|
return {
|
|
48
70
|
description: resolveFormText(entry.section.description),
|
|
49
71
|
dirty: dirtyPaths.length > 0,
|
|
@@ -53,7 +75,7 @@ export function resolveFormPageSectionState(params) {
|
|
|
53
75
|
label: resolveFormText(entry.section.label) ?? entry.section.key,
|
|
54
76
|
missing,
|
|
55
77
|
optional,
|
|
56
|
-
status:
|
|
78
|
+
status: resolveFormPageSectionStatus({ invalid, missing, optional, provided })
|
|
57
79
|
};
|
|
58
80
|
}
|
|
59
81
|
function collectVisibleLeaves(fields, parentPath, runtime, item = false) {
|