@antelopejs/dms 0.7.7 → 0.7.9

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 (85) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/test/attachment-host/capabilities.d.ts +25 -0
  3. package/dist/test/attachment-host/capabilities.js +53 -1
  4. package/dist/test/attachment-host/capabilities.js.map +1 -1
  5. package/dist/test/integration/table-view-capabilities.test.js +13 -0
  6. package/dist/test/integration/table-view-capabilities.test.js.map +1 -1
  7. package/dist/test/unit/interfaces/dms-base/column-display.test.js +15 -0
  8. package/dist/test/unit/interfaces/dms-base/column-display.test.js.map +1 -1
  9. package/dist/test/unit/interfaces/dms-base/page-live.test.d.ts +1 -0
  10. package/dist/test/unit/interfaces/dms-base/page-live.test.js +66 -0
  11. package/dist/test/unit/interfaces/dms-base/page-live.test.js.map +1 -0
  12. package/dist/test/unit/interfaces/dms-base/table-view-reduced-chrome.test.js +6 -1
  13. package/dist/test/unit/interfaces/dms-base/table-view-reduced-chrome.test.js.map +1 -1
  14. package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js +16 -0
  15. package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js.map +1 -1
  16. package/frontend-vue/layers/dms-core/app/composables/period/usePeriod.ts +23 -3
  17. package/frontend-vue/layers/dms-core/i18n/locales/core-en-GB.json +2 -1
  18. package/frontend-vue/layers/dms-core/i18n/locales/core-fr-FR.json +2 -1
  19. package/frontend-vue/layers/dms-layout/app/build/components/layout/RowSidePanel.vue +134 -0
  20. package/frontend-vue/layers/dms-layout/app/build/composables/layout/rowSidePanel.ts +129 -0
  21. package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +6 -0
  22. package/frontend-vue/layers/dms-layout/app/types/page.ts +3 -0
  23. package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +5 -1
  24. package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +5 -1
  25. package/frontend-vue/layers/dms-ui/app/build/components/table/Table.vue +44 -8
  26. package/frontend-vue/layers/dms-ui/app/build/components/table-view/LiveHoldBanner.vue +35 -0
  27. package/frontend-vue/layers/dms-ui/app/build/composables/actions/rowNavigation.ts +17 -0
  28. package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +107 -12
  29. package/frontend-vue/layers/dms-ui/app/build/composables/blocks/useRefreshTriggers.ts +26 -11
  30. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/cellHelpers.ts +24 -0
  31. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/metricCells.ts +58 -16
  32. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +30 -3
  33. package/frontend-vue/layers/dms-ui/app/build/composables/live/currentPageLive.ts +107 -0
  34. package/frontend-vue/layers/dms-ui/app/build/composables/live/formatElapsed.ts +25 -0
  35. package/frontend-vue/layers/dms-ui/app/build/composables/live/pageLiveController.ts +229 -0
  36. package/frontend-vue/layers/dms-ui/app/build/composables/live/shortcuts.ts +8 -0
  37. package/frontend-vue/layers/dms-ui/app/build/composables/live/useLiveToggleShortcut.ts +25 -0
  38. package/frontend-vue/layers/dms-ui/app/build/composables/live/usePageLiveHost.ts +46 -0
  39. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodQuery.ts +330 -0
  40. package/frontend-vue/layers/dms-ui/app/build/composables/period/usePeriodLocation.ts +189 -0
  41. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableListData.ts +15 -3
  42. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableLiveHold.ts +77 -0
  43. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +53 -18
  44. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewConfig.ts +1 -0
  45. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/recordLink.ts +61 -2
  46. package/frontend-vue/layers/dms-ui/app/build/utils/rowClickTrigger.ts +73 -0
  47. package/frontend-vue/layers/dms-ui/app/components/RecursiveComponent.vue +5 -4
  48. package/frontend-vue/layers/dms-ui/app/components/blocks/LiveToggleBlock.vue +92 -0
  49. package/frontend-vue/layers/dms-ui/app/components/field-row/FieldRow.vue +10 -4
  50. package/frontend-vue/layers/dms-ui/app/components/period/PeriodSelector.vue +19 -0
  51. package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +99 -13
  52. package/frontend-vue/layers/dms-ui/app/composables/live/usePageLive.ts +52 -0
  53. package/frontend-vue/layers/dms-ui/app/composables/table-view/shortcuts/index.ts +8 -0
  54. package/frontend-vue/layers/dms-ui/app/composables/table-view/shortcuts/tableViewRowPanel.ts +30 -0
  55. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/action-target.ts +15 -0
  56. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/config.ts +4 -0
  57. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/display.ts +8 -1
  58. package/frontend-vue/layers/dms-ui/app/config/shortcuts-registry.ts +5 -0
  59. package/frontend-vue/layers/dms-ui/app/types/live.ts +8 -0
  60. package/frontend-vue/layers/dms-ui/app/types/row-action.ts +7 -2
  61. package/frontend-vue/layers/dms-ui/app/utils/blockRefresh.ts +44 -6
  62. package/frontend-vue/layers/dms-ui/app/utils/componentId.ts +21 -0
  63. package/frontend-vue/layers/dms-ui/app/utils/rowClickAction.ts +12 -0
  64. package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +28 -0
  65. package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +28 -0
  66. package/frontend-vue/tests/chart-live-pause.test.ts +74 -0
  67. package/frontend-vue/tests/component-id.test.ts +18 -0
  68. package/frontend-vue/tests/field-row.test.ts +14 -0
  69. package/frontend-vue/tests/live-hold-banner.test.ts +37 -0
  70. package/frontend-vue/tests/live-toggle.test.ts +162 -0
  71. package/frontend-vue/tests/page-live-gate.test.ts +241 -0
  72. package/frontend-vue/tests/period-live-pause.test.ts +120 -0
  73. package/frontend-vue/tests/period-selector-url.test.ts +268 -0
  74. package/frontend-vue/tests/period-url-query.test.ts +351 -0
  75. package/frontend-vue/tests/row-side-panel.test.ts +653 -0
  76. package/frontend-vue/tests/table-cell-tones.test.ts +176 -0
  77. package/frontend-vue/tests/table-live-hold.test.ts +204 -0
  78. package/frontend-vue/tests/table-row-hover.test.ts +32 -0
  79. package/frontend-vue/tests/table-source-period-scope.test.ts +144 -0
  80. package/frontend-vue/tests/table-view-refresh.test.ts +2 -2
  81. package/frontend-vue/tests/table-view-sources.test.ts +40 -0
  82. package/frontend-vue/tests/vue-inventory.test.ts +1 -1
  83. package/package.json +2 -2
  84. package/skills/dms/SKILL.md +9 -2
  85. package/skills/dms-pages/REFERENCE.md +5 -0
@@ -432,7 +432,8 @@
432
432
  "components": {
433
433
  "tableview": "📄 TableView",
434
434
  "tab": "🗂️ Onglet",
435
- "global": "🌐 Global"
435
+ "global": "🌐 Global",
436
+ "livetoggle": "📡 Direct / Pause"
436
437
  },
437
438
  "chart": {
438
439
  "no_data": "Aucune donnée",
@@ -0,0 +1,134 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * The body of the side panel a `sidePanel` action target opens on a row (see
4
+ * `rowSidePanel.ts`). The DMS draws the header: the title, the row's position
5
+ * among the rows the table shows, previous and next (J and K too) and close
6
+ * (Escape too). Below it, the module's component receives the row as a
7
+ * drawer's component does, mounted anew on each row. The position follows the
8
+ * table: a refresh that moves or drops the row updates it.
9
+ */
10
+ import { computed, onBeforeUnmount, onMounted } from "vue";
11
+ import {
12
+ listenRowPanelClose,
13
+ listenRowSteps,
14
+ rowNavigation,
15
+ } from "#dms-ui/app/build/composables/actions/rowNavigation";
16
+ import {
17
+ closeRowSidePanel,
18
+ useRowSidePanelContent,
19
+ } from "../../composables/layout/rowSidePanel";
20
+
21
+ const DEFAULT_POSITION_KEY = "side_panel.position";
22
+
23
+ const { t } = useI18n();
24
+ const { processI18n } = useTranslation();
25
+ const content = useRowSidePanelContent();
26
+
27
+ const navigation = computed(() => {
28
+ const current = content.value;
29
+ if (!current?.row || !current.source) return undefined;
30
+ return rowNavigation(current.source, current.row, current.showRow);
31
+ });
32
+
33
+ const position = computed(() => {
34
+ if (!navigation.value) return undefined;
35
+ const params = {
36
+ index: navigation.value.index + 1,
37
+ total: navigation.value.total,
38
+ };
39
+ const label = content.value?.positionLabel;
40
+ return label ? processI18n(label, params) : t(DEFAULT_POSITION_KEY, params);
41
+ });
42
+
43
+ const componentProps = computed(() => ({
44
+ ...content.value?.componentOptions,
45
+ rowData: content.value?.row,
46
+ navigation: navigation.value,
47
+ }));
48
+
49
+ const close = () => closeRowSidePanel();
50
+
51
+ let stopKeys: Array<() => void> = [];
52
+
53
+ onMounted(() => {
54
+ stopKeys = [
55
+ listenRowSteps(() => navigation.value),
56
+ listenRowPanelClose(close),
57
+ ];
58
+ });
59
+
60
+ // Another side panel took its place, or the user signed out: the row is no
61
+ // longer open.
62
+ onBeforeUnmount(() => {
63
+ stopKeys.forEach((stop) => stop());
64
+ close();
65
+ });
66
+ </script>
67
+
68
+ <template>
69
+ <div v-if="content" data-dms-row-side-panel class="flex min-h-0 flex-col">
70
+ <header
71
+ class="border-default flex shrink-0 items-start gap-2 border-b py-3 ps-4 pe-3"
72
+ >
73
+ <div class="min-w-0 flex-1 self-center">
74
+ <h2
75
+ class="text-highlighted truncate text-[15px] leading-[1.3] font-[650] tracking-[-0.015em]"
76
+ >
77
+ {{ content.title }}
78
+ </h2>
79
+ <p
80
+ v-if="position"
81
+ data-dms-row-side-panel-position
82
+ class="text-muted mt-0.5 text-[12.5px] tabular-nums"
83
+ >
84
+ {{ position }}
85
+ </p>
86
+ </div>
87
+ <div class="flex shrink-0 items-center gap-0.5">
88
+ <template v-if="navigation">
89
+ <UButton
90
+ data-dms-row-side-panel-prev
91
+ icon="i-ph-caret-up"
92
+ color="neutral"
93
+ variant="ghost"
94
+ size="sm"
95
+ square
96
+ :aria-label="t('side_panel.previous')"
97
+ :disabled="!navigation.hasPrev"
98
+ @click="navigation.prev()"
99
+ />
100
+ <UButton
101
+ data-dms-row-side-panel-next
102
+ icon="i-ph-caret-down"
103
+ color="neutral"
104
+ variant="ghost"
105
+ size="sm"
106
+ square
107
+ :aria-label="t('side_panel.next')"
108
+ :disabled="!navigation.hasNext"
109
+ @click="navigation.next()"
110
+ />
111
+ </template>
112
+ <UButton
113
+ data-dms-row-side-panel-close
114
+ icon="i-ph-x"
115
+ color="neutral"
116
+ variant="ghost"
117
+ size="sm"
118
+ square
119
+ :aria-label="t('side_panel.close')"
120
+ @click="close"
121
+ />
122
+ </div>
123
+ </header>
124
+ <div class="min-h-0 flex-1 overflow-y-auto p-4">
125
+ <Suspense>
126
+ <component
127
+ :is="content.component"
128
+ :key="content.rowKey"
129
+ v-bind="componentProps"
130
+ />
131
+ </Suspense>
132
+ </div>
133
+ </div>
134
+ </template>
@@ -0,0 +1,129 @@
1
+ import { type Component, type ShallowRef, shallowRef } from "vue";
2
+ import type { RowNavigationSource } from "#dms-ui/app/build/composables/actions/rowNavigation";
3
+ import {
4
+ registerSidePanel,
5
+ unregisterSidePanel,
6
+ } from "../../../composables/useAppSidePanels";
7
+ import {
8
+ isSidePanelOpen,
9
+ setOpenSidePanel,
10
+ useSidePanelPreferences,
11
+ } from "./sidePanelState";
12
+
13
+ /**
14
+ * Id of the side panel a `sidePanel` action target opens. It is registered
15
+ * only while a row is open, so a side panel cookie still naming it after a
16
+ * reload shows nothing.
17
+ *
18
+ * @internal
19
+ */
20
+ export const ROW_SIDE_PANEL_ID = "dms:row-side-panel";
21
+ /** Registered name of `RowSidePanel.vue` (the layer's `Dms` prefix applied). */
22
+ const ROW_SIDE_PANEL_COMPONENT = "DmsRowSidePanel";
23
+
24
+ /**
25
+ * What the row side panel shows: the header the DMS draws, the module's
26
+ * component and the row it receives.
27
+ *
28
+ * @internal
29
+ */
30
+ export interface RowSidePanelContent {
31
+ /** One run of an action: rows shown by stepping share it. */
32
+ session: symbol;
33
+ /** The table (or button bar) that opened the panel; see {@link rowSidePanelOwnerKey}. */
34
+ ownerKey: string;
35
+ title: string;
36
+ /** i18n key (`$`-prefixed) of the position, receiving `{ index, total }`. */
37
+ positionLabel?: string;
38
+ component: Component;
39
+ /** Props of the component besides `rowData` and `navigation`. */
40
+ componentOptions: Record<string, unknown>;
41
+ row?: Data;
42
+ /** Id of `row`: the key the component is mounted under, the row the table highlights. */
43
+ rowKey?: string;
44
+ /** The rows shown, which the panel steps through (J / K). */
45
+ source?: RowNavigationSource;
46
+ /** Shows another row of the same run in place. */
47
+ showRow: (row: Data) => void;
48
+ /** Runs once the panel closes, whatever closed it. */
49
+ onClose?: () => void;
50
+ }
51
+
52
+ // Written on the client only (a click, a table mounting on a deep link), never
53
+ // while the server renders a request, so a module-level ref is safe here. It
54
+ // holds a component and callbacks, which no shared state may serialize.
55
+ const content = shallowRef<RowSidePanelContent | null>(null);
56
+
57
+ /**
58
+ * The key a table's open row is kept under, which the table compares to
59
+ * highlight it.
60
+ *
61
+ * @internal
62
+ */
63
+ export function rowSidePanelOwnerKey(
64
+ pageId: string,
65
+ componentId: string,
66
+ ): string {
67
+ return `${pageId}-${componentId}`;
68
+ }
69
+
70
+ /** @internal */
71
+ export function useRowSidePanelContent(): Readonly<
72
+ ShallowRef<RowSidePanelContent | null>
73
+ > {
74
+ return content;
75
+ }
76
+
77
+ /**
78
+ * Shows `next` in the row side panel, opening it (and closing any other side
79
+ * panel) when it is closed. Another run's content gives way: its `onClose`
80
+ * runs first.
81
+ *
82
+ * @internal
83
+ */
84
+ export function openRowSidePanel(next: RowSidePanelContent): void {
85
+ const current = content.value;
86
+ if (current && current.session !== next.session) current.onClose?.();
87
+ content.value = next;
88
+ if (current?.title !== next.title) {
89
+ registerSidePanel({
90
+ id: ROW_SIDE_PANEL_ID,
91
+ component: ROW_SIDE_PANEL_COMPONENT,
92
+ ariaLabel: next.title,
93
+ });
94
+ }
95
+ setOpenSidePanel(useSidePanelPreferences(), ROW_SIDE_PANEL_ID);
96
+ }
97
+
98
+ function closeContent(current: RowSidePanelContent): void {
99
+ content.value = null;
100
+ unregisterSidePanel(ROW_SIDE_PANEL_ID);
101
+ const preferences = useSidePanelPreferences();
102
+ if (isSidePanelOpen(preferences, ROW_SIDE_PANEL_ID)) {
103
+ setOpenSidePanel(preferences, null);
104
+ }
105
+ current.onClose?.();
106
+ }
107
+
108
+ /**
109
+ * Closes the row side panel; with `session`, only while it still shows that
110
+ * run.
111
+ *
112
+ * @internal
113
+ */
114
+ export function closeRowSidePanel(session?: symbol): void {
115
+ const current = content.value;
116
+ if (!current || (session && current.session !== session)) return;
117
+ closeContent(current);
118
+ }
119
+
120
+ /**
121
+ * Closes the row side panel when `ownerKey` opened it: the table it belongs
122
+ * to is gone.
123
+ *
124
+ * @internal
125
+ */
126
+ export function closeRowSidePanelOf(ownerKey: string): void {
127
+ const current = content.value;
128
+ if (current?.ownerKey === ownerKey) closeContent(current);
129
+ }
@@ -2,6 +2,7 @@
2
2
  import { withoutTrailingSlash } from "ufo";
3
3
  import { holdPageHeaderActions } from "../composables/layout/usePageHeaderActions";
4
4
  import { useDevReloading } from "../build/composables/dev-reload/useDevReloadHolder";
5
+ import { usePageLiveHost } from "#dms-ui/app/build/composables/live/usePageLiveHost";
5
6
 
6
7
  defineOptions({ inheritAttrs: false });
7
8
 
@@ -216,6 +217,11 @@ const { permissions: userPermissions } = usePermissions();
216
217
  const realtimePageId = computed(() => pagelayoutMetadata.value?.fullId ?? "");
217
218
  const realtime = usePageRealtime(realtimePageId);
218
219
  providePageRealtime(realtime);
220
+ // Before the blocks set up: their refetches go through the page's live gate.
221
+ usePageLiveHost(() => ({
222
+ pageId: pagelayoutMetadata.value?.fullId,
223
+ live: pagelayoutMetadata.value?.live,
224
+ }));
219
225
 
220
226
  function buildPageSetupContext(
221
227
  pageInfo: PageInfo,
@@ -1,5 +1,6 @@
1
1
  import type { BlockText } from "#dms-core/app/types/composed-text";
2
2
  import type { BlockAction } from "#dms-ui/app/build/components/blocks/BlockActions.vue";
3
+ import type { PageLiveOptions } from "#dms-ui/app/types/live";
3
4
 
4
5
  export type MenuItemType = "label" | "link";
5
6
  // MenuItemVariant / MenuItemStatus / MenuItemQuery live in dms-core, the layer
@@ -28,6 +29,8 @@ export interface MenuOptions {
28
29
  status?: MenuItemStatus;
29
30
  badge?: string;
30
31
  bypassTenantAccessGate?: boolean;
32
+ /** The page's Direct / Pause valve (see `PageLiveOptions`). */
33
+ live?: PageLiveOptions;
31
34
  }
32
35
 
33
36
  export interface CategoryInfo extends Omit<MenuOptions, "category"> {
@@ -895,6 +895,10 @@
895
895
  "close": "Don't show again"
896
896
  },
897
897
  "side_panel": {
898
- "resize": "Resize the panel"
898
+ "resize": "Resize the panel",
899
+ "position": "{index} of {total}",
900
+ "previous": "Previous",
901
+ "next": "Next",
902
+ "close": "Close"
899
903
  }
900
904
  }
@@ -899,6 +899,10 @@
899
899
  "close": "Ne plus afficher"
900
900
  },
901
901
  "side_panel": {
902
- "resize": "Redimensionner le panneau"
902
+ "resize": "Redimensionner le panneau",
903
+ "position": "{index} sur {total}",
904
+ "previous": "Précédent",
905
+ "next": "Suivant",
906
+ "close": "Fermer"
903
907
  }
904
908
  }
@@ -37,6 +37,7 @@ import type {
37
37
  FormPageUrls,
38
38
  } from "../../composables/table-view/useTableViewConfig";
39
39
  import type { CustomRowAction } from "../../../types/row-action";
40
+ import type { TableViewRowClick } from "../../utils/rowClickTrigger";
40
41
  import type { RowActionConfig } from "#dms-core/app/types/row-action";
41
42
  import type { TableTabItem } from "./Tabs.vue";
42
43
  import type { ColumnDisplay } from "../../composables/data-types/useColumnValueRenderer";
@@ -149,6 +150,13 @@ export interface TableProps<T> {
149
150
 
150
151
  rowIdKey?: string;
151
152
  rowActions?: TableRowActionOptions;
153
+ /**
154
+ * What runs a row's click action: a double-click (the default) or a single
155
+ * click (`"single"`). See `isRowClickTrigger`.
156
+ */
157
+ rowClick?: TableViewRowClick;
158
+ /** Id of the row a side panel has open: the grid highlights it. */
159
+ openRowId?: string;
152
160
  customNavItems?: NavItem[];
153
161
  tabs?: TableTabItem[];
154
162
  customButtons?: CustomButton[];
@@ -412,6 +420,7 @@ import {
412
420
  } from "../../composables/table/constants";
413
421
  import { mergeColumnOrder } from "../../composables/table/utils/columnOrder";
414
422
  import { EYEBROW_CLASS } from "../../utils/eyebrow";
423
+ import { isRowClickTrigger } from "../../utils/rowClickTrigger";
415
424
 
416
425
  // Same color as the surrounding card frame (.dms-card) so sticky
417
426
  // rail/pinned cells blend in instead of showing a contrasting block.
@@ -435,6 +444,12 @@ const ROW_SELECTED_BG = "data-[selected=true]:bg-primary/10";
435
444
  const ROW_SELECTED_CELL_BG =
436
445
  "group-data-[selected=true]:bg-[color-mix(in_srgb,var(--ui-primary)_10%,var(--dms-surface-card))]";
437
446
 
447
+ // The row a side panel has open: the hover tint, held, and an accent rule on
448
+ // its leading edge.
449
+ const ROW_OPEN_BG =
450
+ "data-[open=true]:bg-elevated data-[open=true]:shadow-[inset_2px_0_0_0_var(--ui-primary)]";
451
+ const ROW_OPEN_CELL_BG = "group-data-[open=true]:bg-elevated";
452
+
438
453
  // An expanded row keeps the hover tint and hands its bottom rule to the detail
439
454
  // band below it.
440
455
  const ROW_EXPANDED_BG = "data-[expanded=true]:bg-elevated";
@@ -501,7 +516,7 @@ const theme = tv({
501
516
  colResizer:
502
517
  "bg-primary/40 absolute right-0 top-0 z-10 h-full w-1 cursor-col-resize touch-none select-none opacity-0 hover:opacity-100",
503
518
 
504
- row: `group ${ROW_SELECTED_BG} ${ROW_EXPANDED_BG}`,
519
+ row: `group ${ROW_SELECTED_BG} ${ROW_EXPANDED_BG} ${ROW_OPEN_BG}`,
505
520
  rowCell: `${FIRST_ROW_CELL_GUTTER} border-b-muted border-b h-11 px-3.5 py-0 text-[13px] last:pe-2.5 in-[tr:last-child]:border-b-0 group-data-[expanded=true]:border-b-transparent`,
506
521
  rowInternal: "transition-opacity duration-150",
507
522
  rowContainer: "relative",
@@ -560,11 +575,11 @@ const theme = tv({
560
575
  pinned: {
561
576
  left: {
562
577
  headCell: `${HEADER_MATCH_BG} sticky z-10 shadow-[2px_0_0_0_rgba(0,0,0,0.06)]`,
563
- rowCell: `${PANEL_MATCH_BG} ${ROW_SELECTED_CELL_BG} ${ROW_EXPANDED_CELL_BG} group-data-[presence=true]:bg-elevated sticky z-10 shadow-[2px_0_0_0_rgba(0,0,0,0.06)]`,
578
+ rowCell: `${PANEL_MATCH_BG} ${ROW_SELECTED_CELL_BG} ${ROW_EXPANDED_CELL_BG} ${ROW_OPEN_CELL_BG} group-data-[presence=true]:bg-elevated sticky z-10 shadow-[2px_0_0_0_rgba(0,0,0,0.06)]`,
564
579
  },
565
580
  right: {
566
581
  headCell: `${HEADER_MATCH_BG} sticky z-10 shadow-[-2px_0_0_0_rgba(0,0,0,0.06)]`,
567
- rowCell: `${PANEL_MATCH_BG} ${ROW_SELECTED_CELL_BG} ${ROW_EXPANDED_CELL_BG} group-data-[presence=true]:bg-elevated sticky z-10 shadow-[-2px_0_0_0_rgba(0,0,0,0.06)]`,
582
+ rowCell: `${PANEL_MATCH_BG} ${ROW_SELECTED_CELL_BG} ${ROW_EXPANDED_CELL_BG} ${ROW_OPEN_CELL_BG} group-data-[presence=true]:bg-elevated sticky z-10 shadow-[-2px_0_0_0_rgba(0,0,0,0.06)]`,
568
583
  },
569
584
  },
570
585
  // Dense lists: 36px rows under a 32px header band.
@@ -1205,7 +1220,7 @@ const rowClickAction = (row: T): RowClickAction | undefined =>
1205
1220
  // lights up always opens something.
1206
1221
  const isRowClickable = (row: T): boolean => rowClickAction(row) !== undefined;
1207
1222
 
1208
- const handleRowDoubleClick = (row: T) => {
1223
+ const runRowClickAction = (row: T) => {
1209
1224
  const action = rowClickAction(row);
1210
1225
  if (!action) return;
1211
1226
  if (typeof action === "string") {
@@ -1215,6 +1230,19 @@ const handleRowDoubleClick = (row: T) => {
1215
1230
  props.onCustomRowAction?.(action, row);
1216
1231
  };
1217
1232
 
1233
+ // Both events reach the row: `rowClick` tells which one runs its action.
1234
+ const handleRowPointer = (event: MouseEvent, row: T) => {
1235
+ if (!isRowClickTrigger(props.rowClick, event)) return;
1236
+ runRowClickAction(row);
1237
+ };
1238
+
1239
+ const isOpenRow = (row: T): boolean =>
1240
+ props.openRowId !== undefined &&
1241
+ String(get(row, rowIdKey)) === props.openRowId;
1242
+
1243
+ const isSingleClickRow = (row: T): boolean =>
1244
+ props.rowClick === "single" && isRowClickable(row);
1245
+
1218
1246
  const presenceRailBackground = (row: T): string => {
1219
1247
  if (getRowPresence(row)) return PRESENCE_RAIL_ACTIVE_BG;
1220
1248
  if (props.loading || !isRowClickable(row)) return PANEL_MATCH_BG;
@@ -1339,6 +1367,10 @@ defineShortcuts({
1339
1367
  />
1340
1368
  </template>
1341
1369
 
1370
+ <!-- A strip the table view adds over its rows (the live hold's "N new"),
1371
+ shown whichever chrome the display keeps. -->
1372
+ <slot name="notice" />
1373
+
1342
1374
  <slot name="body" :table="table">
1343
1375
  <section
1344
1376
  ref="tableRoot"
@@ -1472,20 +1504,24 @@ defineShortcuts({
1472
1504
  <tr
1473
1505
  :data-selected="row.getIsSelected()"
1474
1506
  :data-expanded="row.getIsExpanded()"
1507
+ :data-open="isOpenRow(row.original) || undefined"
1508
+ :aria-current="isOpenRow(row.original) || undefined"
1475
1509
  :data-presence="!!getRowPresence(row.original)"
1476
1510
  :title="
1477
1511
  getRowPresence(row.original)
1478
1512
  ? presenceTooltipText(getRowPresence(row.original)!)
1479
1513
  : undefined
1480
1514
  "
1481
- :class="
1515
+ :class="[
1482
1516
  uiTable.row({
1483
1517
  loading,
1484
1518
  rowClickable: isRowClickable(row.original),
1485
1519
  presence: !!getRowPresence(row.original),
1486
- })
1487
- "
1488
- @dblclick="handleRowDoubleClick(row.original)"
1520
+ }),
1521
+ { 'cursor-pointer': isSingleClickRow(row.original) },
1522
+ ]"
1523
+ @click="handleRowPointer($event, row.original)"
1524
+ @dblclick="handleRowPointer($event, row.original)"
1489
1525
  @mouseenter="handleRowHover(row)"
1490
1526
  @mouseleave="handleRowLeave"
1491
1527
  @dragover="reorderHandles.allowDrop"
@@ -0,0 +1,35 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * The strip a `liveHold` table shows while its page is paused and events came
4
+ * in: "N new · Show". Show reads the rows once; the page stays paused.
5
+ */
6
+ defineProps<{
7
+ /** Events the table received since its rows were held or last shown. */
8
+ count: number;
9
+ }>();
10
+
11
+ const emit = defineEmits<{ show: [] }>();
12
+
13
+ const { t } = useI18n();
14
+ </script>
15
+
16
+ <template>
17
+ <div
18
+ role="status"
19
+ class="dms-live-hold-banner text-highlighted flex items-center gap-2.5 border-b border-(--dms-info-line) bg-(--dms-info-tint) py-[7px] ps-[18px] pe-3.5 text-[13px]"
20
+ >
21
+ <UIcon name="i-ph-pause-circle" class="text-info size-4 shrink-0" />
22
+ <span class="min-w-0">
23
+ {{ t("dms.live.new_rows", { count }, count) }}
24
+ </span>
25
+ <span aria-hidden="true" class="text-muted">·</span>
26
+ <UButton
27
+ :label="t('dms.live.show_new')"
28
+ color="neutral"
29
+ variant="link"
30
+ size="xs"
31
+ class="p-0 font-medium"
32
+ @click="emit('show')"
33
+ />
34
+ </div>
35
+ </template>
@@ -83,3 +83,20 @@ export function listenRowSteps(current: () => RowNavigation | undefined) {
83
83
  window.addEventListener("keydown", onKey);
84
84
  return () => window.removeEventListener("keydown", onKey);
85
85
  }
86
+
87
+ const CLOSE_KEY = "Escape";
88
+
89
+ /**
90
+ * Escape closes a row's side panel, unless the user types in a field or a
91
+ * layer above it (a dialog, a menu) already handled the key. Returns the
92
+ * listener's removal.
93
+ */
94
+ export function listenRowPanelClose(close: () => void) {
95
+ const onKey = (event: KeyboardEvent) => {
96
+ if (event.key !== CLOSE_KEY || event.defaultPrevented) return;
97
+ if (isTypingTarget(event.target)) return;
98
+ close();
99
+ };
100
+ window.addEventListener("keydown", onKey);
101
+ return () => window.removeEventListener("keydown", onKey);
102
+ }