nuxt-ui-tools 1.17.0 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) 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-navigation.d.vue.ts +5 -3
  4. package/dist/runtime/form/components/page/form-page-navigation.vue +6 -3
  5. package/dist/runtime/form/components/page/form-page-navigation.vue.d.ts +5 -3
  6. package/dist/runtime/form/components/page/form-page-section.vue +6 -5
  7. package/dist/runtime/form/components/page/form-page-sections.d.vue.ts +4 -3
  8. package/dist/runtime/form/components/page/form-page-sections.vue +7 -2
  9. package/dist/runtime/form/components/page/form-page-sections.vue.d.ts +4 -3
  10. package/dist/runtime/form/components/page/form-page.d.vue.ts +2 -0
  11. package/dist/runtime/form/components/page/form-page.vue +10 -2
  12. package/dist/runtime/form/components/page/form-page.vue.d.ts +2 -0
  13. package/dist/runtime/form/components/renderer/form-field-renderer.vue +6 -4
  14. package/dist/runtime/form/components/renderer/form-field-shell.d.vue.ts +4 -4
  15. package/dist/runtime/form/components/renderer/form-field-shell.vue +28 -4
  16. package/dist/runtime/form/components/renderer/form-field-shell.vue.d.ts +4 -4
  17. package/dist/runtime/form/components/root/form.vue +2 -2
  18. package/dist/runtime/form/composables/use-field-control.d.ts +1 -1
  19. package/dist/runtime/form/composables/use-field-control.js +9 -21
  20. package/dist/runtime/form/composables/use-field-options.js +8 -4
  21. package/dist/runtime/form/composables/use-form-page.d.ts +11 -1
  22. package/dist/runtime/form/composables/use-form-page.js +12 -6
  23. package/dist/runtime/form/composables/use-form-runtime.js +1 -1
  24. package/dist/runtime/form/fields/array-primitive/types.d.ts +1 -1
  25. package/dist/runtime/form/fields/checkbox-card/component.vue +54 -28
  26. package/dist/runtime/form/fields/checkbox-card/config.d.ts +1 -0
  27. package/dist/runtime/form/fields/checkbox-card/config.js +1 -1
  28. package/dist/runtime/form/fields/checkbox-card/types.d.ts +3 -3
  29. package/dist/runtime/form/fields/choice-card/choice-card-label.d.vue.ts +7 -0
  30. package/dist/runtime/form/fields/choice-card/choice-card-label.vue +27 -1
  31. package/dist/runtime/form/fields/choice-card/choice-card-label.vue.d.ts +7 -0
  32. package/dist/runtime/form/fields/choice-card/types.d.ts +54 -1
  33. package/dist/runtime/form/fields/choice-card/use-choice-card.d.ts +7 -3
  34. package/dist/runtime/form/fields/choice-card/use-choice-card.js +37 -15
  35. package/dist/runtime/form/fields/choice-card/utils.d.ts +27 -0
  36. package/dist/runtime/form/fields/choice-card/utils.js +17 -0
  37. package/dist/runtime/form/fields/hierarchy/component.vue +3 -2
  38. package/dist/runtime/form/fields/index.d.ts +2 -0
  39. package/dist/runtime/form/fields/radio-card/component.vue +58 -28
  40. package/dist/runtime/form/fields/radio-card/config.d.ts +1 -0
  41. package/dist/runtime/form/fields/radio-card/config.js +1 -1
  42. package/dist/runtime/form/fields/radio-card/types.d.ts +3 -3
  43. package/dist/runtime/form/types/field-base.d.ts +6 -0
  44. package/dist/runtime/form/types/field-kind.d.ts +3 -1
  45. package/dist/runtime/form/types/field.d.ts +1 -0
  46. package/dist/runtime/form/types/page.d.ts +20 -8
  47. package/dist/runtime/form/types/ui.d.ts +24 -4
  48. package/dist/runtime/form/utils/disabled.d.ts +11 -0
  49. package/dist/runtime/form/utils/disabled.js +11 -0
  50. package/dist/runtime/form/utils/field-instance.js +3 -0
  51. package/dist/runtime/form/utils/options.d.ts +22 -1
  52. package/dist/runtime/form/utils/options.js +15 -0
  53. package/dist/runtime/form/utils/page-surface.d.ts +21 -0
  54. package/dist/runtime/form/utils/page-surface.js +27 -0
  55. package/dist/runtime/form/utils/page.d.ts +21 -1
  56. package/dist/runtime/form/utils/page.js +24 -2
  57. package/dist/runtime/i18n/locales/en.js +28 -8
  58. package/dist/runtime/i18n/locales/fr.js +28 -8
  59. package/dist/runtime/i18n/types.d.ts +46 -7
  60. package/dist/runtime/table/components/actions/row-actions.d.vue.ts +2 -0
  61. package/dist/runtime/table/components/actions/row-actions.vue +8 -3
  62. package/dist/runtime/table/components/actions/row-actions.vue.d.ts +2 -0
  63. package/dist/runtime/table/components/data-list/data-list-content.vue +7 -1
  64. package/dist/runtime/table/components/data-list/data-list-default.d.vue.ts +16 -16
  65. package/dist/runtime/table/components/data-list/data-list-default.vue +3 -0
  66. package/dist/runtime/table/components/data-list/data-list-default.vue.d.ts +16 -16
  67. package/dist/runtime/table/components/data-list/data-list-layout-switch.vue +7 -9
  68. package/dist/runtime/table/components/data-list/data-list-root.vue +5 -6
  69. package/dist/runtime/table/components/data-list/data-list-sort-menu.vue +3 -1
  70. package/dist/runtime/table/components/data-list/data-list-table.d.vue.ts +8 -1
  71. package/dist/runtime/table/components/data-list/data-list-table.vue +18 -1
  72. package/dist/runtime/table/components/data-list/data-list-table.vue.d.ts +8 -1
  73. package/dist/runtime/table/components/data-list/data-list-view-toggle.d.vue.ts +25 -0
  74. package/dist/runtime/table/components/data-list/data-list-view-toggle.vue +57 -0
  75. package/dist/runtime/table/components/data-list/data-list-view-toggle.vue.d.ts +25 -0
  76. package/dist/runtime/table/components/data-list/sort-menu-panel.vue +1 -1
  77. package/dist/runtime/table/components/layout/table-footer.d.vue.ts +6 -4
  78. package/dist/runtime/table/components/layout/table-footer.vue +80 -3
  79. package/dist/runtime/table/components/layout/table-footer.vue.d.ts +6 -4
  80. package/dist/runtime/table/components/summary/summary-content.d.ts +13 -0
  81. package/dist/runtime/table/components/summary/summary-content.js +5 -0
  82. package/dist/runtime/table/components/summary/summary-renderer.d.vue.ts +31 -0
  83. package/dist/runtime/table/components/summary/summary-renderer.vue +282 -0
  84. package/dist/runtime/table/components/summary/summary-renderer.vue.d.ts +31 -0
  85. package/dist/runtime/table/components/summary/summary-row.d.vue.ts +19 -0
  86. package/dist/runtime/table/components/summary/summary-row.vue +102 -0
  87. package/dist/runtime/table/components/summary/summary-row.vue.d.ts +19 -0
  88. package/dist/runtime/table/components/table/table-empty-state.vue +39 -9
  89. package/dist/runtime/table/components/table/table-renderer.vue +1 -1
  90. package/dist/runtime/table/components/table/table-row-actions-control.vue +10 -6
  91. package/dist/runtime/table/components/table/table-selection-control.d.vue.ts +5 -1
  92. package/dist/runtime/table/components/table/table-selection-control.vue +17 -4
  93. package/dist/runtime/table/components/table/table-selection-control.vue.d.ts +5 -1
  94. package/dist/runtime/table/components/table/table-tree-control.vue +2 -1
  95. package/dist/runtime/table/components/table/table-tree-header.vue +0 -1
  96. package/dist/runtime/table/composables/use-table-api.d.ts +2 -0
  97. package/dist/runtime/table/composables/use-table-api.js +8 -0
  98. package/dist/runtime/table/composables/use-table-columns.d.ts +2 -0
  99. package/dist/runtime/table/composables/use-table-controls.d.ts +6 -0
  100. package/dist/runtime/table/composables/use-table-controls.js +6 -3
  101. package/dist/runtime/table/composables/use-table-internals.d.ts +33 -0
  102. package/dist/runtime/table/composables/use-table-internals.js +9 -0
  103. package/dist/runtime/table/composables/use-table-row-labels.d.ts +16 -0
  104. package/dist/runtime/table/composables/use-table-row-labels.js +22 -0
  105. package/dist/runtime/table/composables/use-table-row-summary.d.ts +18 -0
  106. package/dist/runtime/table/composables/use-table-row-summary.js +41 -0
  107. package/dist/runtime/table/constants/mobile.d.ts +79 -0
  108. package/dist/runtime/table/constants/mobile.js +17 -0
  109. package/dist/runtime/table/constants/summary.d.ts +67 -0
  110. package/dist/runtime/table/constants/summary.js +31 -0
  111. package/dist/runtime/table/types/data-list.d.ts +58 -8
  112. package/dist/runtime/table/types/layout.d.ts +73 -0
  113. package/dist/runtime/table/types/schema.d.ts +9 -1
  114. package/dist/runtime/table/types/source.d.ts +6 -1
  115. package/dist/runtime/table/types/table-api.d.ts +12 -0
  116. package/dist/runtime/table/utils/columns/defs.js +3 -1
  117. package/dist/runtime/table/utils/columns/render.d.ts +2 -0
  118. package/dist/runtime/table/utils/columns/render.js +2 -2
  119. package/dist/runtime/table/utils/data-list-ui.js +3 -1
  120. package/dist/runtime/table/utils/mobile-defaults.d.ts +7 -0
  121. package/dist/runtime/table/utils/mobile-defaults.js +25 -0
  122. 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.0",
8
+ "version": "1.18.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",
@@ -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`,
@@ -30,6 +30,10 @@ const root = useFormRoot({
30
30
  });
31
31
  const page = provideFormPage(useFormPage({ element: () => element.value, hash: () => hash, root }));
32
32
  const pageUi = computed(() => root.formUi.ui.value.page?.ui);
33
+ const BODY_UI = {
34
+ 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)]",
35
+ single: "mx-auto grid w-full max-w-[816px] 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"
36
+ };
33
37
  </script>
34
38
 
35
39
  <template>
@@ -37,8 +41,12 @@ const pageUi = computed(() => root.formUi.ui.value.page?.ui);
37
41
  ref="element"
38
42
  novalidate
39
43
  :class="
40
- mergeFormUiClass('@container/form-page h-full min-w-0 overflow-y-auto bg-muted', pageUi?.root)
44
+ mergeFormUiClass(
45
+ `@container/form-page h-full min-w-0 overflow-y-auto ${page.surfaceClasses.value.root}`,
46
+ pageUi?.root
47
+ )
41
48
  "
49
+ :data-surface="page.surface.value"
42
50
  data-form-page
43
51
  @submit.prevent="root.submit"
44
52
  >
@@ -63,7 +71,7 @@ const pageUi = computed(() => root.formUi.ui.value.page?.ui);
63
71
  <div
64
72
  :class="
65
73
  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)]',
74
+ page.navigationVisible.value ? BODY_UI.beside : BODY_UI.single,
67
75
  pageUi?.body
68
76
  )
69
77
  "
@@ -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
  />
@@ -55,7 +55,7 @@ export declare function useFieldControl<TField extends FormField>(field: () => T
55
55
  setSearch: (term: string) => void;
56
56
  };
57
57
  params: import("vue").ComputedRef<import("../types/index.js").FormFieldCallbackParams<{}, import("../types/index.js").FormDependencies, unknown, unknown>>;
58
- placeholder: import("vue").ComputedRef<string>;
58
+ placeholder: import("vue").ComputedRef<string | undefined>;
59
59
  validationPending: import("vue").ComputedRef<boolean>;
60
60
  };
61
61
  export {};
@@ -1,10 +1,11 @@
1
1
  import { watchWithFilter } from "@vueuse/core";
2
2
  import { computed, onScopeDispose } from "vue";
3
- import { useUiToolsLocale } from "../../i18n/use-locale.js";
4
3
  import { isEqualFormValue } from "../utils/compare.js";
4
+ import { isFieldDisabledByCallback } from "../utils/disabled.js";
5
5
  import { createFormFieldInstance } from "../utils/field-instance.js";
6
6
  import { cloneFormValue } from "../utils/path.js";
7
- import { isFunction, isNumber, isObject, isString } from "../utils/predicate.js";
7
+ import { isFunction, isObject, isString } from "../utils/predicate.js";
8
+ import { resolveFieldText } from "../utils/text.js";
8
9
  import { mergeFormUiClass } from "../utils/ui.js";
9
10
  import { useFieldOptions } from "./use-field-options.js";
10
11
  import { useFormFieldControlAttrs } from "./use-form-field-chrome.js";
@@ -24,7 +25,6 @@ export function useResolvedFieldProps(field, path) {
24
25
  export function useFieldControl(field, path, controlOptions = {}) {
25
26
  const form = useFormRuntimeContext();
26
27
  const formUi = useFormUi();
27
- const { t } = useUiToolsLocale();
28
28
  const fieldControlAttrs = useFormFieldControlAttrs();
29
29
  const api = computed(() => form.getFieldApi(path(), field()));
30
30
  const params = computed(() => form.getFieldCallbackParams(path(), field()));
@@ -73,24 +73,12 @@ export function useFieldControl(field, path, controlOptions = {}) {
73
73
  const value = controlProps.value.size;
74
74
  return isFormControlSize(value) ? value : formUi.controlSize.value;
75
75
  });
76
- const disabled = computed(() => {
77
- const current = field();
78
- const value = Object.getOwnPropertyDescriptor(current, "disabled")?.value;
79
- const disabledByCallback = isFunction(value) ? value(params.value) === true : false;
80
- return disabledByCallback || form.actionPending.value !== null || options.disableOnLoading.value && options.loading.value;
81
- });
82
- const placeholder = computed(() => {
83
- const current = field();
84
- const value = Object.getOwnPropertyDescriptor(current, "placeholder")?.value;
85
- if (isFunction(value)) {
86
- const resolved = value(params.value);
87
- return isString(resolved) || isNumber(resolved) ? String(resolved) : t("form.fields.text.defaultPlaceholder");
88
- }
89
- if (isString(value) || isNumber(value)) {
90
- return String(value);
91
- }
92
- return t("form.fields.text.defaultPlaceholder");
93
- });
76
+ const disabled = computed(
77
+ () => isFieldDisabledByCallback(field(), params.value) || form.actionPending.value !== null || options.disableOnLoading.value && options.loading.value
78
+ );
79
+ const placeholder = computed(
80
+ () => resolveFieldText(Object.getOwnPropertyDescriptor(field(), "placeholder")?.value, params.value)
81
+ );
94
82
  let lastValue = cloneFormValue(form.getValue(path()));
95
83
  watchWithFilter(
96
84
  () => form.getValue(path()),
@@ -6,7 +6,9 @@ import {
6
6
  mergeResolvedOptions,
7
7
  normalizeOptionItem,
8
8
  normalizeOptionItems,
9
- normalizeOptionSelection
9
+ normalizeOptionSelection,
10
+ readOptionSource,
11
+ resolveOptionGroupKey
10
12
  } from "../utils/options.js";
11
13
  import { isRecord, relativePathSegments } from "../utils/path.js";
12
14
  import {
@@ -31,7 +33,10 @@ export function useFieldOptions(params) {
31
33
  const promiseResolution = shallowRef(null);
32
34
  const creating = ref(false);
33
35
  const remoteConfig = computed(() => resolveRemoteOptionConfig(params.field()));
34
- const optionKeys = computed(() => resolveOptionKeys(params.field()));
36
+ const optionKeys = computed(() => ({
37
+ ...resolveOptionKeys(params.field()),
38
+ group: resolveOptionGroupKey(params.field())
39
+ }));
35
40
  const remote = useRemoteFieldOptions({
36
41
  api: params.api,
37
42
  callbackParams: params.callbackParams,
@@ -389,8 +394,7 @@ function resolveOptionSource(field, params) {
389
394
  if (!createFormFieldInstance(field).capability.has("options")) {
390
395
  return [];
391
396
  }
392
- const options = Object.getOwnPropertyDescriptor(field, "options")?.value;
393
- const source = isRecord(options) && "source" in options ? options.source : options;
397
+ const source = readOptionSource(field);
394
398
  return isFunction(source) ? source(params) : source;
395
399
  }
396
400
  function emptyTrackedOptionSource() {
@@ -2,7 +2,8 @@ import type { FormPageSectionState } from '../types/index.js';
2
2
  import type { useFormRoot } from './use-form-root.js';
3
3
  /**
4
4
  * State shared by a form page and its parts: the visible sections and where each stands, the
5
- * section in view, the navigation summary, and the actions a part can take on a section.
5
+ * section in view, the navigation summary, whether the navigation shows, the surface the
6
+ * sections are drawn with, and the actions a part can take on a section.
6
7
  */
7
8
  export declare function useFormPage(params: {
8
9
  root: ReturnType<typeof useFormRoot>;
@@ -19,6 +20,7 @@ export declare function useFormPage(params: {
19
20
  }[]>;
20
21
  modified: import("vue").ComputedRef<number>;
21
22
  navigationTitle: import("vue").ComputedRef<string | undefined>;
23
+ navigationVisible: import("vue").ComputedRef<boolean>;
22
24
  register: (key: string, element: HTMLElement) => () => void;
23
25
  remaining: import("vue").ComputedRef<number>;
24
26
  resetSection: (key: string) => Promise<void>;
@@ -42,6 +44,8 @@ export declare function useFormPage(params: {
42
44
  scrollTo: (key: string, options?: import("../types/index.js").FormPageScrollOptions) => boolean;
43
45
  sections: import("vue").ComputedRef<readonly FormPageSectionState[]>;
44
46
  stickyOffset: import("vue").Ref<number, number>;
47
+ surface: import("vue").ComputedRef<import("../types/index.js").FormPageSurface>;
48
+ surfaceClasses: import("vue").ComputedRef<import("../utils/page-surface.js").FormPageSurfaceClasses>;
45
49
  };
46
50
  export declare function provideFormPage(page: ReturnType<typeof useFormPage>): {
47
51
  active: import("vue").Ref<string | undefined, string | undefined>;
@@ -53,6 +57,7 @@ export declare function provideFormPage(page: ReturnType<typeof useFormPage>): {
53
57
  }[]>;
54
58
  modified: import("vue").ComputedRef<number>;
55
59
  navigationTitle: import("vue").ComputedRef<string | undefined>;
60
+ navigationVisible: import("vue").ComputedRef<boolean>;
56
61
  register: (key: string, element: HTMLElement) => () => void;
57
62
  remaining: import("vue").ComputedRef<number>;
58
63
  resetSection: (key: string) => Promise<void>;
@@ -76,6 +81,8 @@ export declare function provideFormPage(page: ReturnType<typeof useFormPage>): {
76
81
  scrollTo: (key: string, options?: import("../types/index.js").FormPageScrollOptions) => boolean;
77
82
  sections: import("vue").ComputedRef<readonly FormPageSectionState[]>;
78
83
  stickyOffset: import("vue").Ref<number, number>;
84
+ surface: import("vue").ComputedRef<import("../types/index.js").FormPageSurface>;
85
+ surfaceClasses: import("vue").ComputedRef<import("../utils/page-surface.js").FormPageSurfaceClasses>;
79
86
  };
80
87
  /** The page a part renders in. Parts only work inside `FormPage`. */
81
88
  export declare function useFormPageContext(): {
@@ -88,6 +95,7 @@ export declare function useFormPageContext(): {
88
95
  }[]>;
89
96
  modified: import("vue").ComputedRef<number>;
90
97
  navigationTitle: import("vue").ComputedRef<string | undefined>;
98
+ navigationVisible: import("vue").ComputedRef<boolean>;
91
99
  register: (key: string, element: HTMLElement) => () => void;
92
100
  remaining: import("vue").ComputedRef<number>;
93
101
  resetSection: (key: string) => Promise<void>;
@@ -111,4 +119,6 @@ export declare function useFormPageContext(): {
111
119
  scrollTo: (key: string, options?: import("../types/index.js").FormPageScrollOptions) => boolean;
112
120
  sections: import("vue").ComputedRef<readonly FormPageSectionState[]>;
113
121
  stickyOffset: import("vue").Ref<number, number>;
122
+ surface: import("vue").ComputedRef<import("../types/index.js").FormPageSurface>;
123
+ surfaceClasses: import("vue").ComputedRef<import("../utils/page-surface.js").FormPageSurfaceClasses>;
114
124
  };
@@ -1,11 +1,14 @@
1
1
  import { computed, inject, provide, ref } from "vue";
2
2
  import { getSchemaDirtyCheck } from "../utils/controls.js";
3
3
  import {
4
+ getFormPageNavigationMinSections,
4
5
  getFormPageNavigationTitle,
5
6
  getFormPageSections,
7
+ isFormPageSectionBlocking,
6
8
  isFormPageSectionVisible,
7
9
  resolveFormPageSectionState
8
10
  } from "../utils/page.js";
11
+ import { getFormPageSurfaceClasses, resolveFormPageSurface } from "../utils/page-surface.js";
9
12
  import { cloneFormValue } from "../utils/path.js";
10
13
  import { useFormPageScroll } from "./use-form-page-scroll.js";
11
14
  export function useFormPage(params) {
@@ -36,11 +39,11 @@ export function useFormPage(params) {
36
39
  );
37
40
  const dirtyCheck = computed(() => getSchemaDirtyCheck(root.schema.value));
38
41
  const navigationTitle = computed(() => getFormPageNavigationTitle(root.schema.value));
39
- const remaining = computed(
40
- () => sections.value.filter(
41
- (section) => section.optional ? section.missing > 0 || section.status === "invalid" : section.status !== "complete"
42
- ).length
43
- );
42
+ const navigationMinSections = computed(() => getFormPageNavigationMinSections(root.schema.value));
43
+ const navigationVisible = computed(() => sections.value.length >= navigationMinSections.value);
44
+ const surface = computed(() => resolveFormPageSurface(root.formUi.ui.value.page?.surface));
45
+ const surfaceClasses = computed(() => getFormPageSurfaceClasses(surface.value));
46
+ const remaining = computed(() => sections.value.filter(isFormPageSectionBlocking).length);
44
47
  const modified = computed(() => sections.value.filter((section) => section.dirty).length);
45
48
  const stickyOffset = ref(0);
46
49
  const scroll = useFormPageScroll({ hash: params.hash, root: params.element, sections });
@@ -62,13 +65,16 @@ export function useFormPage(params) {
62
65
  items,
63
66
  modified,
64
67
  navigationTitle,
68
+ navigationVisible,
65
69
  register: scroll.register,
66
70
  remaining,
67
71
  resetSection,
68
72
  root,
69
73
  scrollTo: scroll.scrollTo,
70
74
  sections,
71
- stickyOffset
75
+ stickyOffset,
76
+ surface,
77
+ surfaceClasses
72
78
  };
73
79
  }
74
80
  const formPageKey = Symbol("nuxt-ui-tools-form-page");
@@ -221,7 +221,7 @@ export function useFormRuntime(params) {
221
221
  active: currentStepIndex.value === index,
222
222
  index,
223
223
  key: step.key ?? String(index + 1),
224
- label: isFunction(step.title) ? String(step.title()) : String(step.title ?? step.key ?? `Step ${index + 1}`),
224
+ label: isFunction(step.title) ? String(step.title()) : String(step.title ?? step.key ?? t("form.steps.fallback", { number: index + 1 })),
225
225
  root: step.root
226
226
  }))
227
227
  );