nuxt-ui-tools 1.20.1 → 1.22.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-section.vue +61 -6
- 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 -5
- package/dist/runtime/form/components/page/form-page-sections.vue.d.ts +4 -3
- package/dist/runtime/form/components/utils/form-option-leading.d.vue.ts +3 -0
- package/dist/runtime/form/components/utils/form-option-leading.vue +3 -2
- package/dist/runtime/form/components/utils/form-option-leading.vue.d.ts +3 -0
- package/dist/runtime/form/components/utils/form-option-token.d.vue.ts +15 -0
- package/dist/runtime/form/components/utils/form-option-token.vue +29 -0
- package/dist/runtime/form/components/utils/form-option-token.vue.d.ts +15 -0
- package/dist/runtime/form/components/utils/form-option-tokens.d.vue.ts +21 -0
- package/dist/runtime/form/components/utils/form-option-tokens.vue +26 -0
- package/dist/runtime/form/components/utils/form-option-tokens.vue.d.ts +21 -0
- package/dist/runtime/form/composables/use-form-page.d.ts +6 -0
- package/dist/runtime/form/composables/use-form-page.js +4 -0
- package/dist/runtime/form/fields/auto-complete/component.vue +14 -1
- package/dist/runtime/form/fields/hierarchy/component.vue +72 -3
- package/dist/runtime/form/fields/select/component.vue +16 -1
- package/dist/runtime/form/fields/switch-group/component.vue +7 -2
- package/dist/runtime/form/types/page.d.ts +11 -0
- package/dist/runtime/form/types/ui.d.ts +11 -1
- package/dist/runtime/form/utils/page-surface.d.ts +4 -0
- package/dist/runtime/form/utils/page-surface.js +13 -0
- package/dist/runtime/form/utils/page.d.ts +2 -0
- package/dist/runtime/form/utils/page.js +5 -0
- package/dist/runtime/i18n/locales/en.js +1 -0
- package/dist/runtime/i18n/locales/fr.js +1 -0
- package/dist/runtime/i18n/types.d.ts +2 -0
- package/package.json +1 -1
package/dist/module.json
CHANGED
|
@@ -23,6 +23,37 @@ const { runtime } = page.root;
|
|
|
23
23
|
const pageUi = computed(() => formUi.ui.value.page?.ui);
|
|
24
24
|
const titleId = useId();
|
|
25
25
|
const dirty = computed(() => page.dirtyCheck.value && props.section.dirty);
|
|
26
|
+
const status = computed(() => {
|
|
27
|
+
if (!page.statusVisible.value) {
|
|
28
|
+
return void 0;
|
|
29
|
+
}
|
|
30
|
+
if (props.section.status === "optional") {
|
|
31
|
+
return { color: "neutral", icon: void 0, label: t("form.page.status.optional") };
|
|
32
|
+
}
|
|
33
|
+
if (props.section.status === "complete") {
|
|
34
|
+
return {
|
|
35
|
+
color: "success",
|
|
36
|
+
icon: "i-lucide-check",
|
|
37
|
+
label: t("form.page.status.complete")
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (props.section.status === "invalid") {
|
|
41
|
+
return {
|
|
42
|
+
color: "error",
|
|
43
|
+
icon: "i-lucide-circle-alert",
|
|
44
|
+
label: t("form.page.status.invalid")
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const { missing, required } = props.section;
|
|
48
|
+
return {
|
|
49
|
+
color: "neutral",
|
|
50
|
+
icon: void 0,
|
|
51
|
+
label: t("form.page.progress", { filled: required - missing, required })
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
const spanClass = computed(
|
|
55
|
+
() => page.spanned.value ? "col-span-12 @3xl/form-sections:col-span-(--nut-form-page-section-span)" : ""
|
|
56
|
+
);
|
|
26
57
|
const layout = computed(
|
|
27
58
|
() => resolveFormLayoutConfig(runtime.currentLayout.value, props.entry.section.layout)
|
|
28
59
|
);
|
|
@@ -47,18 +78,22 @@ onBeforeUnmount(() => unregister?.());
|
|
|
47
78
|
:data-state="section.status"
|
|
48
79
|
:data-dirty="dirty || void 0"
|
|
49
80
|
:style="{
|
|
50
|
-
scrollMarginTop: `calc(${page.stickyOffset.value}px + var(--nut-form-page-gap, 24px))
|
|
81
|
+
scrollMarginTop: `calc(${page.stickyOffset.value}px + var(--nut-form-page-gap, 24px))`,
|
|
82
|
+
'--nut-form-page-section-span': page.spanned.value ? String(section.span) : void 0
|
|
51
83
|
}"
|
|
52
84
|
:class="
|
|
53
85
|
mergeFormUiClass(
|
|
54
|
-
`flex min-w-0 flex-col ${page.surfaceClasses.value.section}`,
|
|
86
|
+
`flex min-w-0 flex-col ${spanClass} ${page.surfaceClasses.value.section}`,
|
|
55
87
|
pageUi?.section
|
|
56
88
|
)
|
|
57
89
|
"
|
|
58
90
|
>
|
|
59
91
|
<header
|
|
60
92
|
:class="
|
|
61
|
-
mergeFormUiClass(
|
|
93
|
+
mergeFormUiClass(
|
|
94
|
+
`flex flex-wrap items-center gap-x-3 gap-y-1 ${page.surfaceClasses.value.sectionHeader}`,
|
|
95
|
+
pageUi?.sectionHeader
|
|
96
|
+
)
|
|
62
97
|
"
|
|
63
98
|
>
|
|
64
99
|
<span
|
|
@@ -99,7 +134,7 @@ onBeforeUnmount(() => unregister?.());
|
|
|
99
134
|
{{ section.description }}
|
|
100
135
|
</p>
|
|
101
136
|
<UBadge
|
|
102
|
-
v-if="section.status === 'optional'"
|
|
137
|
+
v-if="section.status === 'optional' && !status"
|
|
103
138
|
color="neutral"
|
|
104
139
|
variant="soft"
|
|
105
140
|
:label="t('form.page.status.optional')"
|
|
@@ -107,9 +142,24 @@ onBeforeUnmount(() => unregister?.());
|
|
|
107
142
|
data-form-page-optional
|
|
108
143
|
/>
|
|
109
144
|
<div
|
|
110
|
-
v-if="page.dirtyCheck.value || $slots.actions"
|
|
145
|
+
v-if="status || page.dirtyCheck.value || $slots.actions"
|
|
111
146
|
:class="mergeFormUiClass('ms-auto -my-1.5 flex items-center gap-2', pageUi?.sectionActions)"
|
|
112
147
|
>
|
|
148
|
+
<UBadge
|
|
149
|
+
v-if="status"
|
|
150
|
+
:color="status.color"
|
|
151
|
+
variant="soft"
|
|
152
|
+
:icon="status.icon"
|
|
153
|
+
:label="status.label"
|
|
154
|
+
:class="
|
|
155
|
+
mergeFormUiClass(
|
|
156
|
+
section.status === 'optional' ? pageUi?.sectionOptional : void 0,
|
|
157
|
+
pageUi?.sectionStatus
|
|
158
|
+
)
|
|
159
|
+
"
|
|
160
|
+
:data-form-page-status="section.status"
|
|
161
|
+
:data-form-page-optional="section.status === 'optional' ? '' : void 0"
|
|
162
|
+
/>
|
|
113
163
|
<slot name="actions" :section />
|
|
114
164
|
<UButton
|
|
115
165
|
v-if="page.dirtyCheck.value"
|
|
@@ -125,7 +175,12 @@ onBeforeUnmount(() => unregister?.());
|
|
|
125
175
|
/>
|
|
126
176
|
</div>
|
|
127
177
|
</header>
|
|
128
|
-
<div
|
|
178
|
+
<div
|
|
179
|
+
:class="
|
|
180
|
+
mergeFormUiClass(`min-w-0 ${page.surfaceClasses.value.sectionBody}`, pageUi?.sectionBody)
|
|
181
|
+
"
|
|
182
|
+
:style="grid.style.value"
|
|
183
|
+
>
|
|
129
184
|
<FormFieldRenderer
|
|
130
185
|
v-for="field in entry.section.fields"
|
|
131
186
|
:key="field.key"
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { FormPageSectionState } from '../../types/index.js';
|
|
2
2
|
/**
|
|
3
3
|
* Sections of a form page, one per visible section, each with its title, description, fields, and,
|
|
4
|
-
* with `controls.dirtyCheck`, a mark and a reset button while it is modified. They are cards,
|
|
5
|
-
* plain blocks with the `page.surface` UI option.
|
|
6
|
-
*
|
|
4
|
+
* with `controls.dirtyCheck`, a mark and a reset button while it is modified. They are cards,
|
|
5
|
+
* panels, or plain blocks with the `page.surface` UI option. Once a section sets a `span`, they lay
|
|
6
|
+
* out in a twelve-column grid, which is also the container their spans query. Skeletons stand in
|
|
7
|
+
* while the schema context loads.
|
|
7
8
|
*/
|
|
8
9
|
type __VLS_Slots = {
|
|
9
10
|
/** Extra buttons in a section header, before its reset button. */
|
|
@@ -14,20 +14,22 @@ const formUi = useFormUi();
|
|
|
14
14
|
const { t } = useUiToolsLocale();
|
|
15
15
|
const pageUi = computed(() => formUi.ui.value.page?.ui);
|
|
16
16
|
const { contextError, contextLoading, contextPending, refreshContext } = page.root;
|
|
17
|
+
const flow = computed(
|
|
18
|
+
() => page.spanned.value ? "@container/form-sections grid grid-cols-12 items-stretch" : "flex flex-col"
|
|
19
|
+
);
|
|
20
|
+
const fullRow = computed(() => page.spanned.value ? "col-span-12" : "");
|
|
17
21
|
</script>
|
|
18
22
|
|
|
19
23
|
<template>
|
|
20
24
|
<div
|
|
21
25
|
:class="
|
|
22
|
-
mergeFormUiClass(
|
|
23
|
-
`flex min-w-0 flex-col ${page.surfaceClasses.value.sections}`,
|
|
24
|
-
pageUi?.sections
|
|
25
|
-
)
|
|
26
|
+
mergeFormUiClass(`${flow} min-w-0 ${page.surfaceClasses.value.sections}`, pageUi?.sections)
|
|
26
27
|
"
|
|
27
28
|
data-form-page-sections
|
|
28
29
|
>
|
|
29
30
|
<UAlert
|
|
30
31
|
v-if="contextError"
|
|
32
|
+
:class="fullRow"
|
|
31
33
|
color="error"
|
|
32
34
|
variant="soft"
|
|
33
35
|
icon="i-lucide-circle-alert"
|
|
@@ -53,7 +55,7 @@ const { contextError, contextLoading, contextPending, refreshContext } = page.ro
|
|
|
53
55
|
:key="section.key"
|
|
54
56
|
:class="
|
|
55
57
|
mergeFormUiClass(
|
|
56
|
-
`grid gap-4 ${page.surfaceClasses.value.sectionSkeleton}`,
|
|
58
|
+
`grid gap-4 ${fullRow} ${page.surfaceClasses.value.sectionSkeleton}`,
|
|
57
59
|
pageUi?.sectionSkeleton
|
|
58
60
|
)
|
|
59
61
|
"
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { FormPageSectionState } from '../../types/index.js';
|
|
2
2
|
/**
|
|
3
3
|
* Sections of a form page, one per visible section, each with its title, description, fields, and,
|
|
4
|
-
* with `controls.dirtyCheck`, a mark and a reset button while it is modified. They are cards,
|
|
5
|
-
* plain blocks with the `page.surface` UI option.
|
|
6
|
-
*
|
|
4
|
+
* with `controls.dirtyCheck`, a mark and a reset button while it is modified. They are cards,
|
|
5
|
+
* panels, or plain blocks with the `page.surface` UI option. Once a section sets a `span`, they lay
|
|
6
|
+
* out in a twelve-column grid, which is also the container their spans query. Skeletons stand in
|
|
7
|
+
* while the schema context loads.
|
|
7
8
|
*/
|
|
8
9
|
type __VLS_Slots = {
|
|
9
10
|
/** Extra buttons in a section header, before its reset button. */
|
|
@@ -13,11 +13,14 @@ type __VLS_Props = {
|
|
|
13
13
|
avatarSize?: string;
|
|
14
14
|
/** Classes of the box the chip dot is centered in. */
|
|
15
15
|
chipClass?: string;
|
|
16
|
+
/** Nuxt UI chip size: `sm` for the small text of a token. */
|
|
17
|
+
chipSize?: 'sm' | 'md';
|
|
16
18
|
};
|
|
17
19
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
|
|
18
20
|
iconClass: string;
|
|
19
21
|
avatarSize: string;
|
|
20
22
|
chipClass: string;
|
|
23
|
+
chipSize: "sm" | "md";
|
|
21
24
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
22
25
|
declare const _default: typeof __VLS_export;
|
|
23
26
|
export default _default;
|
|
@@ -8,7 +8,8 @@ const props = defineProps({
|
|
|
8
8
|
option: { type: Object, required: false },
|
|
9
9
|
iconClass: { type: String, required: false, default: "size-4 text-muted" },
|
|
10
10
|
avatarSize: { type: String, required: false, default: "xs" },
|
|
11
|
-
chipClass: { type: String, required: false, default: "size-4" }
|
|
11
|
+
chipClass: { type: String, required: false, default: "size-4" },
|
|
12
|
+
chipSize: { type: String, required: false, default: "md" }
|
|
12
13
|
});
|
|
13
14
|
const size = computed(() => AVATAR_SIZES.find((entry) => entry === props.avatarSize) ?? "xs");
|
|
14
15
|
</script>
|
|
@@ -36,6 +37,6 @@ const size = computed(() => AVATAR_SIZES.find((entry) => entry === props.avatarS
|
|
|
36
37
|
aria-hidden="true"
|
|
37
38
|
data-form-option-leading="chip"
|
|
38
39
|
>
|
|
39
|
-
<UChip standalone inset size="
|
|
40
|
+
<UChip standalone inset :size="chipSize" :color="option.chip.color" />
|
|
40
41
|
</span>
|
|
41
42
|
</template>
|
|
@@ -13,11 +13,14 @@ type __VLS_Props = {
|
|
|
13
13
|
avatarSize?: string;
|
|
14
14
|
/** Classes of the box the chip dot is centered in. */
|
|
15
15
|
chipClass?: string;
|
|
16
|
+
/** Nuxt UI chip size: `sm` for the small text of a token. */
|
|
17
|
+
chipSize?: 'sm' | 'md';
|
|
16
18
|
};
|
|
17
19
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
|
|
18
20
|
iconClass: string;
|
|
19
21
|
avatarSize: string;
|
|
20
22
|
chipClass: string;
|
|
23
|
+
chipSize: "sm" | "md";
|
|
21
24
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
22
25
|
declare const _default: typeof __VLS_export;
|
|
23
26
|
export default _default;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { VNodeChild } from 'vue';
|
|
2
|
+
import type { FormOptionValueRenderContext, ResolvedFormOption } from '../../types/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* One chosen option inside a token or a tag: its icon, avatar or chip, then its label, both sized
|
|
5
|
+
* for the small text of a token. A `renderValue` function draws the content instead.
|
|
6
|
+
*/
|
|
7
|
+
type __VLS_Props = {
|
|
8
|
+
option: ResolvedFormOption;
|
|
9
|
+
render?: (context: FormOptionValueRenderContext) => VNodeChild;
|
|
10
|
+
/** Shown instead of the label, such as the path of a hierarchy option. */
|
|
11
|
+
label?: string;
|
|
12
|
+
};
|
|
13
|
+
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>;
|
|
14
|
+
declare const _default: typeof __VLS_export;
|
|
15
|
+
export default _default;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import FormOptionLeading from "./form-option-leading.vue";
|
|
3
|
+
import FormOptionRender from "./form-option-render";
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
option: { type: Object, required: true },
|
|
6
|
+
render: { type: Function, required: false },
|
|
7
|
+
label: { type: String, required: false }
|
|
8
|
+
});
|
|
9
|
+
function content() {
|
|
10
|
+
const { item } = props.option;
|
|
11
|
+
return item === void 0 ? null : props.render?.({ item, option: props.option });
|
|
12
|
+
}
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<template>
|
|
16
|
+
<span class="inline-flex max-w-full min-w-0 items-center gap-1" data-form-option-token-content>
|
|
17
|
+
<FormOptionRender v-if="render && option.item !== void 0" :content="content" />
|
|
18
|
+
<template v-else>
|
|
19
|
+
<FormOptionLeading
|
|
20
|
+
:option="option"
|
|
21
|
+
icon-class="size-3.5 text-muted"
|
|
22
|
+
avatar-size="3xs"
|
|
23
|
+
chip-class="size-3"
|
|
24
|
+
chip-size="sm"
|
|
25
|
+
/>
|
|
26
|
+
<span class="min-w-0 truncate">{{ label ?? option.label }}</span>
|
|
27
|
+
</template>
|
|
28
|
+
</span>
|
|
29
|
+
</template>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { VNodeChild } from 'vue';
|
|
2
|
+
import type { FormOptionValueRenderContext, ResolvedFormOption } from '../../types/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* One chosen option inside a token or a tag: its icon, avatar or chip, then its label, both sized
|
|
5
|
+
* for the small text of a token. A `renderValue` function draws the content instead.
|
|
6
|
+
*/
|
|
7
|
+
type __VLS_Props = {
|
|
8
|
+
option: ResolvedFormOption;
|
|
9
|
+
render?: (context: FormOptionValueRenderContext) => VNodeChild;
|
|
10
|
+
/** Shown instead of the label, such as the path of a hierarchy option. */
|
|
11
|
+
label?: string;
|
|
12
|
+
};
|
|
13
|
+
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>;
|
|
14
|
+
declare const _default: typeof __VLS_export;
|
|
15
|
+
export default _default;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { VNodeChild } from 'vue';
|
|
2
|
+
import type { FormOptionValueRenderContext, ResolvedFormOption } from '../../types/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* The chosen options of a multiple selection as its closed control shows them: one token each,
|
|
5
|
+
* with the option's icon, avatar or chip before its label, wrapping onto more lines when they do
|
|
6
|
+
* not fit. A token is no taller than a line of text, so one row keeps the control as high as the
|
|
7
|
+
* inputs beside it. The tokens ignore the pointer, so a click still opens the control. They look
|
|
8
|
+
* like the tags of a multiple auto-complete, so both read alike. A visually hidden comma between
|
|
9
|
+
* them keeps the value reading as a list ("Euro, Dollar") for assistive technology.
|
|
10
|
+
*/
|
|
11
|
+
type __VLS_Props = {
|
|
12
|
+
options: readonly ResolvedFormOption[];
|
|
13
|
+
render?: (context: FormOptionValueRenderContext) => VNodeChild;
|
|
14
|
+
/** Shown instead of each label, such as the path of a hierarchy option. */
|
|
15
|
+
labelOf?: (option: ResolvedFormOption) => string;
|
|
16
|
+
/** Classes of each token. */
|
|
17
|
+
tokenClass?: string;
|
|
18
|
+
};
|
|
19
|
+
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>;
|
|
20
|
+
declare const _default: typeof __VLS_export;
|
|
21
|
+
export default _default;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { formOptionKey } from "../../utils/options";
|
|
3
|
+
import { mergeFormUiClass } from "../../utils/ui";
|
|
4
|
+
import FormOptionToken from "./form-option-token.vue";
|
|
5
|
+
defineProps({
|
|
6
|
+
options: { type: Array, required: true },
|
|
7
|
+
render: { type: Function, required: false },
|
|
8
|
+
labelOf: { type: Function, required: false },
|
|
9
|
+
tokenClass: { type: String, required: false }
|
|
10
|
+
});
|
|
11
|
+
const TOKEN = "inline-flex max-w-full min-w-0 items-center rounded-sm bg-elevated px-1.5 py-0.5 text-xs font-medium text-default ring ring-accented ring-inset";
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<span
|
|
16
|
+
class="pointer-events-none flex min-w-0 flex-1 flex-wrap gap-1 text-start"
|
|
17
|
+
data-form-option-tokens
|
|
18
|
+
>
|
|
19
|
+
<template v-for="(option, index) in options" :key="formOptionKey(option.value)">
|
|
20
|
+
<span :class="mergeFormUiClass(TOKEN, tokenClass)" data-form-option-token>
|
|
21
|
+
<FormOptionToken :option="option" :render="render" :label="labelOf?.(option)" />
|
|
22
|
+
</span>
|
|
23
|
+
<span v-if="index < options.length - 1" class="sr-only">, </span>
|
|
24
|
+
</template>
|
|
25
|
+
</span>
|
|
26
|
+
</template>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { VNodeChild } from 'vue';
|
|
2
|
+
import type { FormOptionValueRenderContext, ResolvedFormOption } from '../../types/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* The chosen options of a multiple selection as its closed control shows them: one token each,
|
|
5
|
+
* with the option's icon, avatar or chip before its label, wrapping onto more lines when they do
|
|
6
|
+
* not fit. A token is no taller than a line of text, so one row keeps the control as high as the
|
|
7
|
+
* inputs beside it. The tokens ignore the pointer, so a click still opens the control. They look
|
|
8
|
+
* like the tags of a multiple auto-complete, so both read alike. A visually hidden comma between
|
|
9
|
+
* them keeps the value reading as a list ("Euro, Dollar") for assistive technology.
|
|
10
|
+
*/
|
|
11
|
+
type __VLS_Props = {
|
|
12
|
+
options: readonly ResolvedFormOption[];
|
|
13
|
+
render?: (context: FormOptionValueRenderContext) => VNodeChild;
|
|
14
|
+
/** Shown instead of each label, such as the path of a hierarchy option. */
|
|
15
|
+
labelOf?: (option: ResolvedFormOption) => string;
|
|
16
|
+
/** Classes of each token. */
|
|
17
|
+
tokenClass?: string;
|
|
18
|
+
};
|
|
19
|
+
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>;
|
|
20
|
+
declare const _default: typeof __VLS_export;
|
|
21
|
+
export default _default;
|
|
@@ -43,6 +43,8 @@ export declare function useFormPage(params: {
|
|
|
43
43
|
};
|
|
44
44
|
scrollTo: (key: string, options?: import("../types/index.js").FormPageScrollOptions) => boolean;
|
|
45
45
|
sections: import("vue").ComputedRef<readonly FormPageSectionState[]>;
|
|
46
|
+
spanned: import("vue").ComputedRef<boolean>;
|
|
47
|
+
statusVisible: import("vue").ComputedRef<boolean>;
|
|
46
48
|
stickyOffset: import("vue").Ref<number, number>;
|
|
47
49
|
surface: import("vue").ComputedRef<import("../types/index.js").FormPageSurface>;
|
|
48
50
|
surfaceClasses: import("vue").ComputedRef<import("../utils/page-surface.js").FormPageSurfaceClasses>;
|
|
@@ -80,6 +82,8 @@ export declare function provideFormPage(page: ReturnType<typeof useFormPage>): {
|
|
|
80
82
|
};
|
|
81
83
|
scrollTo: (key: string, options?: import("../types/index.js").FormPageScrollOptions) => boolean;
|
|
82
84
|
sections: import("vue").ComputedRef<readonly FormPageSectionState[]>;
|
|
85
|
+
spanned: import("vue").ComputedRef<boolean>;
|
|
86
|
+
statusVisible: import("vue").ComputedRef<boolean>;
|
|
83
87
|
stickyOffset: import("vue").Ref<number, number>;
|
|
84
88
|
surface: import("vue").ComputedRef<import("../types/index.js").FormPageSurface>;
|
|
85
89
|
surfaceClasses: import("vue").ComputedRef<import("../utils/page-surface.js").FormPageSurfaceClasses>;
|
|
@@ -118,6 +122,8 @@ export declare function useFormPageContext(): {
|
|
|
118
122
|
};
|
|
119
123
|
scrollTo: (key: string, options?: import("../types/index.js").FormPageScrollOptions) => boolean;
|
|
120
124
|
sections: import("vue").ComputedRef<readonly FormPageSectionState[]>;
|
|
125
|
+
spanned: import("vue").ComputedRef<boolean>;
|
|
126
|
+
statusVisible: import("vue").ComputedRef<boolean>;
|
|
121
127
|
stickyOffset: import("vue").Ref<number, number>;
|
|
122
128
|
surface: import("vue").ComputedRef<import("../types/index.js").FormPageSurface>;
|
|
123
129
|
surfaceClasses: import("vue").ComputedRef<import("../utils/page-surface.js").FormPageSurfaceClasses>;
|
|
@@ -43,6 +43,8 @@ export function useFormPage(params) {
|
|
|
43
43
|
const navigationVisible = computed(() => sections.value.length >= navigationMinSections.value);
|
|
44
44
|
const surface = computed(() => resolveFormPageSurface(root.formUi.ui.value.page?.surface));
|
|
45
45
|
const surfaceClasses = computed(() => getFormPageSurfaceClasses(surface.value));
|
|
46
|
+
const statusVisible = computed(() => root.formUi.ui.value.page?.status === true);
|
|
47
|
+
const spanned = computed(() => sections.value.some((section) => section.span < 12));
|
|
46
48
|
const remaining = computed(() => sections.value.filter(isFormPageSectionBlocking).length);
|
|
47
49
|
const modified = computed(() => sections.value.filter((section) => section.dirty).length);
|
|
48
50
|
const stickyOffset = ref(0);
|
|
@@ -72,6 +74,8 @@ export function useFormPage(params) {
|
|
|
72
74
|
root,
|
|
73
75
|
scrollTo: scroll.scrollTo,
|
|
74
76
|
sections,
|
|
77
|
+
spanned,
|
|
78
|
+
statusVisible,
|
|
75
79
|
stickyOffset,
|
|
76
80
|
surface,
|
|
77
81
|
surfaceClasses
|
|
@@ -7,10 +7,11 @@ import FormOptionItemLabel from "../../components/utils/form-option-item-label.v
|
|
|
7
7
|
import FormOptionLeading from "../../components/utils/form-option-leading.vue";
|
|
8
8
|
import FormOptionMenuFooter from "../../components/utils/form-option-menu-footer.vue";
|
|
9
9
|
import FormOptionSuffix from "../../components/utils/form-option-suffix.vue";
|
|
10
|
+
import FormOptionToken from "../../components/utils/form-option-token.vue";
|
|
10
11
|
import FormOptionValueOverlay from "../../components/utils/form-option-value-overlay.vue";
|
|
11
12
|
import { useFieldControl } from "../../composables/use-field-control";
|
|
12
13
|
import { useOptionMenu } from "../../composables/use-option-menu";
|
|
13
|
-
import { leadingVisualPadding } from "../../utils/options";
|
|
14
|
+
import { formOptionKey, leadingVisualPadding } from "../../utils/options";
|
|
14
15
|
import { isBoolean, isNumber, isString } from "../../utils/predicate";
|
|
15
16
|
import { mergeFormUiClass } from "../../utils/ui";
|
|
16
17
|
const props = defineProps({
|
|
@@ -102,6 +103,14 @@ async function handleNativeCreate(label) {
|
|
|
102
103
|
}
|
|
103
104
|
await handleCreate(normalizedLabel);
|
|
104
105
|
}
|
|
106
|
+
function tagOption(value) {
|
|
107
|
+
const optionValue = typeof value === "number" || typeof value === "boolean" ? value : String(value);
|
|
108
|
+
const key = formOptionKey(optionValue);
|
|
109
|
+
return options.selectedItems.value.find((option) => formOptionKey(option.value) === key) ?? {
|
|
110
|
+
label: String(value),
|
|
111
|
+
value: optionValue
|
|
112
|
+
};
|
|
113
|
+
}
|
|
105
114
|
function isOptionValue(value) {
|
|
106
115
|
return isString(value) || isNumber(value) || isBoolean(value);
|
|
107
116
|
}
|
|
@@ -151,6 +160,10 @@ function isOptionValue(value) {
|
|
|
151
160
|
/>
|
|
152
161
|
</template>
|
|
153
162
|
|
|
163
|
+
<template v-if="fieldProps.multiple" #tags-item-text="{ item }">
|
|
164
|
+
<FormOptionToken :option="tagOption(item)" :render="menu.renderValue.value" />
|
|
165
|
+
</template>
|
|
166
|
+
|
|
154
167
|
<template #item-leading="{ item }">
|
|
155
168
|
<FormOptionLeading :option="menu.renderOption.value ? void 0 : item" />
|
|
156
169
|
</template>
|
|
@@ -10,10 +10,12 @@ import { computed, ref, watch } from "vue";
|
|
|
10
10
|
import { useUiToolsLocale } from "../../../i18n/use-locale";
|
|
11
11
|
import FormCompositeControl from "../../components/renderer/form-composite-control.vue";
|
|
12
12
|
import FormFieldShell from "../../components/renderer/form-field-shell.vue";
|
|
13
|
+
import FormOptionLeading from "../../components/utils/form-option-leading.vue";
|
|
13
14
|
import FormOptionMenuFooter from "../../components/utils/form-option-menu-footer.vue";
|
|
15
|
+
import FormOptionTokens from "../../components/utils/form-option-tokens.vue";
|
|
14
16
|
import { useFieldControl } from "../../composables/use-field-control";
|
|
15
17
|
import { useFormUi } from "../../composables/use-form-ui";
|
|
16
|
-
import { formOptionKey } from "../../utils/options";
|
|
18
|
+
import { formOptionKey, leadingVisualPadding, resolveOptionLeading } from "../../utils/options";
|
|
17
19
|
import { isBoolean, isNumber, isString } from "../../utils/predicate";
|
|
18
20
|
import { resolveFormText } from "../../utils/text";
|
|
19
21
|
import { mergeFormUiClass } from "../../utils/ui";
|
|
@@ -147,8 +149,43 @@ const treeSelectLabel = computed(() => {
|
|
|
147
149
|
return props.field.type === "tree-select" && fieldProps.value.showPath ? selected.pathLabel : String(selected.label);
|
|
148
150
|
});
|
|
149
151
|
const fieldLabel = computed(() => resolveFormText(props.field.label));
|
|
152
|
+
const optionEntries = computed(() => {
|
|
153
|
+
const entries = /* @__PURE__ */ new Map();
|
|
154
|
+
const walk = (items, labels) => {
|
|
155
|
+
for (const item of items) {
|
|
156
|
+
const path = [...labels, item.label];
|
|
157
|
+
entries.set(formOptionKey(item.value), { option: item, path: path.join(separator.value) });
|
|
158
|
+
walk(item.children ?? [], path);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
walk(options.items.value, []);
|
|
162
|
+
return entries;
|
|
163
|
+
});
|
|
164
|
+
const showsPath = computed(() => props.field.type === "cascader" || hasShowPath(props.field));
|
|
165
|
+
function optionOf(item) {
|
|
166
|
+
return optionEntries.value.get(formOptionKey(item.value))?.option;
|
|
167
|
+
}
|
|
168
|
+
function tokenLabel(option) {
|
|
169
|
+
return showsPath.value ? optionEntries.value.get(formOptionKey(option.value))?.path ?? option.label : option.label;
|
|
170
|
+
}
|
|
171
|
+
const chosenOptions = computed(() => {
|
|
172
|
+
const values = Array.isArray(model.value) ? model.value : [model.value];
|
|
173
|
+
return values.flatMap((value) => {
|
|
174
|
+
const option = value === null ? void 0 : optionEntries.value.get(formOptionKey(value));
|
|
175
|
+
return option ? [option.option] : [];
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
const leadingOption = computed(() => {
|
|
179
|
+
const [option] = chosenOptions.value;
|
|
180
|
+
const multiple = usesTree.value ? treeMultiple.value : fieldProps.value.multiple === true;
|
|
181
|
+
return !multiple && option && resolveOptionLeading(option) ? option : void 0;
|
|
182
|
+
});
|
|
150
183
|
const ownedControlUi = computed(() => ({
|
|
151
184
|
...controlProps.value.ui,
|
|
185
|
+
base: mergeFormUiClass(
|
|
186
|
+
controlProps.value.ui?.base,
|
|
187
|
+
leadingOption.value ? leadingVisualPadding(controlProps.value.size) : void 0
|
|
188
|
+
),
|
|
152
189
|
content: mergeFormUiClass(controlProps.value.ui?.content, interactionOwnerClass.value)
|
|
153
190
|
}));
|
|
154
191
|
watch(treeSelectOpen, (open) => {
|
|
@@ -402,6 +439,7 @@ function isOptionValue(value) {
|
|
|
402
439
|
@click.stop
|
|
403
440
|
@select="selectRadioTreeItem(item)"
|
|
404
441
|
/>
|
|
442
|
+
<FormOptionLeading :option="optionOf(item)" />
|
|
405
443
|
<span :class="ui.linkLabel()">{{ item.label }}</span>
|
|
406
444
|
<button
|
|
407
445
|
v-if="isExpandable(item)"
|
|
@@ -460,7 +498,25 @@ function isOptionValue(value) {
|
|
|
460
498
|
}"
|
|
461
499
|
@blur="handleBlur"
|
|
462
500
|
>
|
|
463
|
-
|
|
501
|
+
<FormOptionTokens
|
|
502
|
+
v-if="treeMultiple && chosenOptions.length"
|
|
503
|
+
:options="chosenOptions"
|
|
504
|
+
:label-of="tokenLabel"
|
|
505
|
+
/>
|
|
506
|
+
<span
|
|
507
|
+
v-else
|
|
508
|
+
:class="
|
|
509
|
+
mergeFormUiClass(
|
|
510
|
+
'flex min-w-0 flex-1 items-center gap-1.5 text-left',
|
|
511
|
+
formUi.ui.value.treeSelect?.ui?.triggerLabel
|
|
512
|
+
)
|
|
513
|
+
"
|
|
514
|
+
>
|
|
515
|
+
<FormOptionLeading v-if="leadingOption" :option="leadingOption" />
|
|
516
|
+
<span :class="chosenOptions.length ? 'truncate' : 'truncate text-dimmed'">
|
|
517
|
+
{{ treeSelectLabel }}
|
|
518
|
+
</span>
|
|
519
|
+
</span>
|
|
464
520
|
</UButton>
|
|
465
521
|
|
|
466
522
|
<template #content>
|
|
@@ -538,6 +594,7 @@ function isOptionValue(value) {
|
|
|
538
594
|
@click.stop
|
|
539
595
|
@select="selectRadioTreeItem(item)"
|
|
540
596
|
/>
|
|
597
|
+
<FormOptionLeading :option="optionOf(item)" />
|
|
541
598
|
<span :class="ui.linkLabel()">{{ item.label }}</span>
|
|
542
599
|
<button
|
|
543
600
|
v-if="isExpandable(item)"
|
|
@@ -610,7 +667,19 @@ function isOptionValue(value) {
|
|
|
610
667
|
:loading="validationPending || options.loading.value"
|
|
611
668
|
:ui="ownedControlUi"
|
|
612
669
|
@blur="handleBlur"
|
|
613
|
-
|
|
670
|
+
>
|
|
671
|
+
<template v-if="leadingOption" #leading="{ ui }">
|
|
672
|
+
<FormOptionLeading
|
|
673
|
+
:option="leadingOption"
|
|
674
|
+
:icon-class="ui.leadingIcon()"
|
|
675
|
+
:avatar-size="ui.itemLeadingAvatarSize()"
|
|
676
|
+
chip-class="size-5"
|
|
677
|
+
/>
|
|
678
|
+
</template>
|
|
679
|
+
<template v-if="fieldProps.multiple && chosenOptions.length" #default>
|
|
680
|
+
<FormOptionTokens :options="chosenOptions" :label-of="tokenLabel" />
|
|
681
|
+
</template>
|
|
682
|
+
</USelectMenu>
|
|
614
683
|
</FormCompositeControl>
|
|
615
684
|
</FormFieldShell>
|
|
616
685
|
</template>
|
|
@@ -9,9 +9,10 @@ import FormOptionItemLabel from "../../components/utils/form-option-item-label.v
|
|
|
9
9
|
import FormOptionLeading from "../../components/utils/form-option-leading.vue";
|
|
10
10
|
import FormOptionMenuFooter from "../../components/utils/form-option-menu-footer.vue";
|
|
11
11
|
import FormOptionSuffix from "../../components/utils/form-option-suffix.vue";
|
|
12
|
+
import FormOptionTokens from "../../components/utils/form-option-tokens.vue";
|
|
12
13
|
import { useFieldControl } from "../../composables/use-field-control";
|
|
13
14
|
import { useOptionMenu } from "../../composables/use-option-menu";
|
|
14
|
-
import { leadingVisualPadding } from "../../utils/options";
|
|
15
|
+
import { formOptionKey, leadingVisualPadding } from "../../utils/options";
|
|
15
16
|
import { isBoolean, isNumber, isString } from "../../utils/predicate";
|
|
16
17
|
import { mergeFormUiClass } from "../../utils/ui";
|
|
17
18
|
const props = defineProps({
|
|
@@ -76,6 +77,17 @@ const controlUi = computed(() => ({
|
|
|
76
77
|
controlProps.value.ui?.itemDescription
|
|
77
78
|
)
|
|
78
79
|
}));
|
|
80
|
+
const tokenOptions = computed(() => {
|
|
81
|
+
if (!fieldProps.value.multiple || !Array.isArray(model.value)) {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
const chosen = new Map(
|
|
85
|
+
options.selectedItems.value.map((option) => [formOptionKey(option.value), option])
|
|
86
|
+
);
|
|
87
|
+
return model.value.map(
|
|
88
|
+
(value) => chosen.get(formOptionKey(value)) ?? { label: String(value), value }
|
|
89
|
+
);
|
|
90
|
+
});
|
|
79
91
|
const createItem = computed(
|
|
80
92
|
() => options.creatable.value && fieldProps.value.createItem ? fieldProps.value.createItem : false
|
|
81
93
|
);
|
|
@@ -166,6 +178,9 @@ function isOptionValue(value) {
|
|
|
166
178
|
/>
|
|
167
179
|
</span>
|
|
168
180
|
</template>
|
|
181
|
+
<template v-else-if="tokenOptions.length" #default>
|
|
182
|
+
<FormOptionTokens :options="tokenOptions" />
|
|
183
|
+
</template>
|
|
169
184
|
|
|
170
185
|
<template #item-leading="{ item }">
|
|
171
186
|
<FormOptionLeading :option="menu.renderOption.value ? void 0 : item" />
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import USwitch from "@nuxt/ui/components/Switch.vue";
|
|
3
3
|
import { computed, useId } from "vue";
|
|
4
4
|
import FormFieldShell from "../../components/renderer/form-field-shell.vue";
|
|
5
|
+
import FormOptionChoiceLabel from "../../components/utils/form-option-choice-label.vue";
|
|
5
6
|
import { useFieldControl } from "../../composables/use-field-control";
|
|
6
7
|
import { formOptionKey } from "../../utils/options";
|
|
7
8
|
import { isBoolean, isNumber, isString } from "../../utils/predicate";
|
|
@@ -40,7 +41,7 @@ function isOptionValue(value) {
|
|
|
40
41
|
<div
|
|
41
42
|
role="group"
|
|
42
43
|
class="grid gap-3"
|
|
43
|
-
:class="fieldProps.orientation === 'horizontal' ? 'sm:flex sm:flex-wrap' : ''"
|
|
44
|
+
:class="fieldProps.orientation === 'horizontal' ? 'sm:flex sm:flex-wrap sm:gap-x-7' : ''"
|
|
44
45
|
>
|
|
45
46
|
<USwitch
|
|
46
47
|
v-for="item in items"
|
|
@@ -56,7 +57,11 @@ function isOptionValue(value) {
|
|
|
56
57
|
:unchecked-icon="fieldProps.uncheckedIcon"
|
|
57
58
|
@update:model-value="toggleOption(item.value, $event === true)"
|
|
58
59
|
@blur="handleBlur"
|
|
59
|
-
|
|
60
|
+
>
|
|
61
|
+
<template #label>
|
|
62
|
+
<FormOptionChoiceLabel :option="item" :selected="model.includes(item.value)" />
|
|
63
|
+
</template>
|
|
64
|
+
</USwitch>
|
|
60
65
|
</div>
|
|
61
66
|
</FormFieldShell>
|
|
62
67
|
</template>
|
|
@@ -54,6 +54,13 @@ export interface FormPageSection<TContext = NonNullable<unknown>, TFields extend
|
|
|
54
54
|
optional?: boolean;
|
|
55
55
|
/** Grid of the section's fields, merged over the schema `layout`. */
|
|
56
56
|
layout?: FormLayoutConfig;
|
|
57
|
+
/**
|
|
58
|
+
* Columns, out of twelve, the section takes once the sections area is at least 48rem wide, so
|
|
59
|
+
* short sections sit side by side: `span: 4` next to `span: 8`. Sections fill the rows in order
|
|
60
|
+
* and the ones sharing a row stretch to the same height. Below that width, or without a `span`,
|
|
61
|
+
* the section takes the whole row.
|
|
62
|
+
*/
|
|
63
|
+
span?: number;
|
|
57
64
|
/** Raw dependency paths read before evaluating `condition`. */
|
|
58
65
|
dependencies?: readonly (string | readonly [string, string])[];
|
|
59
66
|
/** Hides the section, its navigation entry, and its fields while it returns `false`. */
|
|
@@ -186,6 +193,10 @@ export interface FormPageSectionState {
|
|
|
186
193
|
optional: boolean;
|
|
187
194
|
/** Number of required fields without a value, the fields of array items included. */
|
|
188
195
|
missing: number;
|
|
196
|
+
/** Number of required fields, the fields of array items included. */
|
|
197
|
+
required: number;
|
|
198
|
+
/** Columns, out of twelve, the section takes on a wide sections area: 12 without a `span`. */
|
|
199
|
+
span: number;
|
|
189
200
|
/** Where the section stands, see `FormPageSectionStatus`. */
|
|
190
201
|
status: FormPageSectionStatus;
|
|
191
202
|
/** True when a value of the section differs from the form's baseline. */
|
|
@@ -197,10 +197,11 @@ export interface FormOverlayUi {
|
|
|
197
197
|
* How the sections of a form page are drawn:
|
|
198
198
|
*
|
|
199
199
|
* - `card`: each section is a bordered card on the gray canvas of the page (default)
|
|
200
|
+
* - `panel`: a card whose heading sits in a bar across its top, above the fields
|
|
200
201
|
* - `plain`: no box. A section is its heading, description, and fields on the page itself,
|
|
201
202
|
* separated from the next by spacing and a hairline
|
|
202
203
|
*/
|
|
203
|
-
export type FormPageSurface = 'card' | 'plain';
|
|
204
|
+
export type FormPageSurface = 'card' | 'panel' | 'plain';
|
|
204
205
|
/**
|
|
205
206
|
* The `page` part of the form UI config: the surface of the sections and the `ui` slots of the
|
|
206
207
|
* page parts. Both follow the usual layering: app config, then schema `ui`, then the `ui` prop.
|
|
@@ -208,6 +209,13 @@ export type FormPageSurface = 'card' | 'plain';
|
|
|
208
209
|
export interface FormPageUiConfig extends FormUiPartConfig<FormPageUi> {
|
|
209
210
|
/** Defaults to `card`. */
|
|
210
211
|
surface?: FormPageSurface;
|
|
212
|
+
/**
|
|
213
|
+
* Shows where each section stands at the end of its heading: the required fields filled out of
|
|
214
|
+
* the total while some are missing ("2 sur 3"), "Complété" once nothing is, "À corriger" while a
|
|
215
|
+
* field shows an error, and "Facultatif" for an optional section with nothing entered, which
|
|
216
|
+
* then carries its tag there instead of after the title. Off by default.
|
|
217
|
+
*/
|
|
218
|
+
status?: boolean;
|
|
211
219
|
}
|
|
212
220
|
/**
|
|
213
221
|
* Slots of a form page and its parts. Data attributes carry the live state, so a slot can
|
|
@@ -274,6 +282,8 @@ export interface FormPageUi {
|
|
|
274
282
|
sectionDescription?: FormUiClass;
|
|
275
283
|
/** The "Optional" badge of an optional section with nothing entered. */
|
|
276
284
|
sectionOptional?: FormUiClass;
|
|
285
|
+
/** Badge of where the section stands, with `page.status`. Carries `data-form-page-status`. */
|
|
286
|
+
sectionStatus?: FormUiClass;
|
|
277
287
|
sectionActions?: FormUiClass;
|
|
278
288
|
sectionReset?: FormUiClass;
|
|
279
289
|
sectionBody?: FormUiClass;
|
|
@@ -10,6 +10,10 @@ export interface FormPageSurfaceClasses {
|
|
|
10
10
|
sections: string;
|
|
11
11
|
/** The box of a card, or the spacing, hairline, and modified marker of a plain section. */
|
|
12
12
|
section: string;
|
|
13
|
+
/** The bar of a panel heading. */
|
|
14
|
+
sectionHeader: string;
|
|
15
|
+
/** Padding of the fields under a panel bar. */
|
|
16
|
+
sectionBody: string;
|
|
13
17
|
/** Where the description sits against the title. */
|
|
14
18
|
sectionDescription: string;
|
|
15
19
|
/** Placeholder shown while the schema context loads. */
|
|
@@ -5,14 +5,27 @@ const FORM_PAGE_SURFACES = {
|
|
|
5
5
|
card: {
|
|
6
6
|
root: "bg-muted",
|
|
7
7
|
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",
|
|
8
|
+
sectionBody: "",
|
|
8
9
|
sectionDescription: "order-last basis-full @3xl/form-page:order-none @3xl/form-page:basis-auto",
|
|
10
|
+
sectionHeader: "",
|
|
9
11
|
sectionSkeleton: "rounded-xl border border-default bg-default px-6 pt-[22px] pb-6",
|
|
10
12
|
sections: "gap-[18px]"
|
|
11
13
|
},
|
|
14
|
+
panel: {
|
|
15
|
+
root: "bg-muted",
|
|
16
|
+
section: "overflow-hidden rounded-xl border border-default bg-default transition-[border-color,box-shadow] duration-200 focus-within:border-accented data-[dirty]:border-primary data-[dirty]:ring-4 data-[dirty]:ring-primary/20 data-[dirty]:ring-inset",
|
|
17
|
+
sectionBody: "px-5 pt-5 pb-6 @3xl/form-page:px-6",
|
|
18
|
+
sectionDescription: "order-last basis-full",
|
|
19
|
+
sectionHeader: "min-h-13 border-b border-default bg-elevated/60 px-4 py-2.5 @3xl/form-page:px-5",
|
|
20
|
+
sectionSkeleton: "rounded-xl border border-default bg-default px-6 pt-[22px] pb-6",
|
|
21
|
+
sections: "gap-4"
|
|
22
|
+
},
|
|
12
23
|
plain: {
|
|
13
24
|
root: "bg-default",
|
|
14
25
|
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",
|
|
26
|
+
sectionBody: "",
|
|
15
27
|
sectionDescription: "order-last basis-full",
|
|
28
|
+
sectionHeader: "",
|
|
16
29
|
sectionSkeleton: "border-t border-default py-8 first:border-t-0 first:pt-0",
|
|
17
30
|
sections: "gap-0"
|
|
18
31
|
}
|
|
@@ -38,3 +38,5 @@ export declare function resolveFormPageSectionState(params: {
|
|
|
38
38
|
/** False when the validation mode does not enforce required fields. */
|
|
39
39
|
requiredEnforced: boolean;
|
|
40
40
|
}): FormPageSectionState;
|
|
41
|
+
/** Columns a section takes out of twelve: a whole number from 1 to 12, the full row otherwise. */
|
|
42
|
+
export declare function resolveFormPageSectionSpan(value: FormValue): number;
|
|
@@ -76,9 +76,14 @@ export function resolveFormPageSectionState(params) {
|
|
|
76
76
|
label: resolveFormText(entry.section.label) ?? entry.section.key,
|
|
77
77
|
missing,
|
|
78
78
|
optional,
|
|
79
|
+
required: required.length,
|
|
80
|
+
span: resolveFormPageSectionSpan(entry.section.span),
|
|
79
81
|
status: resolveFormPageSectionStatus({ invalid, missing, optional, provided })
|
|
80
82
|
};
|
|
81
83
|
}
|
|
84
|
+
export function resolveFormPageSectionSpan(value) {
|
|
85
|
+
return isNumber(value) && value >= 1 ? Math.min(12, Math.trunc(value)) : 12;
|
|
86
|
+
}
|
|
82
87
|
function collectVisibleLeaves(fields, parentPath, runtime, item = false) {
|
|
83
88
|
return fields.flatMap((field) => {
|
|
84
89
|
if (field.ignore === true) {
|
|
@@ -206,6 +206,7 @@ export default defineUiToolsLocale({
|
|
|
206
206
|
},
|
|
207
207
|
page: {
|
|
208
208
|
complete: "Tout est pr\xEAt.",
|
|
209
|
+
progress: "{filled} sur {required}",
|
|
209
210
|
modifiedOne: "{count} modifi\xE9e.",
|
|
210
211
|
modifiedOther: "{count} modifi\xE9es.",
|
|
211
212
|
navigation: "Sections du formulaire",
|
|
@@ -464,6 +464,8 @@ export interface UiToolsFormMessages {
|
|
|
464
464
|
remaining: string;
|
|
465
465
|
/** Navigation summary once every required section is complete. */
|
|
466
466
|
complete: string;
|
|
467
|
+
/** Status of a section with required fields left, with `page.status`: "{filled} of {required}". */
|
|
468
|
+
progress: string;
|
|
467
469
|
/** Navigation summary of a form with dirty checking: "{count} modified." */
|
|
468
470
|
modifiedOne: string;
|
|
469
471
|
modifiedOther: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-ui-tools",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.22.0",
|
|
4
4
|
"description": "Nuxt module and runtime toolkit for UI-heavy Nuxt applications",
|
|
5
5
|
"homepage": "https://github.com/ChronicStone/nuxt-ui-tools#readme",
|
|
6
6
|
"bugs": "https://github.com/ChronicStone/nuxt-ui-tools/issues",
|