@revenexx/studio-shared 0.3.3 → 0.4.1

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 (205) hide show
  1. package/dist/module.d.mts +7 -3
  2. package/dist/module.json +2 -2
  3. package/dist/module.mjs +10 -4
  4. package/dist/runtime/components/Dashboard/DashboardConfirmDialog.d.vue.ts +2 -1
  5. package/dist/runtime/components/Dashboard/DashboardConfirmDialog.vue +1 -1
  6. package/dist/runtime/components/Dashboard/DashboardConfirmDialog.vue.d.ts +2 -1
  7. package/dist/runtime/components/Dashboard/DashboardControls.d.vue.ts +8 -7
  8. package/dist/runtime/components/Dashboard/DashboardControls.vue.d.ts +8 -7
  9. package/dist/runtime/components/Dashboard/DashboardGrid.d.vue.ts +2 -1
  10. package/dist/runtime/components/Dashboard/DashboardGrid.vue +45 -1
  11. package/dist/runtime/components/Dashboard/DashboardGrid.vue.d.ts +2 -1
  12. package/dist/runtime/components/Dashboard/DashboardNameDialog.d.vue.ts +2 -1
  13. package/dist/runtime/components/Dashboard/DashboardNameDialog.vue +1 -1
  14. package/dist/runtime/components/Dashboard/DashboardNameDialog.vue.d.ts +2 -1
  15. package/dist/runtime/components/Dashboard/DashboardPickerDialog.d.vue.ts +2 -1
  16. package/dist/runtime/components/Dashboard/DashboardPickerDialog.vue +1 -1
  17. package/dist/runtime/components/Dashboard/DashboardPickerDialog.vue.d.ts +2 -1
  18. package/dist/runtime/components/Dashboard/DashboardShareDialog.d.vue.ts +2 -1
  19. package/dist/runtime/components/Dashboard/DashboardShareDialog.vue +1 -1
  20. package/dist/runtime/components/Dashboard/DashboardShareDialog.vue.d.ts +2 -1
  21. package/dist/runtime/components/Dashboard/DashboardTopBar.d.vue.ts +9 -4
  22. package/dist/runtime/components/Dashboard/DashboardTopBar.vue.d.ts +9 -4
  23. package/dist/runtime/components/Dashboard/DashboardWidgetConfigDialog.d.vue.ts +8 -6
  24. package/dist/runtime/components/Dashboard/DashboardWidgetConfigDialog.vue +1 -1
  25. package/dist/runtime/components/Dashboard/DashboardWidgetConfigDialog.vue.d.ts +8 -6
  26. package/dist/runtime/components/Dashboard/DashboardWidgetFrame.d.vue.ts +5 -4
  27. package/dist/runtime/components/Dashboard/DashboardWidgetFrame.vue +14 -3
  28. package/dist/runtime/components/Dashboard/DashboardWidgetFrame.vue.d.ts +5 -4
  29. package/dist/runtime/components/Dashboard/DashboardWidgetHost.d.vue.ts +2 -1
  30. package/dist/runtime/components/Dashboard/DashboardWidgetHost.vue.d.ts +2 -1
  31. package/dist/runtime/components/Dashboard/DashboardWidgetIcon.d.vue.ts +2 -1
  32. package/dist/runtime/components/Dashboard/DashboardWidgetIcon.vue.d.ts +2 -1
  33. package/dist/runtime/components/Dashboard/DashboardWidgetLibrary.d.vue.ts +2 -1
  34. package/dist/runtime/components/Dashboard/DashboardWidgetLibrary.vue.d.ts +2 -1
  35. package/dist/runtime/components/Dashboard/DashboardWidgetPreview.d.vue.ts +2 -1
  36. package/dist/runtime/components/Dashboard/DashboardWidgetPreview.vue.d.ts +2 -1
  37. package/dist/runtime/components/DataTable/ColumnsMenu.d.vue.ts +2 -1
  38. package/dist/runtime/components/DataTable/ColumnsMenu.vue.d.ts +2 -1
  39. package/dist/runtime/components/DataTable/DataTable.d.vue.ts +120 -37
  40. package/dist/runtime/components/DataTable/DataTable.vue +493 -53
  41. package/dist/runtime/components/DataTable/DataTable.vue.d.ts +120 -37
  42. package/dist/runtime/components/DataTable/FacetChips.d.vue.ts +2 -1
  43. package/dist/runtime/components/DataTable/FacetChips.vue.d.ts +2 -1
  44. package/dist/runtime/components/DataTable/FacetPanel.d.vue.ts +6 -5
  45. package/dist/runtime/components/DataTable/FacetPanel.vue +57 -8
  46. package/dist/runtime/components/DataTable/FacetPanel.vue.d.ts +6 -5
  47. package/dist/runtime/components/DataTable/FilterMenu.d.vue.ts +6 -5
  48. package/dist/runtime/components/DataTable/FilterMenu.vue +5 -5
  49. package/dist/runtime/components/DataTable/FilterMenu.vue.d.ts +6 -5
  50. package/dist/runtime/components/DataTable/SavedViewsMenu.d.vue.ts +2 -1
  51. package/dist/runtime/components/DataTable/SavedViewsMenu.vue +61 -12
  52. package/dist/runtime/components/DataTable/SavedViewsMenu.vue.d.ts +2 -1
  53. package/dist/runtime/components/DataTable/SortMenu.d.vue.ts +4 -2
  54. package/dist/runtime/components/DataTable/SortMenu.vue +4 -1
  55. package/dist/runtime/components/DataTable/SortMenu.vue.d.ts +4 -2
  56. package/dist/runtime/components/DataTable/ToolbarLabelTrigger.d.vue.ts +23 -0
  57. package/dist/runtime/components/DataTable/ToolbarLabelTrigger.vue +29 -0
  58. package/dist/runtime/components/DataTable/ToolbarLabelTrigger.vue.d.ts +23 -0
  59. package/dist/runtime/components/DataTable/ViewModeSwitch.d.vue.ts +4 -2
  60. package/dist/runtime/components/DataTable/ViewModeSwitch.vue.d.ts +4 -2
  61. package/dist/runtime/components/DataTable/types.d.ts +17 -0
  62. package/dist/runtime/components/Form/FieldCell.d.vue.ts +32 -0
  63. package/dist/runtime/components/Form/FieldCell.vue +46 -0
  64. package/dist/runtime/components/Form/FieldCell.vue.d.ts +32 -0
  65. package/dist/runtime/components/Form/FieldGrid.d.vue.ts +13 -0
  66. package/dist/runtime/components/Form/FieldGrid.vue +11 -0
  67. package/dist/runtime/components/Form/FieldGrid.vue.d.ts +13 -0
  68. package/dist/runtime/components/Form/FormContextCard.d.vue.ts +33 -0
  69. package/dist/runtime/components/Form/FormContextCard.vue +56 -0
  70. package/dist/runtime/components/Form/FormContextCard.vue.d.ts +33 -0
  71. package/dist/runtime/components/Form/FormLayout.d.vue.ts +47 -0
  72. package/dist/runtime/components/Form/FormLayout.vue +169 -0
  73. package/dist/runtime/components/Form/FormLayout.vue.d.ts +47 -0
  74. package/dist/runtime/components/Form/FormSection.d.vue.ts +25 -0
  75. package/dist/runtime/components/Form/FormSection.vue +48 -0
  76. package/dist/runtime/components/Form/FormSection.vue.d.ts +25 -0
  77. package/dist/runtime/components/Form/SaveBar.d.vue.ts +44 -0
  78. package/dist/runtime/components/Form/SaveBar.vue +71 -0
  79. package/dist/runtime/components/Form/SaveBar.vue.d.ts +44 -0
  80. package/dist/runtime/components/Media/MediaFolderTree.d.vue.ts +6 -5
  81. package/dist/runtime/components/Media/MediaFolderTree.vue +1 -2
  82. package/dist/runtime/components/Media/MediaFolderTree.vue.d.ts +6 -5
  83. package/dist/runtime/components/Media/MediaPickerDialog.d.vue.ts +13 -1
  84. package/dist/runtime/components/Media/MediaPickerDialog.vue +206 -54
  85. package/dist/runtime/components/Media/MediaPickerDialog.vue.d.ts +13 -1
  86. package/dist/runtime/components/Media/MediaUploader.d.vue.ts +2 -1
  87. package/dist/runtime/components/Media/MediaUploader.vue +1 -1
  88. package/dist/runtime/components/Media/MediaUploader.vue.d.ts +2 -1
  89. package/dist/runtime/components/Shared/AppIcon.d.vue.ts +21 -0
  90. package/dist/runtime/components/Shared/AppIcon.vue +47 -0
  91. package/dist/runtime/components/Shared/AppIcon.vue.d.ts +21 -0
  92. package/dist/runtime/components/Shared/BuildLogsFlyout.d.vue.ts +2 -1
  93. package/dist/runtime/components/Shared/BuildLogsFlyout.vue.d.ts +2 -1
  94. package/dist/runtime/components/Shared/CardSizeToggle.d.vue.ts +11 -0
  95. package/dist/runtime/components/Shared/CardSizeToggle.vue +19 -0
  96. package/dist/runtime/components/Shared/CardSizeToggle.vue.d.ts +11 -0
  97. package/dist/runtime/components/Shared/DetailSheet.d.vue.ts +60 -0
  98. package/dist/runtime/components/Shared/DetailSheet.vue +211 -0
  99. package/dist/runtime/components/Shared/DetailSheet.vue.d.ts +60 -0
  100. package/dist/runtime/components/Shared/EmptyNote.d.vue.ts +19 -0
  101. package/dist/runtime/components/Shared/EmptyNote.vue +25 -0
  102. package/dist/runtime/components/Shared/EmptyNote.vue.d.ts +19 -0
  103. package/dist/runtime/components/Shared/FloatingPanel.d.vue.ts +7 -6
  104. package/dist/runtime/components/Shared/FloatingPanel.vue.d.ts +7 -6
  105. package/dist/runtime/components/Shared/GitDeploymentSettings.d.vue.ts +2 -1
  106. package/dist/runtime/components/Shared/GitDeploymentSettings.vue.d.ts +2 -1
  107. package/dist/runtime/components/Shared/KpiTile.d.vue.ts +8 -1
  108. package/dist/runtime/components/Shared/KpiTile.vue +10 -9
  109. package/dist/runtime/components/Shared/KpiTile.vue.d.ts +8 -1
  110. package/dist/runtime/components/Shared/ListSurface.d.vue.ts +3 -2
  111. package/dist/runtime/components/Shared/ListSurface.vue.d.ts +3 -2
  112. package/dist/runtime/components/Shared/PageHeader.d.vue.ts +7 -6
  113. package/dist/runtime/components/Shared/PageHeader.vue +15 -4
  114. package/dist/runtime/components/Shared/PageHeader.vue.d.ts +7 -6
  115. package/dist/runtime/components/Shared/RecordLayout.d.vue.ts +8 -7
  116. package/dist/runtime/components/Shared/RecordLayout.vue +1 -2
  117. package/dist/runtime/components/Shared/RecordLayout.vue.d.ts +8 -7
  118. package/dist/runtime/components/Shared/RecordRailCard.d.vue.ts +3 -2
  119. package/dist/runtime/components/Shared/RecordRailCard.vue +3 -3
  120. package/dist/runtime/components/Shared/RecordRailCard.vue.d.ts +3 -2
  121. package/dist/runtime/components/Shared/SelectionBar.d.vue.ts +22 -0
  122. package/dist/runtime/components/Shared/SelectionBar.vue +40 -0
  123. package/dist/runtime/components/Shared/SelectionBar.vue.d.ts +22 -0
  124. package/dist/runtime/components/Shared/SidebarNav.d.vue.ts +39 -4
  125. package/dist/runtime/components/Shared/SidebarNav.vue +71 -25
  126. package/dist/runtime/components/Shared/SidebarNav.vue.d.ts +39 -4
  127. package/dist/runtime/components/Shared/SidebarNavAreas.d.vue.ts +7 -0
  128. package/dist/runtime/components/Shared/SidebarNavAreas.vue +291 -0
  129. package/dist/runtime/components/Shared/SidebarNavAreas.vue.d.ts +7 -0
  130. package/dist/runtime/components/Shared/SidebarNavLink.d.vue.ts +3 -10
  131. package/dist/runtime/components/Shared/SidebarNavLink.vue +31 -5
  132. package/dist/runtime/components/Shared/SidebarNavLink.vue.d.ts +3 -10
  133. package/dist/runtime/components/Shared/SortHeader.d.vue.ts +3 -2
  134. package/dist/runtime/components/Shared/SortHeader.vue +6 -2
  135. package/dist/runtime/components/Shared/SortHeader.vue.d.ts +3 -2
  136. package/dist/runtime/components/Shared/StateCard.d.vue.ts +6 -5
  137. package/dist/runtime/components/Shared/StateCard.vue +1 -1
  138. package/dist/runtime/components/Shared/StateCard.vue.d.ts +6 -5
  139. package/dist/runtime/components/Shared/StudioCard.d.vue.ts +5 -4
  140. package/dist/runtime/components/Shared/StudioCard.vue.d.ts +5 -4
  141. package/dist/runtime/components/Shared/TabBar.d.vue.ts +4 -21
  142. package/dist/runtime/components/Shared/TabBar.vue +30 -13
  143. package/dist/runtime/components/Shared/TabBar.vue.d.ts +4 -21
  144. package/dist/runtime/components/Shared/ViewTab.d.vue.ts +2 -1
  145. package/dist/runtime/components/Shared/ViewTab.vue.d.ts +2 -1
  146. package/dist/runtime/components/Shared/Wizard.d.vue.ts +20 -7
  147. package/dist/runtime/components/Shared/Wizard.vue +9 -5
  148. package/dist/runtime/components/Shared/Wizard.vue.d.ts +20 -7
  149. package/dist/runtime/composables/useDetailSheet.d.ts +20 -0
  150. package/dist/runtime/composables/useDetailSheet.js +50 -0
  151. package/dist/runtime/composables/useMediaLibrary.d.ts +12 -0
  152. package/dist/runtime/composables/usePlatformTenant.d.ts +6 -0
  153. package/dist/runtime/composables/useSavedViews.d.ts +11 -1
  154. package/dist/runtime/composables/useSavedViews.js +4 -4
  155. package/dist/runtime/composables/useWidgetFigures.d.ts +12 -0
  156. package/dist/runtime/composables/useWidgetFigures.js +24 -0
  157. package/dist/runtime/composables/useWidgetHeader.d.ts +15 -0
  158. package/dist/runtime/composables/useWidgetHeader.js +15 -0
  159. package/dist/runtime/lib/activeTenant.d.ts +58 -0
  160. package/dist/runtime/lib/activeTenant.js +45 -0
  161. package/dist/runtime/lib/appIcon.d.ts +48 -0
  162. package/dist/runtime/lib/appIcon.js +27 -0
  163. package/dist/runtime/lib/auditEnvelope.d.ts +43 -0
  164. package/dist/runtime/lib/auditEnvelope.js +55 -0
  165. package/dist/runtime/lib/cardSize.d.ts +13 -0
  166. package/dist/runtime/lib/cardSize.js +20 -0
  167. package/dist/runtime/lib/dashboardIcons.js +2 -0
  168. package/dist/runtime/lib/dataTableFilters.d.ts +22 -0
  169. package/dist/runtime/lib/dataTableFilters.js +5 -0
  170. package/dist/runtime/lib/detailSheet.d.ts +43 -0
  171. package/dist/runtime/lib/detailSheet.js +34 -0
  172. package/dist/runtime/lib/facets.d.ts +13 -0
  173. package/dist/runtime/lib/formLayout.d.ts +164 -0
  174. package/dist/runtime/lib/formLayout.js +117 -0
  175. package/dist/runtime/lib/formLayoutContext.d.ts +26 -0
  176. package/dist/runtime/lib/formLayoutContext.js +1 -0
  177. package/dist/runtime/lib/gridTracks.d.ts +14 -1
  178. package/dist/runtime/lib/gridTracks.js +13 -2
  179. package/dist/runtime/lib/isoLists.d.ts +37 -2
  180. package/dist/runtime/lib/isoLists.js +54 -1
  181. package/dist/runtime/lib/notice.d.ts +14 -0
  182. package/dist/runtime/lib/notice.js +19 -0
  183. package/dist/runtime/lib/pageSize.d.ts +15 -0
  184. package/dist/runtime/lib/pageSize.js +26 -0
  185. package/dist/runtime/lib/rowMove.d.ts +38 -0
  186. package/dist/runtime/lib/rowMove.js +20 -0
  187. package/dist/runtime/lib/sidebarAreas.d.ts +46 -0
  188. package/dist/runtime/lib/sidebarAreas.js +25 -0
  189. package/dist/runtime/lib/sidebarNav.d.ts +21 -2
  190. package/dist/runtime/lib/sidebarNav.js +23 -4
  191. package/dist/runtime/lib/solarIconMap.d.ts +16 -0
  192. package/dist/runtime/lib/solarIconMap.js +326 -0
  193. package/dist/runtime/lib/solarIcons.d.ts +37 -0
  194. package/dist/runtime/lib/solarIcons.js +25 -0
  195. package/dist/runtime/lib/swipeToClose.d.ts +25 -0
  196. package/dist/runtime/lib/swipeToClose.js +118 -0
  197. package/dist/runtime/lib/tableExport.d.ts +31 -0
  198. package/dist/runtime/lib/tableExport.js +143 -0
  199. package/dist/runtime/lib/talkback.d.ts +5 -0
  200. package/dist/runtime/lib/talkback.js +8 -0
  201. package/dist/runtime/lib/talkbackTokenBatch.d.ts +35 -0
  202. package/dist/runtime/lib/talkbackTokenBatch.js +101 -0
  203. package/dist/runtime/types/nav.d.ts +9 -0
  204. package/dist/types.d.mts +8 -0
  205. package/package.json +4 -3
@@ -1,8 +1,11 @@
1
1
  <script setup>
2
2
  import {
3
+ AlignVerticalSpacing,
4
+ AltArrowRight,
3
5
  Eye,
4
6
  EyeClosed,
5
7
  Filter,
8
+ HamburgerMenu as DragHandle,
6
9
  ListArrowDownMinimalistic,
7
10
  Magnifier as Search,
8
11
  SidebarMinimalistic,
@@ -18,14 +21,18 @@ import {
18
21
  } from "@revenexx/studio";
19
22
  import { orderColumnsBy } from "../../lib/columnOrder";
20
23
  import { initiallyHidden, visibleColumnsOf } from "../../lib/columnVisibility";
21
- import { countByValue, matchesFilter } from "../../lib/dataTableFilters";
22
- import { gridTemplateColumns } from "../../lib/gridTracks";
24
+ import { facetCounts, matchesFilter } from "../../lib/dataTableFilters";
25
+ import { gridTemplateColumns, headerFloor } from "../../lib/gridTracks";
26
+ import { dropEdge, dropTargetIndex, moveKey, stepTarget } from "../../lib/rowMove";
23
27
  import SortHeader from "../Shared/SortHeader.vue";
24
28
  import FacetChips from "./FacetChips.vue";
25
29
  import FacetPanel from "./FacetPanel.vue";
26
30
  import FilterMenu from "./FilterMenu.vue";
27
31
  import SavedViewsMenu from "./SavedViewsMenu.vue";
32
+ import ToolbarLabelTrigger from "./ToolbarLabelTrigger.vue";
28
33
  import SortMenu from "./SortMenu.vue";
34
+ import { DEFAULT_PAGE_SIZE, pageSizeOptions as pageSizeChoices, readPageSize, writePageSize } from "../../lib/pageSize";
35
+ const RIGHT_ALIGNED = "text-right pr-4";
29
36
  const props = defineProps({
30
37
  data: { type: Array, required: true },
31
38
  columns: { type: Array, required: true },
@@ -40,6 +47,7 @@ const props = defineProps({
40
47
  viewModes: { type: [Array, null], required: false, default: null },
41
48
  rail: { type: [Boolean, Object], required: false, default: false },
42
49
  scrollBody: { type: [Boolean, String, Object], required: false },
50
+ reserveBottom: { type: Number, required: false },
43
51
  pagination: { type: [Boolean, Object], required: false, default: true },
44
52
  defaultSort: { type: Object, required: false },
45
53
  serverMode: { type: Boolean, required: false, default: false },
@@ -47,11 +55,21 @@ const props = defineProps({
47
55
  loading: { type: Boolean, required: false, default: false },
48
56
  emptyText: { type: String, required: false },
49
57
  groupBy: { type: [Function, null], required: false, default: null },
50
- groupEmptyLabel: { type: String, required: false }
58
+ groupEmptyLabel: { type: String, required: false },
59
+ activeRowKey: { type: [String, Number, null], required: false },
60
+ rowClass: { type: [Function, null], required: false, default: null },
61
+ reorderable: { type: Boolean, required: false, default: false },
62
+ reorderDisabled: { type: [String, null], required: false, default: null },
63
+ mobileCards: { type: Boolean, required: false, default: true },
64
+ densityKey: { type: String, required: false, default: "cockpit.datatable.density" },
65
+ pageSizeKey: { type: [String, null], required: false, default: null },
66
+ embedded: { type: Boolean, required: false, default: false },
67
+ toolbar: { type: Boolean, required: false, default: true }
51
68
  });
52
- const emit = defineEmits(["rowClick", "stateChange"]);
69
+ const emit = defineEmits(["rowClick", "stateChange", "orderChange", "rowMove"]);
70
+ defineSlots();
53
71
  const t = useT();
54
- const query = ref("");
72
+ const query = defineModel("query", { type: String, ...{ default: "" } });
55
73
  const sort = defineModel("sort", { type: Object, ...{
56
74
  default: () => ({
57
75
  column: null,
@@ -61,19 +79,66 @@ const sort = defineModel("sort", { type: Object, ...{
61
79
  const page = defineModel("page", { type: Number, ...{ default: 0 } });
62
80
  const viewMode = defineModel("viewMode", { type: String, ...{ default: "list" } });
63
81
  const railOpen = defineModel("railOpen", { type: Boolean, ...{ default: true } });
82
+ const density = defineModel("density", { type: String, ...{ default: "standard" } });
83
+ if (import.meta.client) {
84
+ try {
85
+ const stored = localStorage.getItem(props.densityKey);
86
+ if (stored === "compact" || stored === "standard") density.value = stored;
87
+ } catch {
88
+ }
89
+ }
90
+ function setDensity(next) {
91
+ density.value = next;
92
+ try {
93
+ localStorage.setItem(props.densityKey, next);
94
+ } catch {
95
+ }
96
+ }
97
+ const narrow = ref(false);
98
+ let narrowMql = null;
99
+ const onNarrow = (e) => {
100
+ narrow.value = e.matches;
101
+ };
102
+ onMounted(() => {
103
+ narrowMql = window.matchMedia("(max-width: 767px)");
104
+ narrow.value = narrowMql.matches;
105
+ narrowMql.addEventListener("change", onNarrow);
106
+ });
107
+ onBeforeUnmount(() => narrowMql?.removeEventListener("change", onNarrow));
108
+ const cardMode = computed(() => props.mobileCards && narrow.value);
109
+ const CONTROL_KEYS = /* @__PURE__ */ new Set(["__select", "__actions", "__move"]);
110
+ const STATUS_KEY = /(^|_)(status|state|phase|stage|lifecycle)(_|$)/i;
111
+ const cardLayout = computed(() => {
112
+ const cols = visibleColumns.value;
113
+ const data = cols.filter((c) => !CONTROL_KEYS.has(c.key) && c.card !== "hidden");
114
+ const title = data.find((c) => c.card === "title") ?? data.find((c) => c.primary) ?? data[0] ?? null;
115
+ const status = data.find((c) => c.card === "status") ?? data.find((c) => c !== title && c.card !== "meta" && STATUS_KEY.test(c.key)) ?? null;
116
+ const explicitMeta = data.filter((c) => c.card === "meta");
117
+ const meta = (explicitMeta.length ? explicitMeta : data.filter((c) => c !== title && c !== status)).slice(0, 4);
118
+ return {
119
+ select: cols.find((c) => c.key === "__select") ?? null,
120
+ actions: cols.find((c) => c.key === "__actions") ?? null,
121
+ title,
122
+ status,
123
+ meta
124
+ };
125
+ });
64
126
  const facetSelection = defineModel("facetSelection", { type: Object, ...{ default: () => ({}) } });
65
127
  const panelFacets = computed(() => props.facets);
66
128
  if (props.defaultSort && sort.value.column == null) {
67
129
  sort.value = { column: props.defaultSort.column, direction: props.defaultSort.direction };
68
130
  }
131
+ const pageSizeTable = computed(() => props.pageSizeKey ?? props.savedViews?.storageKey ?? null);
69
132
  const pageSize = ref(
70
- typeof props.pagination === "object" && props.pagination?.pageSize ? props.pagination.pageSize : 20
133
+ readPageSize(pageSizeTable.value) ?? (typeof props.pagination === "object" && props.pagination?.pageSize ? props.pagination.pageSize : DEFAULT_PAGE_SIZE)
71
134
  );
72
- const pageSizeOptions = computed(() => Array.from(/* @__PURE__ */ new Set([10, 20, 50, 100, pageSize.value])).sort((a, b) => a - b));
135
+ const pageSizeOptions = computed(() => pageSizeChoices(pageSize.value));
136
+ watch(pageSize, (n) => writePageSize(pageSizeTable.value, n));
73
137
  const hidden = ref(initiallyHidden(props.columns));
74
138
  const columnOrder = ref([]);
75
139
  const arrangedColumns = computed(() => orderColumnsBy(props.columns, columnOrder.value));
76
140
  const visibleColumns = computed(() => visibleColumnsOf(arrangedColumns.value, hidden.value));
141
+ defineExpose({ visibleColumns });
77
142
  const pickerColumns = computed(() => arrangedColumns.value.filter((c) => (c.label ?? "").trim().length > 0).map((c) => ({
78
143
  key: c.key,
79
144
  label: c.label ?? c.key,
@@ -120,22 +185,22 @@ const filterFacets = computed(() => filterDims.value.map((d) => {
120
185
  };
121
186
  }));
122
187
  function dimCounts(d) {
123
- return countByValue(props.data, d.getter);
188
+ return facetCounts(queried.value, filterDims.value, activeFilters.value, d);
124
189
  }
125
- const filtered = computed(() => {
126
- if (props.serverMode || !filterDims.value.length) return props.data;
127
- const active = filterDims.value.filter((d) => activeFilters.value[d.key]?.length);
128
- if (!active.length) return props.data;
129
- return props.data.filter((row) => active.every((d) => matchesFilter(d.getter, row, activeFilters.value[d.key])));
130
- });
131
- const searched = computed(() => {
190
+ const queried = computed(() => {
132
191
  if (props.serverMode) return props.data;
133
192
  const q = query.value.trim().toLowerCase();
134
- if (!q) return filtered.value;
135
- return filtered.value.filter(
193
+ if (!q) return props.data;
194
+ return props.data.filter(
136
195
  (row) => props.columns.some((col) => searchOf(col, row).includes(q))
137
196
  );
138
197
  });
198
+ const searched = computed(() => {
199
+ if (props.serverMode || !filterDims.value.length) return queried.value;
200
+ const active = filterDims.value.filter((d) => activeFilters.value[d.key]?.length);
201
+ if (!active.length) return queried.value;
202
+ return queried.value.filter((row) => active.every((d) => matchesFilter(d.getter, row, activeFilters.value[d.key])));
203
+ });
139
204
  function sortKey(col, row) {
140
205
  if (col.sortGetter) return col.sortGetter(row);
141
206
  const raw = row[col.key];
@@ -164,9 +229,24 @@ const sorted = computed(() => {
164
229
  return 0;
165
230
  });
166
231
  });
232
+ watch(sorted, (rows) => emit("orderChange", rows.map(rowKeyFor)), { immediate: true });
167
233
  const totalCount = computed(
168
234
  () => props.serverMode ? props.total ?? props.data.length : sorted.value.length
169
235
  );
236
+ const activeNarrowing = computed(() => filterDims.value.map((d) => ({
237
+ label: d.label,
238
+ values: (activeFilters.value[d.key] ?? []).map((v) => d.options.find((o) => o.value === v)?.label ?? v)
239
+ })).filter((d) => d.values.length > 0));
240
+ function clearNarrowing() {
241
+ query.value = "";
242
+ activeFilters.value = {};
243
+ facetSelection.value = {};
244
+ }
245
+ const liveStatus = computed(() => {
246
+ if (!showTable.value) return "";
247
+ if (props.loading) return t("shared.datatable.state.loading");
248
+ return totalCount.value === 0 ? t("shared.datatable.footer.noresults") : t("shared.datatable.footer.showing", { start: rangeStart.value, end: rangeEnd.value, total: totalCount.value });
249
+ });
170
250
  const paginated = computed(() => {
171
251
  if (props.serverMode || !props.pagination) return sorted.value;
172
252
  const start = page.value * pageSize.value;
@@ -240,7 +320,94 @@ function rowKeyFor(row) {
240
320
  if (typeof props.rowKey === "function") return props.rowKey(row);
241
321
  return row[props.rowKey];
242
322
  }
243
- const gridTemplate = computed(() => `grid-template-columns: ${gridTemplateColumns(visibleColumns.value.map((c) => c.width))};`);
323
+ const gridTemplate = computed(() => `grid-template-columns: ${gridTemplateColumns(
324
+ renderColumns.value.map((c) => c.width),
325
+ void 0,
326
+ // A flexible column keeps room for its one-line heading and the (?)/sort icons beside it.
327
+ renderColumns.value.map((c) => c.primary ? c.floor ?? PRIMARY_FLOOR : c.floor ?? headerFloor(c.label, { icons: (c.sortable ? 1 : 0) + 1, maxChars: HEADER_NOWRAP_MAX }))
328
+ )};`);
329
+ const PRIMARY_FLOOR = "260px";
330
+ const pinnedCount = computed(() => {
331
+ if (cardMode.value) return 0;
332
+ const i = renderColumns.value.findIndex((c) => c.primary);
333
+ return i < 0 ? 0 : i + 1;
334
+ });
335
+ const pinnedOffsets = ref([]);
336
+ const scrolledX = ref(false);
337
+ const moreRight = ref(false);
338
+ function measurePinned() {
339
+ const row = headerRowEl.value;
340
+ if (!row || !pinnedCount.value) {
341
+ pinnedOffsets.value = [];
342
+ return;
343
+ }
344
+ const cells = Array.from(row.querySelectorAll(':scope > [role="columnheader"]'));
345
+ const rowLeft = row.getBoundingClientRect().left;
346
+ const style = getComputedStyle(row);
347
+ const pad = Number.parseFloat(style.paddingLeft) || 0;
348
+ const gap = Number.parseFloat(style.columnGap) || 0;
349
+ pinnedOffsets.value = cells.slice(0, pinnedCount.value).map((cell, i) => {
350
+ const left = cell.getBoundingClientRect().left - rowLeft + (Number.parseFloat(cell.style.marginLeft) ? -Number.parseFloat(cell.style.marginLeft) : 0);
351
+ const gutter = i === 0 ? pad : gap;
352
+ return { left: left - gutter, gutter };
353
+ });
354
+ }
355
+ function pinnedStyle(index) {
356
+ const o = pinnedOffsets.value[index];
357
+ if (!o || index >= pinnedCount.value) return void 0;
358
+ return {
359
+ position: "sticky",
360
+ left: `${o.left}px`,
361
+ marginLeft: `-${o.gutter}px`,
362
+ paddingLeft: `${o.gutter}px`,
363
+ backgroundColor: "inherit",
364
+ zIndex: "1"
365
+ };
366
+ }
367
+ const HEADER_NOWRAP_MAX = 18;
368
+ function headerNoWrap(col) {
369
+ return (col.label ?? "").length <= HEADER_NOWRAP_MAX ? "whitespace-nowrap" : "";
370
+ }
371
+ function rightPinStyle(col) {
372
+ if (!col.pinRight || cardMode.value) return void 0;
373
+ return { position: "sticky", right: "0px", backgroundColor: "inherit", zIndex: "1" };
374
+ }
375
+ function rightPinClass(col) {
376
+ return col.pinRight && moreRight.value ? "shadow-[-6px_0_8px_-6px_rgba(11,5,34,0.18)]" : "";
377
+ }
378
+ function pinnedClass(index) {
379
+ return index === pinnedCount.value - 1 && scrolledX.value ? "shadow-[6px_0_8px_-6px_rgba(11,5,34,0.18)]" : "";
380
+ }
381
+ function syncScrollX() {
382
+ const el = scrollEl.value;
383
+ if (!el) return;
384
+ scrolledX.value = el.scrollLeft > 0;
385
+ moreRight.value = el.scrollWidth - el.clientWidth - el.scrollLeft > 2;
386
+ }
387
+ function scrollRight() {
388
+ scrollEl.value?.scrollBy({ left: Math.max(240, (scrollEl.value?.clientWidth ?? 0) * 0.6), behavior: "smooth" });
389
+ }
390
+ let pinObserver = null;
391
+ onMounted(() => {
392
+ pinObserver = new ResizeObserver(() => {
393
+ measurePinned();
394
+ syncScrollX();
395
+ });
396
+ if (scrollEl.value) pinObserver.observe(scrollEl.value);
397
+ if (headerRowEl.value) pinObserver.observe(headerRowEl.value);
398
+ nextTick(() => {
399
+ measurePinned();
400
+ syncScrollX();
401
+ });
402
+ watch([() => renderColumns.value.map((c) => `${c.key}:${c.width ?? ""}`).join("|"), () => props.data, cardMode, density], () => {
403
+ nextTick(() => {
404
+ if (headerRowEl.value && pinObserver) pinObserver.observe(headerRowEl.value);
405
+ measurePinned();
406
+ syncScrollX();
407
+ });
408
+ });
409
+ });
410
+ onBeforeUnmount(() => pinObserver?.disconnect());
244
411
  const SUBGRID_ROW = "grid-column: 1 / -1; grid-template-columns: subgrid;";
245
412
  const placeholder = computed(
246
413
  () => typeof props.search === "object" ? props.search.placeholder ?? t("shared.datatable.toolbar.search.placeholder.default") : t("shared.datatable.toolbar.search.placeholder.default")
@@ -251,7 +418,7 @@ const searchLabel = computed(() => {
251
418
  });
252
419
  const slots = useSlots();
253
420
  const showToolbar = computed(
254
- () => props.search || props.columnsMenu || props.filter || !!props.filterTabs || !!props.savedViews || !!props.viewModes?.length || !!props.rail || !!slots.actions || !!slots.scope || !!slots["toolbar-left"] || !!slots["toolbar-right"]
421
+ () => props.toolbar && (props.search || props.columnsMenu || props.filter || !!props.filterTabs || !!props.savedViews || !!props.viewModes?.length || !!props.rail || !!slots.actions || !!slots.scope || !!slots["toolbar-left"] || !!slots["toolbar-right"])
255
422
  );
256
423
  const railLabel = computed(
257
424
  () => (typeof props.rail === "object" ? props.rail.label : void 0) ?? null
@@ -282,7 +449,7 @@ function measureFit() {
282
449
  if (!el) return;
283
450
  const container = scrollParent(el);
284
451
  const offset = el.getBoundingClientRect().top - container.getBoundingClientRect().top + container.scrollTop;
285
- const reserve = (footerEl.value?.offsetHeight ?? 0) + BOTTOM_GAP_PX;
452
+ const reserve = (footerEl.value?.offsetHeight ?? 0) + BOTTOM_GAP_PX + (props.reserveBottom ?? 0);
286
453
  const overflow = Math.max(container.scrollHeight - container.clientHeight, 0);
287
454
  const available = Math.max(
288
455
  Math.floor(container.clientHeight - offset - reserve - overflow) - 1,
@@ -317,6 +484,7 @@ if (import.meta.client) {
317
484
  schedule();
318
485
  syncThumb();
319
486
  }));
487
+ watch(() => props.reserveBottom, () => nextTick(schedule));
320
488
  onMounted(() => {
321
489
  nextTick(syncThumb);
322
490
  window.addEventListener("resize", syncThumb);
@@ -346,6 +514,7 @@ function syncThumb() {
346
514
  }
347
515
  function onRowsScroll() {
348
516
  syncThumb();
517
+ syncScrollX();
349
518
  }
350
519
  function startThumbDrag(event) {
351
520
  const el = scrollEl.value;
@@ -380,7 +549,7 @@ const scrollStyle = computed(() => {
380
549
  const configured = typeof props.scrollBody === "object" ? props.scrollBody.maxHeight : void 0;
381
550
  return { maxHeight: configured ?? "calc(100vh - 22rem)" };
382
551
  });
383
- const showFooter = computed(() => props.pagination !== false && !props.groupBy);
552
+ const showFooter = computed(() => props.pagination !== false && !props.groupBy && !(totalCount.value === 0 && !props.loading));
384
553
  const lastPage = computed(
385
554
  () => pageSize.value > 0 ? Math.max(0, Math.ceil(totalCount.value / pageSize.value) - 1) : 0
386
555
  );
@@ -394,6 +563,88 @@ function prevPage() {
394
563
  function nextPage() {
395
564
  if (page.value < lastPage.value) page.value += 1;
396
565
  }
566
+ const MOVE_COLUMN = "__move";
567
+ const renderColumns = computed(() => props.reorderable ? [{ key: MOVE_COLUMN, label: "", width: "max-content" }, ...visibleColumns.value] : visibleColumns.value);
568
+ const reorderBlocked = computed(() => {
569
+ if (!props.reorderable) return null;
570
+ if (props.reorderDisabled) return props.reorderDisabled;
571
+ if (!props.serverMode && query.value.trim()) return t("shared.datatable.reorder.blocked.search");
572
+ if (Object.values(activeFilters.value).some((v) => v.length)) return t("shared.datatable.reorder.blocked.filter");
573
+ if (props.groupBy) return t("shared.datatable.reorder.blocked.group");
574
+ return null;
575
+ });
576
+ const dragKey = ref(null);
577
+ const dropAt = ref(null);
578
+ const moveStatus = ref("");
579
+ function orderedKeys() {
580
+ return sorted.value.map(rowKeyFor);
581
+ }
582
+ function commitMove(keys, from, to) {
583
+ emit("rowMove", { key: keys[from], from, to, keys: moveKey(keys, from, to) });
584
+ moveStatus.value = t("shared.datatable.reorder.moved", { position: to + 1, total: keys.length });
585
+ }
586
+ function finishDrag() {
587
+ dragKey.value = null;
588
+ dropAt.value = null;
589
+ }
590
+ function onHandleDragStart(event, row) {
591
+ if (reorderBlocked.value) {
592
+ event.preventDefault();
593
+ return;
594
+ }
595
+ const key = rowKeyFor(row);
596
+ dragKey.value = key;
597
+ if (event.dataTransfer) {
598
+ event.dataTransfer.effectAllowed = "move";
599
+ event.dataTransfer.setData("text/plain", String(key));
600
+ const rowEl = event.target?.closest('[role="row"]');
601
+ if (rowEl) event.dataTransfer.setDragImage(rowEl, 16, 16);
602
+ }
603
+ }
604
+ function onRowDragOver(event, row) {
605
+ if (dragKey.value == null) return;
606
+ event.preventDefault();
607
+ if (event.dataTransfer) event.dataTransfer.dropEffect = "move";
608
+ const rect = event.currentTarget.getBoundingClientRect();
609
+ dropAt.value = { key: rowKeyFor(row), edge: dropEdge(event.clientY, rect.top, rect.height) };
610
+ }
611
+ function onRowDrop(event, row) {
612
+ if (dragKey.value == null) return;
613
+ event.preventDefault();
614
+ const keys = orderedKeys();
615
+ const from = keys.indexOf(dragKey.value);
616
+ const target = keys.indexOf(rowKeyFor(row));
617
+ const edge = dropAt.value?.edge ?? "before";
618
+ finishDrag();
619
+ const to = dropTargetIndex(from, target, edge);
620
+ if (to !== null) commitMove(keys, from, to);
621
+ }
622
+ function onHandleKeydown(event, row) {
623
+ if (event.key !== "ArrowUp" && event.key !== "ArrowDown") return;
624
+ event.preventDefault();
625
+ if (reorderBlocked.value) return;
626
+ const keys = orderedKeys();
627
+ const key = rowKeyFor(row);
628
+ const from = keys.indexOf(key);
629
+ const to = stepTarget(from, event.key === "ArrowUp" ? -1 : 1, keys.length);
630
+ if (to === null) return;
631
+ commitMove(keys, from, to);
632
+ const handle = event.currentTarget;
633
+ const root = handle.closest('[role="table"]');
634
+ void nextTick(() => setTimeout(() => {
635
+ const again = root?.querySelector(`[data-move-key="${CSS.escape(String(key))}"]`);
636
+ again?.focus();
637
+ }, 0));
638
+ }
639
+ function dropClass(row) {
640
+ const at = dropAt.value;
641
+ if (!at || dragKey.value == null || at.key !== rowKeyFor(row)) return "";
642
+ return at.edge === "before" ? "shadow-[inset_0_2px_0_0_var(--color-primary)]" : "shadow-[inset_0_-2px_0_0_var(--color-primary)]";
643
+ }
644
+ function rowGround(row) {
645
+ if (props.activeRowKey != null && rowKeyFor(row) === props.activeRowKey) return "bg-[#f5f1fe]";
646
+ return props.rowClass?.(row) || "bg-gray-50";
647
+ }
397
648
  function defaultCell(col, row) {
398
649
  const v = col.sortGetter ? col.sortGetter(row) : row[col.key];
399
650
  if (v == null) return "\u2014";
@@ -402,7 +653,7 @@ function defaultCell(col, row) {
402
653
  </script>
403
654
 
404
655
  <template>
405
- <div class="rounded-lg border border-black/[0.06] overflow-hidden flex flex-col">
656
+ <div class="overflow-hidden flex flex-col" :class="embedded ? 'bg-white' : 'rounded-lg border border-black/[0.06]'">
406
657
  <!-- Toolbar ------------------------------------------------------ -->
407
658
  <!-- The single control row of a list surface.
408
659
 
@@ -439,9 +690,11 @@ function defaultCell(col, row) {
439
690
  in it. -->
440
691
  <div
441
692
  v-if="showToolbar"
442
- class="flex flex-wrap items-center justify-between gap-2 px-4 py-3 bg-gradient-to-r from-violet-50/80 via-white to-violet-50/60"
693
+ class="flex flex-wrap items-center justify-between gap-2 px-4 py-3 xl:flex-nowrap"
694
+ :class="embedded ? 'bg-white' : 'bg-gradient-to-r from-violet-50/80 via-white to-violet-50/60'"
443
695
  >
444
- <div class="flex items-center gap-2 min-w-0">
696
+ <!-- One line on a wide screen (UI-F10): the search gives way first. -->
697
+ <div class="flex min-w-0 flex-wrap items-center gap-2 max-sm:w-full xl:flex-1 xl:flex-nowrap">
445
698
  <Button
446
699
  v-if="rail"
447
700
  variant="outline"
@@ -469,7 +722,7 @@ function defaultCell(col, row) {
469
722
  v-model="query"
470
723
  :placeholder="placeholder"
471
724
  :label="searchLabel"
472
- class="w-[21rem] min-w-40 max-w-full shrink"
725
+ class="w-full min-w-40 max-w-full shrink sm:w-[21rem] xl:w-auto xl:flex-1 xl:max-w-[21rem]"
473
726
  >
474
727
  <template #icon>
475
728
  <Search :size="14" weight="Linear" />
@@ -501,8 +754,20 @@ function defaultCell(col, row) {
501
754
  :reset-label="t('shared.datatable.toolbar.filter.reset')"
502
755
  @update:model-value="applyPanelSelection"
503
756
  >
504
- <template #icon>
505
- <Filter :size="14" weight="BoldDuotone" />
757
+ <!-- D7 (Charge 2): tool buttons carry their word on desktop, the
758
+ glyph alone on phones. studio's trigger is icon-only by design
759
+ (ToolbarFilterTrigger icon-only), so the dummy draws its own
760
+ trigger through the panel's `trigger` slot. -->
761
+ <template #trigger="{ count }">
762
+ <ToolbarLabelTrigger
763
+ :active="count > 0"
764
+ :count="count"
765
+ :label="t('shared.datatable.toolbar.filter.action')"
766
+ >
767
+ <template #icon>
768
+ <Filter :size="14" weight="BoldDuotone" />
769
+ </template>
770
+ </ToolbarLabelTrigger>
506
771
  </template>
507
772
  </FilterPanel>
508
773
  <!--
@@ -517,8 +782,14 @@ function defaultCell(col, row) {
517
782
  v-model="facetSelection"
518
783
  :facets="facets"
519
784
  />
785
+ <!--
786
+ No `#filter-menu`, no trigger: `filter` alone used to draw a Filter
787
+ button that opened on "No filters defined" — a control promising to
788
+ narrow the list and refusing to (PO-313). The other two routes draw
789
+ only when the surface declared facets or dimensions.
790
+ -->
520
791
  <FilterMenu
521
- v-if="filter && !panelFacets.length"
792
+ v-if="filter && slots['filter-menu'] && !panelFacets.length"
522
793
  :width="typeof filter === 'object' ? filter.width : void 0"
523
794
  :active="typeof filter === 'object' ? filter.active : void 0"
524
795
  :count="typeof filter === 'object' ? filter.count : void 0"
@@ -540,10 +811,16 @@ function defaultCell(col, row) {
540
811
  :model-value="pickerColumns"
541
812
  :label="t('shared.datatable.columnsmenu.tooltip')"
542
813
  :heading="t('shared.datatable.columnsmenu.heading')"
814
+ class="w-8 gap-1.5 px-0 [&_span.bg-primary]:hidden"
543
815
  @update:model-value="onPickerUpdate"
544
816
  >
817
+ <!-- No count on the trigger (Michael: no count badges, PE-1082): the
818
+ studio picker draws one; hidden here until it has a prop.
819
+ D7: the word rides in the icon slot — studio's picker has no
820
+ label slot yet (see the studio change list in the ledger). -->
545
821
  <template #icon>
546
822
  <Columns :size="15" weight="BoldDuotone" />
823
+ <span class="sr-only">{{ t("shared.datatable.columnsmenu.tooltip") }}</span>
547
824
  </template>
548
825
  <template #handle>
549
826
  <ListArrowDownMinimalistic :size="15" weight="Linear" />
@@ -555,6 +832,33 @@ function defaultCell(col, row) {
555
832
  <EyeClosed :size="16" weight="Linear" />
556
833
  </template>
557
834
  </ColumnPicker>
835
+ <!-- Row height (D4, Charge 2): standard 44px, compact 36px. -->
836
+ <SegmentedToggle
837
+ v-if="showTable && !cardMode"
838
+ :model-value="density"
839
+ variant="inset"
840
+ :aria-label="t('shared.datatable.toolbar.density.label')"
841
+ @update:model-value="(v) => v && setDensity(v)"
842
+ >
843
+ <SegmentedToggleItem
844
+ value="standard"
845
+ :aria-label="t('shared.datatable.toolbar.density.standard')"
846
+ :title="t('shared.datatable.toolbar.density.standard')"
847
+ class="transition-colors data-[state=on]:text-primary data-[state=off]:hover:text-primary!"
848
+ >
849
+ <AlignVerticalSpacing :size="14" weight="Linear" />
850
+ <span class="sr-only">{{ t("shared.datatable.toolbar.density.standard") }}</span>
851
+ </SegmentedToggleItem>
852
+ <SegmentedToggleItem
853
+ value="compact"
854
+ :aria-label="t('shared.datatable.toolbar.density.compact')"
855
+ :title="t('shared.datatable.toolbar.density.compact')"
856
+ class="transition-colors data-[state=on]:text-primary data-[state=off]:hover:text-primary!"
857
+ >
858
+ <DragHandle :size="14" weight="Linear" />
859
+ <span class="sr-only">{{ t("shared.datatable.toolbar.density.compact") }}</span>
860
+ </SegmentedToggleItem>
861
+ </SegmentedToggle>
558
862
  <!--
559
863
  Saved views, beside the columns picker rather than out past the market
560
864
  scope and the view-mode switch.
@@ -656,41 +960,55 @@ function defaultCell(col, row) {
656
960
  that. A static table read with a screen reader's own table commands is
657
961
  what these lists actually are, and both roles announce the column
658
962
  heading with the cell, which is the thing that was missing. -->
659
- <div :class="scrollBody ? 'relative flex min-h-0 flex-col' : ''">
963
+ <div :class="scrollBody && !cardMode ? 'relative flex min-h-0 flex-col' : 'relative'">
660
964
  <div
661
965
  ref="scrollEl"
662
- :class="scrollBody ? 'rx-rows min-h-0 overflow-auto' : 'overflow-x-auto'"
663
- :style="scrollStyle"
966
+ :class="scrollBody && !cardMode ? 'rx-rows min-h-0 overflow-auto' : 'overflow-x-auto'"
967
+ :style="cardMode ? void 0 : scrollStyle"
664
968
  @scroll="onRowsScroll"
665
969
  >
666
970
  <div
667
971
  :role="showTable ? 'table' : void 0"
668
972
  :aria-busy="showTable && loading ? 'true' : void 0"
669
- :class="showTable ? 'grid min-w-min gap-x-2' : ''"
670
- :style="showTable ? gridTemplate : void 0"
973
+ :class="showTable && !cardMode ? 'grid min-w-min gap-x-2' : ''"
974
+ :style="showTable && !cardMode ? gridTemplate : void 0"
671
975
  >
672
976
  <!-- Header row -------------------------------------------------- -->
673
977
  <div
674
- v-if="showTable"
978
+ v-if="showTable && !cardMode"
675
979
  ref="headerRowEl"
676
980
  role="row"
677
- class="grid items-center gap-x-2 border-t rounded-t-lg border-gray-200 bg-gray-100 px-4 py-2.5 text-sm font-medium uppercase tracking-normal text-muted-foreground"
678
- :class="scrollBody ? 'sticky top-0 z-10' : ''"
981
+ data-rx-head
982
+ class="grid items-center gap-x-2 border-gray-200 px-4 py-2.5 text-sm font-medium uppercase tracking-normal text-muted-foreground"
983
+ :class="[
984
+ scrollBody ? 'sticky top-0 z-10' : '',
985
+ embedded ? 'bg-gray-100' : 'rounded-t-lg bg-gray-100',
986
+ // Embedded with the search row closed, the card head\'s own bottom
987
+ // edge is already the line above the headings; a second one here drew
988
+ // a double rule (Michael 14.10.).
989
+ embedded && !showToolbar ? '' : 'border-t'
990
+ ]"
679
991
  :style="SUBGRID_ROW"
680
992
  >
993
+ <!-- The handle column's heading: nothing to see, but a columnheader the
994
+ handles' cells can be named by. -->
995
+ <div v-if="reorderable" role="columnheader" class="w-6" :style="pinnedStyle(0)">
996
+ <span class="sr-only">{{ t("shared.datatable.reorder.column") }}</span>
997
+ </div>
681
998
  <!-- `columnheader` is what names every cell underneath it, so this is
682
999
  the half of the fix that does the work. `aria-sort` goes with it:
683
1000
  a heading that carries a sort button and says nothing about the
684
1001
  current direction is a control whose state is visible only to
685
1002
  people who can see the arrow. -->
686
1003
  <div
687
- v-for="col in visibleColumns"
1004
+ v-for="(col, ci) in visibleColumns"
688
1005
  :key="col.key"
689
1006
  role="columnheader"
690
1007
  :aria-sort="ariaSortFor(col)"
691
1008
  :title="col.label || void 0"
692
1009
  class="min-w-0"
693
- :class="col.alignRight ? 'text-right' : ''"
1010
+ :class="[col.alignRight ? RIGHT_ALIGNED : '', pinnedClass(ci + (reorderable ? 1 : 0)), rightPinClass(col)]"
1011
+ :style="rightPinStyle(col) ?? pinnedStyle(ci + (reorderable ? 1 : 0))"
694
1012
  >
695
1013
  <!-- `#header-<key>` lets a page take over a header cell (e.g. a
696
1014
  select-all checkbox in a selection column). Default: sortable
@@ -704,19 +1022,74 @@ function defaultCell(col, row) {
704
1022
  :align-right="col.alignRight"
705
1023
  @toggle="toggleSort"
706
1024
  >
707
- {{ col.label }}
1025
+ <span :class="headerNoWrap(col)">{{ col.label }}</span>
708
1026
  </SortHeader>
709
1027
  <!-- `normal-case` to match SortHeader, whose button does not inherit
710
1028
  the row's uppercase. Without it a column reads "LAST CHECK"
711
1029
  beside "Channel" and "Provider" for no reason a reader can see —
712
1030
  the header shouts wherever a column happens not to be sortable. -->
713
- <span v-else class="block truncate normal-case">{{ col.label }}</span>
1031
+ <span v-else class="block break-normal leading-tight normal-case" :class="headerNoWrap(col)">{{ col.label }}</span>
714
1032
  </slot>
715
1033
  </div>
716
1034
  </div>
717
1035
 
718
1036
  <!-- Body --------------------------------------------------------- -->
719
- <template v-for="entry in showTable ? renderRows : []" :key="entry.key">
1037
+ <!-- Phone (R4, Charge 2): one card per row. Title and status on the first
1038
+ line, then up to four key facts in two lines, in column priority. The
1039
+ same cell slots render inside, so a card shows exactly what the row
1040
+ would. -->
1041
+ <template v-if="showTable && cardMode">
1042
+ <div
1043
+ v-for="entry in renderRows"
1044
+ :key="entry.key"
1045
+ :role="entry.kind === 'header' ? void 0 : 'button'"
1046
+ :data-row="entry.kind === 'header' ? void 0 : ''"
1047
+ :tabindex="entry.kind === 'header' ? void 0 : 0"
1048
+ class="border-t border-black/[0.04]"
1049
+ :class="entry.kind === 'header' ? 'bg-gray-100/70 px-4 py-1.5 text-xs font-medium uppercase tracking-wide text-muted-foreground' : ['group cursor-pointer px-4 py-3', rowGround(entry.row)]"
1050
+ @click="entry.kind !== 'header' && emit('rowClick', entry.row)"
1051
+ @keydown.enter="entry.kind !== 'header' && emit('rowClick', entry.row)"
1052
+ >
1053
+ <template v-if="entry.kind === 'header'">
1054
+ {{ entry.label }} <span class="tabular-nums normal-case text-muted-foreground/70">{{ entry.count }}</span>
1055
+ </template>
1056
+ <template v-else>
1057
+ <div class="flex items-start gap-3">
1058
+ <div v-if="cardLayout.select" class="pt-0.5" @click.stop>
1059
+ <slot :name="`cell-${cardLayout.select.key}`" :row="entry.row" />
1060
+ </div>
1061
+ <div class="min-w-0 flex-1">
1062
+ <div class="flex items-start justify-between gap-2">
1063
+ <div v-if="cardLayout.title" class="min-w-0 flex-1 text-sm font-semibold text-[#0B0522] [&_*]:max-w-full">
1064
+ <slot :name="`cell-${cardLayout.title.key}`" :row="entry.row">
1065
+ <span class="block truncate">{{ defaultCell(cardLayout.title, entry.row) }}</span>
1066
+ </slot>
1067
+ </div>
1068
+ <div v-if="cardLayout.status" class="shrink-0">
1069
+ <slot :name="`cell-${cardLayout.status.key}`" :row="entry.row">
1070
+ <span class="text-sm">{{ defaultCell(cardLayout.status, entry.row) }}</span>
1071
+ </slot>
1072
+ </div>
1073
+ <div v-if="cardLayout.actions" class="-mr-1 shrink-0" @click.stop>
1074
+ <slot :name="`cell-${cardLayout.actions.key}`" :row="entry.row" />
1075
+ </div>
1076
+ </div>
1077
+ <dl v-if="cardLayout.meta.length" class="mt-1.5 grid grid-cols-2 gap-x-3 gap-y-1">
1078
+ <div v-for="m in cardLayout.meta" :key="m.key" class="min-w-0">
1079
+ <dt class="truncate text-xs text-muted-foreground">{{ m.label }}</dt>
1080
+ <dd class="min-w-0 truncate text-sm">
1081
+ <slot :name="`cell-${m.key}`" :row="entry.row">
1082
+ <span class="block truncate">{{ defaultCell(m, entry.row) }}</span>
1083
+ </slot>
1084
+ </dd>
1085
+ </div>
1086
+ </dl>
1087
+ </div>
1088
+ </div>
1089
+ </template>
1090
+ </div>
1091
+ </template>
1092
+ <template v-for="entry in showTable && !cardMode ? renderRows : []" :key="entry.key">
720
1093
  <!-- Group header (groupBy mode) — a row of one cell that spans the lot.
721
1094
  The flex moves one element in, onto the cell: a `row` may hold
722
1095
  cells and the label and the count are neither. -->
@@ -728,24 +1101,50 @@ function defaultCell(col, row) {
728
1101
  <!-- Spans every column like the rows do, so a table wide enough to
729
1102
  scroll does not show this band stopping short of the rows it
730
1103
  groups. No subgrid: it holds one cell, not a set of columns. -->
731
- <div role="cell" :aria-colspan="visibleColumns.length" class="flex items-center gap-2">
732
- {{ entry.label }}
1104
+ <div role="cell" :aria-colspan="renderColumns.length" class="flex items-center gap-2">
1105
+ <slot name="group-header" :label="entry.label">{{ entry.label }}</slot>
733
1106
  <span class="tabular-nums normal-case text-muted-foreground/70">{{ entry.count }}</span>
734
1107
  </div>
735
1108
  </div>
736
1109
  <div
737
1110
  v-else
738
1111
  role="row"
739
- class="group grid items-center gap-x-2 border-t border-black/[0.04] px-4 py-3 cursor-pointer bg-gray-50 transition-colors"
1112
+ data-row
1113
+ class="group grid items-center gap-x-2 border-t border-black/[0.04] px-4 cursor-pointer transition-colors"
1114
+ :class="[density === 'compact' ? 'min-h-9 py-1' : 'min-h-11 py-2', rowGround(entry.row), dropClass(entry.row), dragKey != null && dragKey === rowKeyFor(entry.row) ? 'opacity-50' : '']"
1115
+ :aria-current="activeRowKey != null && rowKeyFor(entry.row) === activeRowKey ? 'true' : void 0"
740
1116
  :style="SUBGRID_ROW"
741
1117
  @click="emit('rowClick', entry.row)"
1118
+ @dragover="onRowDragOver($event, entry.row)"
1119
+ @drop="onRowDrop($event, entry.row)"
742
1120
  >
1121
+ <!-- The grip. A button, so it is reachable and the arrow keys can move
1122
+ the row; `draggable` on it rather than on the row, so selecting a
1123
+ cell's text or clicking an inline input never starts a drag. -->
1124
+ <div v-if="reorderable" role="cell" class="flex items-center" :style="pinnedStyle(0)" @click.stop>
1125
+ <button
1126
+ type="button"
1127
+ :data-move-key="String(rowKeyFor(entry.row))"
1128
+ :draggable="!reorderBlocked"
1129
+ :aria-disabled="reorderBlocked ? 'true' : void 0"
1130
+ :aria-label="t('shared.datatable.reorder.handle')"
1131
+ :title="reorderBlocked ?? t('shared.datatable.reorder.hint')"
1132
+ class="inline-flex size-6 items-center justify-center rounded-md text-muted-foreground transition-colors"
1133
+ :class="reorderBlocked ? 'cursor-not-allowed opacity-30' : 'cursor-grab hover:bg-muted hover:text-primary active:cursor-grabbing'"
1134
+ @dragstart="onHandleDragStart($event, entry.row)"
1135
+ @dragend="finishDrag"
1136
+ @keydown="onHandleKeydown($event, entry.row)"
1137
+ >
1138
+ <DragHandle :size="14" weight="Linear" />
1139
+ </button>
1140
+ </div>
743
1141
  <div
744
- v-for="col in visibleColumns"
1142
+ v-for="(col, ci) in visibleColumns"
745
1143
  :key="col.key"
746
1144
  role="cell"
747
1145
  class="min-w-0"
748
- :class="col.alignRight ? 'text-right' : ''"
1146
+ :class="[col.alignRight ? RIGHT_ALIGNED : '', pinnedClass(ci + (reorderable ? 1 : 0)), rightPinClass(col)]"
1147
+ :style="rightPinStyle(col) ?? pinnedStyle(ci + (reorderable ? 1 : 0))"
749
1148
  >
750
1149
  <slot :name="`cell-${col.key}`" :row="entry.row">
751
1150
  <!-- Primary column: bold record anchor + optional tinted icon tile. -->
@@ -772,7 +1171,7 @@ function defaultCell(col, row) {
772
1171
  information to read out, and announcing them as six empty rows would
773
1172
  be worse than silence; the table says it is `aria-busy` instead,
774
1173
  which is the same news in one word. -->
775
- <template v-if="showTable && loading && !displayedRows.length">
1174
+ <template v-if="showTable && !cardMode && loading && !displayedRows.length">
776
1175
  <div
777
1176
  v-for="n in 6"
778
1177
  :key="`sk-${n}`"
@@ -781,7 +1180,7 @@ function defaultCell(col, row) {
781
1180
  :style="SUBGRID_ROW"
782
1181
  >
783
1182
  <div
784
- v-for="col in visibleColumns"
1183
+ v-for="col in renderColumns"
785
1184
  :key="col.key"
786
1185
  :class="col.alignRight ? 'flex justify-end' : ''"
787
1186
  >
@@ -799,8 +1198,8 @@ function defaultCell(col, row) {
799
1198
  role="row"
800
1199
  class="col-[1/-1] px-3 py-8 text-center text-sm text-muted-foreground"
801
1200
  >
802
- <div role="cell" :aria-colspan="visibleColumns.length">
803
- <slot name="empty">
1201
+ <div role="cell" :aria-colspan="renderColumns.length">
1202
+ <slot name="empty" :query="query" :filters="activeNarrowing" :clear="clearNarrowing">
804
1203
  {{ emptyText ?? t("shared.datatable.empty.default") }}
805
1204
  </slot>
806
1205
  </div>
@@ -809,6 +1208,24 @@ function defaultCell(col, row) {
809
1208
  </div>
810
1209
  </div>
811
1210
 
1211
+ <!-- More columns to the right (D5, Charge 2): a soft edge over the rows
1212
+ and a button that scrolls on, so a wide table says that it is wide
1213
+ instead of ending in a clipped "Stat…". -->
1214
+ <div
1215
+ v-if="showTable && !cardMode && moreRight"
1216
+ class="pointer-events-none absolute inset-y-0 right-0 z-[15] w-10 bg-gradient-to-l from-[rgb(11_5_34_/_0.07)] to-transparent"
1217
+ />
1218
+ <!-- Never over the rows (UI-F02, EC-F30): a table without a footer gets
1219
+ a slim line under it instead of a pill covering the last cells. -->
1220
+ <div
1221
+ v-if="showTable && !cardMode && moreRight && !showFooter"
1222
+ class="relative z-20 flex justify-end border-t border-black/[0.06] bg-white/70 px-2 py-1"
1223
+ >
1224
+ <Button variant="ghost" size="sm" class="h-7 gap-1 text-xs" @click="scrollRight">
1225
+ {{ t("shared.datatable.scroll.more") }}
1226
+ <AltArrowRight :size="12" weight="Bold" />
1227
+ </Button>
1228
+ </div>
812
1229
  <!-- The rows' scrollbar ---------------------------------------- -->
813
1230
  <!-- Drawn, not borrowed. A browser's own bar belongs to the scrolling
814
1231
  box, and the column head is inside that box — it has to be, because
@@ -829,11 +1246,18 @@ function defaultCell(col, row) {
829
1246
  </div>
830
1247
  </div>
831
1248
 
1249
+ <!-- Result count, spoken. Outside the table role: a live region is not
1250
+ a row, and the table may own rows only. -->
1251
+ <span role="status" class="sr-only">{{ liveStatus }}</span>
1252
+ <!-- A move, said once: a reader who stepped a row with the arrow keys
1253
+ hears where it landed. -->
1254
+ <span v-if="reorderable" role="status" class="sr-only">{{ moveStatus }}</span>
1255
+
832
1256
  <!-- Footer ------------------------------------------------------- -->
833
1257
  <div
834
1258
  v-if="showFooter"
835
1259
  ref="footerEl"
836
- class="flex items-center justify-between gap-2 border-t border-gray-200 bg-white/55 backdrop-blur-md px-4 py-3 text-sm text-muted-foreground/60"
1260
+ class="flex items-center justify-between gap-2 border-t border-gray-200 bg-white/55 backdrop-blur-md px-4 py-3 text-sm text-muted-foreground"
837
1261
  >
838
1262
  <div class="flex items-center gap-2.5">
839
1263
  <span class="tabular-nums">
@@ -851,11 +1275,23 @@ function defaultCell(col, row) {
851
1275
  </select>
852
1276
  </label>
853
1277
  </div>
1278
+ <div class="ml-auto flex items-center gap-2">
1279
+ <Button
1280
+ v-if="showTable && !cardMode && moreRight"
1281
+ variant="ghost"
1282
+ size="sm"
1283
+ class="gap-1"
1284
+ @click="scrollRight"
1285
+ >
1286
+ {{ t("shared.datatable.scroll.more") }}
1287
+ <AltArrowRight :size="12" weight="Bold" />
1288
+ </Button>
1289
+ </div>
854
1290
  <div v-if="lastPage > 0" class="flex items-center gap-2">
855
1291
  <Button
856
1292
  variant="outline"
857
1293
  size="sm"
858
- class="h-7 text-sm"
1294
+ class="text-sm"
859
1295
  :disabled="page === 0 || loading"
860
1296
  @click="prevPage"
861
1297
  >
@@ -867,7 +1303,7 @@ function defaultCell(col, row) {
867
1303
  <Button
868
1304
  variant="outline"
869
1305
  size="sm"
870
- class="h-7 text-sm"
1306
+ class="text-sm"
871
1307
  :disabled="page >= lastPage || loading"
872
1308
  @click="nextPage"
873
1309
  >
@@ -881,3 +1317,7 @@ function defaultCell(col, row) {
881
1317
  <style scoped>
882
1318
  .rx-rows::-webkit-scrollbar{height:10px;width:0}.rx-rows::-webkit-scrollbar-track{background:transparent}.rx-rows::-webkit-scrollbar-thumb{background:rgba(11,5,34,.18);background-clip:padding-box;border:3px solid transparent;border-radius:9999px}.rx-thumb{min-height:32px}
883
1319
  </style>
1320
+
1321
+ <style>
1322
+ @media (max-width:767.98px){[data-reka-popper-content-wrapper]:has(>[data-slot=popover-content].w-\[34rem\]){inset:auto 0 0 0!important;min-width:0!important;position:fixed!important;transform:none!important}[data-reka-popper-content-wrapper]>[data-slot=popover-content].w-\[34rem\]{border-radius:.75rem .75rem 0 0!important;max-height:80svh;max-width:100vw!important;overflow:auto;padding-bottom:env(safe-area-inset-bottom);width:100vw!important}}
1323
+ </style>