nuxt-ui-tools 1.17.1 → 1.19.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 (178) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/module.mjs +1 -0
  3. package/dist/runtime/form/components/page/form-page-header.d.vue.ts +3 -1
  4. package/dist/runtime/form/components/page/form-page-header.vue +44 -34
  5. package/dist/runtime/form/components/page/form-page-header.vue.d.ts +3 -1
  6. package/dist/runtime/form/components/page/form-page-navigation.d.vue.ts +5 -3
  7. package/dist/runtime/form/components/page/form-page-navigation.vue +6 -3
  8. package/dist/runtime/form/components/page/form-page-navigation.vue.d.ts +5 -3
  9. package/dist/runtime/form/components/page/form-page-section.vue +6 -5
  10. package/dist/runtime/form/components/page/form-page-sections.d.vue.ts +4 -3
  11. package/dist/runtime/form/components/page/form-page-sections.vue +7 -2
  12. package/dist/runtime/form/components/page/form-page-sections.vue.d.ts +4 -3
  13. package/dist/runtime/form/components/page/form-page.d.vue.ts +2 -0
  14. package/dist/runtime/form/components/page/form-page.vue +11 -2
  15. package/dist/runtime/form/components/page/form-page.vue.d.ts +2 -0
  16. package/dist/runtime/form/components/renderer/form-field-renderer.vue +6 -4
  17. package/dist/runtime/form/components/renderer/form-field-shell.d.vue.ts +4 -4
  18. package/dist/runtime/form/components/renderer/form-field-shell.vue +28 -4
  19. package/dist/runtime/form/components/renderer/form-field-shell.vue.d.ts +4 -4
  20. package/dist/runtime/form/components/root/form.vue +2 -2
  21. package/dist/runtime/form/components/utils/form-option-choice-label.d.vue.ts +16 -0
  22. package/dist/runtime/form/components/utils/form-option-choice-label.vue +37 -0
  23. package/dist/runtime/form/components/utils/form-option-choice-label.vue.d.ts +16 -0
  24. package/dist/runtime/form/components/utils/form-option-display-value.d.vue.ts +14 -0
  25. package/dist/runtime/form/components/utils/form-option-display-value.vue +20 -0
  26. package/dist/runtime/form/components/utils/form-option-display-value.vue.d.ts +14 -0
  27. package/dist/runtime/form/components/utils/form-option-item-label.d.vue.ts +9 -2
  28. package/dist/runtime/form/components/utils/form-option-item-label.vue +20 -13
  29. package/dist/runtime/form/components/utils/form-option-item-label.vue.d.ts +9 -2
  30. package/dist/runtime/form/components/utils/form-option-leading.d.vue.ts +23 -0
  31. package/dist/runtime/form/components/utils/form-option-leading.vue +41 -0
  32. package/dist/runtime/form/components/utils/form-option-leading.vue.d.ts +23 -0
  33. package/dist/runtime/form/components/utils/form-option-render.d.ts +11 -0
  34. package/dist/runtime/form/components/utils/form-option-render.js +5 -0
  35. package/dist/runtime/form/components/utils/form-option-suffix.d.vue.ts +8 -0
  36. package/dist/runtime/form/components/utils/form-option-suffix.vue +15 -0
  37. package/dist/runtime/form/components/utils/form-option-suffix.vue.d.ts +8 -0
  38. package/dist/runtime/form/components/utils/form-option-value-overlay.d.vue.ts +30 -0
  39. package/dist/runtime/form/components/utils/form-option-value-overlay.vue +44 -0
  40. package/dist/runtime/form/components/utils/form-option-value-overlay.vue.d.ts +30 -0
  41. package/dist/runtime/form/composables/use-choice-group-items.d.ts +27 -0
  42. package/dist/runtime/form/composables/use-choice-group-items.js +24 -0
  43. package/dist/runtime/form/composables/use-field-control.d.ts +6 -6
  44. package/dist/runtime/form/composables/use-field-control.js +9 -21
  45. package/dist/runtime/form/composables/use-field-options.js +8 -4
  46. package/dist/runtime/form/composables/use-form-layout.js +4 -3
  47. package/dist/runtime/form/composables/use-form-page.d.ts +11 -1
  48. package/dist/runtime/form/composables/use-form-page.js +12 -6
  49. package/dist/runtime/form/composables/use-form-runtime.js +1 -1
  50. package/dist/runtime/form/composables/use-option-menu.d.ts +20 -0
  51. package/dist/runtime/form/composables/use-option-menu.js +41 -0
  52. package/dist/runtime/form/fields/array-primitive/types.d.ts +1 -1
  53. package/dist/runtime/form/fields/auto-complete/component.vue +92 -51
  54. package/dist/runtime/form/fields/auto-complete/config.d.ts +1 -0
  55. package/dist/runtime/form/fields/auto-complete/config.js +1 -1
  56. package/dist/runtime/form/fields/auto-complete/types.d.ts +2 -2
  57. package/dist/runtime/form/fields/checkbox-card/component.vue +55 -29
  58. package/dist/runtime/form/fields/checkbox-card/config.d.ts +1 -0
  59. package/dist/runtime/form/fields/checkbox-card/config.js +1 -1
  60. package/dist/runtime/form/fields/checkbox-card/types.d.ts +3 -3
  61. package/dist/runtime/form/fields/checkbox-group/component.vue +18 -4
  62. package/dist/runtime/form/fields/checkbox-group/types.d.ts +2 -2
  63. package/dist/runtime/form/fields/choice-card/choice-card-label.d.vue.ts +7 -0
  64. package/dist/runtime/form/fields/choice-card/choice-card-label.vue +27 -1
  65. package/dist/runtime/form/fields/choice-card/choice-card-label.vue.d.ts +7 -0
  66. package/dist/runtime/form/fields/choice-card/types.d.ts +35 -1
  67. package/dist/runtime/form/fields/choice-card/use-choice-card.d.ts +7 -3
  68. package/dist/runtime/form/fields/choice-card/use-choice-card.js +37 -15
  69. package/dist/runtime/form/fields/choice-card/utils.d.ts +10 -0
  70. package/dist/runtime/form/fields/choice-card/utils.js +6 -0
  71. package/dist/runtime/form/fields/hierarchy/component.vue +3 -2
  72. package/dist/runtime/form/fields/index.d.ts +4 -0
  73. package/dist/runtime/form/fields/radio/component.vue +19 -2
  74. package/dist/runtime/form/fields/radio/types.d.ts +2 -2
  75. package/dist/runtime/form/fields/radio-card/component.vue +58 -28
  76. package/dist/runtime/form/fields/radio-card/config.d.ts +1 -0
  77. package/dist/runtime/form/fields/radio-card/config.js +1 -1
  78. package/dist/runtime/form/fields/radio-card/types.d.ts +3 -3
  79. package/dist/runtime/form/fields/select/component.vue +56 -58
  80. package/dist/runtime/form/fields/select/config.d.ts +1 -0
  81. package/dist/runtime/form/fields/select/config.js +1 -1
  82. package/dist/runtime/form/fields/select/types.d.ts +2 -2
  83. package/dist/runtime/form/types/field-base.d.ts +6 -0
  84. package/dist/runtime/form/types/field-kind.d.ts +3 -1
  85. package/dist/runtime/form/types/field.d.ts +1 -0
  86. package/dist/runtime/form/types/layout.d.ts +6 -0
  87. package/dist/runtime/form/types/options.d.ts +137 -0
  88. package/dist/runtime/form/types/page.d.ts +20 -8
  89. package/dist/runtime/form/types/ui.d.ts +29 -4
  90. package/dist/runtime/form/utils/disabled.d.ts +11 -0
  91. package/dist/runtime/form/utils/disabled.js +11 -0
  92. package/dist/runtime/form/utils/field-instance.js +3 -0
  93. package/dist/runtime/form/utils/options.d.ts +56 -11
  94. package/dist/runtime/form/utils/options.js +86 -0
  95. package/dist/runtime/form/utils/page-surface.d.ts +26 -0
  96. package/dist/runtime/form/utils/page-surface.js +28 -0
  97. package/dist/runtime/form/utils/page.d.ts +21 -1
  98. package/dist/runtime/form/utils/page.js +24 -2
  99. package/dist/runtime/i18n/locales/en.js +28 -8
  100. package/dist/runtime/i18n/locales/fr.js +28 -8
  101. package/dist/runtime/i18n/types.d.ts +46 -7
  102. package/dist/runtime/spreadsheet/components/internal/spreadsheet-import-column-row.d.vue.ts +2 -2
  103. package/dist/runtime/spreadsheet/components/internal/spreadsheet-import-column-row.vue.d.ts +2 -2
  104. package/dist/runtime/table/components/actions/row-actions.d.vue.ts +2 -0
  105. package/dist/runtime/table/components/actions/row-actions.vue +8 -3
  106. package/dist/runtime/table/components/actions/row-actions.vue.d.ts +2 -0
  107. package/dist/runtime/table/components/data-list/data-list-content.vue +7 -1
  108. package/dist/runtime/table/components/data-list/data-list-default.d.vue.ts +16 -16
  109. package/dist/runtime/table/components/data-list/data-list-default.vue +3 -0
  110. package/dist/runtime/table/components/data-list/data-list-default.vue.d.ts +16 -16
  111. package/dist/runtime/table/components/data-list/data-list-layout-switch.vue +7 -9
  112. package/dist/runtime/table/components/data-list/data-list-root.vue +5 -6
  113. package/dist/runtime/table/components/data-list/data-list-sort-menu.vue +3 -1
  114. package/dist/runtime/table/components/data-list/data-list-table.d.vue.ts +8 -1
  115. package/dist/runtime/table/components/data-list/data-list-table.vue +18 -1
  116. package/dist/runtime/table/components/data-list/data-list-table.vue.d.ts +8 -1
  117. package/dist/runtime/table/components/data-list/data-list-view-toggle.d.vue.ts +25 -0
  118. package/dist/runtime/table/components/data-list/data-list-view-toggle.vue +57 -0
  119. package/dist/runtime/table/components/data-list/data-list-view-toggle.vue.d.ts +25 -0
  120. package/dist/runtime/table/components/data-list/sort-menu-panel.vue +1 -1
  121. package/dist/runtime/table/components/filters/tags/boolean-filter-tag.d.vue.ts +2 -2
  122. package/dist/runtime/table/components/filters/tags/boolean-filter-tag.vue.d.ts +2 -2
  123. package/dist/runtime/table/components/filters/tags/date-filter-tag.d.vue.ts +2 -2
  124. package/dist/runtime/table/components/filters/tags/date-filter-tag.vue.d.ts +2 -2
  125. package/dist/runtime/table/components/filters/tags/number-filter-tag.d.vue.ts +2 -2
  126. package/dist/runtime/table/components/filters/tags/number-filter-tag.vue.d.ts +2 -2
  127. package/dist/runtime/table/components/filters/tags/option-filter-tag.d.vue.ts +2 -2
  128. package/dist/runtime/table/components/filters/tags/option-filter-tag.vue.d.ts +2 -2
  129. package/dist/runtime/table/components/filters/tags/text-filter-tag.d.vue.ts +2 -2
  130. package/dist/runtime/table/components/filters/tags/text-filter-tag.vue.d.ts +2 -2
  131. package/dist/runtime/table/components/layout/table-footer.d.vue.ts +6 -4
  132. package/dist/runtime/table/components/layout/table-footer.vue +80 -3
  133. package/dist/runtime/table/components/layout/table-footer.vue.d.ts +6 -4
  134. package/dist/runtime/table/components/summary/summary-content.d.ts +13 -0
  135. package/dist/runtime/table/components/summary/summary-content.js +5 -0
  136. package/dist/runtime/table/components/summary/summary-renderer.d.vue.ts +31 -0
  137. package/dist/runtime/table/components/summary/summary-renderer.vue +282 -0
  138. package/dist/runtime/table/components/summary/summary-renderer.vue.d.ts +31 -0
  139. package/dist/runtime/table/components/summary/summary-row.d.vue.ts +19 -0
  140. package/dist/runtime/table/components/summary/summary-row.vue +102 -0
  141. package/dist/runtime/table/components/summary/summary-row.vue.d.ts +19 -0
  142. package/dist/runtime/table/components/table/table-empty-state.vue +39 -9
  143. package/dist/runtime/table/components/table/table-renderer.vue +1 -1
  144. package/dist/runtime/table/components/table/table-row-actions-control.vue +10 -6
  145. package/dist/runtime/table/components/table/table-selection-control.d.vue.ts +5 -1
  146. package/dist/runtime/table/components/table/table-selection-control.vue +17 -4
  147. package/dist/runtime/table/components/table/table-selection-control.vue.d.ts +5 -1
  148. package/dist/runtime/table/components/table/table-tree-control.vue +2 -1
  149. package/dist/runtime/table/components/table/table-tree-header.vue +0 -1
  150. package/dist/runtime/table/composables/use-table-api.d.ts +2 -0
  151. package/dist/runtime/table/composables/use-table-api.js +8 -0
  152. package/dist/runtime/table/composables/use-table-columns.d.ts +2 -0
  153. package/dist/runtime/table/composables/use-table-controls.d.ts +6 -0
  154. package/dist/runtime/table/composables/use-table-controls.js +6 -3
  155. package/dist/runtime/table/composables/use-table-internals.d.ts +33 -0
  156. package/dist/runtime/table/composables/use-table-internals.js +9 -0
  157. package/dist/runtime/table/composables/use-table-row-labels.d.ts +16 -0
  158. package/dist/runtime/table/composables/use-table-row-labels.js +22 -0
  159. package/dist/runtime/table/composables/use-table-row-summary.d.ts +18 -0
  160. package/dist/runtime/table/composables/use-table-row-summary.js +41 -0
  161. package/dist/runtime/table/constants/mobile.d.ts +79 -0
  162. package/dist/runtime/table/constants/mobile.js +17 -0
  163. package/dist/runtime/table/constants/summary.d.ts +67 -0
  164. package/dist/runtime/table/constants/summary.js +31 -0
  165. package/dist/runtime/table/types/data-list.d.ts +58 -8
  166. package/dist/runtime/table/types/layout.d.ts +73 -0
  167. package/dist/runtime/table/types/remote-options.d.ts +4 -1
  168. package/dist/runtime/table/types/schema.d.ts +9 -1
  169. package/dist/runtime/table/types/table-api.d.ts +12 -0
  170. package/dist/runtime/table/utils/columns/defs.js +3 -1
  171. package/dist/runtime/table/utils/columns/render.d.ts +2 -0
  172. package/dist/runtime/table/utils/columns/render.js +2 -2
  173. package/dist/runtime/table/utils/data-list-ui.d.ts +1 -1
  174. package/dist/runtime/table/utils/data-list-ui.js +3 -1
  175. package/dist/runtime/table/utils/mobile-defaults.d.ts +7 -0
  176. package/dist/runtime/table/utils/mobile-defaults.js +25 -0
  177. package/dist/runtime/table/utils/remote-table-options.js +1 -1
  178. package/package.json +1 -1
package/dist/module.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "configKey": "nuxtUiTools",
6
6
  "docs": "https://ui.nuxt.com/docs/getting-started/installation/nuxt",
7
7
  "name": "nuxt-ui-tools",
8
- "version": "1.17.1",
8
+ "version": "1.19.0",
9
9
  "builder": {
10
10
  "@nuxt/module-builder": "1.0.3",
11
11
  "unbuild": "unknown"
package/dist/module.mjs CHANGED
@@ -35,6 +35,7 @@ function getPublicComponents(runtimeDir, options) {
35
35
  "ColumnPanel",
36
36
  "SortMenu",
37
37
  "LayoutSwitch",
38
+ "ViewToggle",
38
39
  "Content",
39
40
  "ErrorState",
40
41
  "Table",
@@ -3,7 +3,9 @@
3
3
  * badge while a form with `controls.dirtyCheck` is modified, and the page actions. It stays
4
4
  * pinned while the page scrolls on wide layouts, and sections scroll to just below it. The badge
5
5
  * keeps its place while the form is unmodified, hidden, and does not grow its line, so the first
6
- * edit neither shifts the page nor resizes the pinned header.
6
+ * edit neither shifts the page nor resizes the pinned header. The header spans the page, but on a
7
+ * page without a navigation its row takes the centered column of the body, so both share their
8
+ * edges.
7
9
  */
8
10
  type __VLS_Slots = {
9
11
  /** Before the heading, e.g. an avatar of the record. */
@@ -6,6 +6,7 @@ import { useUiToolsLocale } from "../../../i18n/use-locale";
6
6
  import { useFormPageContext } from "../../composables/use-form-page";
7
7
  import { useFormUi } from "../../composables/use-form-ui";
8
8
  import { getFormHeader } from "../../utils/overlay";
9
+ import { FORM_PAGE_COLUMN } from "../../utils/page-surface";
9
10
  import { isRecord } from "../../utils/path";
10
11
  import { resolveFormBoundaryText } from "../../utils/text";
11
12
  import { mergeFormUiClass } from "../../utils/ui";
@@ -20,6 +21,12 @@ const eyebrow = computed(() => headerText(header.value, "eyebrow"));
20
21
  const title = computed(() => headerText(header.value, "title"));
21
22
  const description = computed(() => headerText(header.value, "description"));
22
23
  const unsaved = computed(() => page.dirtyCheck.value && page.root.runtime.isDirty.value);
24
+ const ROW = "flex flex-col gap-3 @3xl/form-page:flex-row @3xl/form-page:items-center @3xl/form-page:justify-between @3xl/form-page:gap-5";
25
+ const PADDING_X = "px-4 @3xl/form-page:px-7";
26
+ const headerClass = computed(() => page.navigationVisible.value ? `${ROW} ${PADDING_X}` : "");
27
+ const rowClass = computed(
28
+ () => page.navigationVisible.value ? "contents" : `${ROW} ${PADDING_X} ${FORM_PAGE_COLUMN}`
29
+ );
23
30
  const element = useTemplateRef("element");
24
31
  useResizeObserver(element, () => {
25
32
  const node = element.value;
@@ -38,62 +45,65 @@ function headerText(config, key) {
38
45
  ref="element"
39
46
  :class="
40
47
  mergeFormUiClass(
41
- 'z-10 flex flex-col gap-3 border-b border-default bg-default px-4 py-4 @3xl/form-page:sticky @3xl/form-page:top-0 @3xl/form-page:flex-row @3xl/form-page:items-center @3xl/form-page:justify-between @3xl/form-page:gap-5 @3xl/form-page:px-7 @3xl/form-page:pt-[18px] @3xl/form-page:pb-4',
48
+ 'z-10 border-b border-default bg-default py-4 @3xl/form-page:sticky @3xl/form-page:top-0 @3xl/form-page:pt-[18px] @3xl/form-page:pb-4',
49
+ headerClass,
42
50
  pageUi?.header
43
51
  )
44
52
  "
45
53
  data-form-page-header
46
54
  >
47
- <div :class="mergeFormUiClass('flex min-w-0 items-center gap-3.5', pageUi?.headerContent)">
48
- <slot name="leading" />
49
- <div :class="mergeFormUiClass('grid min-w-0 gap-1.5', pageUi?.heading)">
50
- <p
51
- v-if="eyebrow || $slots.eyebrow"
52
- :class="
55
+ <div :class="mergeFormUiClass(rowClass, pageUi?.headerRow)" data-form-page-header-row>
56
+ <div :class="mergeFormUiClass('flex min-w-0 items-center gap-3.5', pageUi?.headerContent)">
57
+ <slot name="leading" />
58
+ <div :class="mergeFormUiClass('grid min-w-0 gap-1.5', pageUi?.heading)">
59
+ <p
60
+ v-if="eyebrow || $slots.eyebrow"
61
+ :class="
53
62
  mergeFormUiClass(
54
63
  'text-[11px] font-semibold tracking-[0.08em] text-dimmed uppercase',
55
64
  pageUi?.eyebrow
56
65
  )
57
66
  "
58
- >
59
- <slot name="eyebrow">{{ eyebrow }}</slot>
60
- </p>
61
- <h1
62
- v-if="title || $slots.title"
63
- :class="
67
+ >
68
+ <slot name="eyebrow">{{ eyebrow }}</slot>
69
+ </p>
70
+ <h1
71
+ v-if="title || $slots.title"
72
+ :class="
64
73
  mergeFormUiClass(
65
74
  'm-0 truncate text-[22px] leading-[1.1] font-semibold tracking-[-0.025em] text-highlighted @3xl/form-page:text-[28px]',
66
75
  pageUi?.title
67
76
  )
68
77
  "
69
- >
70
- <slot name="title">{{ title }}</slot>
71
- </h1>
72
- <div
73
- v-if="description || page.dirtyCheck.value || $slots.description"
74
- :class="
78
+ >
79
+ <slot name="title">{{ title }}</slot>
80
+ </h1>
81
+ <div
82
+ v-if="description || page.dirtyCheck.value || $slots.description"
83
+ :class="
75
84
  mergeFormUiClass(
76
85
  'flex flex-wrap items-center gap-2.5 text-[13.5px] text-muted',
77
86
  pageUi?.meta
78
87
  )
79
88
  "
80
- >
81
- <slot name="description">
82
- <span v-if="description">{{ description }}</span>
83
- </slot>
84
- <UBadge
85
- v-if="page.dirtyCheck.value"
86
- color="warning"
87
- variant="soft"
88
- :label="t('form.page.unsavedChanges')"
89
- :class="mergeFormUiClass(unsaved ? '-my-1' : '-my-1 invisible', pageUi?.unsaved)"
90
- :data-form-page-unsaved="unsaved ? '' : void 0"
91
- />
89
+ >
90
+ <slot name="description">
91
+ <span v-if="description">{{ description }}</span>
92
+ </slot>
93
+ <UBadge
94
+ v-if="page.dirtyCheck.value"
95
+ color="warning"
96
+ variant="soft"
97
+ :label="t('form.page.unsavedChanges')"
98
+ :class="mergeFormUiClass(unsaved ? '-my-1' : '-my-1 invisible', pageUi?.unsaved)"
99
+ :data-form-page-unsaved="unsaved ? '' : void 0"
100
+ />
101
+ </div>
92
102
  </div>
93
103
  </div>
104
+ <slot name="actions">
105
+ <FormPageActions />
106
+ </slot>
94
107
  </div>
95
- <slot name="actions">
96
- <FormPageActions />
97
- </slot>
98
108
  </header>
99
109
  </template>
@@ -3,7 +3,9 @@
3
3
  * badge while a form with `controls.dirtyCheck` is modified, and the page actions. It stays
4
4
  * pinned while the page scrolls on wide layouts, and sections scroll to just below it. The badge
5
5
  * keeps its place while the form is unmodified, hidden, and does not grow its line, so the first
6
- * edit neither shifts the page nor resizes the pinned header.
6
+ * edit neither shifts the page nor resizes the pinned header. The header spans the page, but on a
7
+ * page without a navigation its row takes the centered column of the body, so both share their
8
+ * edges.
7
9
  */
8
10
  type __VLS_Slots = {
9
11
  /** Before the heading, e.g. an avatar of the record. */
@@ -1,11 +1,13 @@
1
1
  import type { FormPageSectionState } from '../../types/index.js';
2
2
  /**
3
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.
4
+ * An entry shows whether its section is complete, to fix, to complete, or optional with nothing
5
+ * entered (a dashed circle and an "Optional" tag, never "to complete"), and, with
6
+ * `controls.dirtyCheck`, a dot on modified sections. The footer sums up the sections left to
7
+ * complete, which leaves out untouched optional ones, or the modified ones with dirty checking.
7
8
  *
8
9
  * From 768px of page width it is a pinned column; below, a row of chips that scrolls sideways.
10
+ * It renders nothing while the page has fewer visible sections than `navigation.minSections`.
9
11
  */
10
12
  type __VLS_Slots = {
11
13
  /** Replaces the heading, which comes from the schema `navigation.title`. */
@@ -73,6 +73,7 @@ function select(event, key) {
73
73
 
74
74
  <template>
75
75
  <nav
76
+ v-if="page.navigationVisible.value"
76
77
  :aria-labelledby="page.navigationTitle.value || $slots.title ? titleId : void 0"
77
78
  :aria-label="page.navigationTitle.value || $slots.title ? void 0 : t('form.page.navigation')"
78
79
  :style="{ '--nut-form-page-header': `${page.stickyOffset.value}px` }"
@@ -138,7 +139,7 @@ function select(event, key) {
138
139
  aria-hidden="true"
139
140
  :class="
140
141
  mergeFormUiClass(
141
- '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',
142
+ '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=optional]:border-dashed 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',
142
143
  pageUi?.navigationIndicator
143
144
  )
144
145
  "
@@ -182,15 +183,17 @@ function select(event, key) {
182
183
  data-form-page-dirty
183
184
  />
184
185
  <span
185
- v-else-if="section.optional && section.status !== 'complete'"
186
+ v-else-if="section.status === 'optional'"
187
+ aria-hidden="true"
186
188
  :class="
187
189
  mergeFormUiClass(
188
190
  'hidden shrink-0 text-[11px] font-normal text-dimmed @3xl/form-page:inline',
189
191
  pageUi?.navigationOptional
190
192
  )
191
193
  "
194
+ data-form-page-optional
192
195
  >
193
- {{ t("form.page.optional") }}
196
+ {{ t("form.page.status.optional") }}
194
197
  </span>
195
198
  </a>
196
199
  </slot>
@@ -1,11 +1,13 @@
1
1
  import type { FormPageSectionState } from '../../types/index.js';
2
2
  /**
3
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.
4
+ * An entry shows whether its section is complete, to fix, to complete, or optional with nothing
5
+ * entered (a dashed circle and an "Optional" tag, never "to complete"), and, with
6
+ * `controls.dirtyCheck`, a dot on modified sections. The footer sums up the sections left to
7
+ * complete, which leaves out untouched optional ones, or the modified ones with dirty checking.
7
8
  *
8
9
  * From 768px of page width it is a pinned column; below, a row of chips that scrolls sideways.
10
+ * It renders nothing while the page has fewer visible sections than `navigation.minSections`.
9
11
  */
10
12
  type __VLS_Slots = {
11
13
  /** Replaces the heading, which comes from the schema `navigation.title`. */
@@ -50,7 +50,7 @@ onBeforeUnmount(() => unregister?.());
50
50
  }"
51
51
  :class="
52
52
  mergeFormUiClass(
53
- 'flex min-w-0 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 data-[dirty]:ring-inset',
53
+ `flex min-w-0 flex-col ${page.surfaceClasses.value.section}`,
54
54
  pageUi?.section
55
55
  )
56
56
  "
@@ -77,7 +77,7 @@ onBeforeUnmount(() => unregister?.());
77
77
  v-if="section.description"
78
78
  :class="
79
79
  mergeFormUiClass(
80
- 'order-last m-0 basis-full text-[13px] text-muted @3xl/form-page:order-none @3xl/form-page:basis-auto',
80
+ `m-0 text-[13px] text-muted ${page.surfaceClasses.value.sectionDescription}`,
81
81
  pageUi?.sectionDescription
82
82
  )
83
83
  "
@@ -85,11 +85,12 @@ onBeforeUnmount(() => unregister?.());
85
85
  {{ section.description }}
86
86
  </p>
87
87
  <UBadge
88
- v-if="section.optional"
88
+ v-if="section.status === 'optional'"
89
89
  color="neutral"
90
90
  variant="soft"
91
- :label="t('form.page.optional')"
92
- :class="mergeFormUiClass('capitalize', pageUi?.sectionOptional)"
91
+ :label="t('form.page.status.optional')"
92
+ :class="pageUi?.sectionOptional"
93
+ data-form-page-optional
93
94
  />
94
95
  <div
95
96
  v-if="page.dirtyCheck.value || $slots.actions"
@@ -1,8 +1,9 @@
1
1
  import type { FormPageSectionState } from '../../types/index.js';
2
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.
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, or
5
+ * plain blocks with the `page.surface` UI option. Skeletons stand in while the schema context
6
+ * loads.
6
7
  */
7
8
  type __VLS_Slots = {
8
9
  /** Extra buttons in a section header, before its reset button. */
@@ -18,7 +18,12 @@ const { contextError, contextLoading, contextPending, refreshContext } = page.ro
18
18
 
19
19
  <template>
20
20
  <div
21
- :class="mergeFormUiClass('flex min-w-0 flex-col gap-[18px]', pageUi?.sections)"
21
+ :class="
22
+ mergeFormUiClass(
23
+ `flex min-w-0 flex-col ${page.surfaceClasses.value.sections}`,
24
+ pageUi?.sections
25
+ )
26
+ "
22
27
  data-form-page-sections
23
28
  >
24
29
  <UAlert
@@ -48,7 +53,7 @@ const { contextError, contextLoading, contextPending, refreshContext } = page.ro
48
53
  :key="section.key"
49
54
  :class="
50
55
  mergeFormUiClass(
51
- 'grid gap-4 rounded-xl border border-default bg-default px-6 pt-[22px] pb-6',
56
+ `grid gap-4 ${page.surfaceClasses.value.sectionSkeleton}`,
52
57
  pageUi?.sectionSkeleton
53
58
  )
54
59
  "
@@ -1,8 +1,9 @@
1
1
  import type { FormPageSectionState } from '../../types/index.js';
2
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.
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, or
5
+ * plain blocks with the `page.surface` UI option. Skeletons stand in while the schema context
6
+ * loads.
6
7
  */
7
8
  type __VLS_Slots = {
8
9
  /** Extra buttons in a section header, before its reset button. */
@@ -2,6 +2,8 @@ import type { FormPageSectionState, FormRendererController, FormUiConfig, FormVa
2
2
  /**
3
3
  * A form laid out as a page: a header with the title and the actions, a navigation that follows
4
4
  * the section in view, and one card per section. Render a schema from `defineFormPageSchema`.
5
+ * The `page.surface` UI option draws the sections as `plain` blocks instead of cards, and the
6
+ * schema `navigation.minSections` leaves the navigation out of a page with few sections.
5
7
  *
6
8
  * The page scrolls on its own, so give it a height (`h-full` in a layout that sizes its main
7
9
  * area). Put parts in the default slot to compose another layout: `FormPageHeader`,
@@ -2,6 +2,7 @@
2
2
  import { computed, useTemplateRef } from "vue";
3
3
  import { provideFormPage, useFormPage } from "../../composables/use-form-page";
4
4
  import { useFormRoot } from "../../composables/use-form-root";
5
+ import { FORM_PAGE_COLUMN } from "../../utils/page-surface";
5
6
  import { mergeFormUiClass } from "../../utils/ui";
6
7
  import FormPageHeader from "./form-page-header.vue";
7
8
  import FormPageNavigation from "./form-page-navigation.vue";
@@ -30,6 +31,10 @@ const root = useFormRoot({
30
31
  });
31
32
  const page = provideFormPage(useFormPage({ element: () => element.value, hash: () => hash, root }));
32
33
  const pageUi = computed(() => root.formUi.ui.value.page?.ui);
34
+ const BODY_UI = {
35
+ beside: "grid gap-4 px-4 pt-4 pb-16 @3xl/form-page:grid-cols-[200px_minmax(0,1fr)] @3xl/form-page:items-start @3xl/form-page:gap-7 @3xl/form-page:px-7 @3xl/form-page:pt-6 @3xl/form-page:pb-20 @5xl/form-page:grid-cols-[230px_minmax(0,760px)]",
36
+ single: `${FORM_PAGE_COLUMN} grid gap-4 px-4 pt-4 pb-16 @3xl/form-page:gap-7 @3xl/form-page:px-7 @3xl/form-page:pt-6 @3xl/form-page:pb-20`
37
+ };
33
38
  </script>
34
39
 
35
40
  <template>
@@ -37,8 +42,12 @@ const pageUi = computed(() => root.formUi.ui.value.page?.ui);
37
42
  ref="element"
38
43
  novalidate
39
44
  :class="
40
- mergeFormUiClass('@container/form-page h-full min-w-0 overflow-y-auto bg-muted', pageUi?.root)
45
+ mergeFormUiClass(
46
+ `@container/form-page h-full min-w-0 overflow-y-auto ${page.surfaceClasses.value.root}`,
47
+ pageUi?.root
48
+ )
41
49
  "
50
+ :data-surface="page.surface.value"
42
51
  data-form-page
43
52
  @submit.prevent="root.submit"
44
53
  >
@@ -63,7 +72,7 @@ const pageUi = computed(() => root.formUi.ui.value.page?.ui);
63
72
  <div
64
73
  :class="
65
74
  mergeFormUiClass(
66
- 'grid gap-4 px-4 pt-4 pb-16 @3xl/form-page:grid-cols-[200px_minmax(0,1fr)] @3xl/form-page:items-start @3xl/form-page:gap-7 @3xl/form-page:px-7 @3xl/form-page:pt-6 @3xl/form-page:pb-20 @5xl/form-page:grid-cols-[230px_minmax(0,760px)]',
75
+ page.navigationVisible.value ? BODY_UI.beside : BODY_UI.single,
67
76
  pageUi?.body
68
77
  )
69
78
  "
@@ -2,6 +2,8 @@ import type { FormPageSectionState, FormRendererController, FormUiConfig, FormVa
2
2
  /**
3
3
  * A form laid out as a page: a header with the title and the actions, a navigation that follows
4
4
  * the section in view, and one card per section. Render a schema from `defineFormPageSchema`.
5
+ * The `page.surface` UI option draws the sections as `plain` blocks instead of cards, and the
6
+ * schema `navigation.minSections` leaves the navigation out of a page with few sections.
5
7
  *
6
8
  * The page scrolls on its own, so give it a height (`h-full` in a layout that sizes its main
7
9
  * area). Put parts in the default slot to compose another layout: `FormPageHeader`,
@@ -1,6 +1,7 @@
1
1
  <script setup>
2
2
  import UAlert from "@nuxt/ui/components/Alert.vue";
3
3
  import { computed, ref, useId, watchEffect } from "vue";
4
+ import { useUiToolsLocale } from "../../../i18n/use-locale";
4
5
  import { useFieldEffects } from "../../composables/use-field-effects";
5
6
  import {
6
7
  provideFormFieldBare,
@@ -67,6 +68,7 @@ const props = defineProps({
67
68
  controlAttrs: { type: null, required: false }
68
69
  });
69
70
  const form = useFormRuntimeContext();
71
+ const { t } = useUiToolsLocale();
70
72
  const element = ref(null);
71
73
  const bare = computed(() => props.bare === true);
72
74
  const controlId = useId();
@@ -208,8 +210,8 @@ function resolveControlLabel(controlField) {
208
210
  color="neutral"
209
211
  variant="soft"
210
212
  icon="i-lucide-construction"
211
- title="Unsupported field"
212
- :description="`The ${field.type.value} field renderer is not implemented in this slice.`"
213
+ :title="t('form.fields.unsupported.title')"
214
+ :description="t('form.fields.unsupported.description', { type: field.type.value })"
213
215
  />
214
216
  </template>
215
217
 
@@ -225,8 +227,8 @@ function resolveControlLabel(controlField) {
225
227
  color="neutral"
226
228
  variant="soft"
227
229
  icon="i-lucide-construction"
228
- title="Unsupported field"
229
- :description="`The ${field.type.value} field renderer is not implemented in this slice.`"
230
+ :title="t('form.fields.unsupported.title')"
231
+ :description="t('form.fields.unsupported.description', { type: field.type.value })"
230
232
  />
231
233
  </div>
232
234
  </template>
@@ -9,12 +9,12 @@ declare var __VLS_1: {
9
9
  description: string | undefined;
10
10
  hint: string | undefined;
11
11
  required: any;
12
- }, __VLS_61: {
12
+ }, __VLS_67: {
13
13
  label: string | undefined;
14
14
  description: string | undefined;
15
15
  hint: string | undefined;
16
16
  required: any;
17
- }, __VLS_75: {
17
+ }, __VLS_81: {
18
18
  label: string | undefined;
19
19
  description: string | undefined;
20
20
  hint: string | undefined;
@@ -23,9 +23,9 @@ declare var __VLS_1: {
23
23
  type __VLS_Slots = {} & {
24
24
  default?: (props: typeof __VLS_1) => any;
25
25
  } & {
26
- default?: (props: typeof __VLS_61) => any;
26
+ default?: (props: typeof __VLS_67) => any;
27
27
  } & {
28
- default?: (props: typeof __VLS_75) => any;
28
+ default?: (props: typeof __VLS_81) => any;
29
29
  };
30
30
  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>;
31
31
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -11,6 +11,7 @@ import { useFormFieldBare } from "../../composables/use-form-field-chrome";
11
11
  import { useFormRuntimeContext } from "../../composables/use-form-runtime";
12
12
  import { useFormUi } from "../../composables/use-form-ui";
13
13
  import { getSchemaDirtyCheck } from "../../utils/controls";
14
+ import { resolveFieldDisabledReason } from "../../utils/disabled";
14
15
  import { createFormFieldInstance } from "../../utils/field-instance";
15
16
  import { isFunction, isNumber, isString } from "../../utils/predicate";
16
17
  import { resolveRequired } from "../../utils/state";
@@ -85,6 +86,9 @@ const help = computed(
85
86
  () => field.value.capability.has("hint") && "help" in props.field ? resolveFieldText(props.field.help, params.value) : void 0
86
87
  );
87
88
  const shellHelp = computed(() => props.inlineLabel ? void 0 : help.value);
89
+ const disabledReason = computed(
90
+ () => field.value.capability.has("hint") ? resolveFieldDisabledReason(props.field, params.value) : void 0
91
+ );
88
92
  const dirty = computed(
89
93
  () => "dirtyCheck" in props.field && (props.field.dirtyCheck === true || getSchemaDirtyCheck(form.schema.value)) && form.dirtyPaths.value.includes(props.path.join("."))
90
94
  );
@@ -130,7 +134,7 @@ function renderLabelExtra() {
130
134
  :description="shellDescription"
131
135
  :hint="shellLabelExtra === void 0 || shellLabelExtra === null ? shellHint : void 0"
132
136
  :error="error"
133
- :help="shellHelp"
137
+ :help="shellHelp ?? disabledReason"
134
138
  :required="required"
135
139
  :size="formUi.controlSize.value"
136
140
  :orientation="orientation"
@@ -183,6 +187,26 @@ function renderLabelExtra() {
183
187
  <component :is="renderLabelExtra" />
184
188
  </span>
185
189
  </template>
190
+ <template v-if="disabledReason" #help>
191
+ <span v-if="shellHelp" class="block">{{ shellHelp }}</span>
192
+ <span
193
+ :class="
194
+ mergeFormUiClass(
195
+ 'flex items-start gap-1.5',
196
+ shellHelp ? 'mt-1' : void 0,
197
+ fieldUi?.disabledReason
198
+ )
199
+ "
200
+ data-form-disabled-reason=""
201
+ >
202
+ <UIcon
203
+ name="i-lucide-lock"
204
+ aria-hidden="true"
205
+ :class="mergeFormUiClass('mt-0.5 size-3.5 shrink-0', fieldUi?.disabledReasonIcon)"
206
+ />
207
+ <span>{{ disabledReason }}</span>
208
+ </span>
209
+ </template>
186
210
  <UCollapsible
187
211
  v-if="collapsible"
188
212
  v-model:open="open"
@@ -195,7 +219,7 @@ function renderLabelExtra() {
195
219
  :icon="open ? 'i-lucide-chevron-down' : 'i-lucide-chevron-right'"
196
220
  :class="mergeFormUiClass('justify-self-start', fieldUi?.collapseTrigger)"
197
221
  >
198
- {{ open ? "Collapse" : "Expand" }}
222
+ {{ open ? t("form.fields.shell.collapse") : t("form.fields.shell.expand") }}
199
223
  </UButton>
200
224
  <template #content>
201
225
  <div :class="mergeFormUiClass('flex items-start gap-2', fieldUi?.body)">
@@ -216,7 +240,7 @@ function renderLabelExtra() {
216
240
  color="neutral"
217
241
  variant="ghost"
218
242
  size="xs"
219
- aria-label="Reset field"
243
+ :aria-label="t('form.fields.shell.reset')"
220
244
  :class="fieldUi?.reset"
221
245
  @click="resetField"
222
246
  />
@@ -239,7 +263,7 @@ function renderLabelExtra() {
239
263
  color="neutral"
240
264
  variant="ghost"
241
265
  size="xs"
242
- aria-label="Reset field"
266
+ :aria-label="t('form.fields.shell.reset')"
243
267
  :class="fieldUi?.reset"
244
268
  @click="resetField"
245
269
  />
@@ -9,12 +9,12 @@ declare var __VLS_1: {
9
9
  description: string | undefined;
10
10
  hint: string | undefined;
11
11
  required: any;
12
- }, __VLS_61: {
12
+ }, __VLS_67: {
13
13
  label: string | undefined;
14
14
  description: string | undefined;
15
15
  hint: string | undefined;
16
16
  required: any;
17
- }, __VLS_75: {
17
+ }, __VLS_81: {
18
18
  label: string | undefined;
19
19
  description: string | undefined;
20
20
  hint: string | undefined;
@@ -23,9 +23,9 @@ declare var __VLS_1: {
23
23
  type __VLS_Slots = {} & {
24
24
  default?: (props: typeof __VLS_1) => any;
25
25
  } & {
26
- default?: (props: typeof __VLS_61) => any;
26
+ default?: (props: typeof __VLS_67) => any;
27
27
  } & {
28
- default?: (props: typeof __VLS_75) => any;
28
+ default?: (props: typeof __VLS_81) => any;
29
29
  };
30
30
  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>;
31
31
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -143,7 +143,7 @@ defineExpose({
143
143
  validate: runtime.validate
144
144
  });
145
145
  function stepLabel(index) {
146
- return runtime.steps.value[index]?.label ?? `Step ${index + 1}`;
146
+ return runtime.steps.value[index]?.label ?? t("form.steps.fallback", { number: index + 1 });
147
147
  }
148
148
  function getHeaderText(headerConfig, key) {
149
149
  if (!isRecord(headerConfig)) {
@@ -245,7 +245,7 @@ function getSchemaShowStepper(schema) {
245
245
  :size="formUi.controlSize.value"
246
246
  icon="i-lucide-x"
247
247
  :disabled="runtime.actionPending.value !== null"
248
- aria-label="Close form"
248
+ :aria-label="t('form.actions.closeButton')"
249
249
  :class="formUi.ui.value.root?.ui?.close"
250
250
  @click="cancel"
251
251
  />
@@ -0,0 +1,16 @@
1
+ import type { VNodeChild } from 'vue';
2
+ import type { FormOptionRenderContext, ResolvedFormOption } from '../../types/index.js';
3
+ /**
4
+ * Label of an option in a radio or checkbox list: the leading visual, the label, and the suffix at
5
+ * the end of the row. The description stays with the Nuxt UI group, under the row. An option with a
6
+ * `renderOption` function shows what it returns instead, and a plain option shows its label as it
7
+ * always did.
8
+ */
9
+ type __VLS_Props = {
10
+ option: ResolvedFormOption;
11
+ selected?: boolean;
12
+ render?: (context: FormOptionRenderContext) => VNodeChild;
13
+ };
14
+ 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>;
15
+ declare const _default: typeof __VLS_export;
16
+ export default _default;
@@ -0,0 +1,37 @@
1
+ <script setup>
2
+ import { resolveOptionLeading } from "../../utils/options";
3
+ import FormOptionLeading from "./form-option-leading.vue";
4
+ import FormOptionRender from "./form-option-render";
5
+ import FormOptionSuffix from "./form-option-suffix.vue";
6
+ const props = defineProps({
7
+ option: { type: Object, required: true },
8
+ selected: { type: Boolean, required: false },
9
+ render: { type: Function, required: false }
10
+ });
11
+ function content() {
12
+ const { item } = props.option;
13
+ if (item === void 0) return null;
14
+ return props.render?.({
15
+ disabled: props.option.disabled === true,
16
+ item,
17
+ option: props.option,
18
+ selected: props.selected === true
19
+ });
20
+ }
21
+ </script>
22
+
23
+ <template>
24
+ <span v-if="render" class="block whitespace-normal" data-form-option-custom>
25
+ <FormOptionRender :content="content" />
26
+ </span>
27
+ <span
28
+ v-else-if="resolveOptionLeading(option) || option.suffix"
29
+ class="flex min-w-0 items-center gap-2"
30
+ data-form-option-row
31
+ >
32
+ <FormOptionLeading :option="option" />
33
+ <span class="min-w-0 flex-1 break-words">{{ option.label }}</span>
34
+ <FormOptionSuffix :option="option" class="ps-2 font-normal" />
35
+ </span>
36
+ <template v-else>{{ option.label }}</template>
37
+ </template>