vueless 1.4.12-beta.2 → 1.4.12-beta.21

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 (109) hide show
  1. package/README.md +1 -1
  2. package/composables/useUI.ts +19 -1
  3. package/index.d.ts +128 -0
  4. package/index.ts +128 -0
  5. package/package.json +10 -9
  6. package/ui.button/config.ts +1 -1
  7. package/ui.button-link/config.ts +1 -1
  8. package/ui.button-toggle/UToggle.vue +6 -4
  9. package/ui.button-toggle/storybook/stories.ts +4 -1
  10. package/ui.button-toggle/tests/UToggle.test-d.ts +48 -0
  11. package/ui.button-toggle/tests/UToggleTypes.vue +55 -0
  12. package/ui.button-toggle/types.ts +18 -6
  13. package/ui.container-accordion/types.ts +5 -2
  14. package/ui.container-collapsible/config.ts +1 -1
  15. package/ui.container-drawer/tests/UDrawer.test.ts +10 -17
  16. package/ui.container-modal/tests/UModal.test.ts +10 -17
  17. package/ui.data-list/UDataList.vue +48 -17
  18. package/ui.data-list/config.ts +2 -2
  19. package/ui.data-list/storybook/stories.ts +5 -2
  20. package/ui.data-list/tests/UDataList.test-d.ts +76 -0
  21. package/ui.data-list/tests/UDataList.test.ts +52 -3
  22. package/ui.data-list/tests/UDataListTypes.vue +110 -0
  23. package/ui.data-list/types.ts +37 -5
  24. package/ui.data-table/UTable.vue +202 -24
  25. package/ui.data-table/UTableRow.vue +4 -3
  26. package/ui.data-table/config.ts +11 -2
  27. package/ui.data-table/storybook/stories.ts +34 -1
  28. package/ui.data-table/tests/UTable.test-d.ts +179 -0
  29. package/ui.data-table/tests/UTable.test.ts +85 -7
  30. package/ui.data-table/tests/UTableEmptyColumnsTypes.vue +27 -0
  31. package/ui.data-table/tests/UTableTypes.vue +193 -0
  32. package/ui.data-table/types.ts +106 -9
  33. package/ui.data-table/utilTable.ts +1 -1
  34. package/ui.dropdown/UDropdown.vue +19 -8
  35. package/ui.dropdown/storybook/stories.ts +4 -1
  36. package/ui.dropdown/tests/UDropdown.test-d.ts +61 -0
  37. package/ui.dropdown/tests/UDropdown.test.ts +8 -1
  38. package/ui.dropdown/tests/UDropdownTypes.vue +111 -0
  39. package/ui.dropdown/types.ts +27 -3
  40. package/ui.dropdown-badge/UDropdownBadge.vue +9 -4
  41. package/ui.dropdown-badge/storybook/stories.ts +6 -1
  42. package/ui.dropdown-badge/tests/UDropdownBadge.test-d.ts +55 -0
  43. package/ui.dropdown-badge/tests/UDropdownBadge.test.ts +8 -1
  44. package/ui.dropdown-badge/tests/UDropdownBadgeTypes.vue +107 -0
  45. package/ui.dropdown-badge/types.ts +17 -3
  46. package/ui.dropdown-button/UDropdownButton.vue +9 -4
  47. package/ui.dropdown-button/storybook/stories.ts +6 -1
  48. package/ui.dropdown-button/tests/UDropdownButton.test-d.ts +57 -0
  49. package/ui.dropdown-button/tests/UDropdownButton.test.ts +8 -1
  50. package/ui.dropdown-button/tests/UDropdownButtonTypes.vue +107 -0
  51. package/ui.dropdown-button/types.ts +17 -3
  52. package/ui.dropdown-link/UDropdownLink.vue +9 -4
  53. package/ui.dropdown-link/config.ts +1 -1
  54. package/ui.dropdown-link/storybook/stories.ts +4 -1
  55. package/ui.dropdown-link/tests/UDropdownLink.test-d.ts +55 -0
  56. package/ui.dropdown-link/tests/UDropdownLink.test.ts +8 -1
  57. package/ui.dropdown-link/tests/UDropdownLinkTypes.vue +107 -0
  58. package/ui.dropdown-link/types.ts +17 -3
  59. package/ui.form-calendar/config.ts +1 -1
  60. package/ui.form-checkbox/config.ts +1 -1
  61. package/ui.form-checkbox/tests/UCheckbox.test.ts +4 -4
  62. package/ui.form-checkbox/types.ts +4 -2
  63. package/ui.form-checkbox-group/UCheckboxGroup.vue +8 -2
  64. package/ui.form-date-picker/UDatePicker.vue +11 -1
  65. package/ui.form-date-picker/tests/UDatePicker.test.ts +47 -0
  66. package/ui.form-input/UInput.vue +1 -0
  67. package/ui.form-input/config.ts +1 -1
  68. package/ui.form-input/tests/UInput.test.ts +16 -0
  69. package/ui.form-input-file/config.ts +1 -1
  70. package/ui.form-listbox/UListbox.vue +29 -12
  71. package/ui.form-listbox/config.ts +1 -1
  72. package/ui.form-listbox/storybook/stories.ts +4 -1
  73. package/ui.form-listbox/tests/UListbox.test-d.ts +63 -0
  74. package/ui.form-listbox/tests/UListboxTypes.vue +107 -0
  75. package/ui.form-listbox/types.ts +34 -2
  76. package/ui.form-radio/config.ts +1 -1
  77. package/ui.form-radio-group/URadioGroup.vue +15 -3
  78. package/ui.form-radio-group/config.ts +8 -3
  79. package/ui.form-radio-group/types.ts +2 -3
  80. package/ui.form-select/USelect.vue +49 -17
  81. package/ui.form-select/config.ts +4 -4
  82. package/ui.form-select/storybook/stories.ts +4 -1
  83. package/ui.form-select/tests/USelect.test-d.ts +76 -0
  84. package/ui.form-select/tests/USelect.test.ts +65 -1
  85. package/ui.form-select/tests/USelectTypes.vue +113 -0
  86. package/ui.form-select/types.ts +55 -4
  87. package/ui.form-switch/USwitch.vue +13 -12
  88. package/ui.form-switch/config.ts +2 -1
  89. package/ui.form-switch/tests/USwitch.test.ts +151 -0
  90. package/ui.form-textarea/config.ts +1 -1
  91. package/ui.image-avatar/types.ts +5 -2
  92. package/ui.image-avatar-group/types.ts +5 -2
  93. package/ui.image-icon/UIcon.vue +3 -2
  94. package/ui.image-icon/types.ts +2 -2
  95. package/ui.navigation-breadcrumbs/types.ts +5 -2
  96. package/ui.navigation-tab/UTab.vue +14 -9
  97. package/ui.navigation-tab/tests/UTab.test.ts +14 -0
  98. package/ui.navigation-tabs/UTabs.vue +158 -6
  99. package/ui.navigation-tabs/config.ts +2 -1
  100. package/ui.navigation-tabs/constants.ts +2 -0
  101. package/ui.navigation-tabs/storybook/stories.ts +12 -1
  102. package/ui.navigation-tabs/tests/UTabs.test-d.ts +50 -0
  103. package/ui.navigation-tabs/tests/UTabs.test.ts +213 -1
  104. package/ui.navigation-tabs/tests/UTabsTypes.vue +65 -0
  105. package/ui.navigation-tabs/types.ts +26 -4
  106. package/ui.skeleton-text/constants.ts +1 -1
  107. package/ui.text-badge/config.ts +1 -1
  108. package/utils/node/loaderIcon.js +1 -1
  109. package/v.tooltip/vTooltip.ts +1 -1
@@ -0,0 +1,179 @@
1
+ import { describe, it, expectTypeOf } from "vitest";
2
+
3
+ import type {
4
+ Props,
5
+ RowId,
6
+ SelectedSlotRow,
7
+ SlotRow,
8
+ TableRow,
9
+ UTableDynamicSlots,
10
+ UTableSlots,
11
+ } from "../types";
12
+
13
+ interface UserRow {
14
+ id: number;
15
+ name: string;
16
+ qty: number;
17
+ }
18
+
19
+ type CellSlot = NonNullable<UTableSlots<UserRow>["cell-name"]>;
20
+ type CellProps = Parameters<CellSlot>[0];
21
+
22
+ /* The caller's own row members must all stay optional — see `SlotRow`. */
23
+ type RequiredKeys<T> = {
24
+ [K in keyof T]-?: object extends Pick<T, K> ? never : K;
25
+ }[keyof T];
26
+
27
+ describe("UTable types", () => {
28
+ it("infers the caller's row shape in a cell slot", () => {
29
+ expectTypeOf<CellProps["row"]["name"]>().toEqualTypeOf<string | undefined>();
30
+ expectTypeOf<CellProps["row"]["qty"]>().toEqualTypeOf<number | undefined>();
31
+ expectTypeOf<CellProps["index"]>().toEqualTypeOf<number>();
32
+ expectTypeOf<CellProps["cellIndex"]>().toEqualTypeOf<number>();
33
+ });
34
+
35
+ it("does not expose undeclared row properties to a cell slot", () => {
36
+ /* An index signature would make any key legal, so `row.nonExistent` must not resolve. */
37
+ type HasIndexSignature = string extends keyof CellProps["row"] ? true : false;
38
+
39
+ expectTypeOf<HasIndexSignature>().toEqualTypeOf<false>();
40
+ expectTypeOf<keyof CellProps["row"]>().not.toEqualTypeOf<string>();
41
+ });
42
+
43
+ it("keeps the row flattening metadata available", () => {
44
+ expectTypeOf<CellProps["row"]>().toHaveProperty("nestedLevel");
45
+ expectTypeOf<SlotRow<UserRow>["nestedLevel"]>().toEqualTypeOf<number>();
46
+ });
47
+
48
+ it("types the static slots", () => {
49
+ type SelectedRows = Parameters<NonNullable<UTableSlots<UserRow>["header-actions"]>>[0];
50
+
51
+ expectTypeOf<SelectedRows["selectedRows"]>().toEqualTypeOf<SelectedSlotRow<UserRow>[]>();
52
+
53
+ type Expand = NonNullable<UTableSlots<UserRow>["expand"]>;
54
+
55
+ expectTypeOf<Parameters<Expand>[0]["expanded"]>().toEqualTypeOf<boolean>();
56
+ });
57
+
58
+ it("does not promise the caller's members on selected rows", () => {
59
+ /* Select-all flattens nested children into the selection, and those need not carry
60
+ * `TRow`'s members — so the slot must not type them as guaranteed. */
61
+ expectTypeOf<SelectedSlotRow<UserRow>["name"]>().toEqualTypeOf<string | undefined>();
62
+ expectTypeOf<SelectedSlotRow<UserRow>["qty"]>().toEqualTypeOf<number | undefined>();
63
+
64
+ /* A nested child, as `getFlatRows` actually produces it, must be assignable. */
65
+ expectTypeOf<{
66
+ id: number;
67
+ nestedLevel: number;
68
+ parentRowId: number;
69
+ }>().toExtend<SelectedSlotRow<UserRow>>();
70
+
71
+ /* Flattening metadata stays guaranteed. */
72
+ expectTypeOf<SelectedSlotRow<UserRow>["nestedLevel"]>().toEqualTypeOf<number>();
73
+ });
74
+
75
+ it("does not promise the caller's members on any row slot", () => {
76
+ /* `getFlatRows` recurses into `row.row`, and a nested child is typed `TableRow`, not `TRow`.
77
+ * Every row slot receives those children verbatim; `nested-row` receives nothing else. */
78
+ type Expand = Parameters<NonNullable<UTableSlots<UserRow>["expand"]>>[0];
79
+ type NestedRow = Parameters<NonNullable<UTableSlots<UserRow>["nested-row"]>>[0];
80
+
81
+ expectTypeOf<CellProps["row"]["name"]>().toEqualTypeOf<string | undefined>();
82
+ expectTypeOf<Expand["row"]["name"]>().toEqualTypeOf<string | undefined>();
83
+ expectTypeOf<NestedRow["row"]["name"]>().toEqualTypeOf<string | undefined>();
84
+ expectTypeOf<NestedRow["row"]["qty"]>().toEqualTypeOf<number | undefined>();
85
+
86
+ /* The real nested-child shape recorded at these slots must be assignable to all of them. */
87
+ type NestedChild = {
88
+ id: number;
89
+ label: string;
90
+ nestedLevel: number;
91
+ parentRowId: number;
92
+ };
93
+
94
+ expectTypeOf<NestedChild>().toExtend<CellProps["row"]>();
95
+ expectTypeOf<NestedChild>().toExtend<Expand["row"]>();
96
+ expectTypeOf<NestedChild>().toExtend<NestedRow["row"]>();
97
+
98
+ /* Flattening metadata is always set by `getFlatRows`, so it stays guaranteed. */
99
+ expectTypeOf<Expand["row"]["nestedLevel"]>().toEqualTypeOf<number>();
100
+ expectTypeOf<NestedRow["row"]["nestedLevel"]>().toEqualTypeOf<number>();
101
+ expectTypeOf<NestedRow["row"]["id"]>().toExtend<RowId>();
102
+ expectTypeOf<undefined>().not.toExtend<NestedRow["row"]["id"]>();
103
+ });
104
+
105
+ it("resolves Props with no type argument", () => {
106
+ expectTypeOf<Props>().toEqualTypeOf<Props<TableRow>>();
107
+ expectTypeOf<Props<UserRow>["rows"]>().toEqualTypeOf<UserRow[]>();
108
+ });
109
+
110
+ it("narrows the dynamic slot keys to the known column keys", () => {
111
+ type Narrowed = keyof UTableDynamicSlots<UserRow, "name" | "qty">;
112
+
113
+ expectTypeOf<"cell-name">().toExtend<Narrowed>();
114
+ expectTypeOf<"header-qty">().toExtend<Narrowed>();
115
+ expectTypeOf<"cell-typo">().not.toExtend<Narrowed>();
116
+ expectTypeOf<"header-typo">().not.toExtend<Narrowed>();
117
+ });
118
+
119
+ it("accepts every dynamic slot key when the columns are not statically known", () => {
120
+ /* Columns from a `ref`, an API, or a plain `string[]` widen `TCol` to `string`. Erroring on
121
+ * a valid slot there would break existing callers, so the keys must stay open. */
122
+ type Loose = keyof UTableDynamicSlots<UserRow>;
123
+
124
+ expectTypeOf<"cell-name">().toExtend<Loose>();
125
+ expectTypeOf<"cell-anything-at-all">().toExtend<Loose>();
126
+ expectTypeOf<"header-whatever">().toExtend<Loose>();
127
+
128
+ expectTypeOf<UTableDynamicSlots<UserRow>>().toEqualTypeOf<
129
+ UTableDynamicSlots<UserRow, string>
130
+ >();
131
+ });
132
+
133
+ it("accepts every dynamic slot key when the columns are empty", () => {
134
+ /* An empty `columns` array infers `TCol` as `never`, which means "not statically known",
135
+ * not "no keys allowed". Without the `[TCol]` tuple guard the mapped type collapses to `{}`
136
+ * and every slot is rejected — the "columns load async, start empty" pattern breaks. */
137
+ type Empty = keyof UTableDynamicSlots<UserRow, never>;
138
+
139
+ /* `toEqualTypeOf`, not `toExtend`: the bug made `Empty` collapse to `never`, and `never`
140
+ * satisfies every `toExtend` assertion vacuously. */
141
+ expectTypeOf<Empty>().toEqualTypeOf<`cell-${string}` | `header-${string}`>();
142
+ expectTypeOf<Empty>().not.toEqualTypeOf<never>();
143
+
144
+ expectTypeOf<UTableDynamicSlots<UserRow, never>>().toEqualTypeOf<
145
+ UTableDynamicSlots<UserRow, string>
146
+ >();
147
+ });
148
+
149
+ it("keeps the row shape intact when the keys are narrowed", () => {
150
+ type NarrowCell = Parameters<NonNullable<UTableSlots<UserRow, "name" | "qty">["cell-name"]>>[0];
151
+
152
+ expectTypeOf<NarrowCell["row"]["name"]>().toEqualTypeOf<string | undefined>();
153
+ expectTypeOf<NarrowCell["row"]["nestedLevel"]>().toEqualTypeOf<number>();
154
+ });
155
+
156
+ it("keeps the slot payloads identical however the keys resolve", () => {
157
+ /* Narrowing must only change which keys exist, never what a slot receives. */
158
+ type WideCell = Parameters<NonNullable<UTableSlots<UserRow>["cell-name"]>>[0];
159
+ type NarrowCell = Parameters<NonNullable<UTableSlots<UserRow, "name" | "qty">["cell-name"]>>[0];
160
+ type EmptyCell = Parameters<NonNullable<UTableSlots<UserRow, never>["cell-name"]>>[0];
161
+
162
+ expectTypeOf<NarrowCell>().toEqualTypeOf<WideCell>();
163
+ expectTypeOf<EmptyCell>().toEqualTypeOf<WideCell>();
164
+
165
+ type WideHeader = Parameters<NonNullable<UTableSlots<UserRow>["header-name"]>>[0];
166
+ type EmptyHeader = Parameters<NonNullable<UTableSlots<UserRow, never>["header-name"]>>[0];
167
+
168
+ expectTypeOf<EmptyHeader>().toEqualTypeOf<WideHeader>();
169
+
170
+ /* No row slot may promise the caller's members, whatever `TCol` is. Only the flattening
171
+ * metadata stays required; `name`/`qty` must never appear among the required keys. */
172
+ expectTypeOf<RequiredKeys<EmptyCell["row"]>>().toEqualTypeOf<RequiredKeys<WideCell["row"]>>();
173
+ expectTypeOf<RequiredKeys<NarrowCell["row"]>>().toEqualTypeOf<RequiredKeys<WideCell["row"]>>();
174
+
175
+ type CallerKeys = Extract<RequiredKeys<SlotRow<UserRow>>, "name" | "qty">;
176
+
177
+ expectTypeOf<CallerKeys>().toEqualTypeOf<never>();
178
+ });
179
+ });
@@ -7,6 +7,7 @@ import UTableRow from "../UTableRow.vue";
7
7
  import UEmpty from "../../ui.container-empty/UEmpty.vue";
8
8
  import ULoaderProgress from "../../ui.loader-progress/ULoaderProgress.vue";
9
9
  import UDivider from "../../ui.container-divider/UDivider.vue";
10
+ import USkeleton from "../../ui.skeleton/USkeleton.vue";
10
11
  import {
11
12
  LoaderProgressSymbol,
12
13
  createLoaderProgress,
@@ -77,7 +78,7 @@ describe("UTable.vue", () => {
77
78
 
78
79
  function mountUTable(props: Props, options = {}) {
79
80
  return mount(UTable, {
80
- props,
81
+ props: { ...props },
81
82
  global: getGlobalOptions(),
82
83
  ...options,
83
84
  });
@@ -149,6 +150,29 @@ describe("UTable.vue", () => {
149
150
  expect(emptyComponent.exists()).toBe(true);
150
151
  });
151
152
 
153
+ it("Skeleton loading – renders skeleton body instead of empty state", () => {
154
+ const component = mountUTable(getDefaultProps({ rows: [], skeletonLoading: true }));
155
+
156
+ expect(component.findComponent(UEmpty).exists()).toBe(false);
157
+ expect(component.findAllComponents(USkeleton).length).toBeGreaterThan(0);
158
+ expect(component.find("table").exists()).toBe(true);
159
+ });
160
+
161
+ it("Skeleton loading – applies skeleton rows and widths from props", () => {
162
+ const component = mountUTable(
163
+ getDefaultProps({
164
+ rows: [],
165
+ skeletonLoading: true,
166
+ skeletonRows: 2,
167
+ skeletonWidths: ["w-1/3"],
168
+ }),
169
+ );
170
+
171
+ expect(component.findAll("tbody tr")).toHaveLength(2);
172
+ expect(component.findAllComponents(USkeleton)).toHaveLength(defaultColumns.length * 2);
173
+ expect(component.findComponent(USkeleton).classes()).toContain("w-1/3");
174
+ });
175
+
152
176
  it("Selectable – renders select all checkbox when selectable is true", () => {
153
177
  const component = mountUTable(getDefaultProps({ selectable: true }));
154
178
 
@@ -221,6 +245,20 @@ describe("UTable.vue", () => {
221
245
  expect(loader.props("loading")).toBe(false);
222
246
  });
223
247
 
248
+ it("Skeleton loading – hides progress loader while skeleton is shown", () => {
249
+ const component = mountUTable(
250
+ getDefaultProps({
251
+ rows: [],
252
+ loading: true,
253
+ skeletonLoading: true,
254
+ }),
255
+ );
256
+
257
+ const loader = component.findComponent(ULoaderProgress);
258
+
259
+ expect(loader.props("loading")).toBe(false);
260
+ });
261
+
224
262
  it("Data Test – applies correct data-test attributes", () => {
225
263
  const dataTest = "test-table";
226
264
 
@@ -644,7 +682,14 @@ describe("UTable.vue", () => {
644
682
  name: "Parent Row",
645
683
  email: "parent@example.com",
646
684
  role: "Admin",
647
- row: [{ id: "1-1", name: "Child", email: "child@example.com", role: "User" }],
685
+ row: [
686
+ {
687
+ id: "1-1",
688
+ name: "Child",
689
+ email: "child@example.com",
690
+ role: "User",
691
+ },
692
+ ],
648
693
  },
649
694
  ];
650
695
 
@@ -710,7 +755,14 @@ describe("UTable.vue", () => {
710
755
  name: "Parent Row",
711
756
  email: "parent@example.com",
712
757
  role: "Admin",
713
- row: [{ id: "1-1", name: "Child", email: "child@example.com", role: "User" }],
758
+ row: [
759
+ {
760
+ id: "1-1",
761
+ name: "Child",
762
+ email: "child@example.com",
763
+ role: "User",
764
+ },
765
+ ],
714
766
  },
715
767
  ];
716
768
 
@@ -792,7 +844,14 @@ describe("UTable.vue", () => {
792
844
  name: "Parent Row",
793
845
  email: "parent@example.com",
794
846
  role: "Admin",
795
- row: [{ id: "1-1", name: "Child", email: "child@example.com", role: "User" }],
847
+ row: [
848
+ {
849
+ id: "1-1",
850
+ name: "Child",
851
+ email: "child@example.com",
852
+ role: "User",
853
+ },
854
+ ],
796
855
  };
797
856
 
798
857
  const component = mountUTable(getDefaultProps({ rows: [expandableRow] }));
@@ -813,7 +872,14 @@ describe("UTable.vue", () => {
813
872
  name: "Parent Row",
814
873
  email: "parent@example.com",
815
874
  role: "Admin",
816
- row: [{ id: "1-1", name: "Child", email: "child@example.com", role: "User" }],
875
+ row: [
876
+ {
877
+ id: "1-1",
878
+ name: "Child",
879
+ email: "child@example.com",
880
+ role: "User",
881
+ },
882
+ ],
817
883
  };
818
884
 
819
885
  const component = mountUTable(
@@ -879,7 +945,12 @@ describe("UTable.vue", () => {
879
945
  email: "child1@example.com",
880
946
  role: "User",
881
947
  row: [
882
- { id: "1-1-1", name: "Grandchild", email: "grandchild@example.com", role: "Guest" },
948
+ {
949
+ id: "1-1-1",
950
+ name: "Grandchild",
951
+ email: "grandchild@example.com",
952
+ role: "Guest",
953
+ },
883
954
  ],
884
955
  },
885
956
  ],
@@ -902,7 +973,14 @@ describe("UTable.vue", () => {
902
973
  name: "Parent Row",
903
974
  email: "parent@example.com",
904
975
  role: "Admin",
905
- row: [{ id: "1-1", name: "Child", email: "child@example.com", role: "User" }],
976
+ row: [
977
+ {
978
+ id: "1-1",
979
+ name: "Child",
980
+ email: "child@example.com",
981
+ role: "User",
982
+ },
983
+ ],
906
984
  };
907
985
 
908
986
  const component = mountUTable(getDefaultProps({ rows: [expandableRow] }));
@@ -0,0 +1,27 @@
1
+ <!-- Type-level fixture: checked by `npm run ts:check`, never mounted.
2
+ Regression guard for an inline empty `columns` array, which infers `TCol` as `never`.
3
+ `never` means "not statically known", not "no keys allowed", so every `cell-*` / `header-*`
4
+ slot must still compile. This shipped broken once and rejected every slot.
5
+
6
+ It needs its own file: next to other `#cell-*` call sites (as in `UTableTypes.vue`) Vue
7
+ reuses a widened instantiation, and the guard stops proving anything. -->
8
+ <template>
9
+ <UTable :columns="[]" :rows="rows">
10
+ <template #cell-name="{ row }">{{ expectOptionalString(row.name) }}</template>
11
+ <template #header-name="{ column }">{{ expectString(column.key) }}</template>
12
+ </UTable>
13
+ </template>
14
+
15
+ <script setup lang="ts">
16
+ import UTable from "../UTable.vue";
17
+
18
+ const rows = [{ id: 1, name: "a", qty: 2 }];
19
+
20
+ function expectString(value: string) {
21
+ return value;
22
+ }
23
+
24
+ function expectOptionalString(value: string | undefined) {
25
+ return value;
26
+ }
27
+ </script>
@@ -0,0 +1,193 @@
1
+ <!-- Type-level fixture: checked by `npm run ts:check`, never mounted.
2
+ Proves slot props infer from the `rows` prop at a real call site. -->
3
+ <template>
4
+ <UTable :columns="columns" :rows="rows">
5
+ <!-- A flattened nested child may lack the parent's members, so they are optional;
6
+ the flattening metadata (`id`, `nestedLevel`) is always set and stays guaranteed. -->
7
+ <template #cell-name="{ row, value, index, cellIndex }">
8
+ {{ expectOptionalString(row.name) }}
9
+ {{ expectOptionalNumber(row.qty) }}
10
+ {{ expectNumber(row.id) }}
11
+ {{ expectNumber(row.nestedLevel) }}
12
+ {{ expectNumber(index) }}
13
+ {{ expectNumber(cellIndex) }}
14
+ {{ value }}
15
+ </template>
16
+
17
+ <template #expand="{ row, expanded, index }">
18
+ {{ expectOptionalString(row.name) }}
19
+ {{ expectNumber(row.nestedLevel) }}
20
+ {{ expectBoolean(expanded) }}
21
+ {{ expectNumber(index) }}
22
+ </template>
23
+
24
+ <template #nested-row="{ row, nestedLevel }">
25
+ {{ expectOptionalString(row.name) }}
26
+ {{ expectNumber(row.nestedLevel) }}
27
+ {{ expectNumber(nestedLevel) }}
28
+ </template>
29
+
30
+ <template #header-actions="{ selectedRows }">
31
+ {{ expectNumber(selectedRows.length) }}
32
+ </template>
33
+
34
+ <template #header-name="{ column, index }">
35
+ {{ expectString(column.key) }}
36
+ {{ expectNumber(index) }}
37
+ </template>
38
+
39
+ <template #header-counter="{ total }">{{ expectNumber(total) }}</template>
40
+ <template #footer="{ colsCount }">{{ expectNumber(colsCount) }}</template>
41
+
42
+ <template #cell-qty="{ row }">
43
+ <!-- @vue-expect-error `nonExistent` is not a key of the inferred row. -->
44
+ {{ row.nonExistent }}
45
+ </template>
46
+
47
+ <!-- @vue-expect-error `typo` is not one of the literal column keys. -->
48
+ <template #cell-typo>{{ "" }}</template>
49
+
50
+ <!-- @vue-expect-error `typo` is not one of the literal column keys. -->
51
+ <template #header-typo>{{ "" }}</template>
52
+ </UTable>
53
+
54
+ <!-- Literal object columns narrow the slot keys the same way as literal strings. -->
55
+ <UTable :columns="objectColumns" :rows="rows">
56
+ <template #cell-name="{ row }">{{ expectOptionalString(row.name) }}</template>
57
+ <template #header-qty="{ column }">{{ expectString(column.key) }}</template>
58
+
59
+ <!-- @vue-expect-error `typo` is not one of the literal column keys. -->
60
+ <template #cell-typo>{{ "" }}</template>
61
+ </UTable>
62
+
63
+ <!-- Rows from a non-literal source still expose the declared row shape. -->
64
+ <UTable :columns="columns" :rows="typedRows">
65
+ <template #cell-name="{ row }">{{ expectOptionalString(row.name) }}</template>
66
+ </UTable>
67
+
68
+ <!-- Graceful degradation: columns from a `ref<Column[]>` cannot be narrowed, so EVERY
69
+ `cell-*` / `header-*` slot must still compile. A false error here would break
70
+ existing users, so it matters more than catching a typo. -->
71
+ <UTable :columns="dynamicColumns" :rows="rows">
72
+ <template #cell-name="{ row, index }">
73
+ {{ expectOptionalString(row.name) }}{{ expectNumber(index) }}
74
+ </template>
75
+ <template #cell-anything-at-all="{ row }">{{ expectOptionalNumber(row.qty) }}</template>
76
+ <template #header-name="{ column }">{{ expectString(column.key) }}</template>
77
+ <template #header-whatever="{ index }">{{ expectNumber(index) }}</template>
78
+ </UTable>
79
+
80
+ <!-- Same degradation for a plain `string[]` with no literal types. -->
81
+ <UTable :columns="looseColumns" :rows="rows">
82
+ <template #cell-name="{ row }">{{ expectOptionalString(row.name) }}</template>
83
+ <template #cell-unknown-key="{ row }">{{ expectOptionalNumber(row.qty) }}</template>
84
+ </UTable>
85
+
86
+ <!-- A `const` array without `as const` widens to `string[]`, so it degrades too. Narrowing
87
+ is opt-in via `as const`; it must never cost a false error on the plain form. -->
88
+ <UTable :columns="widenedColumns" :rows="rows">
89
+ <template #cell-name="{ row }">{{ expectOptionalString(row.name) }}</template>
90
+ <template #cell-not-a-column="{ row }">{{ expectOptionalNumber(row.qty) }}</template>
91
+ </UTable>
92
+
93
+ <!-- Regression guard: an EMPTY `columns` array infers `TCol` as `never`. That is "not statically
94
+ known", not "no keys allowed" — every `cell-*` / `header-*` slot must still compile.
95
+ "columns load async, start empty" is the common real-world pattern; this shipped broken once. -->
96
+ <UTable :columns="emptyRefColumns" :rows="rows">
97
+ <template #cell-name="{ row }">{{ expectOptionalString(row.name) }}</template>
98
+ <template #header-name="{ column }">{{ expectString(column.key) }}</template>
99
+ </UTable>
100
+
101
+ <UTable :columns="emptyInferredRefColumns" :rows="rows">
102
+ <template #cell-name="{ row }">{{ expectOptionalString(row.name) }}</template>
103
+ <template #header-name="{ column }">{{ expectString(column.key) }}</template>
104
+ </UTable>
105
+
106
+ <UTable :columns="emptyComputedColumns" :rows="rows">
107
+ <template #cell-name="{ row }">{{ expectOptionalString(row.name) }}</template>
108
+ <template #header-name="{ column }">{{ expectString(column.key) }}</template>
109
+ </UTable>
110
+
111
+ <UTable :columns="emptyConstColumns" :rows="rows">
112
+ <template #cell-name="{ row }">{{ expectOptionalString(row.name) }}</template>
113
+ <template #header-name="{ column }">{{ expectString(column.key) }}</template>
114
+ </UTable>
115
+
116
+ <!-- Regression guard: a row slot must never promise the caller's members as present.
117
+ `getFlatRows` recurses into `row.row`, and a nested child is typed `TableRow`, not `TRow` —
118
+ `#nested-row` in particular fires ONLY for such children. -->
119
+ <UTable :columns="columns" :rows="nestedRows">
120
+ <template #nested-row="{ row }">
121
+ <!-- @vue-expect-error a flattened nested child need not carry `name`. -->
122
+ {{ expectString(row.name) }}
123
+ </template>
124
+ <template #expand="{ row }">
125
+ <!-- @vue-expect-error a flattened nested child need not carry `qty`. -->
126
+ {{ expectNumber(row.qty) }}
127
+ </template>
128
+ <template #cell-name="{ row }">
129
+ <!-- @vue-expect-error a flattened nested child need not carry `name`. -->
130
+ {{ expectString(row.name) }}
131
+ </template>
132
+ <template #header-actions="{ selectedRows }">
133
+ <!-- @vue-expect-error select-all flattens nested children into the selection. -->
134
+ {{ expectString(selectedRows[0].name) }}
135
+ </template>
136
+ </UTable>
137
+ </template>
138
+
139
+ <script setup lang="ts">
140
+ import { computed, ref } from "vue";
141
+
142
+ import UTable from "../UTable.vue";
143
+
144
+ import type { Column } from "../types";
145
+
146
+ interface UserRow {
147
+ id: number;
148
+ name: string;
149
+ qty: number;
150
+ }
151
+
152
+ const columns = ["name", "qty"] as const;
153
+ const objectColumns = [{ key: "name" }, { key: "qty" }] as const;
154
+
155
+ /* Non-literal column sources — slot keys must degrade to `cell-${string}`. */
156
+ const dynamicColumns = ref<Column[]>([]);
157
+ const looseColumns: string[] = ["name", "qty"];
158
+ const widenedColumns = ["name", "qty"];
159
+
160
+ /* Empty column sources — `TCol` infers as `never` and must degrade, not collapse.
161
+ The inline `:columns="[]"` form lives in `UTableEmptyColumnsTypes.vue`: alongside other
162
+ `#cell-*` call sites here, Vue reuses a widened instantiation and the guard goes vacuous. */
163
+ const emptyRefColumns = ref<Column[]>([]);
164
+ const emptyInferredRefColumns = ref([]);
165
+ const emptyComputedColumns = computed(() => []);
166
+ const emptyConstColumns = [] as const;
167
+
168
+ const rows = [{ id: 1, name: "a", qty: 2 }];
169
+ const typedRows = ref<UserRow[]>([]);
170
+
171
+ /* A parent whose child lacks the parent's members — exactly what `getFlatRows` emits. */
172
+ const nestedRows = [{ id: 1, name: "parent", qty: 2, row: [{ id: 2, label: "child" }] }];
173
+
174
+ function expectString(value: string) {
175
+ return value;
176
+ }
177
+
178
+ function expectNumber(value: number) {
179
+ return value;
180
+ }
181
+
182
+ function expectBoolean(value: boolean) {
183
+ return value;
184
+ }
185
+
186
+ function expectOptionalString(value: string | undefined) {
187
+ return value;
188
+ }
189
+
190
+ function expectOptionalNumber(value: number | undefined) {
191
+ return value;
192
+ }
193
+ </script>