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
@@ -5,7 +5,7 @@ import type { FormStatefulFieldBase } from '../../types/field-base.js';
5
5
  import type { ResolveFormFieldValue } from '../../types/field-output.js';
6
6
  import type { FormObject, FormRenderable, FormText } from '../../types/utils.js';
7
7
  type ContainerFieldType = 'array-list' | 'array-table' | 'array-tabs' | 'array-variant' | 'array-collapse' | 'array-primitive' | 'object' | 'group' | 'input-group' | 'card' | 'column' | 'matrix' | 'info' | 'divider' | 'section' | 'button' | 'hidden';
8
- type ItemFieldChrome = 'key' | 'label' | 'description' | 'hint' | 'help' | 'labelExtra' | 'layout';
8
+ type ItemFieldChrome = 'key' | 'label' | 'description' | 'hint' | 'help' | 'disabledReason' | 'labelExtra' | 'layout';
9
9
  type DistributiveOmit<TValue, TKey extends PropertyKey> = TValue extends unknown ? Omit<TValue, TKey> : never;
10
10
  export type FormArrayPrimitiveItemField<TContext = NonNullable<unknown>, TDeps = NonNullable<unknown>> = DistributiveOmit<Exclude<FormField<TContext, TDeps>, {
11
11
  type: ContainerFieldType;
@@ -7,6 +7,7 @@ import { formOptionKey } from "../../utils/options";
7
7
  import { isBoolean, isNumber, isString, isUndefined } from "../../utils/predicate";
8
8
  import ChoiceCardLabel from "../choice-card/choice-card-label.vue";
9
9
  import { CHOICE_CARD_PROPS, useChoiceCard } from "../choice-card/use-choice-card";
10
+ import { choiceCardGroupAttrs, groupChoiceCardItems } from "../choice-card/utils";
10
11
  const props = defineProps({
11
12
  field: { type: Object, required: true },
12
13
  path: { type: Array, required: true }
@@ -16,7 +17,11 @@ const { fieldProps, form, controlProps, disabled, handleBlur, options } = useFie
16
17
  () => props.path,
17
18
  { omit: CHOICE_CARD_PROPS }
18
19
  );
19
- const cards = useChoiceCard({ props: () => fieldProps.value, ui: () => controlProps.value.ui });
20
+ const cards = useChoiceCard({
21
+ kind: "checkbox",
22
+ props: () => fieldProps.value,
23
+ ui: () => controlProps.value.ui
24
+ });
20
25
  const optionByKey = computed(
21
26
  () => new Map(options.items.value.map((item) => [formOptionKey(item.value), item]))
22
27
  );
@@ -36,9 +41,20 @@ const model = computed({
36
41
  const items = computed(
37
42
  () => options.items.value.map(({ icon: _icon, ...item }) => ({
38
43
  ...item,
44
+ description: cards.compact.value ? void 0 : item.description,
39
45
  value: formOptionKey(item.value)
40
46
  }))
41
47
  );
48
+ const groups = computed(() => groupChoiceCardItems(items.value));
49
+ function selectionOf(group) {
50
+ return model.value.filter((key) => group.items.some((item) => item.value === key));
51
+ }
52
+ function update(group, keys) {
53
+ const kept = model.value.filter(
54
+ (key) => keys.includes(key) || !group.items.some((item) => item.value === key)
55
+ );
56
+ model.value = [...kept, ...keys.filter((key) => !kept.includes(key))];
57
+ }
42
58
  function isOptionValue(value) {
43
59
  return isString(value) || isNumber(value) || isBoolean(value);
44
60
  }
@@ -46,32 +62,42 @@ function isOptionValue(value) {
46
62
 
47
63
  <template>
48
64
  <FormFieldShell :field="field" :path="path">
49
- <UCheckboxGroup
50
- v-model="model"
51
- v-bind="controlProps"
52
- :items="items"
53
- value-key="value"
54
- label-key="label"
55
- description-key="description"
56
- variant="card"
57
- :orientation="cards.orientation.value"
58
- :indicator="cards.indicator.value"
59
- :ui="cards.ui.value"
60
- :style="cards.style.value"
61
- :disabled="disabled"
62
- @blur="handleBlur"
63
- >
64
- <template #label="{ item }">
65
- <ChoiceCardLabel
66
- :label="optionByKey.get(item.value)?.label ?? item.label"
67
- :icon="optionByKey.get(item.value)?.icon"
68
- :value="optionByKey.get(item.value)?.value"
69
- :tile="cards.tile.value"
70
- :mark="cards.mark.value"
71
- :selected="model.includes(item.value)"
72
- :ui="cards.ui.value"
73
- />
74
- </template>
75
- </UCheckboxGroup>
65
+ <div :class="cards.groupsClass.value">
66
+ <UCheckboxGroup
67
+ v-for="(group, index) in groups"
68
+ :key="group.key"
69
+ :model-value="selectionOf(group)"
70
+ v-bind="choiceCardGroupAttrs(controlProps, index)"
71
+ :items="group.items"
72
+ value-key="value"
73
+ label-key="label"
74
+ description-key="description"
75
+ variant="card"
76
+ :legend="group.heading"
77
+ :orientation="cards.orientation.value"
78
+ :indicator="cards.indicator.value"
79
+ :ui="cards.ui.value"
80
+ :style="cards.style.value"
81
+ :disabled="disabled"
82
+ @update:model-value="update(group, $event)"
83
+ @blur="handleBlur"
84
+ >
85
+ <template #label="{ item }">
86
+ <ChoiceCardLabel
87
+ :label="optionByKey.get(item.value)?.label ?? item.label"
88
+ :description="
89
+ cards.compact.value ? optionByKey.get(item.value)?.description : void 0
90
+ "
91
+ :icon="optionByKey.get(item.value)?.icon"
92
+ :value="optionByKey.get(item.value)?.value"
93
+ :tile="cards.tile.value"
94
+ :compact="cards.compact.value"
95
+ :mark="cards.mark.value"
96
+ :selected="model.includes(item.value)"
97
+ :ui="cards.ui.value"
98
+ />
99
+ </template>
100
+ </UCheckboxGroup>
101
+ </div>
76
102
  </FormFieldShell>
77
103
  </template>
@@ -4,6 +4,7 @@ export declare const checkboxCardFieldKind: {
4
4
  };
5
5
  readonly options: {
6
6
  readonly enabled: true;
7
+ readonly groups: true;
7
8
  };
8
9
  readonly state: "stateful";
9
10
  readonly transform: true;
@@ -1,7 +1,7 @@
1
1
  import { defineFormFieldKind } from "../../utils/field-kind.js";
2
2
  export const checkboxCardFieldKind = defineFormFieldKind({
3
3
  layout: { item: true },
4
- options: { enabled: true },
4
+ options: { enabled: true, groups: true },
5
5
  state: "stateful",
6
6
  transform: true,
7
7
  type: "checkbox-card",
@@ -1,8 +1,8 @@
1
1
  import type { FormStatefulFieldBase } from '../../types/field-base.js';
2
2
  import type { FieldOptionValue, NullableValue } from '../../types/field-output-utils.js';
3
- import type { FormOptionConfig, FormOptionItem, FormOptionValue, FormOptionsSource } from '../../types/options.js';
4
- import type { FormChoiceCardProps } from '../choice-card/types.js';
5
- export interface FormCheckboxCardField<TContext = NonNullable<unknown>, TDeps = NonNullable<unknown>, TValue extends FormOptionValue = FormOptionValue, TOption extends FormOptionItem<TValue> = FormOptionItem<TValue>> extends FormStatefulFieldBase<'checkbox-card', readonly TValue[] | null, TContext, TDeps, FormChoiceCardProps> {
3
+ import type { FormOptionConfig, FormOptionValue, FormOptionsSource } from '../../types/options.js';
4
+ import type { FormChoiceCardGrouping, FormChoiceCardOptionItem, FormChoiceCardProps } from '../choice-card/types.js';
5
+ export interface FormCheckboxCardField<TContext = NonNullable<unknown>, TDeps = NonNullable<unknown>, TValue extends FormOptionValue = FormOptionValue, TOption extends FormChoiceCardOptionItem<TValue> = FormChoiceCardOptionItem<TValue>> extends FormStatefulFieldBase<'checkbox-card', readonly TValue[] | null, TContext, TDeps, FormChoiceCardProps>, FormChoiceCardGrouping {
6
6
  options: FormOptionConfig<TOption, TContext, TDeps, readonly TValue[] | null> | FormOptionsSource<TOption, TContext, TDeps, readonly TValue[] | null>;
7
7
  }
8
8
  export type CheckboxCardFieldOutput<TField> = readonly FieldOptionValue<TField>[] | NullableValue;
@@ -7,12 +7,19 @@ import type { ChoiceCardMark } from './types.js';
7
7
  * semantics. Styled through `ui.tile`, `ui.tileIcon`, `ui.optionIcon`, `ui.check`,
8
8
  * `ui.checkIcon`, `ui.switch`, and `ui.switchThumb`; the tile and the switch carry the option
9
9
  * value as `data-value` for per-option styling.
10
+ *
11
+ * A `compact` card lays itself out as one row inside the grid its label becomes: the icon (or a
12
+ * smaller tile) beside the title, over a one-line description. Both lines truncate and keep the
13
+ * full text in their tooltip, and the marks center on the row. The card passes the description
14
+ * here because the Nuxt UI group would draw it below the icon instead.
10
15
  */
11
16
  type __VLS_Props = {
12
17
  label?: string;
18
+ description?: string;
13
19
  icon?: string;
14
20
  value?: string | number | boolean;
15
21
  tile: boolean;
22
+ compact?: boolean;
16
23
  mark: ChoiceCardMark;
17
24
  selected: boolean;
18
25
  ui?: FormControlUi;
@@ -3,13 +3,17 @@ import UIcon from "@nuxt/ui/components/Icon.vue";
3
3
  import { mergeFormUiClass } from "../../utils/ui";
4
4
  defineProps({
5
5
  label: { type: String, required: false },
6
+ description: { type: String, required: false },
6
7
  icon: { type: String, required: false },
7
8
  value: { type: [String, Number, Boolean], required: false },
8
9
  tile: { type: Boolean, required: true },
10
+ compact: { type: Boolean, required: false },
9
11
  mark: { type: String, required: true },
10
12
  selected: { type: Boolean, required: true },
11
13
  ui: { type: Object, required: false }
12
14
  });
15
+ const COMPACT_MARK = "end-2.5 top-1/2 -translate-y-1/2";
16
+ const COMPACT_TILE = "col-start-1 row-span-2 me-2.5 size-7 rounded-md";
13
17
  </script>
14
18
 
15
19
  <template>
@@ -20,6 +24,7 @@ defineProps({
20
24
  :class="
21
25
  mergeFormUiClass(
22
26
  'grid size-8 place-items-center rounded-lg bg-elevated text-muted transition-colors data-[selected]:bg-primary/10 data-[selected]:text-primary',
27
+ compact ? COMPACT_TILE : void 0,
23
28
  ui?.tile
24
29
  )
25
30
  "
@@ -27,7 +32,26 @@ defineProps({
27
32
  >
28
33
  <UIcon :name="icon" :class="mergeFormUiClass('size-4', ui?.tileIcon)" aria-hidden="true" />
29
34
  </span>
30
- <span v-if="!tile && icon" class="inline-flex items-center gap-2">
35
+ <template v-if="compact">
36
+ <UIcon
37
+ v-if="icon && !tile"
38
+ :name="icon"
39
+ :class="
40
+ mergeFormUiClass('col-start-1 row-span-2 me-2.5 size-4 shrink-0 text-muted', ui?.optionIcon)
41
+ "
42
+ aria-hidden="true"
43
+ />
44
+ <span :title="label" class="col-start-2 block truncate" data-choice-title>{{ label }}</span>
45
+ <span
46
+ v-if="description"
47
+ :title="description"
48
+ class="col-start-2 block truncate text-xs font-normal text-muted"
49
+ data-choice-description
50
+ >
51
+ {{ description }}
52
+ </span>
53
+ </template>
54
+ <span v-else-if="!tile && icon" class="inline-flex items-center gap-2">
31
55
  <UIcon
32
56
  :name="icon"
33
57
  :class="mergeFormUiClass('size-4 shrink-0', ui?.optionIcon)"
@@ -42,6 +66,7 @@ defineProps({
42
66
  :class="
43
67
  mergeFormUiClass(
44
68
  'absolute end-3 top-3 grid size-4 place-items-center rounded-[4px] bg-primary text-inverted',
69
+ compact ? COMPACT_MARK : void 0,
45
70
  ui?.check
46
71
  )
47
72
  "
@@ -57,6 +82,7 @@ defineProps({
57
82
  :class="
58
83
  mergeFormUiClass(
59
84
  'group/switch absolute end-3 top-3.5 inline-flex h-[18px] w-8 shrink-0 items-center rounded-full bg-accented p-0.5 transition-colors duration-150 data-[state=checked]:bg-primary',
85
+ compact ? COMPACT_MARK : void 0,
60
86
  ui?.switch
61
87
  )
62
88
  "
@@ -7,12 +7,19 @@ import type { ChoiceCardMark } from './types.js';
7
7
  * semantics. Styled through `ui.tile`, `ui.tileIcon`, `ui.optionIcon`, `ui.check`,
8
8
  * `ui.checkIcon`, `ui.switch`, and `ui.switchThumb`; the tile and the switch carry the option
9
9
  * value as `data-value` for per-option styling.
10
+ *
11
+ * A `compact` card lays itself out as one row inside the grid its label becomes: the icon (or a
12
+ * smaller tile) beside the title, over a one-line description. Both lines truncate and keep the
13
+ * full text in their tooltip, and the marks center on the row. The card passes the description
14
+ * here because the Nuxt UI group would draw it below the icon instead.
10
15
  */
11
16
  type __VLS_Props = {
12
17
  label?: string;
18
+ description?: string;
13
19
  icon?: string;
14
20
  value?: string | number | boolean;
15
21
  tile: boolean;
22
+ compact?: boolean;
16
23
  mark: ChoiceCardMark;
17
24
  selected: boolean;
18
25
  ui?: FormControlUi;
@@ -1,3 +1,7 @@
1
+ import type { FormOption, FormOptionValue } from '../../types/options.js';
2
+ import type { FormText } from '../../types/utils.js';
3
+ /** Size of the cards: `compact` packs long option lists into smaller, single-row cards. */
4
+ export type FormChoiceCardDensity = 'default' | 'compact';
1
5
  /**
2
6
  * Presentation of the radio and checkbox cards.
3
7
  *
@@ -6,6 +10,9 @@
6
10
  * // A fixed grid of cards, each with its icon in a tile above the label and a check in the
7
11
  * // corner once selected.
8
12
  * props: { columns: '1 md:2 lg:3', icon: 'tile', indicator: 'corner' }
13
+ *
14
+ * // Eight options in four groups, as smaller cards with the icon beside a one-line label.
15
+ * props: { columns: '1 md:2', density: 'compact', icon: 'tile', indicator: 'corner' }
9
16
  * ```
10
17
  */
11
18
  export interface FormChoiceCardProps {
@@ -25,12 +32,58 @@ export interface FormChoiceCardProps {
25
32
  indicator?: 'start' | 'end' | 'hidden' | 'corner' | 'switch';
26
33
  /**
27
34
  * Where an option's `icon` shows: before its label (`inline`, the default), or in a tile above
28
- * it (`tile`) that takes the selection color. Style the parts the engine renders with the
35
+ * it (`tile`) that takes the selection color, beside the label in the `compact` density. Style the parts the engine renders with the
29
36
  * `ui` slots `tile`, `tileIcon`, `optionIcon`, `check`, `checkIcon`, `switch`, and
30
37
  * `switchThumb`, next to the Nuxt UI slots of the group. The tile and the switch carry the
31
38
  * option value as `data-value`, so one option can take its own colors.
32
39
  */
33
40
  icon?: 'inline' | 'tile';
41
+ /**
42
+ * Size of the cards. `compact` shrinks the padding and tile, puts the icon beside the label
43
+ * instead of above it, and keeps the title and the description on one line each, truncated with
44
+ * an ellipsis (the full description stays in the card's tooltip). Defaults to `default`.
45
+ */
46
+ density?: FormChoiceCardDensity;
47
+ }
48
+ /**
49
+ * Option grouping of the radio and checkbox cards, declared on the field next to its `options`.
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * // Rows from the API, each with a `section` to list the cards under.
54
+ * { type: 'radio-card', options: () => queryOptions({ queryKey: ['plans'], queryFn: fetchPlans }), groupKey: 'section' }
55
+ * ```
56
+ */
57
+ export interface FormChoiceCardGrouping {
58
+ /**
59
+ * Property of the options that holds the heading they are listed under. Options written inline
60
+ * in the schema group by their `group` without it. The records a function, a promise or a query
61
+ * returns group only when this names the property, so records that already carry a `group` for
62
+ * another purpose are never regrouped by surprise. `false` lists every option together, even
63
+ * inline options that carry a `group`.
64
+ */
65
+ groupKey?: string | false;
66
+ }
67
+ /**
68
+ * Option of a radio or checkbox card: the shared option shape plus the icon and the heading the
69
+ * cards can show.
70
+ */
71
+ export interface FormChoiceCardOption<TValue extends FormOptionValue = FormOptionValue> extends FormOption<TValue> {
72
+ /** Icon shown with the label, inline or in a tile (see `props.icon`). */
73
+ icon?: string;
74
+ /**
75
+ * Heading this option is listed under. Options sharing a heading are listed together, and the
76
+ * headings follow the order in which they first appear in the options. Options without one
77
+ * stay together, without a heading. Read from this property for options written inline in the
78
+ * schema; options a function, a promise or a query returns need `groupKey`.
79
+ */
80
+ group?: FormText;
34
81
  }
82
+ /** Authored option item accepted by radio and checkbox cards. */
83
+ export type FormChoiceCardOptionItem<TValue extends FormOptionValue = FormOptionValue> = TValue | FormChoiceCardOption<TValue>;
84
+ /** The card layouts: icon before the label, icon tile above it, or one compact row. */
85
+ export type ChoiceCardLayout = 'inline' | 'stacked' | 'compact';
35
86
  /** The mark a card draws itself, in place of the Nuxt UI indicator. */
36
87
  export type ChoiceCardMark = 'corner' | 'switch' | 'none';
88
+ /** The Nuxt UI group a card field wraps. */
89
+ export type ChoiceCardKind = 'checkbox' | 'radio';
@@ -1,16 +1,20 @@
1
1
  import type { FormControlUi } from '../../types/index.js';
2
- import type { ChoiceCardMark, FormChoiceCardProps } from './types.js';
2
+ import type { ChoiceCardKind, ChoiceCardMark, FormChoiceCardProps } from './types.js';
3
3
  /** Props the card fields render themselves, kept away from the Nuxt UI group. */
4
4
  export declare const CHOICE_CARD_PROPS: string[];
5
5
  /**
6
6
  * Presentation of a radio or checkbox card group: the flowing row or fixed grid of cards, the
7
- * selection mark, and where option icons go. Returns the props and `ui` slots for the Nuxt UI
8
- * group, and what the card content renders (`tile`, `mark`).
7
+ * selection mark, where option icons go, the density, and the headings of grouped options.
8
+ * Returns the props and `ui` slots for the Nuxt UI group, and what the card content renders
9
+ * (`compact`, `tile`, `mark`).
9
10
  */
10
11
  export declare function useChoiceCard(params: {
12
+ kind: ChoiceCardKind;
11
13
  props: () => FormChoiceCardProps;
12
14
  ui: () => FormControlUi | undefined;
13
15
  }): {
16
+ compact: import("vue").ComputedRef<boolean>;
17
+ groupsClass: import("vue").ComputedRef<string>;
14
18
  indicator: import("vue").ComputedRef<"end" | "start" | "hidden" | undefined>;
15
19
  mark: import("vue").ComputedRef<ChoiceCardMark>;
16
20
  orientation: import("vue").ComputedRef<"horizontal" | "vertical" | undefined>;
@@ -2,7 +2,25 @@ import { computed } from "vue";
2
2
  import { useResponsiveValue } from "../../../shared/composables/use-responsive-value.js";
3
3
  import { mergeFormUiClass } from "../../utils/ui.js";
4
4
  import { CARD_SELECTED_RING } from "../card-selection.js";
5
- export const CHOICE_CARD_PROPS = ["columns", "icon", "indicator", "orientation"];
5
+ export const CHOICE_CARD_PROPS = ["columns", "density", "icon", "indicator", "orientation"];
6
+ const LAYOUT_UI = {
7
+ compact: {
8
+ gap: "gap-2",
9
+ item: "items-center p-2.5",
10
+ label: "grid min-w-0 grid-cols-[auto_minmax(0,1fr)] items-center",
11
+ wrapper: "min-w-0 items-stretch"
12
+ },
13
+ inline: { gap: "gap-2.5" },
14
+ stacked: {
15
+ gap: "gap-2.5",
16
+ label: "flex flex-col items-start gap-2.5 font-semibold",
17
+ wrapper: "flex flex-col items-start gap-1.5"
18
+ }
19
+ };
20
+ const ICON_UI = {
21
+ checkbox: void 0,
22
+ radio: "hidden"
23
+ };
6
24
  export function useChoiceCard(params) {
7
25
  const columnsValue = useResponsiveValue(() => String(params.props().columns ?? ""));
8
26
  const columns = computed(() => {
@@ -13,7 +31,12 @@ export function useChoiceCard(params) {
13
31
  const value = params.props().indicator;
14
32
  return value === "corner" || value === "switch" ? value : "none";
15
33
  });
34
+ const compact = computed(() => params.props().density === "compact");
16
35
  const tile = computed(() => params.props().icon === "tile");
36
+ const layout = computed(() => {
37
+ if (compact.value) return "compact";
38
+ return tile.value ? "stacked" : "inline";
39
+ });
17
40
  const indicator = computed(() => {
18
41
  const value = params.props().indicator;
19
42
  return value === "corner" || value === "switch" ? "hidden" : value;
@@ -24,36 +47,35 @@ export function useChoiceCard(params) {
24
47
  );
25
48
  const ui = computed(() => {
26
49
  const current = params.ui();
50
+ const layoutUi = LAYOUT_UI[layout.value];
51
+ const stacked = layout.value === "stacked";
27
52
  return {
28
53
  ...current,
29
54
  fieldset: mergeFormUiClass(
30
- columns.value ? "grid grid-cols-[repeat(var(--nut-choice-columns),minmax(0,1fr))] gap-2.5" : `gap-2.5${orientation.value === "horizontal" ? " flex-wrap" : ""}`,
55
+ columns.value ? `grid grid-cols-[repeat(var(--nut-choice-columns),minmax(0,1fr))] ${layoutUi.gap}` : `${layoutUi.gap}${orientation.value === "horizontal" ? " flex-wrap" : ""}`,
31
56
  current?.fieldset
32
57
  ),
33
58
  item: mergeFormUiClass(
34
- `border-default ${CARD_SELECTED_RING}`,
59
+ `border-default ${CARD_SELECTED_RING} ${layoutUi.item ?? ""}`,
35
60
  mark.value === "none" ? void 0 : "relative",
36
61
  current?.item
37
62
  ),
38
- // The card label renders the option icon itself (inline or as a tile): Nuxt UI's own copy
39
- // would show it twice.
40
- icon: mergeFormUiClass("hidden", current?.icon),
41
- // A tile stacks above the title, and the description below it, with gaps.
42
- label: mergeFormUiClass(
43
- tile.value ? "flex flex-col items-start gap-2.5 font-semibold" : void 0,
44
- current?.label
45
- ),
63
+ icon: mergeFormUiClass(ICON_UI[params.kind], current?.icon),
64
+ // The heading of a group of options, set above its cards.
65
+ legend: mergeFormUiClass("mb-2 text-sm font-medium text-muted", current?.legend),
66
+ label: mergeFormUiClass(layoutUi.label, current?.label),
46
67
  // A hidden indicator centers the text: keep it aligned, and clear of an inline corner check.
47
68
  wrapper: mergeFormUiClass(
48
- tile.value ? "flex flex-col items-start gap-1.5" : void 0,
69
+ layoutUi.wrapper,
49
70
  [
50
71
  indicator.value === "hidden" ? "ms-0 text-start" : "",
51
- mark.value === "corner" && !tile.value ? "pe-7" : "",
52
- mark.value === "switch" && !tile.value ? "pe-12" : ""
72
+ mark.value === "corner" && !stacked ? "pe-7" : "",
73
+ mark.value === "switch" && !stacked ? "pe-12" : ""
53
74
  ].join(" "),
54
75
  current?.wrapper
55
76
  )
56
77
  };
57
78
  });
58
- return { indicator, mark, orientation, style, tile, ui };
79
+ const groupsClass = computed(() => mergeFormUiClass("flex flex-col gap-5", params.ui()?.groups));
80
+ return { compact, groupsClass, indicator, mark, orientation, style, tile, ui };
59
81
  }
@@ -0,0 +1,27 @@
1
+ import type { FormObject } from '../../types/index.js';
2
+ export interface ChoiceCardGroup<TItem> {
3
+ /** Stable key of the group: its heading, or an empty string for the options without one. */
4
+ key: string;
5
+ /** Heading shown above the cards of the group; none for options that declare no group. */
6
+ heading?: string;
7
+ /** The group's options, as the mutable list the Nuxt UI groups take. */
8
+ items: TItem[];
9
+ }
10
+ /**
11
+ * Lists options under their group headings. Headings follow the order in which they first appear
12
+ * in `items`, and each keeps its options in their authored order. The options without a group form
13
+ * one more group with no heading, placed where its first option appears. Without any group the
14
+ * result is a single headingless group, so an ungrouped list renders as it always did.
15
+ */
16
+ export declare function groupChoiceCardItems<TItem extends {
17
+ group?: string;
18
+ }>(items: readonly TItem[]): readonly ChoiceCardGroup<TItem>[];
19
+ /**
20
+ * Attributes for the Nuxt UI group at `index` among the groups of one field. Every group carries
21
+ * the control attributes of the field, but an `id` names a single element: the first group keeps
22
+ * it, as the control the label points to, and each later group takes `<id>-group-<index>`. The other
23
+ * attributes (`data-form-field`, `aria-label`) stay on every group, so focus moving from one
24
+ * group to the next does not leave the field. Attributes without an `id` are returned as they
25
+ * are: each Nuxt UI group then generates its own.
26
+ */
27
+ export declare function choiceCardGroupAttrs<TAttrs extends FormObject>(attrs: TAttrs, index: number): TAttrs;
@@ -0,0 +1,17 @@
1
+ import { isString } from "../../utils/predicate.js";
2
+ export function groupChoiceCardItems(items) {
3
+ const groups = /* @__PURE__ */ new Map();
4
+ for (const item of items) {
5
+ const key = item.group?.trim() ?? "";
6
+ const members = groups.get(key);
7
+ if (members) members.push(item);
8
+ else groups.set(key, [item]);
9
+ }
10
+ if (groups.size === 0) return [{ items: [], key: "" }];
11
+ return [...groups].map(([key, members]) => ({ heading: key || void 0, items: members, key }));
12
+ }
13
+ export function choiceCardGroupAttrs(attrs, index) {
14
+ const id = attrs.id;
15
+ if (index === 0 || !isString(id)) return attrs;
16
+ return { ...attrs, id: `${id}-group-${index}` };
17
+ }
@@ -133,15 +133,16 @@ const selectedTreeItems = computed(() => {
133
133
  return Array.isArray(selected) ? selected : [selected];
134
134
  });
135
135
  const treeSelectLabel = computed(() => {
136
+ const empty = placeholder.value ?? "";
136
137
  if (!selectedTreeItems.value.length) {
137
- return placeholder.value;
138
+ return empty;
138
139
  }
139
140
  if (treeMultiple.value) {
140
141
  return selectedTreeItems.value.map((item) => item.pathLabel).join(", ");
141
142
  }
142
143
  const [selected] = selectedTreeItems.value;
143
144
  if (!selected) {
144
- return placeholder.value;
145
+ return empty;
145
146
  }
146
147
  return props.field.type === "tree-select" && fieldProps.value.showPath ? selected.pathLabel : String(selected.label);
147
148
  });
@@ -545,6 +545,7 @@ export declare const formFieldKinds: readonly [{
545
545
  };
546
546
  readonly options: {
547
547
  readonly enabled: true;
548
+ readonly groups: true;
548
549
  };
549
550
  readonly state: "stateful";
550
551
  readonly transform: true;
@@ -561,6 +562,7 @@ export declare const formFieldKinds: readonly [{
561
562
  };
562
563
  readonly options: {
563
564
  readonly enabled: true;
565
+ readonly groups: true;
564
566
  };
565
567
  readonly state: "stateful";
566
568
  readonly transform: true;