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
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import UIcon from "@nuxt/ui/components/Icon.vue";
|
|
3
|
+
import { computed, nextTick, useId, useTemplateRef, watch } from "vue";
|
|
4
|
+
import { useUiToolsLocale } from "../../../i18n/use-locale";
|
|
5
|
+
import { useFormPageContext } from "../../composables/use-form-page";
|
|
6
|
+
import { useFormUi } from "../../composables/use-form-ui";
|
|
7
|
+
import { mergeFormUiClass } from "../../utils/ui";
|
|
8
|
+
defineSlots();
|
|
9
|
+
const CHIP_ROW_INSET = 16;
|
|
10
|
+
const page = useFormPageContext();
|
|
11
|
+
const formUi = useFormUi();
|
|
12
|
+
const { code, t } = useUiToolsLocale();
|
|
13
|
+
const pageUi = computed(() => formUi.ui.value.page?.ui);
|
|
14
|
+
const titleId = useId();
|
|
15
|
+
const plurals = computed(() => new Intl.PluralRules(code.value));
|
|
16
|
+
const summary = computed(() => {
|
|
17
|
+
if (page.dirtyCheck.value) {
|
|
18
|
+
const count2 = page.modified.value;
|
|
19
|
+
if (!count2) {
|
|
20
|
+
return { text: t("form.page.unmodified") };
|
|
21
|
+
}
|
|
22
|
+
return sentence(count2, isOne(count2) ? "form.page.modifiedOne" : "form.page.modifiedOther");
|
|
23
|
+
}
|
|
24
|
+
const count = page.remaining.value;
|
|
25
|
+
return count ? sentence(count, "form.page.remaining") : { text: t("form.page.complete") };
|
|
26
|
+
});
|
|
27
|
+
function isOne(count) {
|
|
28
|
+
return plurals.value.select(count) === "one";
|
|
29
|
+
}
|
|
30
|
+
function sentence(count, message) {
|
|
31
|
+
const [before = "", after = ""] = t(message).split("{count}");
|
|
32
|
+
const phrase = t(isOne(count) ? "form.page.sectionsOne" : "form.page.sectionsOther", { count });
|
|
33
|
+
return { after, before, phrase };
|
|
34
|
+
}
|
|
35
|
+
function dirtyShown(section) {
|
|
36
|
+
return page.dirtyCheck.value && section.dirty;
|
|
37
|
+
}
|
|
38
|
+
function statusLabel(section) {
|
|
39
|
+
const status = t(`form.page.status.${section.status}`);
|
|
40
|
+
return dirtyShown(section) ? `${status}, ${t("form.page.unsavedChanges")}` : status;
|
|
41
|
+
}
|
|
42
|
+
const list = useTemplateRef("list");
|
|
43
|
+
watch(
|
|
44
|
+
() => page.active.value,
|
|
45
|
+
async (key) => {
|
|
46
|
+
await nextTick();
|
|
47
|
+
const row = list.value;
|
|
48
|
+
if (!row || !key || row.scrollWidth <= row.clientWidth) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const entry = [...row.children].find(
|
|
52
|
+
(child) => child.getAttribute("data-form-page-entry") === key
|
|
53
|
+
);
|
|
54
|
+
if (!entry) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const box = row.getBoundingClientRect();
|
|
58
|
+
const rect = entry.getBoundingClientRect();
|
|
59
|
+
const overflow = rect.left < box.left ? rect.left - box.left : Math.max(0, rect.right - box.right);
|
|
60
|
+
if (overflow) {
|
|
61
|
+
row.scrollBy?.({ behavior: "smooth", left: overflow + Math.sign(overflow) * CHIP_ROW_INSET });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
function select(event, key) {
|
|
66
|
+
if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
event.preventDefault();
|
|
70
|
+
page.scrollTo(key, { focus: true });
|
|
71
|
+
}
|
|
72
|
+
</script>
|
|
73
|
+
|
|
74
|
+
<template>
|
|
75
|
+
<nav
|
|
76
|
+
:aria-labelledby="page.navigationTitle.value || $slots.title ? titleId : void 0"
|
|
77
|
+
:aria-label="page.navigationTitle.value || $slots.title ? void 0 : t('form.page.navigation')"
|
|
78
|
+
:class="
|
|
79
|
+
mergeFormUiClass(
|
|
80
|
+
'flex min-w-0 flex-col gap-3.5 @3xl/form-page:sticky @3xl/form-page:top-[calc(var(--nut-form-page-header,0px)+var(--nut-form-page-gap,24px))]',
|
|
81
|
+
pageUi?.navigation
|
|
82
|
+
)
|
|
83
|
+
"
|
|
84
|
+
data-form-page-navigation
|
|
85
|
+
>
|
|
86
|
+
<div :class="mergeFormUiClass('flex min-w-0 flex-col gap-2', pageUi?.navigationGroup)">
|
|
87
|
+
<p
|
|
88
|
+
v-if="page.navigationTitle.value || $slots.title"
|
|
89
|
+
:id="titleId"
|
|
90
|
+
:class="
|
|
91
|
+
mergeFormUiClass(
|
|
92
|
+
'hidden px-2.5 text-[11px] font-semibold tracking-[0.08em] text-dimmed uppercase @3xl/form-page:block',
|
|
93
|
+
pageUi?.navigationTitle
|
|
94
|
+
)
|
|
95
|
+
"
|
|
96
|
+
>
|
|
97
|
+
<slot name="title">{{ page.navigationTitle.value }}</slot>
|
|
98
|
+
</p>
|
|
99
|
+
<ul
|
|
100
|
+
ref="list"
|
|
101
|
+
role="list"
|
|
102
|
+
:class="
|
|
103
|
+
mergeFormUiClass(
|
|
104
|
+
'm-0 flex list-none gap-1.5 overflow-x-auto p-0 [scrollbar-width:none] @3xl/form-page:flex-col @3xl/form-page:gap-0.5 @3xl/form-page:overflow-visible',
|
|
105
|
+
pageUi?.navigationList
|
|
106
|
+
)
|
|
107
|
+
"
|
|
108
|
+
>
|
|
109
|
+
<li
|
|
110
|
+
v-for="section in page.sections.value"
|
|
111
|
+
:key="section.key"
|
|
112
|
+
:data-form-page-entry="section.key"
|
|
113
|
+
:class="mergeFormUiClass('shrink-0 @3xl/form-page:shrink', pageUi?.navigationEntry)"
|
|
114
|
+
>
|
|
115
|
+
<slot
|
|
116
|
+
name="item"
|
|
117
|
+
:section
|
|
118
|
+
:active="section.key === page.active.value"
|
|
119
|
+
:select="() => page.scrollTo(section.key, { focus: true })"
|
|
120
|
+
>
|
|
121
|
+
<a
|
|
122
|
+
:href="`#${section.key}`"
|
|
123
|
+
:aria-current="section.key === page.active.value ? 'location' : void 0"
|
|
124
|
+
:data-active="section.key === page.active.value || void 0"
|
|
125
|
+
:data-state="section.status"
|
|
126
|
+
:data-dirty="dirtyShown(section) || void 0"
|
|
127
|
+
:class="
|
|
128
|
+
mergeFormUiClass(
|
|
129
|
+
'group flex h-8 min-w-0 items-center gap-2 rounded-md border border-default bg-default px-2.5 text-[12.5px] font-medium text-muted no-underline transition-colors hover:text-highlighted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary data-[active]:border-primary data-[active]:bg-primary/10 data-[active]:text-highlighted @3xl/form-page:h-9 @3xl/form-page:gap-2.5 @3xl/form-page:rounded-[7px] @3xl/form-page:border-transparent @3xl/form-page:bg-transparent @3xl/form-page:text-[13.5px] @3xl/form-page:hover:bg-elevated @3xl/form-page:data-[active]:border-transparent @3xl/form-page:data-[active]:bg-default @3xl/form-page:data-[active]:font-semibold @3xl/form-page:data-[active]:shadow-[inset_2px_0_0_var(--ui-primary)]',
|
|
130
|
+
pageUi?.navigationItem
|
|
131
|
+
)
|
|
132
|
+
"
|
|
133
|
+
@click="select($event, section.key)"
|
|
134
|
+
>
|
|
135
|
+
<span
|
|
136
|
+
:data-state="section.status"
|
|
137
|
+
aria-hidden="true"
|
|
138
|
+
:class="
|
|
139
|
+
mergeFormUiClass(
|
|
140
|
+
'grid size-4 shrink-0 place-items-center rounded-full border-[1.5px] border-accented text-[10px] leading-none font-bold group-data-[active]:border-primary data-[state=complete]:border-(--ui-text-highlighted) data-[state=complete]:bg-(--ui-text-highlighted) data-[state=complete]:text-(--ui-bg) data-[state=invalid]:border-error data-[state=invalid]:bg-error data-[state=invalid]:text-inverted group-data-[active]:data-[state=complete]:border-primary group-data-[active]:data-[state=complete]:bg-primary group-data-[active]:data-[state=complete]:text-inverted',
|
|
141
|
+
pageUi?.navigationIndicator
|
|
142
|
+
)
|
|
143
|
+
"
|
|
144
|
+
>
|
|
145
|
+
<UIcon
|
|
146
|
+
v-if="section.status === 'complete'"
|
|
147
|
+
name="i-lucide-check"
|
|
148
|
+
:class="mergeFormUiClass('size-2.5', pageUi?.navigationIndicatorIcon)"
|
|
149
|
+
/>
|
|
150
|
+
<template v-else-if="section.status === 'invalid'">!</template>
|
|
151
|
+
<span
|
|
152
|
+
v-else
|
|
153
|
+
:class="
|
|
154
|
+
mergeFormUiClass(
|
|
155
|
+
'hidden size-1.5 rounded-full bg-primary group-data-[active]:block',
|
|
156
|
+
pageUi?.navigationIndicatorMarker
|
|
157
|
+
)
|
|
158
|
+
"
|
|
159
|
+
/>
|
|
160
|
+
</span>
|
|
161
|
+
<span
|
|
162
|
+
:class="
|
|
163
|
+
mergeFormUiClass(
|
|
164
|
+
'whitespace-nowrap @3xl/form-page:min-w-0 @3xl/form-page:flex-1 @3xl/form-page:truncate',
|
|
165
|
+
pageUi?.navigationLabel
|
|
166
|
+
)
|
|
167
|
+
"
|
|
168
|
+
>
|
|
169
|
+
{{ section.label }}
|
|
170
|
+
</span>
|
|
171
|
+
<span class="sr-only">{{ statusLabel(section) }}</span>
|
|
172
|
+
<span
|
|
173
|
+
v-if="dirtyShown(section)"
|
|
174
|
+
aria-hidden="true"
|
|
175
|
+
:class="
|
|
176
|
+
mergeFormUiClass(
|
|
177
|
+
'size-1.5 shrink-0 rounded-full bg-primary',
|
|
178
|
+
pageUi?.navigationDirty
|
|
179
|
+
)
|
|
180
|
+
"
|
|
181
|
+
data-form-page-dirty
|
|
182
|
+
/>
|
|
183
|
+
<span
|
|
184
|
+
v-else-if="section.optional && section.status !== 'complete'"
|
|
185
|
+
:class="
|
|
186
|
+
mergeFormUiClass(
|
|
187
|
+
'hidden shrink-0 text-[11px] font-normal text-dimmed @3xl/form-page:inline',
|
|
188
|
+
pageUi?.navigationOptional
|
|
189
|
+
)
|
|
190
|
+
"
|
|
191
|
+
>
|
|
192
|
+
{{ t("form.page.optional") }}
|
|
193
|
+
</span>
|
|
194
|
+
</a>
|
|
195
|
+
</slot>
|
|
196
|
+
</li>
|
|
197
|
+
</ul>
|
|
198
|
+
</div>
|
|
199
|
+
<div :class="mergeFormUiClass('hidden px-2.5 @3xl/form-page:block', pageUi?.navigationFooter)">
|
|
200
|
+
<div
|
|
201
|
+
:class="
|
|
202
|
+
mergeFormUiClass(
|
|
203
|
+
'border-t border-muted pt-3.5 text-[12.5px] leading-normal text-muted',
|
|
204
|
+
pageUi?.navigationSummary
|
|
205
|
+
)
|
|
206
|
+
"
|
|
207
|
+
data-form-page-summary
|
|
208
|
+
>
|
|
209
|
+
<slot
|
|
210
|
+
name="footer"
|
|
211
|
+
:sections="page.sections.value"
|
|
212
|
+
:remaining="page.remaining.value"
|
|
213
|
+
:modified="page.modified.value"
|
|
214
|
+
>
|
|
215
|
+
<template v-if="'phrase' in summary">
|
|
216
|
+
{{ summary.before
|
|
217
|
+
}}<b
|
|
218
|
+
:class="
|
|
219
|
+
mergeFormUiClass('font-semibold text-highlighted', pageUi?.navigationSummaryCount)
|
|
220
|
+
"
|
|
221
|
+
>{{ summary.phrase }}</b
|
|
222
|
+
>{{ summary.after }}
|
|
223
|
+
</template>
|
|
224
|
+
<template v-else>{{ summary.text }}</template>
|
|
225
|
+
</slot>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
</nav>
|
|
229
|
+
</template>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { FormPageSectionState } from '../../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Navigation of a form page: one entry per visible section, following the section in view.
|
|
4
|
+
* An entry shows whether its section is complete, invalid, or still to fill, marks optional
|
|
5
|
+
* sections, and, with `controls.dirtyCheck`, a dot on modified sections. The footer sums up
|
|
6
|
+
* the sections left to complete, or the modified ones with dirty checking.
|
|
7
|
+
*
|
|
8
|
+
* From 768px of page width it is a pinned column; below, a row of chips that scrolls sideways.
|
|
9
|
+
*/
|
|
10
|
+
type __VLS_Slots = {
|
|
11
|
+
/** Replaces the heading, which comes from the schema `navigation.title`. */
|
|
12
|
+
title?: () => unknown;
|
|
13
|
+
/** Replaces an entry. Call `select` to scroll to the section. */
|
|
14
|
+
item?: (props: {
|
|
15
|
+
section: FormPageSectionState;
|
|
16
|
+
active: boolean;
|
|
17
|
+
select: () => void;
|
|
18
|
+
}) => unknown;
|
|
19
|
+
/** Replaces the summary under the entries. */
|
|
20
|
+
footer?: (props: {
|
|
21
|
+
sections: readonly FormPageSectionState[];
|
|
22
|
+
remaining: number;
|
|
23
|
+
modified: number;
|
|
24
|
+
}) => unknown;
|
|
25
|
+
};
|
|
26
|
+
declare const __VLS_base: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
27
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
28
|
+
declare const _default: typeof __VLS_export;
|
|
29
|
+
export default _default;
|
|
30
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
31
|
+
new (): {
|
|
32
|
+
$slots: S;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { FormPageSectionEntry, FormPageSectionState } from '../../types/index.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
entry: FormPageSectionEntry;
|
|
4
|
+
section: FormPageSectionState;
|
|
5
|
+
};
|
|
6
|
+
type __VLS_Slots = {
|
|
7
|
+
actions?: (props: {
|
|
8
|
+
section: FormPageSectionState;
|
|
9
|
+
}) => unknown;
|
|
10
|
+
};
|
|
11
|
+
declare const __VLS_base: 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>;
|
|
12
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
13
|
+
declare const _default: typeof __VLS_export;
|
|
14
|
+
export default _default;
|
|
15
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
16
|
+
new (): {
|
|
17
|
+
$slots: S;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import UBadge from "@nuxt/ui/components/Badge.vue";
|
|
3
|
+
import UButton from "@nuxt/ui/components/Button.vue";
|
|
4
|
+
import { computed, onBeforeUnmount, onMounted, useId, useTemplateRef } from "vue";
|
|
5
|
+
import { useUiToolsLocale } from "../../../i18n/use-locale";
|
|
6
|
+
import { useFormGridLayout } from "../../composables/use-form-layout";
|
|
7
|
+
import { useFormPageContext } from "../../composables/use-form-page";
|
|
8
|
+
import { provideFormRuntime } from "../../composables/use-form-runtime";
|
|
9
|
+
import { useFormUi } from "../../composables/use-form-ui";
|
|
10
|
+
import { resolveFormLayoutConfig } from "../../utils/layout";
|
|
11
|
+
import { mergeFormUiClass } from "../../utils/ui";
|
|
12
|
+
import FormFieldRenderer from "../renderer/form-field-renderer.vue";
|
|
13
|
+
const props = defineProps({
|
|
14
|
+
entry: { type: Object, required: true },
|
|
15
|
+
section: { type: Object, required: true }
|
|
16
|
+
});
|
|
17
|
+
defineSlots();
|
|
18
|
+
const page = useFormPageContext();
|
|
19
|
+
const formUi = useFormUi();
|
|
20
|
+
const { t } = useUiToolsLocale();
|
|
21
|
+
const { runtime } = page.root;
|
|
22
|
+
const pageUi = computed(() => formUi.ui.value.page?.ui);
|
|
23
|
+
const titleId = useId();
|
|
24
|
+
const dirty = computed(() => page.dirtyCheck.value && props.section.dirty);
|
|
25
|
+
const layout = computed(
|
|
26
|
+
() => resolveFormLayoutConfig(runtime.currentLayout.value, props.entry.section.layout)
|
|
27
|
+
);
|
|
28
|
+
provideFormRuntime({ ...runtime, currentLayout: layout });
|
|
29
|
+
const grid = useFormGridLayout({ layout });
|
|
30
|
+
const element = useTemplateRef("element");
|
|
31
|
+
let unregister;
|
|
32
|
+
onMounted(() => {
|
|
33
|
+
if (element.value) {
|
|
34
|
+
unregister = page.register(props.section.key, element.value);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
onBeforeUnmount(() => unregister?.());
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<template>
|
|
41
|
+
<section
|
|
42
|
+
:id="section.key"
|
|
43
|
+
ref="element"
|
|
44
|
+
:aria-labelledby="titleId"
|
|
45
|
+
:data-form-page-section="section.key"
|
|
46
|
+
:data-state="section.status"
|
|
47
|
+
:data-dirty="dirty || void 0"
|
|
48
|
+
:class="
|
|
49
|
+
mergeFormUiClass(
|
|
50
|
+
'flex min-w-0 scroll-mt-[calc(var(--nut-form-page-header,0px)+var(--nut-form-page-gap,24px))] flex-col 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',
|
|
51
|
+
pageUi?.section
|
|
52
|
+
)
|
|
53
|
+
"
|
|
54
|
+
>
|
|
55
|
+
<header
|
|
56
|
+
:class="
|
|
57
|
+
mergeFormUiClass('flex flex-wrap items-center gap-x-3 gap-y-1', pageUi?.sectionHeader)
|
|
58
|
+
"
|
|
59
|
+
>
|
|
60
|
+
<h2
|
|
61
|
+
:id="titleId"
|
|
62
|
+
tabindex="-1"
|
|
63
|
+
:class="
|
|
64
|
+
mergeFormUiClass(
|
|
65
|
+
'm-0 text-lg font-semibold tracking-[-0.015em] text-highlighted focus:outline-none',
|
|
66
|
+
pageUi?.sectionTitle
|
|
67
|
+
)
|
|
68
|
+
"
|
|
69
|
+
data-form-page-section-title
|
|
70
|
+
>
|
|
71
|
+
{{ section.label }}
|
|
72
|
+
</h2>
|
|
73
|
+
<p
|
|
74
|
+
v-if="section.description"
|
|
75
|
+
:class="mergeFormUiClass('m-0 text-[13px] text-muted', pageUi?.sectionDescription)"
|
|
76
|
+
>
|
|
77
|
+
{{ section.description }}
|
|
78
|
+
</p>
|
|
79
|
+
<UBadge
|
|
80
|
+
v-if="section.optional"
|
|
81
|
+
color="neutral"
|
|
82
|
+
variant="soft"
|
|
83
|
+
:label="t('form.page.optional')"
|
|
84
|
+
:class="mergeFormUiClass('capitalize', pageUi?.sectionOptional)"
|
|
85
|
+
/>
|
|
86
|
+
<div
|
|
87
|
+
v-if="dirty || $slots.actions"
|
|
88
|
+
:class="mergeFormUiClass('ms-auto flex items-center gap-2', pageUi?.sectionActions)"
|
|
89
|
+
>
|
|
90
|
+
<slot name="actions" :section />
|
|
91
|
+
<UButton
|
|
92
|
+
v-if="dirty"
|
|
93
|
+
type="button"
|
|
94
|
+
color="neutral"
|
|
95
|
+
variant="ghost"
|
|
96
|
+
size="sm"
|
|
97
|
+
icon="i-lucide-rotate-ccw"
|
|
98
|
+
:label="t('form.page.resetSection')"
|
|
99
|
+
:class="pageUi?.sectionReset"
|
|
100
|
+
data-form-page-reset
|
|
101
|
+
@click="page.resetSection(section.key)"
|
|
102
|
+
/>
|
|
103
|
+
</div>
|
|
104
|
+
</header>
|
|
105
|
+
<div :class="mergeFormUiClass('min-w-0', pageUi?.sectionBody)" :style="grid.style.value">
|
|
106
|
+
<FormFieldRenderer
|
|
107
|
+
v-for="field in entry.section.fields"
|
|
108
|
+
:key="field.key"
|
|
109
|
+
:field="field"
|
|
110
|
+
:parent-path="[]"
|
|
111
|
+
/>
|
|
112
|
+
</div>
|
|
113
|
+
</section>
|
|
114
|
+
</template>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { FormPageSectionEntry, FormPageSectionState } from '../../types/index.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
entry: FormPageSectionEntry;
|
|
4
|
+
section: FormPageSectionState;
|
|
5
|
+
};
|
|
6
|
+
type __VLS_Slots = {
|
|
7
|
+
actions?: (props: {
|
|
8
|
+
section: FormPageSectionState;
|
|
9
|
+
}) => unknown;
|
|
10
|
+
};
|
|
11
|
+
declare const __VLS_base: 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>;
|
|
12
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
13
|
+
declare const _default: typeof __VLS_export;
|
|
14
|
+
export default _default;
|
|
15
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
16
|
+
new (): {
|
|
17
|
+
$slots: S;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { FormPageSectionState } from '../../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Cards of a form page, one per visible section, each with its title, description, fields, and,
|
|
4
|
+
* with `controls.dirtyCheck`, a ring and a reset button while it is modified. Skeleton cards
|
|
5
|
+
* stand in while the schema context loads.
|
|
6
|
+
*/
|
|
7
|
+
type __VLS_Slots = {
|
|
8
|
+
/** Extra buttons in a section header, before its reset button. */
|
|
9
|
+
actions?: (props: {
|
|
10
|
+
section: FormPageSectionState;
|
|
11
|
+
}) => unknown;
|
|
12
|
+
};
|
|
13
|
+
declare const __VLS_base: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
14
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
15
|
+
declare const _default: typeof __VLS_export;
|
|
16
|
+
export default _default;
|
|
17
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
18
|
+
new (): {
|
|
19
|
+
$slots: S;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import UAlert from "@nuxt/ui/components/Alert.vue";
|
|
3
|
+
import UButton from "@nuxt/ui/components/Button.vue";
|
|
4
|
+
import USkeleton from "@nuxt/ui/components/Skeleton.vue";
|
|
5
|
+
import { computed } from "vue";
|
|
6
|
+
import { useUiToolsLocale } from "../../../i18n/use-locale";
|
|
7
|
+
import { useFormPageContext } from "../../composables/use-form-page";
|
|
8
|
+
import { useFormUi } from "../../composables/use-form-ui";
|
|
9
|
+
import { mergeFormUiClass } from "../../utils/ui";
|
|
10
|
+
import FormPageSection from "./form-page-section.vue";
|
|
11
|
+
defineSlots();
|
|
12
|
+
const page = useFormPageContext();
|
|
13
|
+
const formUi = useFormUi();
|
|
14
|
+
const { t } = useUiToolsLocale();
|
|
15
|
+
const pageUi = computed(() => formUi.ui.value.page?.ui);
|
|
16
|
+
const { contextError, contextLoading, contextPending, refreshContext } = page.root;
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<template>
|
|
20
|
+
<div
|
|
21
|
+
:class="mergeFormUiClass('flex min-w-0 flex-col gap-[18px]', pageUi?.sections)"
|
|
22
|
+
data-form-page-sections
|
|
23
|
+
>
|
|
24
|
+
<UAlert
|
|
25
|
+
v-if="contextError"
|
|
26
|
+
color="error"
|
|
27
|
+
variant="soft"
|
|
28
|
+
icon="i-lucide-circle-alert"
|
|
29
|
+
:title="t('form.states.contextError.title')"
|
|
30
|
+
:description="contextError"
|
|
31
|
+
>
|
|
32
|
+
<template #actions>
|
|
33
|
+
<UButton
|
|
34
|
+
type="button"
|
|
35
|
+
color="error"
|
|
36
|
+
variant="outline"
|
|
37
|
+
size="xs"
|
|
38
|
+
:loading="contextLoading"
|
|
39
|
+
@click="refreshContext"
|
|
40
|
+
>
|
|
41
|
+
{{ t("form.states.contextError.action") }}
|
|
42
|
+
</UButton>
|
|
43
|
+
</template>
|
|
44
|
+
</UAlert>
|
|
45
|
+
<template v-if="contextPending">
|
|
46
|
+
<div
|
|
47
|
+
v-for="section in page.sections.value"
|
|
48
|
+
:key="section.key"
|
|
49
|
+
:class="
|
|
50
|
+
mergeFormUiClass(
|
|
51
|
+
'grid gap-4 rounded-xl border border-default bg-default px-6 pt-[22px] pb-6',
|
|
52
|
+
pageUi?.sectionSkeleton
|
|
53
|
+
)
|
|
54
|
+
"
|
|
55
|
+
data-form-skeleton
|
|
56
|
+
>
|
|
57
|
+
<USkeleton class="h-5 w-40" />
|
|
58
|
+
<USkeleton class="h-9 w-full" />
|
|
59
|
+
<USkeleton class="h-9 w-2/3" />
|
|
60
|
+
</div>
|
|
61
|
+
</template>
|
|
62
|
+
<template v-else>
|
|
63
|
+
<FormPageSection
|
|
64
|
+
v-for="item in page.items.value"
|
|
65
|
+
:key="item.section.key"
|
|
66
|
+
:entry="item.entry"
|
|
67
|
+
:section="item.section"
|
|
68
|
+
>
|
|
69
|
+
<template v-if="$slots.actions" #actions="slotProps">
|
|
70
|
+
<slot name="actions" v-bind="slotProps" />
|
|
71
|
+
</template>
|
|
72
|
+
</FormPageSection>
|
|
73
|
+
</template>
|
|
74
|
+
</div>
|
|
75
|
+
</template>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { FormPageSectionState } from '../../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Cards of a form page, one per visible section, each with its title, description, fields, and,
|
|
4
|
+
* with `controls.dirtyCheck`, a ring and a reset button while it is modified. Skeleton cards
|
|
5
|
+
* stand in while the schema context loads.
|
|
6
|
+
*/
|
|
7
|
+
type __VLS_Slots = {
|
|
8
|
+
/** Extra buttons in a section header, before its reset button. */
|
|
9
|
+
actions?: (props: {
|
|
10
|
+
section: FormPageSectionState;
|
|
11
|
+
}) => unknown;
|
|
12
|
+
};
|
|
13
|
+
declare const __VLS_base: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
14
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
15
|
+
declare const _default: typeof __VLS_export;
|
|
16
|
+
export default _default;
|
|
17
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
18
|
+
new (): {
|
|
19
|
+
$slots: S;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { FormPageSectionState, FormRendererController, FormUiConfig, FormValue } from '../../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* A form laid out as a page: a header with the title and the actions, a navigation that follows
|
|
4
|
+
* the section in view, and one card per section. Render a schema from `defineFormPageSchema`.
|
|
5
|
+
*
|
|
6
|
+
* The page scrolls on its own, so give it a height (`h-full` in a layout that sizes its main
|
|
7
|
+
* area). Put parts in the default slot to compose another layout: `FormPageHeader`,
|
|
8
|
+
* `FormPageNavigation`, `FormPageSections`, and `FormPageActions` read the page they render in.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```vue
|
|
12
|
+
* <!-- const form = useForm({ schema: accountFormSchema(), onSubmit: createAccount }) -->
|
|
13
|
+
* <UiFormPage :form />
|
|
14
|
+
*
|
|
15
|
+
* <UiFormPage :form>
|
|
16
|
+
* <UiFormPageHeader />
|
|
17
|
+
* <div class="grid grid-cols-[minmax(0,1fr)_240px] gap-6 p-6">
|
|
18
|
+
* <UiFormPageSections />
|
|
19
|
+
* <UiFormPageNavigation />
|
|
20
|
+
* </div>
|
|
21
|
+
* </UiFormPage>
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
type __VLS_Props = {
|
|
25
|
+
form: FormRendererController;
|
|
26
|
+
/**
|
|
27
|
+
* Records the section picked in the navigation in the URL hash (`#identity`), and opens the
|
|
28
|
+
* page on the section the hash names. Defaults to `true`.
|
|
29
|
+
*/
|
|
30
|
+
hash?: boolean;
|
|
31
|
+
/** Presentation overrides, merged after the app config and the schema `ui`. */
|
|
32
|
+
ui?: FormUiConfig;
|
|
33
|
+
};
|
|
34
|
+
type __VLS_Slots = {
|
|
35
|
+
/** Replaces the whole layout with parts of the page. */
|
|
36
|
+
default?: () => unknown;
|
|
37
|
+
/** Slots of the default header: see `FormPageHeader`. */
|
|
38
|
+
'header-leading'?: () => unknown;
|
|
39
|
+
'header-eyebrow'?: () => unknown;
|
|
40
|
+
'header-title'?: () => unknown;
|
|
41
|
+
'header-description'?: () => unknown;
|
|
42
|
+
'header-actions'?: () => unknown;
|
|
43
|
+
/** Slots of the default navigation: see `FormPageNavigation`. */
|
|
44
|
+
'navigation-title'?: () => unknown;
|
|
45
|
+
'navigation-item'?: (props: {
|
|
46
|
+
section: FormPageSectionState;
|
|
47
|
+
active: boolean;
|
|
48
|
+
select: () => void;
|
|
49
|
+
}) => unknown;
|
|
50
|
+
'navigation-footer'?: (props: {
|
|
51
|
+
sections: readonly FormPageSectionState[];
|
|
52
|
+
remaining: number;
|
|
53
|
+
modified: number;
|
|
54
|
+
}) => unknown;
|
|
55
|
+
/** Extra buttons in a section header: see `FormPageSections`. */
|
|
56
|
+
'section-actions'?: (props: {
|
|
57
|
+
section: FormPageSectionState;
|
|
58
|
+
}) => unknown;
|
|
59
|
+
};
|
|
60
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
61
|
+
submit: (value: import("../../../shared/index.js").GenericObject, result: {
|
|
62
|
+
success: boolean;
|
|
63
|
+
data?: FormValue;
|
|
64
|
+
}) => any;
|
|
65
|
+
cancel: (value: import("../../../shared/index.js").GenericObject) => any;
|
|
66
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
67
|
+
onSubmit?: ((value: import("../../../shared/index.js").GenericObject, result: {
|
|
68
|
+
success: boolean;
|
|
69
|
+
data?: FormValue;
|
|
70
|
+
}) => any) | undefined;
|
|
71
|
+
onCancel?: ((value: import("../../../shared/index.js").GenericObject) => any) | undefined;
|
|
72
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
73
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
74
|
+
declare const _default: typeof __VLS_export;
|
|
75
|
+
export default _default;
|
|
76
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
77
|
+
new (): {
|
|
78
|
+
$slots: S;
|
|
79
|
+
};
|
|
80
|
+
};
|