@revenexx/studio-shared 0.4.0 → 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 (110) hide show
  1. package/dist/module.d.mts +5 -3
  2. package/dist/module.json +2 -2
  3. package/dist/module.mjs +7 -3
  4. package/dist/runtime/components/Dashboard/DashboardConfirmDialog.vue +1 -1
  5. package/dist/runtime/components/Dashboard/DashboardGrid.vue +45 -1
  6. package/dist/runtime/components/Dashboard/DashboardNameDialog.vue +1 -1
  7. package/dist/runtime/components/Dashboard/DashboardPickerDialog.vue +1 -1
  8. package/dist/runtime/components/Dashboard/DashboardShareDialog.vue +1 -1
  9. package/dist/runtime/components/Dashboard/DashboardWidgetConfigDialog.vue +1 -1
  10. package/dist/runtime/components/Dashboard/DashboardWidgetFrame.d.vue.ts +2 -2
  11. package/dist/runtime/components/Dashboard/DashboardWidgetFrame.vue +14 -3
  12. package/dist/runtime/components/Dashboard/DashboardWidgetFrame.vue.d.ts +2 -2
  13. package/dist/runtime/components/DataTable/DataTable.d.vue.ts +73 -34
  14. package/dist/runtime/components/DataTable/DataTable.vue +352 -52
  15. package/dist/runtime/components/DataTable/DataTable.vue.d.ts +73 -34
  16. package/dist/runtime/components/DataTable/FacetPanel.vue +57 -8
  17. package/dist/runtime/components/DataTable/FilterMenu.vue +5 -5
  18. package/dist/runtime/components/DataTable/SavedViewsMenu.vue +61 -12
  19. package/dist/runtime/components/DataTable/SortMenu.vue +4 -1
  20. package/dist/runtime/components/DataTable/ToolbarLabelTrigger.d.vue.ts +23 -0
  21. package/dist/runtime/components/DataTable/ToolbarLabelTrigger.vue +29 -0
  22. package/dist/runtime/components/DataTable/ToolbarLabelTrigger.vue.d.ts +23 -0
  23. package/dist/runtime/components/DataTable/types.d.ts +17 -0
  24. package/dist/runtime/components/Form/FieldGrid.vue +1 -1
  25. package/dist/runtime/components/Form/SaveBar.d.vue.ts +1 -1
  26. package/dist/runtime/components/Form/SaveBar.vue.d.ts +1 -1
  27. package/dist/runtime/components/Media/MediaFolderTree.d.vue.ts +4 -4
  28. package/dist/runtime/components/Media/MediaFolderTree.vue +1 -2
  29. package/dist/runtime/components/Media/MediaFolderTree.vue.d.ts +4 -4
  30. package/dist/runtime/components/Media/MediaPickerDialog.d.vue.ts +11 -0
  31. package/dist/runtime/components/Media/MediaPickerDialog.vue +206 -54
  32. package/dist/runtime/components/Media/MediaPickerDialog.vue.d.ts +11 -0
  33. package/dist/runtime/components/Media/MediaUploader.vue +1 -1
  34. package/dist/runtime/components/Shared/AppIcon.d.vue.ts +1 -1
  35. package/dist/runtime/components/Shared/AppIcon.vue.d.ts +1 -1
  36. package/dist/runtime/components/Shared/CardSizeToggle.d.vue.ts +11 -0
  37. package/dist/runtime/components/Shared/CardSizeToggle.vue +19 -0
  38. package/dist/runtime/components/Shared/CardSizeToggle.vue.d.ts +11 -0
  39. package/dist/runtime/components/Shared/DetailSheet.d.vue.ts +1 -1
  40. package/dist/runtime/components/Shared/DetailSheet.vue +9 -5
  41. package/dist/runtime/components/Shared/DetailSheet.vue.d.ts +1 -1
  42. package/dist/runtime/components/Shared/EmptyNote.d.vue.ts +19 -0
  43. package/dist/runtime/components/Shared/EmptyNote.vue +25 -0
  44. package/dist/runtime/components/Shared/EmptyNote.vue.d.ts +19 -0
  45. package/dist/runtime/components/Shared/KpiTile.d.vue.ts +6 -0
  46. package/dist/runtime/components/Shared/KpiTile.vue +10 -9
  47. package/dist/runtime/components/Shared/KpiTile.vue.d.ts +6 -0
  48. package/dist/runtime/components/Shared/PageHeader.vue +15 -4
  49. package/dist/runtime/components/Shared/RecordLayout.vue +1 -2
  50. package/dist/runtime/components/Shared/RecordRailCard.vue +3 -3
  51. package/dist/runtime/components/Shared/SelectionBar.d.vue.ts +22 -0
  52. package/dist/runtime/components/Shared/SelectionBar.vue +40 -0
  53. package/dist/runtime/components/Shared/SelectionBar.vue.d.ts +22 -0
  54. package/dist/runtime/components/Shared/SidebarNav.d.vue.ts +36 -2
  55. package/dist/runtime/components/Shared/SidebarNav.vue +71 -25
  56. package/dist/runtime/components/Shared/SidebarNav.vue.d.ts +36 -2
  57. package/dist/runtime/components/Shared/SidebarNavAreas.d.vue.ts +7 -0
  58. package/dist/runtime/components/Shared/SidebarNavAreas.vue +291 -0
  59. package/dist/runtime/components/Shared/SidebarNavAreas.vue.d.ts +7 -0
  60. package/dist/runtime/components/Shared/SidebarNavLink.d.vue.ts +1 -9
  61. package/dist/runtime/components/Shared/SidebarNavLink.vue +31 -5
  62. package/dist/runtime/components/Shared/SidebarNavLink.vue.d.ts +1 -9
  63. package/dist/runtime/components/Shared/SortHeader.vue +6 -2
  64. package/dist/runtime/components/Shared/StateCard.vue +1 -1
  65. package/dist/runtime/components/Shared/TabBar.vue +6 -7
  66. package/dist/runtime/components/Shared/Wizard.d.vue.ts +11 -1
  67. package/dist/runtime/components/Shared/Wizard.vue +9 -5
  68. package/dist/runtime/components/Shared/Wizard.vue.d.ts +11 -1
  69. package/dist/runtime/composables/useMediaLibrary.d.ts +12 -0
  70. package/dist/runtime/composables/useSavedViews.d.ts +11 -1
  71. package/dist/runtime/composables/useSavedViews.js +4 -4
  72. package/dist/runtime/composables/useWidgetFigures.d.ts +12 -0
  73. package/dist/runtime/composables/useWidgetFigures.js +24 -0
  74. package/dist/runtime/composables/useWidgetHeader.d.ts +15 -0
  75. package/dist/runtime/composables/useWidgetHeader.js +15 -0
  76. package/dist/runtime/lib/auditEnvelope.d.ts +43 -0
  77. package/dist/runtime/lib/auditEnvelope.js +55 -0
  78. package/dist/runtime/lib/cardSize.d.ts +13 -0
  79. package/dist/runtime/lib/cardSize.js +20 -0
  80. package/dist/runtime/lib/dashboardIcons.js +2 -0
  81. package/dist/runtime/lib/dataTableFilters.d.ts +22 -0
  82. package/dist/runtime/lib/dataTableFilters.js +5 -0
  83. package/dist/runtime/lib/facets.d.ts +13 -0
  84. package/dist/runtime/lib/formLayout.d.ts +74 -4
  85. package/dist/runtime/lib/formLayout.js +53 -6
  86. package/dist/runtime/lib/gridTracks.d.ts +14 -1
  87. package/dist/runtime/lib/gridTracks.js +13 -2
  88. package/dist/runtime/lib/isoLists.d.ts +37 -2
  89. package/dist/runtime/lib/isoLists.js +54 -1
  90. package/dist/runtime/lib/notice.d.ts +14 -0
  91. package/dist/runtime/lib/notice.js +19 -0
  92. package/dist/runtime/lib/pageSize.d.ts +15 -0
  93. package/dist/runtime/lib/pageSize.js +26 -0
  94. package/dist/runtime/lib/sidebarAreas.d.ts +46 -0
  95. package/dist/runtime/lib/sidebarAreas.js +25 -0
  96. package/dist/runtime/lib/sidebarNav.d.ts +21 -2
  97. package/dist/runtime/lib/sidebarNav.js +23 -4
  98. package/dist/runtime/lib/solarIconMap.d.ts +2 -1
  99. package/dist/runtime/lib/solarIconMap.js +50 -4
  100. package/dist/runtime/lib/swipeToClose.d.ts +25 -0
  101. package/dist/runtime/lib/swipeToClose.js +118 -0
  102. package/dist/runtime/lib/tableExport.d.ts +31 -0
  103. package/dist/runtime/lib/tableExport.js +143 -0
  104. package/dist/runtime/lib/talkback.d.ts +5 -0
  105. package/dist/runtime/lib/talkback.js +8 -0
  106. package/dist/runtime/lib/talkbackTokenBatch.d.ts +35 -0
  107. package/dist/runtime/lib/talkbackTokenBatch.js +101 -0
  108. package/dist/runtime/types/nav.d.ts +9 -0
  109. package/dist/types.d.mts +4 -0
  110. package/package.json +4 -3
@@ -1,5 +1,7 @@
1
1
  <script setup>
2
2
  import {
3
+ AlignVerticalSpacing,
4
+ AltArrowRight,
3
5
  Eye,
4
6
  EyeClosed,
5
7
  Filter,
@@ -19,16 +21,18 @@ import {
19
21
  } from "@revenexx/studio";
20
22
  import { orderColumnsBy } from "../../lib/columnOrder";
21
23
  import { initiallyHidden, visibleColumnsOf } from "../../lib/columnVisibility";
22
- import { countByValue, matchesFilter } from "../../lib/dataTableFilters";
23
- import { gridTemplateColumns } from "../../lib/gridTracks";
24
- const RIGHT_ALIGNED = "text-right pr-4";
24
+ import { facetCounts, matchesFilter } from "../../lib/dataTableFilters";
25
+ import { gridTemplateColumns, headerFloor } from "../../lib/gridTracks";
25
26
  import { dropEdge, dropTargetIndex, moveKey, stepTarget } from "../../lib/rowMove";
26
27
  import SortHeader from "../Shared/SortHeader.vue";
27
28
  import FacetChips from "./FacetChips.vue";
28
29
  import FacetPanel from "./FacetPanel.vue";
29
30
  import FilterMenu from "./FilterMenu.vue";
30
31
  import SavedViewsMenu from "./SavedViewsMenu.vue";
32
+ import ToolbarLabelTrigger from "./ToolbarLabelTrigger.vue";
31
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";
32
36
  const props = defineProps({
33
37
  data: { type: Array, required: true },
34
38
  columns: { type: Array, required: true },
@@ -43,6 +47,7 @@ const props = defineProps({
43
47
  viewModes: { type: [Array, null], required: false, default: null },
44
48
  rail: { type: [Boolean, Object], required: false, default: false },
45
49
  scrollBody: { type: [Boolean, String, Object], required: false },
50
+ reserveBottom: { type: Number, required: false },
46
51
  pagination: { type: [Boolean, Object], required: false, default: true },
47
52
  defaultSort: { type: Object, required: false },
48
53
  serverMode: { type: Boolean, required: false, default: false },
@@ -54,11 +59,17 @@ const props = defineProps({
54
59
  activeRowKey: { type: [String, Number, null], required: false },
55
60
  rowClass: { type: [Function, null], required: false, default: null },
56
61
  reorderable: { type: Boolean, required: false, default: false },
57
- reorderDisabled: { type: [String, null], required: false, default: null }
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 }
58
68
  });
59
69
  const emit = defineEmits(["rowClick", "stateChange", "orderChange", "rowMove"]);
70
+ defineSlots();
60
71
  const t = useT();
61
- const query = ref("");
72
+ const query = defineModel("query", { type: String, ...{ default: "" } });
62
73
  const sort = defineModel("sort", { type: Object, ...{
63
74
  default: () => ({
64
75
  column: null,
@@ -68,19 +79,66 @@ const sort = defineModel("sort", { type: Object, ...{
68
79
  const page = defineModel("page", { type: Number, ...{ default: 0 } });
69
80
  const viewMode = defineModel("viewMode", { type: String, ...{ default: "list" } });
70
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
+ });
71
126
  const facetSelection = defineModel("facetSelection", { type: Object, ...{ default: () => ({}) } });
72
127
  const panelFacets = computed(() => props.facets);
73
128
  if (props.defaultSort && sort.value.column == null) {
74
129
  sort.value = { column: props.defaultSort.column, direction: props.defaultSort.direction };
75
130
  }
131
+ const pageSizeTable = computed(() => props.pageSizeKey ?? props.savedViews?.storageKey ?? null);
76
132
  const pageSize = ref(
77
- 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)
78
134
  );
79
- 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));
80
137
  const hidden = ref(initiallyHidden(props.columns));
81
138
  const columnOrder = ref([]);
82
139
  const arrangedColumns = computed(() => orderColumnsBy(props.columns, columnOrder.value));
83
140
  const visibleColumns = computed(() => visibleColumnsOf(arrangedColumns.value, hidden.value));
141
+ defineExpose({ visibleColumns });
84
142
  const pickerColumns = computed(() => arrangedColumns.value.filter((c) => (c.label ?? "").trim().length > 0).map((c) => ({
85
143
  key: c.key,
86
144
  label: c.label ?? c.key,
@@ -127,22 +185,22 @@ const filterFacets = computed(() => filterDims.value.map((d) => {
127
185
  };
128
186
  }));
129
187
  function dimCounts(d) {
130
- return countByValue(props.data, d.getter);
188
+ return facetCounts(queried.value, filterDims.value, activeFilters.value, d);
131
189
  }
132
- const filtered = computed(() => {
133
- if (props.serverMode || !filterDims.value.length) return props.data;
134
- const active = filterDims.value.filter((d) => activeFilters.value[d.key]?.length);
135
- if (!active.length) return props.data;
136
- return props.data.filter((row) => active.every((d) => matchesFilter(d.getter, row, activeFilters.value[d.key])));
137
- });
138
- const searched = computed(() => {
190
+ const queried = computed(() => {
139
191
  if (props.serverMode) return props.data;
140
192
  const q = query.value.trim().toLowerCase();
141
- if (!q) return filtered.value;
142
- return filtered.value.filter(
193
+ if (!q) return props.data;
194
+ return props.data.filter(
143
195
  (row) => props.columns.some((col) => searchOf(col, row).includes(q))
144
196
  );
145
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
+ });
146
204
  function sortKey(col, row) {
147
205
  if (col.sortGetter) return col.sortGetter(row);
148
206
  const raw = row[col.key];
@@ -262,7 +320,94 @@ function rowKeyFor(row) {
262
320
  if (typeof props.rowKey === "function") return props.rowKey(row);
263
321
  return row[props.rowKey];
264
322
  }
265
- const gridTemplate = computed(() => `grid-template-columns: ${gridTemplateColumns(renderColumns.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());
266
411
  const SUBGRID_ROW = "grid-column: 1 / -1; grid-template-columns: subgrid;";
267
412
  const placeholder = computed(
268
413
  () => typeof props.search === "object" ? props.search.placeholder ?? t("shared.datatable.toolbar.search.placeholder.default") : t("shared.datatable.toolbar.search.placeholder.default")
@@ -273,7 +418,7 @@ const searchLabel = computed(() => {
273
418
  });
274
419
  const slots = useSlots();
275
420
  const showToolbar = computed(
276
- () => 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"])
277
422
  );
278
423
  const railLabel = computed(
279
424
  () => (typeof props.rail === "object" ? props.rail.label : void 0) ?? null
@@ -304,7 +449,7 @@ function measureFit() {
304
449
  if (!el) return;
305
450
  const container = scrollParent(el);
306
451
  const offset = el.getBoundingClientRect().top - container.getBoundingClientRect().top + container.scrollTop;
307
- const reserve = (footerEl.value?.offsetHeight ?? 0) + BOTTOM_GAP_PX;
452
+ const reserve = (footerEl.value?.offsetHeight ?? 0) + BOTTOM_GAP_PX + (props.reserveBottom ?? 0);
308
453
  const overflow = Math.max(container.scrollHeight - container.clientHeight, 0);
309
454
  const available = Math.max(
310
455
  Math.floor(container.clientHeight - offset - reserve - overflow) - 1,
@@ -339,6 +484,7 @@ if (import.meta.client) {
339
484
  schedule();
340
485
  syncThumb();
341
486
  }));
487
+ watch(() => props.reserveBottom, () => nextTick(schedule));
342
488
  onMounted(() => {
343
489
  nextTick(syncThumb);
344
490
  window.addEventListener("resize", syncThumb);
@@ -368,6 +514,7 @@ function syncThumb() {
368
514
  }
369
515
  function onRowsScroll() {
370
516
  syncThumb();
517
+ syncScrollX();
371
518
  }
372
519
  function startThumbDrag(event) {
373
520
  const el = scrollEl.value;
@@ -402,7 +549,7 @@ const scrollStyle = computed(() => {
402
549
  const configured = typeof props.scrollBody === "object" ? props.scrollBody.maxHeight : void 0;
403
550
  return { maxHeight: configured ?? "calc(100vh - 22rem)" };
404
551
  });
405
- const showFooter = computed(() => props.pagination !== false && !props.groupBy);
552
+ const showFooter = computed(() => props.pagination !== false && !props.groupBy && !(totalCount.value === 0 && !props.loading));
406
553
  const lastPage = computed(
407
554
  () => pageSize.value > 0 ? Math.max(0, Math.ceil(totalCount.value / pageSize.value) - 1) : 0
408
555
  );
@@ -495,7 +642,7 @@ function dropClass(row) {
495
642
  return at.edge === "before" ? "shadow-[inset_0_2px_0_0_var(--color-primary)]" : "shadow-[inset_0_-2px_0_0_var(--color-primary)]";
496
643
  }
497
644
  function rowGround(row) {
498
- if (props.activeRowKey != null && rowKeyFor(row) === props.activeRowKey) return "bg-primary/[0.06]";
645
+ if (props.activeRowKey != null && rowKeyFor(row) === props.activeRowKey) return "bg-[#f5f1fe]";
499
646
  return props.rowClass?.(row) || "bg-gray-50";
500
647
  }
501
648
  function defaultCell(col, row) {
@@ -506,7 +653,7 @@ function defaultCell(col, row) {
506
653
  </script>
507
654
 
508
655
  <template>
509
- <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]'">
510
657
  <!-- Toolbar ------------------------------------------------------ -->
511
658
  <!-- The single control row of a list surface.
512
659
 
@@ -543,9 +690,11 @@ function defaultCell(col, row) {
543
690
  in it. -->
544
691
  <div
545
692
  v-if="showToolbar"
546
- 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'"
547
695
  >
548
- <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">
549
698
  <Button
550
699
  v-if="rail"
551
700
  variant="outline"
@@ -573,7 +722,7 @@ function defaultCell(col, row) {
573
722
  v-model="query"
574
723
  :placeholder="placeholder"
575
724
  :label="searchLabel"
576
- 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]"
577
726
  >
578
727
  <template #icon>
579
728
  <Search :size="14" weight="Linear" />
@@ -605,8 +754,20 @@ function defaultCell(col, row) {
605
754
  :reset-label="t('shared.datatable.toolbar.filter.reset')"
606
755
  @update:model-value="applyPanelSelection"
607
756
  >
608
- <template #icon>
609
- <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>
610
771
  </template>
611
772
  </FilterPanel>
612
773
  <!--
@@ -621,8 +782,14 @@ function defaultCell(col, row) {
621
782
  v-model="facetSelection"
622
783
  :facets="facets"
623
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
+ -->
624
791
  <FilterMenu
625
- v-if="filter && !panelFacets.length"
792
+ v-if="filter && slots['filter-menu'] && !panelFacets.length"
626
793
  :width="typeof filter === 'object' ? filter.width : void 0"
627
794
  :active="typeof filter === 'object' ? filter.active : void 0"
628
795
  :count="typeof filter === 'object' ? filter.count : void 0"
@@ -644,10 +811,16 @@ function defaultCell(col, row) {
644
811
  :model-value="pickerColumns"
645
812
  :label="t('shared.datatable.columnsmenu.tooltip')"
646
813
  :heading="t('shared.datatable.columnsmenu.heading')"
814
+ class="w-8 gap-1.5 px-0 [&_span.bg-primary]:hidden"
647
815
  @update:model-value="onPickerUpdate"
648
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). -->
649
821
  <template #icon>
650
822
  <Columns :size="15" weight="BoldDuotone" />
823
+ <span class="sr-only">{{ t("shared.datatable.columnsmenu.tooltip") }}</span>
651
824
  </template>
652
825
  <template #handle>
653
826
  <ListArrowDownMinimalistic :size="15" weight="Linear" />
@@ -659,6 +832,33 @@ function defaultCell(col, row) {
659
832
  <EyeClosed :size="16" weight="Linear" />
660
833
  </template>
661
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>
662
862
  <!--
663
863
  Saved views, beside the columns picker rather than out past the market
664
864
  scope and the view-mode switch.
@@ -760,31 +960,39 @@ function defaultCell(col, row) {
760
960
  that. A static table read with a screen reader's own table commands is
761
961
  what these lists actually are, and both roles announce the column
762
962
  heading with the cell, which is the thing that was missing. -->
763
- <div :class="scrollBody ? 'relative flex min-h-0 flex-col' : ''">
963
+ <div :class="scrollBody && !cardMode ? 'relative flex min-h-0 flex-col' : 'relative'">
764
964
  <div
765
965
  ref="scrollEl"
766
- :class="scrollBody ? 'rx-rows min-h-0 overflow-auto' : 'overflow-x-auto'"
767
- :style="scrollStyle"
966
+ :class="scrollBody && !cardMode ? 'rx-rows min-h-0 overflow-auto' : 'overflow-x-auto'"
967
+ :style="cardMode ? void 0 : scrollStyle"
768
968
  @scroll="onRowsScroll"
769
969
  >
770
970
  <div
771
971
  :role="showTable ? 'table' : void 0"
772
972
  :aria-busy="showTable && loading ? 'true' : void 0"
773
- :class="showTable ? 'grid min-w-min gap-x-2' : ''"
774
- :style="showTable ? gridTemplate : void 0"
973
+ :class="showTable && !cardMode ? 'grid min-w-min gap-x-2' : ''"
974
+ :style="showTable && !cardMode ? gridTemplate : void 0"
775
975
  >
776
976
  <!-- Header row -------------------------------------------------- -->
777
977
  <div
778
- v-if="showTable"
978
+ v-if="showTable && !cardMode"
779
979
  ref="headerRowEl"
780
980
  role="row"
781
- 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"
782
- :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
+ ]"
783
991
  :style="SUBGRID_ROW"
784
992
  >
785
993
  <!-- The handle column's heading: nothing to see, but a columnheader the
786
994
  handles' cells can be named by. -->
787
- <div v-if="reorderable" role="columnheader" class="w-6">
995
+ <div v-if="reorderable" role="columnheader" class="w-6" :style="pinnedStyle(0)">
788
996
  <span class="sr-only">{{ t("shared.datatable.reorder.column") }}</span>
789
997
  </div>
790
998
  <!-- `columnheader` is what names every cell underneath it, so this is
@@ -793,13 +1001,14 @@ function defaultCell(col, row) {
793
1001
  current direction is a control whose state is visible only to
794
1002
  people who can see the arrow. -->
795
1003
  <div
796
- v-for="col in visibleColumns"
1004
+ v-for="(col, ci) in visibleColumns"
797
1005
  :key="col.key"
798
1006
  role="columnheader"
799
1007
  :aria-sort="ariaSortFor(col)"
800
1008
  :title="col.label || void 0"
801
1009
  class="min-w-0"
802
- :class="col.alignRight ? RIGHT_ALIGNED : ''"
1010
+ :class="[col.alignRight ? RIGHT_ALIGNED : '', pinnedClass(ci + (reorderable ? 1 : 0)), rightPinClass(col)]"
1011
+ :style="rightPinStyle(col) ?? pinnedStyle(ci + (reorderable ? 1 : 0))"
803
1012
  >
804
1013
  <!-- `#header-<key>` lets a page take over a header cell (e.g. a
805
1014
  select-all checkbox in a selection column). Default: sortable
@@ -813,19 +1022,74 @@ function defaultCell(col, row) {
813
1022
  :align-right="col.alignRight"
814
1023
  @toggle="toggleSort"
815
1024
  >
816
- {{ col.label }}
1025
+ <span :class="headerNoWrap(col)">{{ col.label }}</span>
817
1026
  </SortHeader>
818
1027
  <!-- `normal-case` to match SortHeader, whose button does not inherit
819
1028
  the row's uppercase. Without it a column reads "LAST CHECK"
820
1029
  beside "Channel" and "Provider" for no reason a reader can see —
821
1030
  the header shouts wherever a column happens not to be sortable. -->
822
- <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>
823
1032
  </slot>
824
1033
  </div>
825
1034
  </div>
826
1035
 
827
1036
  <!-- Body --------------------------------------------------------- -->
828
- <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">
829
1093
  <!-- Group header (groupBy mode) — a row of one cell that spans the lot.
830
1094
  The flex moves one element in, onto the cell: a `row` may hold
831
1095
  cells and the label and the count are neither. -->
@@ -838,15 +1102,16 @@ function defaultCell(col, row) {
838
1102
  scroll does not show this band stopping short of the rows it
839
1103
  groups. No subgrid: it holds one cell, not a set of columns. -->
840
1104
  <div role="cell" :aria-colspan="renderColumns.length" class="flex items-center gap-2">
841
- {{ entry.label }}
1105
+ <slot name="group-header" :label="entry.label">{{ entry.label }}</slot>
842
1106
  <span class="tabular-nums normal-case text-muted-foreground/70">{{ entry.count }}</span>
843
1107
  </div>
844
1108
  </div>
845
1109
  <div
846
1110
  v-else
847
1111
  role="row"
848
- class="group grid items-center gap-x-2 border-t border-black/[0.04] px-4 py-3 cursor-pointer transition-colors"
849
- :class="[rowGround(entry.row), dropClass(entry.row), dragKey != null && dragKey === rowKeyFor(entry.row) ? 'opacity-50' : '']"
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' : '']"
850
1115
  :aria-current="activeRowKey != null && rowKeyFor(entry.row) === activeRowKey ? 'true' : void 0"
851
1116
  :style="SUBGRID_ROW"
852
1117
  @click="emit('rowClick', entry.row)"
@@ -856,7 +1121,7 @@ function defaultCell(col, row) {
856
1121
  <!-- The grip. A button, so it is reachable and the arrow keys can move
857
1122
  the row; `draggable` on it rather than on the row, so selecting a
858
1123
  cell's text or clicking an inline input never starts a drag. -->
859
- <div v-if="reorderable" role="cell" class="flex items-center" @click.stop>
1124
+ <div v-if="reorderable" role="cell" class="flex items-center" :style="pinnedStyle(0)" @click.stop>
860
1125
  <button
861
1126
  type="button"
862
1127
  :data-move-key="String(rowKeyFor(entry.row))"
@@ -874,11 +1139,12 @@ function defaultCell(col, row) {
874
1139
  </button>
875
1140
  </div>
876
1141
  <div
877
- v-for="col in visibleColumns"
1142
+ v-for="(col, ci) in visibleColumns"
878
1143
  :key="col.key"
879
1144
  role="cell"
880
1145
  class="min-w-0"
881
- :class="col.alignRight ? RIGHT_ALIGNED : ''"
1146
+ :class="[col.alignRight ? RIGHT_ALIGNED : '', pinnedClass(ci + (reorderable ? 1 : 0)), rightPinClass(col)]"
1147
+ :style="rightPinStyle(col) ?? pinnedStyle(ci + (reorderable ? 1 : 0))"
882
1148
  >
883
1149
  <slot :name="`cell-${col.key}`" :row="entry.row">
884
1150
  <!-- Primary column: bold record anchor + optional tinted icon tile. -->
@@ -905,7 +1171,7 @@ function defaultCell(col, row) {
905
1171
  information to read out, and announcing them as six empty rows would
906
1172
  be worse than silence; the table says it is `aria-busy` instead,
907
1173
  which is the same news in one word. -->
908
- <template v-if="showTable && loading && !displayedRows.length">
1174
+ <template v-if="showTable && !cardMode && loading && !displayedRows.length">
909
1175
  <div
910
1176
  v-for="n in 6"
911
1177
  :key="`sk-${n}`"
@@ -942,6 +1208,24 @@ function defaultCell(col, row) {
942
1208
  </div>
943
1209
  </div>
944
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>
945
1229
  <!-- The rows' scrollbar ---------------------------------------- -->
946
1230
  <!-- Drawn, not borrowed. A browser's own bar belongs to the scrolling
947
1231
  box, and the column head is inside that box — it has to be, because
@@ -991,11 +1275,23 @@ function defaultCell(col, row) {
991
1275
  </select>
992
1276
  </label>
993
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>
994
1290
  <div v-if="lastPage > 0" class="flex items-center gap-2">
995
1291
  <Button
996
1292
  variant="outline"
997
1293
  size="sm"
998
- class="h-7 text-sm disabled:opacity-100 disabled:text-muted-foreground"
1294
+ class="text-sm"
999
1295
  :disabled="page === 0 || loading"
1000
1296
  @click="prevPage"
1001
1297
  >
@@ -1007,7 +1303,7 @@ function defaultCell(col, row) {
1007
1303
  <Button
1008
1304
  variant="outline"
1009
1305
  size="sm"
1010
- class="h-7 text-sm disabled:opacity-100 disabled:text-muted-foreground"
1306
+ class="text-sm"
1011
1307
  :disabled="page >= lastPage || loading"
1012
1308
  @click="nextPage"
1013
1309
  >
@@ -1021,3 +1317,7 @@ function defaultCell(col, row) {
1021
1317
  <style scoped>
1022
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}
1023
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>