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.
Files changed (94) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/module.mjs +10 -2
  3. package/dist/runtime/dashboard/composables/use-dashboard-remote-options.js +8 -3
  4. package/dist/runtime/dashboard/types/filters.d.ts +7 -1
  5. package/dist/runtime/dashboard/utils/builders/dashboard-filters.js +19 -2
  6. package/dist/runtime/form/components/actions/form-actions.vue +13 -127
  7. package/dist/runtime/form/components/page/form-page-actions.d.vue.ts +3 -0
  8. package/dist/runtime/form/components/page/form-page-actions.vue +40 -0
  9. package/dist/runtime/form/components/page/form-page-actions.vue.d.ts +3 -0
  10. package/dist/runtime/form/components/page/form-page-header.d.vue.ts +24 -0
  11. package/dist/runtime/form/components/page/form-page-header.vue +99 -0
  12. package/dist/runtime/form/components/page/form-page-header.vue.d.ts +24 -0
  13. package/dist/runtime/form/components/page/form-page-navigation.d.vue.ts +34 -0
  14. package/dist/runtime/form/components/page/form-page-navigation.vue +229 -0
  15. package/dist/runtime/form/components/page/form-page-navigation.vue.d.ts +34 -0
  16. package/dist/runtime/form/components/page/form-page-section.d.vue.ts +19 -0
  17. package/dist/runtime/form/components/page/form-page-section.vue +114 -0
  18. package/dist/runtime/form/components/page/form-page-section.vue.d.ts +19 -0
  19. package/dist/runtime/form/components/page/form-page-sections.d.vue.ts +21 -0
  20. package/dist/runtime/form/components/page/form-page-sections.vue +75 -0
  21. package/dist/runtime/form/components/page/form-page-sections.vue.d.ts +21 -0
  22. package/dist/runtime/form/components/page/form-page.d.vue.ts +80 -0
  23. package/dist/runtime/form/components/page/form-page.vue +91 -0
  24. package/dist/runtime/form/components/page/form-page.vue.d.ts +80 -0
  25. package/dist/runtime/form/components/renderer/form-field-shell.vue +7 -11
  26. package/dist/runtime/form/components/root/form.vue +25 -194
  27. package/dist/runtime/form/composables/use-field-options.js +19 -0
  28. package/dist/runtime/form/composables/use-form-action-buttons.d.ts +32 -0
  29. package/dist/runtime/form/composables/use-form-action-buttons.js +89 -0
  30. package/dist/runtime/form/composables/use-form-actions.d.ts +8 -5
  31. package/dist/runtime/form/composables/use-form-actions.js +1 -1
  32. package/dist/runtime/form/composables/use-form-page-scroll.d.ts +22 -0
  33. package/dist/runtime/form/composables/use-form-page-scroll.js +178 -0
  34. package/dist/runtime/form/composables/use-form-page.d.ts +114 -0
  35. package/dist/runtime/form/composables/use-form-page.js +83 -0
  36. package/dist/runtime/form/composables/use-form-root.d.ts +34 -0
  37. package/dist/runtime/form/composables/use-form-root.js +171 -0
  38. package/dist/runtime/form/composables/use-form-runtime.js +13 -4
  39. package/dist/runtime/form/composables/use-form.js +2 -22
  40. package/dist/runtime/form/composables/use-remote-field-options.js +21 -2
  41. package/dist/runtime/form/fields/checkbox-card/component.vue +28 -19
  42. package/dist/runtime/form/fields/checkbox-card/types.d.ts +2 -5
  43. package/dist/runtime/form/fields/choice-card/choice-card-label.d.vue.ts +17 -0
  44. package/dist/runtime/form/fields/choice-card/choice-card-label.vue +50 -0
  45. package/dist/runtime/form/fields/choice-card/choice-card-label.vue.d.ts +17 -0
  46. package/dist/runtime/form/fields/choice-card/types.d.ts +31 -0
  47. package/dist/runtime/form/fields/choice-card/types.js +0 -0
  48. package/dist/runtime/form/fields/choice-card/use-choice-card.d.ts +22 -0
  49. package/dist/runtime/form/fields/choice-card/use-choice-card.js +52 -0
  50. package/dist/runtime/form/fields/radio-card/component.vue +17 -18
  51. package/dist/runtime/form/fields/radio-card/types.d.ts +2 -4
  52. package/dist/runtime/form/index.d.ts +1 -1
  53. package/dist/runtime/form/index.js +7 -1
  54. package/dist/runtime/form/schema/index.d.ts +1 -1
  55. package/dist/runtime/form/schema/index.js +1 -0
  56. package/dist/runtime/form/schema/page.d.ts +50 -0
  57. package/dist/runtime/form/schema/page.js +25 -0
  58. package/dist/runtime/form/types/index.d.ts +1 -0
  59. package/dist/runtime/form/types/options.d.ts +22 -2
  60. package/dist/runtime/form/types/page.d.ts +172 -0
  61. package/dist/runtime/form/types/page.js +0 -0
  62. package/dist/runtime/form/types/ui.d.ts +60 -0
  63. package/dist/runtime/form/utils/controls.d.ts +15 -0
  64. package/dist/runtime/form/utils/controls.js +38 -0
  65. package/dist/runtime/form/utils/layout.js +3 -1
  66. package/dist/runtime/form/utils/page.d.ts +20 -0
  67. package/dist/runtime/form/utils/page.js +107 -0
  68. package/dist/runtime/form/utils/state.d.ts +2 -0
  69. package/dist/runtime/form/utils/state.js +1 -1
  70. package/dist/runtime/form/utils/ui.js +1 -0
  71. package/dist/runtime/i18n/locales/en.js +18 -0
  72. package/dist/runtime/i18n/locales/fr.js +18 -0
  73. package/dist/runtime/i18n/types.d.ts +29 -0
  74. package/dist/runtime/shared/index.d.ts +1 -0
  75. package/dist/runtime/shared/index.js +1 -0
  76. package/dist/runtime/shared/types/query.d.ts +5 -1
  77. package/dist/runtime/shared/types/remote-options.d.ts +56 -0
  78. package/dist/runtime/shared/utils/define-remote-options.d.ts +23 -0
  79. package/dist/runtime/shared/utils/define-remote-options.js +24 -0
  80. package/dist/runtime/shared/utils/remote-options-query.d.ts +6 -0
  81. package/dist/runtime/shared/utils/remote-options-query.js +18 -0
  82. package/dist/runtime/table/components/data-list/data-list-filter-tags.d.vue.ts +1 -1
  83. package/dist/runtime/table/components/data-list/data-list-filter-tags.vue +1 -1
  84. package/dist/runtime/table/components/data-list/data-list-filter-tags.vue.d.ts +1 -1
  85. package/dist/runtime/table/components/filters/panel/filter-panel.vue +14 -2
  86. package/dist/runtime/table/components/filters/shared/filter-option-multiple-list.vue +2 -2
  87. package/dist/runtime/table/components/filters/shared/filter-option-single-list.vue +2 -2
  88. package/dist/runtime/table/components/filters/shared/filter-option-tree-content.vue +5 -5
  89. package/dist/runtime/table/components/filters/shared/filter-trigger-tag.vue +7 -1
  90. package/dist/runtime/table/composables/use-table-filter-selected-options.js +16 -1
  91. package/dist/runtime/table/types/filters.d.ts +7 -0
  92. package/dist/runtime/table/types/remote-options.d.ts +3 -16
  93. package/dist/runtime/table/utils/remote-table-options.js +37 -37
  94. package/package.json +1 -1
@@ -0,0 +1,172 @@
1
+ import type { FormValue } from './/index.js';
2
+ import type { FormAction } from './actions.js';
3
+ import type { FormApi, FormSubmitHandler } from './api.js';
4
+ import type { FormFieldCallback } from './callbacks.js';
5
+ import type { FormContextData, FormContextDefinition } from './context.js';
6
+ import type { FormField } from './field.js';
7
+ import type { FormLayoutConfig } from './layout.js';
8
+ import type { FormControlsConfig, FormDrawerConfig, FormFullscreenConfig, FormHeaderConfig, FormModalConfig } from './schema.js';
9
+ import type { FormUiConfig } from './ui.js';
10
+ import type { FormText } from './utils.js';
11
+ /**
12
+ * One section of a form page: a card in the page and an entry in its navigation.
13
+ *
14
+ * Every text accepts a function, so it can be translated:
15
+ * `label: () => t('account.sections.identity')`.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * export function identitySection() {
20
+ * return defineFormPageSection({
21
+ * key: 'identity',
22
+ * label: 'Identity',
23
+ * description: 'display name, legal entity and official identifiers',
24
+ * fields: [
25
+ * { key: 'name', type: 'text', label: 'Name', required: true },
26
+ * { key: 'legalEntity', type: 'text', label: 'Legal entity', required: true },
27
+ * ],
28
+ * })
29
+ * }
30
+ * ```
31
+ */
32
+ export interface FormPageSection<TContext = NonNullable<unknown>, TFields extends readonly FormField<TContext>[] = readonly FormField<TContext>[]> {
33
+ /**
34
+ * Names the section: its navigation entry, its scroll target, and the URL hash that opens the
35
+ * page on it (`#identity`). Also used as the section element's `id`, so keep it unique on the
36
+ * page. It does not prefix the keys of the section's fields.
37
+ */
38
+ key: string;
39
+ /** Title on the section card and in the navigation. */
40
+ label: FormText;
41
+ /** Supporting copy next to the title. */
42
+ description?: FormText;
43
+ /**
44
+ * Shows the section as optional and leaves it out of the sections left to complete. A section
45
+ * without a required field is optional already; set this when its required fields are filled
46
+ * by their defaults and the user has nothing to do there.
47
+ */
48
+ optional?: boolean;
49
+ /** Grid of the section's fields, merged over the schema `layout`. */
50
+ layout?: FormLayoutConfig;
51
+ /** Raw dependency paths read before evaluating `condition`. */
52
+ dependencies?: readonly (string | readonly [string, string])[];
53
+ /** Hides the section, its navigation entry, and its fields while it returns `false`. */
54
+ condition?: FormFieldCallback<boolean, TContext>;
55
+ /** Fields of the section. They write at the form root, as if the section were not there. */
56
+ fields: TFields;
57
+ }
58
+ /**
59
+ * Side navigation of a form page. Its entries are the page sections.
60
+ */
61
+ export interface FormPageNavigationConfig {
62
+ /** Heading above the entries, e.g. `'Création'`. Accepts a function to translate it. */
63
+ title?: FormText;
64
+ }
65
+ /**
66
+ * Schema accepted by `defineFormPageSchema`: a form schema whose `sections` replace `fields`.
67
+ */
68
+ export interface FormPageSchemaInput<TContext extends FormContextDefinition | undefined, TSections extends readonly FormPageSection<FormContextData<TContext>>[]> {
69
+ /** Stable key used by persistence, diagnostics, and test selectors. */
70
+ formKey?: string;
71
+ /** Page heading: `title`, `description`, and the `eyebrow` above the title. */
72
+ header?: FormHeaderConfig;
73
+ /** Form-scoped data sources exposed to fields as `ctx`. */
74
+ context?: TContext;
75
+ /** Default grid of every section. */
76
+ layout?: FormLayoutConfig;
77
+ /** Form-scoped presentation overrides, merged after app defaults. */
78
+ ui?: FormUiConfig;
79
+ /** Runtime lifecycle and validation controls. `dirtyCheck` rings the modified sections. */
80
+ controls?: FormControlsConfig;
81
+ /** Modal-shell sizing, used when the same schema opens in a modal. */
82
+ modal?: FormModalConfig;
83
+ /** Drawer-shell sizing, used when the same schema opens in a drawer. */
84
+ drawer?: FormDrawerConfig;
85
+ /** Fullscreen-shell behavior, used when the same schema opens fullscreen. */
86
+ fullscreen?: FormFullscreenConfig;
87
+ /** Page actions. Omit to render the built-in submit action. */
88
+ actions?: readonly FormAction[];
89
+ /** Runs after validation and before the external submit handler. Return `false` to cancel submit. */
90
+ onBeforeSubmit?: FormSubmitHandler<FormValue, never>;
91
+ /** Submit lifecycle hook. */
92
+ submit?: (params: {
93
+ value: FormValue;
94
+ api: FormApi;
95
+ ctx: FormContextData<TContext>;
96
+ }) => Promise<void> | void;
97
+ /** Side navigation of the page. */
98
+ navigation?: FormPageNavigationConfig;
99
+ /** Sections of the page, in navigation order. */
100
+ sections: TSections;
101
+ }
102
+ /**
103
+ * Card field generated from a section. It renders the section when the schema opens in an
104
+ * overlay, and its fields keep their root paths because a card adds no path segment.
105
+ */
106
+ export type FormPageSectionCard<TSection> = TSection extends {
107
+ readonly key: infer TKey extends string;
108
+ readonly fields: infer TFields;
109
+ } ? {
110
+ readonly type: 'card';
111
+ readonly key: TKey;
112
+ readonly fields: TFields;
113
+ } : never;
114
+ /** Card fields generated from the sections of a form page, in order. */
115
+ export type FormPageSectionCards<TSections extends readonly FormValue[]> = {
116
+ readonly [TIndex in keyof TSections]: FormPageSectionCard<TSections[TIndex]>;
117
+ };
118
+ /**
119
+ * Schema returned by `defineFormPageSchema`: a normal form schema whose `fields` are the cards
120
+ * generated from `sections`, plus the `sections` and `navigation` that `FormPage` reads.
121
+ */
122
+ export type FormPageSchema<TInput extends {
123
+ readonly sections: readonly FormValue[];
124
+ }> = TInput & {
125
+ readonly fields: FormPageSectionCards<TInput['sections']>;
126
+ };
127
+ /** A section paired with the card generated for it, as the page runtime reads them. */
128
+ export interface FormPageSectionEntry {
129
+ section: FormPageSection;
130
+ card: FormField;
131
+ }
132
+ /** How a form page scrolls to a section. */
133
+ export interface FormPageScrollOptions {
134
+ /** Defaults to `smooth`, or `instant` when the user prefers reduced motion. */
135
+ behavior?: ScrollBehavior;
136
+ /** Records the section in the URL hash. Defaults to the page `hash` option. */
137
+ hash?: boolean;
138
+ /** Moves focus to the section title, for keyboard and screen reader users. */
139
+ focus?: boolean;
140
+ }
141
+ /**
142
+ * Where a section stands:
143
+ *
144
+ * - `invalid`: it shows at least one validation error
145
+ * - `complete`: nothing required is missing and it shows no error; an optional section also
146
+ * needs a value, entered by the user or provided by the form input
147
+ * - `pending`: anything else
148
+ */
149
+ export type FormPageSectionStatus = 'invalid' | 'complete' | 'pending';
150
+ /**
151
+ * Live state of a visible section, as the page navigation and section slots receive it.
152
+ */
153
+ export interface FormPageSectionState {
154
+ /** Section key. */
155
+ key: string;
156
+ /** Position among the visible sections. */
157
+ index: number;
158
+ /** Resolved title. */
159
+ label: string;
160
+ /** Resolved supporting copy. */
161
+ description?: string;
162
+ /** True when the section is declared optional or has no required field. */
163
+ optional: boolean;
164
+ /** Number of required fields without a value. */
165
+ missing: number;
166
+ /** Summary of the section's requirements, see `FormPageSectionStatus`. */
167
+ status: FormPageSectionStatus;
168
+ /** True when a value of the section differs from the form's baseline. */
169
+ dirty: boolean;
170
+ /** Dirty paths inside the section. */
171
+ dirtyPaths: readonly string[];
172
+ }
File without changes
@@ -183,6 +183,64 @@ export interface FormOverlayUi {
183
183
  overlay?: FormUiClass;
184
184
  content?: FormUiClass;
185
185
  }
186
+ /**
187
+ * Slots of a form page and its parts. Data attributes carry the live state, so a slot can
188
+ * restyle one state: `data-active` on the current navigation entry, `data-state`
189
+ * (`complete`, `invalid`, `pending`) on entries and indicators, `data-dirty` on modified
190
+ * sections and entries.
191
+ *
192
+ * Two CSS variables place sections under the pinned header: `--nut-form-page-header` (its
193
+ * measured height, set by the page) and `--nut-form-page-gap` (the room below it, `24px` by
194
+ * default). Set them on an element around the navigation and sections, such as the root.
195
+ */
196
+ export interface FormPageUi {
197
+ /** The `<form>` element, which scrolls on its own and is the container of the page queries. */
198
+ root?: FormUiClass;
199
+ /** Grid holding the navigation and the sections. */
200
+ body?: FormUiClass;
201
+ header?: FormUiClass;
202
+ headerContent?: FormUiClass;
203
+ heading?: FormUiClass;
204
+ eyebrow?: FormUiClass;
205
+ title?: FormUiClass;
206
+ /** Line under the title: the description and the unsaved-changes badge. */
207
+ meta?: FormUiClass;
208
+ unsaved?: FormUiClass;
209
+ actions?: FormUiClass;
210
+ navigation?: FormUiClass;
211
+ /** Holds the navigation title and the entries. */
212
+ navigationGroup?: FormUiClass;
213
+ navigationTitle?: FormUiClass;
214
+ navigationList?: FormUiClass;
215
+ /** List item around each entry. */
216
+ navigationEntry?: FormUiClass;
217
+ navigationItem?: FormUiClass;
218
+ navigationIndicator?: FormUiClass;
219
+ /** Check of a complete section in the indicator. */
220
+ navigationIndicatorIcon?: FormUiClass;
221
+ /** Dot in the indicator of the current section while it is pending. */
222
+ navigationIndicatorMarker?: FormUiClass;
223
+ navigationLabel?: FormUiClass;
224
+ navigationOptional?: FormUiClass;
225
+ navigationDirty?: FormUiClass;
226
+ /** Wrapper under the entries, whose padding insets the summary. */
227
+ navigationFooter?: FormUiClass;
228
+ /** The summary, with its top border: "3 sections left to complete". */
229
+ navigationSummary?: FormUiClass;
230
+ /** The bold "3 sections" of the summary. */
231
+ navigationSummaryCount?: FormUiClass;
232
+ sections?: FormUiClass;
233
+ /** Placeholder card shown while the schema context loads. */
234
+ sectionSkeleton?: FormUiClass;
235
+ section?: FormUiClass;
236
+ sectionHeader?: FormUiClass;
237
+ sectionTitle?: FormUiClass;
238
+ sectionDescription?: FormUiClass;
239
+ sectionOptional?: FormUiClass;
240
+ sectionActions?: FormUiClass;
241
+ sectionReset?: FormUiClass;
242
+ sectionBody?: FormUiClass;
243
+ }
186
244
  export interface FormUiPartConfig<TUi> {
187
245
  ui?: TUi;
188
246
  }
@@ -219,4 +277,6 @@ export interface FormUiConfig {
219
277
  modal?: FormUiPartConfig<FormOverlayUi>;
220
278
  drawer?: FormUiPartConfig<FormOverlayUi>;
221
279
  fullscreen?: FormUiPartConfig<FormOverlayUi>;
280
+ /** Form page rendered by `FormPage` and its parts. */
281
+ page?: FormUiPartConfig<FormPageUi>;
222
282
  }
@@ -0,0 +1,15 @@
1
+ import type { FormUiConfig, FormValidationMode, FormValue } from '../types/index.js';
2
+ /** Reads the schema `controls` object. */
3
+ export declare function getSchemaControls(schema: FormValue): any;
4
+ /** Resolves `controls.syncInput`: `true`, a path list, or no synchronization. */
5
+ export declare function getSchemaSyncInput(schema: FormValue): boolean | readonly string[];
6
+ /** Resolves `controls.validate`, validating everything by default. */
7
+ export declare function getSchemaValidationMode(schema: FormValue): FormValidationMode;
8
+ /** Resolves `controls.autoFocus`: a raw field path, `true` for the first field, or `false`. */
9
+ export declare function getSchemaAutoFocus(schema: FormValue): string | boolean;
10
+ /** True when `controls.dirtyCheck` enables dirty metadata and reset affordances. */
11
+ export declare function getSchemaDirtyCheck(schema: FormValue): boolean;
12
+ /** Reads `controls.confirmNavOnDirty` as authored. */
13
+ export declare function getSchemaDirtyNavigation(schema: FormValue): unknown;
14
+ /** Reads the schema-level `ui` overrides. */
15
+ export declare function getSchemaUi(schema: FormValue): FormUiConfig | undefined;
@@ -0,0 +1,38 @@
1
+ import { isRecord } from "./path.js";
2
+ import { isBoolean, isString, stringArray } from "./predicate.js";
3
+ export function getSchemaControls(schema) {
4
+ if (!isRecord(schema)) {
5
+ return;
6
+ }
7
+ const controls = Object.getOwnPropertyDescriptor(schema, "controls")?.value;
8
+ return isRecord(controls) ? controls : void 0;
9
+ }
10
+ function getSchemaControl(schema, name) {
11
+ const controls = getSchemaControls(schema);
12
+ return controls ? Object.getOwnPropertyDescriptor(controls, name)?.value : void 0;
13
+ }
14
+ export function getSchemaSyncInput(schema) {
15
+ const value = getSchemaControl(schema, "syncInput");
16
+ return isBoolean(value) ? value : stringArray(value);
17
+ }
18
+ export function getSchemaValidationMode(schema) {
19
+ const value = getSchemaControl(schema, "validate");
20
+ return value === false || value === "required" || value === "validators" ? value : true;
21
+ }
22
+ export function getSchemaAutoFocus(schema) {
23
+ const value = getSchemaControl(schema, "autoFocus");
24
+ return isString(value) || isBoolean(value) ? value : false;
25
+ }
26
+ export function getSchemaDirtyCheck(schema) {
27
+ return getSchemaControl(schema, "dirtyCheck") === true;
28
+ }
29
+ export function getSchemaDirtyNavigation(schema) {
30
+ return getSchemaControl(schema, "confirmNavOnDirty");
31
+ }
32
+ export function getSchemaUi(schema) {
33
+ if (!isRecord(schema)) {
34
+ return void 0;
35
+ }
36
+ const value = Object.getOwnPropertyDescriptor(schema, "ui")?.value;
37
+ return isRecord(value) ? value : void 0;
38
+ }
@@ -8,7 +8,9 @@ export function resolveFormLayoutConfig(base, override) {
8
8
  return {
9
9
  columns: override?.columns ?? base?.columns ?? FORM_LAYOUT_DEFAULTS.columns,
10
10
  fieldSpan: override?.fieldSpan ?? base?.fieldSpan ?? FORM_LAYOUT_DEFAULTS.fieldSpan,
11
- gap: override?.gap ?? base?.gap ?? FORM_LAYOUT_DEFAULTS.gap
11
+ gap: override?.gap ?? base?.gap ?? FORM_LAYOUT_DEFAULTS.gap,
12
+ labelPosition: override?.labelPosition ?? base?.labelPosition,
13
+ labelWidth: override?.labelWidth ?? base?.labelWidth
12
14
  };
13
15
  }
14
16
  export function normalizeFormLayoutGap(value) {
@@ -0,0 +1,20 @@
1
+ import type { FormObject, FormPageSectionEntry, FormPageSectionState, FormRuntime, FormValue } from '../types/index.js';
2
+ /** Pairs each section of a page schema with the card `defineFormPageSchema` generated for it. */
3
+ export declare function getFormPageSections(schema: FormValue): readonly FormPageSectionEntry[];
4
+ /** Resolved heading of the page navigation. */
5
+ export declare function getFormPageNavigationTitle(schema: FormValue): string | undefined;
6
+ /** True while the section's `condition` lets it render. */
7
+ export declare function isFormPageSectionVisible(entry: FormPageSectionEntry, runtime: FormRuntime): boolean;
8
+ /**
9
+ * Live state of one visible section: what it still misses, whether it shows errors, and what
10
+ * changed since the baseline.
11
+ */
12
+ export declare function resolveFormPageSectionState(params: {
13
+ entry: FormPageSectionEntry;
14
+ index: number;
15
+ runtime: FormRuntime;
16
+ /** Input the form was opened with: a value it provides counts as filled in. */
17
+ input: FormObject | undefined;
18
+ /** False when the validation mode does not enforce required fields. */
19
+ requiredEnforced: boolean;
20
+ }): FormPageSectionState;
@@ -0,0 +1,107 @@
1
+ import { createFormFieldInstance } from "./field-instance.js";
2
+ import { getPathValue, isRecord } from "./path.js";
3
+ import { isBoolean, isFunction, isNumber, isString } from "./predicate.js";
4
+ import {
5
+ fieldPath,
6
+ getChildFields,
7
+ getSchemaFields,
8
+ isEmptyValue,
9
+ isFlatPassthroughField,
10
+ isObjectContainerField,
11
+ resolveRequired
12
+ } from "./state.js";
13
+ import { resolveFormText } from "./text.js";
14
+ export function getFormPageSections(schema) {
15
+ if (!isRecord(schema) || !Array.isArray(schema.sections)) {
16
+ return [];
17
+ }
18
+ const cards = getSchemaFields(schema);
19
+ return schema.sections.filter(isFormPageSection).flatMap((section) => {
20
+ const card = cards.find((field) => field.key === section.key);
21
+ return card ? [{ card, section }] : [];
22
+ });
23
+ }
24
+ export function getFormPageNavigationTitle(schema) {
25
+ if (!isRecord(schema) || !isRecord(schema.navigation)) {
26
+ return void 0;
27
+ }
28
+ const { title } = schema.navigation;
29
+ return isFormText(title) ? resolveFormText(title) : void 0;
30
+ }
31
+ export function isFormPageSectionVisible(entry, runtime) {
32
+ return !hasCondition(entry.card) || runtime.shouldRender(entry.card, [entry.card.key]);
33
+ }
34
+ export function resolveFormPageSectionState(params) {
35
+ const { entry, runtime } = params;
36
+ const leaves = collectVisibleLeaves(entry.section.fields, [], runtime);
37
+ const paths = leaves.map((leaf) => leaf.path.join("."));
38
+ const contains = (candidate) => paths.some((path) => isPathWithin(candidate, path));
39
+ const dirtyPaths = runtime.dirtyPaths.value.filter(contains);
40
+ const invalid = runtime.errors.value.some((error) => contains(error.path));
41
+ const required = params.requiredEnforced ? leaves.filter((leaf) => isRequiredLeaf(leaf, runtime)) : [];
42
+ const missing = required.filter((leaf) => isEmptyValue(runtime.getValue(leaf.path))).length;
43
+ const optional = entry.section.optional === true || required.length === 0;
44
+ const filled = !optional || leaves.some((leaf) => isProvided(leaf, runtime, dirtyPaths, params.input));
45
+ return {
46
+ description: resolveFormText(entry.section.description),
47
+ dirty: dirtyPaths.length > 0,
48
+ dirtyPaths,
49
+ index: params.index,
50
+ key: entry.section.key,
51
+ label: resolveFormText(entry.section.label) ?? entry.section.key,
52
+ missing,
53
+ optional,
54
+ status: invalid ? "invalid" : missing === 0 && filled ? "complete" : "pending"
55
+ };
56
+ }
57
+ function collectVisibleLeaves(fields, parentPath, runtime) {
58
+ return fields.flatMap((field) => {
59
+ if (field.ignore === true) {
60
+ return [];
61
+ }
62
+ const path = fieldPath(parentPath, field);
63
+ if (hasCondition(field) && !runtime.shouldRender(field, path)) {
64
+ return [];
65
+ }
66
+ if (createFormFieldInstance(field).state.is("stateless")) {
67
+ return [];
68
+ }
69
+ if (isFlatPassthroughField(field)) {
70
+ return collectVisibleLeaves(getChildFields(field), parentPath, runtime);
71
+ }
72
+ if (isObjectContainerField(field)) {
73
+ return collectVisibleLeaves(getChildFields(field), path, runtime);
74
+ }
75
+ return [{ field, path }];
76
+ });
77
+ }
78
+ function isRequiredLeaf(leaf, runtime) {
79
+ const required = Object.getOwnPropertyDescriptor(leaf.field, "required")?.value;
80
+ if (isBoolean(required)) {
81
+ return required;
82
+ }
83
+ const validation = Object.getOwnPropertyDescriptor(leaf.field, "validation")?.value;
84
+ if (!isFunction(required) && !isRecord(validation)) {
85
+ return false;
86
+ }
87
+ return resolveRequired(leaf.field, runtime.getFieldCallbackParams(leaf.path, leaf.field)) === true;
88
+ }
89
+ function isProvided(leaf, runtime, dirtyPaths, input) {
90
+ if (isEmptyValue(runtime.getValue(leaf.path))) {
91
+ return false;
92
+ }
93
+ const path = leaf.path.join(".");
94
+ return dirtyPaths.some((dirtyPath) => isPathWithin(dirtyPath, path)) || !isEmptyValue(getPathValue(input ?? {}, leaf.path));
95
+ }
96
+ function hasCondition(field) {
97
+ return isFunction(Object.getOwnPropertyDescriptor(field, "condition")?.value);
98
+ }
99
+ function isPathWithin(path, scope) {
100
+ return path === scope || path.startsWith(`${scope}.`);
101
+ }
102
+ function isFormPageSection(value) {
103
+ return isRecord(value) && isString(value.key) && isFormText(value.label) && Array.isArray(value.fields);
104
+ }
105
+ function isFormText(value) {
106
+ return isString(value) || isNumber(value) || isFunction(value);
107
+ }
@@ -30,4 +30,6 @@ export declare function getMatrixRows(field: FormField): any[];
30
30
  export declare function resolveRequired(field: FormField, params: FormFieldCallbackParams): any;
31
31
  export declare function resolveRequiredMessage(field: FormField, fallback?: string): string;
32
32
  export declare function isEmptyValue(value: FormValue): boolean;
33
+ /** Child fields of a container, with the fields of every tab for tabs. */
34
+ export declare function getChildFields(field: FormField): FormField[];
33
35
  export declare function getArrayItemFields(field: FormField, item: FormObject): FormField[];
@@ -588,7 +588,7 @@ function isFormField(value) {
588
588
  const type = Object.getOwnPropertyDescriptor(value, "type")?.value;
589
589
  return isString(value.key) && isString(type) && isRegisteredFormFieldType(type);
590
590
  }
591
- function getChildFields(field) {
591
+ export function getChildFields(field) {
592
592
  const tabs = Object.getOwnPropertyDescriptor(field, "tabs")?.value;
593
593
  if (Array.isArray(tabs)) {
594
594
  return tabs.flatMap((tab) => {
@@ -39,6 +39,7 @@ function mergeFormUiPair(current, next) {
39
39
  group: mergePart(current.group, next.group),
40
40
  matrix: mergePart(current.matrix, next.matrix),
41
41
  modal: mergePart(current.modal, next.modal),
42
+ page: mergePart(current.page, next.page),
42
43
  root: mergePart(current.root, next.root),
43
44
  tree: mergePart(current.tree, next.tree),
44
45
  treeSelect: mergePart(current.treeSelect, next.treeSelect)
@@ -101,6 +101,24 @@ export default defineUiToolsLocale({
101
101
  resetButton: "Reset",
102
102
  submitButton: "Submit"
103
103
  },
104
+ page: {
105
+ complete: "Everything is ready.",
106
+ modifiedOne: "{count} modified.",
107
+ modifiedOther: "{count} modified.",
108
+ navigation: "Form sections",
109
+ optional: "optional",
110
+ remaining: "{count} left to complete.",
111
+ resetSection: "Reset",
112
+ sectionsOne: "{count} section",
113
+ sectionsOther: "{count} sections",
114
+ status: {
115
+ complete: "Complete",
116
+ invalid: "Has errors",
117
+ pending: "To complete"
118
+ },
119
+ unmodified: "No unsaved changes.",
120
+ unsavedChanges: "Unsaved changes"
121
+ },
104
122
  fields: {
105
123
  array: {
106
124
  addItem: "Add item",
@@ -101,6 +101,24 @@ export default defineUiToolsLocale({
101
101
  resetButton: "R\xE9initialiser",
102
102
  submitButton: "Soumettre"
103
103
  },
104
+ page: {
105
+ complete: "Tout est pr\xEAt.",
106
+ modifiedOne: "{count} modifi\xE9e.",
107
+ modifiedOther: "{count} modifi\xE9es.",
108
+ navigation: "Sections du formulaire",
109
+ optional: "optionnel",
110
+ remaining: "{count} \xE0 compl\xE9ter.",
111
+ resetSection: "R\xE9initialiser",
112
+ sectionsOne: "{count} section",
113
+ sectionsOther: "{count} sections",
114
+ status: {
115
+ complete: "Compl\xE8te",
116
+ invalid: "Contient des erreurs",
117
+ pending: "\xC0 compl\xE9ter"
118
+ },
119
+ unmodified: "Aucune modification en cours.",
120
+ unsavedChanges: "Modifications non enregistr\xE9es"
121
+ },
104
122
  fields: {
105
123
  array: {
106
124
  addItem: "Ajouter un \xE9l\xE9ment",
@@ -275,6 +275,35 @@ export interface UiToolsFormMessages {
275
275
  cancelButton: string;
276
276
  resetButton: string;
277
277
  };
278
+ /** Form page chrome. Counts use `Intl.PluralRules`: `One` for "one", `Other` otherwise. */
279
+ page: {
280
+ /** Accessible name of the section navigation when the schema gives it no title. */
281
+ navigation: string;
282
+ /** Marks an optional section, in the navigation and on its card. */
283
+ optional: string;
284
+ /** Badge under the page title and name of a section's dirty marker. */
285
+ unsavedChanges: string;
286
+ /** Button of a modified section that puts its values back. */
287
+ resetSection: string;
288
+ /** Bold count inserted in `remaining` and `modified`: "{count} sections". */
289
+ sectionsOne: string;
290
+ sectionsOther: string;
291
+ /** Navigation summary while required sections are incomplete: "{count} to complete." */
292
+ remaining: string;
293
+ /** Navigation summary once every required section is complete. */
294
+ complete: string;
295
+ /** Navigation summary of a form with dirty checking: "{count} modified." */
296
+ modifiedOne: string;
297
+ modifiedOther: string;
298
+ /** Navigation summary of a form with dirty checking and no change. */
299
+ unmodified: string;
300
+ /** Accessible status of a navigation entry. */
301
+ status: {
302
+ complete: string;
303
+ pending: string;
304
+ invalid: string;
305
+ };
306
+ };
278
307
  states: {
279
308
  contextError: {
280
309
  title: string;
@@ -1,5 +1,6 @@
1
1
  export * from './composables/use-responsive-value.js';
2
2
  export * from './composables/use-range-select.js';
3
+ export { defineRemoteOptions } from './utils/define-remote-options.js';
3
4
  export * from './types/responsive.js';
4
5
  export * from './types/utils.js';
5
6
  export * from './utils/object.js';
@@ -1,5 +1,6 @@
1
1
  export * from "./composables/use-responsive-value.js";
2
2
  export * from "./composables/use-range-select.js";
3
+ export { defineRemoteOptions } from "./utils/define-remote-options.js";
3
4
  export * from "./types/responsive.js";
4
5
  export * from "./types/utils.js";
5
6
  export * from "./utils/object.js";
@@ -1,4 +1,4 @@
1
- import type { QueryFunction, QueryKey, UseQueryOptions } from '@tanstack/vue-query';
1
+ import type { QueryFunction, QueryKey, QueryOptions, UseQueryOptions } from '@tanstack/vue-query';
2
2
  /**
3
3
  * TanStack query definition accepted by runtime domains that own the `useQuery` call themselves
4
4
  * (table sources, dashboard queries): an explicit `queryKey`, an optional `queryFn`, and any other
@@ -16,3 +16,7 @@ export interface QueryFnDefinition<TData = unknown> {
16
16
  queryKey: QueryKey;
17
17
  queryFn: QueryFunction<TData, QueryKey, string | null>;
18
18
  }
19
+ /** Data returned by a query function, including generated options whose function may be a ref. */
20
+ export type QueryFunctionResult<TQuery> = TQuery extends {
21
+ queryFn: (...args: never[]) => infer TResult;
22
+ } ? Awaited<TResult> : TQuery extends QueryOptions<infer TQueryFnData, infer _TError, infer _TData, infer _TQueryData, infer _TQueryKey> ? Awaited<TQueryFnData> : TQuery extends UseQueryOptions<infer _TQueryFnData, infer _TError, infer TData, infer _TQueryData, infer _TQueryKey> ? Awaited<TData> : never;
@@ -1,3 +1,11 @@
1
+ import type { QueryKey } from '@tanstack/vue-query';
2
+ import type { QueryFnDefinition, QueryFunctionResult } from './query.js';
3
+ export type RemoteOptionValue = string | number | boolean;
4
+ /** Option shape shared by dashboard, table, and form remote pickers. */
5
+ export interface RemoteOption<TValue extends RemoteOptionValue = RemoteOptionValue> {
6
+ value: TValue;
7
+ label: string;
8
+ }
1
9
  /**
2
10
  * Page envelope returned by an index-paginated remote option source.
3
11
  */
@@ -35,3 +43,51 @@ export interface RemoteOptionsSearch {
35
43
  /** Minimum term length before a search request runs. Defaults to 0. */
36
44
  minLength?: number;
37
45
  }
46
+ /** One reusable, query-backed option source. Inline remote definitions remain supported. */
47
+ export interface RemoteOptionsLoader<TOption> {
48
+ load: (request: {
49
+ search: string;
50
+ page: RemoteOptionsPageRequest;
51
+ }) => QueryFnDefinition<RemoteOptionsResult<TOption>>;
52
+ resolveSelected: (request: {
53
+ values: readonly RemoteOptionValue[];
54
+ }) => QueryFnDefinition<readonly TOption[]>;
55
+ pagination: RemoteOptionsPagination;
56
+ search?: RemoteOptionsSearch;
57
+ /** Identity of a page query, used by pickers that cache an infinite list as one query. */
58
+ queryKeyFor?: (request: {
59
+ search: string;
60
+ page: RemoteOptionsPageRequest;
61
+ }) => QueryKey;
62
+ /** Identity of a selected-value lookup, including any scope used only by that endpoint. */
63
+ selectedQueryKeyFor?: (request: {
64
+ values: readonly RemoteOptionValue[];
65
+ }) => QueryKey;
66
+ }
67
+ /** Query options returned by an endpoint or generated client. */
68
+ export interface RemoteOptionsQueryDefinition {
69
+ queryKey: QueryKey;
70
+ }
71
+ /** Endpoint query definitions; supplied before mapping so their result types are inferred first. */
72
+ export interface RemoteOptionsQueries<TPageQuery extends RemoteOptionsQueryDefinition, TSelectedQuery extends RemoteOptionsQueryDefinition> {
73
+ load: (request: {
74
+ search: string;
75
+ page: RemoteOptionsPageRequest;
76
+ }) => TPageQuery;
77
+ resolveSelected: (request: {
78
+ values: readonly RemoteOptionValue[];
79
+ }) => TSelectedQuery;
80
+ }
81
+ /** Maps endpoint responses to the option result shared by all three pickers. */
82
+ export interface DefineRemoteOptionsConfig<TPageQuery extends RemoteOptionsQueryDefinition, TSelectedQuery extends RemoteOptionsQueryDefinition, TOption extends RemoteOption> {
83
+ /** Stable namespace for mapped query results; requests still use their endpoint query keys. */
84
+ key: string;
85
+ /** Maps the endpoint page to options and the next-page marker. */
86
+ mapPage: (result: QueryFunctionResult<TPageQuery>) => RemoteOptionsResult<TOption>;
87
+ /** Maps selected lookup results to option items. */
88
+ mapSelected: (result: QueryFunctionResult<TSelectedQuery>) => readonly NoInfer<TOption>[];
89
+ /** Page type and size expected by the endpoint. */
90
+ pagination: RemoteOptionsPagination;
91
+ /** Debounce and minimum term length applied by the consuming picker. */
92
+ search?: RemoteOptionsSearch;
93
+ }