@antelopejs/dms 0.7.1 → 0.7.3

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/CHANGELOG.md +47 -0
  2. package/dist/hooks/tenant-instances.d.ts +1 -0
  3. package/dist/hooks/tenant-instances.js +13 -0
  4. package/dist/hooks/tenant-instances.js.map +1 -0
  5. package/dist/index.js +3 -0
  6. package/dist/index.js.map +1 -1
  7. package/dist/test/unit/interfaces/dms/page-topics.test.js +67 -0
  8. package/dist/test/unit/interfaces/dms/page-topics.test.js.map +1 -1
  9. package/dist/test/unit/interfaces/dms-base/block-vocabulary.test.js +1 -0
  10. package/dist/test/unit/interfaces/dms-base/block-vocabulary.test.js.map +1 -1
  11. package/dist/test/unit/interfaces/dms-base/code-block-catalog.test.d.ts +1 -0
  12. package/dist/test/unit/interfaces/dms-base/code-block-catalog.test.js +46 -0
  13. package/dist/test/unit/interfaces/dms-base/code-block-catalog.test.js.map +1 -0
  14. package/dist/test/unit/interfaces/dms-base/column-display.test.js +11 -0
  15. package/dist/test/unit/interfaces/dms-base/column-display.test.js.map +1 -1
  16. package/dist/test/unit/interfaces/dms-base/display-blocks-catalog.test.js +31 -0
  17. package/dist/test/unit/interfaces/dms-base/display-blocks-catalog.test.js.map +1 -1
  18. package/dist/test/unit/utils/tenant-instances.test.d.ts +1 -0
  19. package/dist/test/unit/utils/tenant-instances.test.js +53 -0
  20. package/dist/test/unit/utils/tenant-instances.test.js.map +1 -0
  21. package/dist/utils/index.d.ts +1 -0
  22. package/dist/utils/index.js +1 -0
  23. package/dist/utils/index.js.map +1 -1
  24. package/dist/utils/tenant-instances.d.ts +2 -0
  25. package/dist/utils/tenant-instances.js +38 -0
  26. package/dist/utils/tenant-instances.js.map +1 -0
  27. package/dist/validation/auth.schema.d.ts +2 -2
  28. package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +4 -0
  29. package/frontend-vue/layers/dms-ui/app/build/components/activity-feed/activityFeedDays.ts +35 -23
  30. package/frontend-vue/layers/dms-ui/app/build/components/blocks/BlockActions.vue +34 -6
  31. package/frontend-vue/layers/dms-ui/app/build/components/containers/drawer/DynamicDrawer.vue +26 -8
  32. package/frontend-vue/layers/dms-ui/app/build/components/table/Actions.vue +3 -1
  33. package/frontend-vue/layers/dms-ui/app/build/components/table/Empty.vue +1 -1
  34. package/frontend-vue/layers/dms-ui/app/build/components/table/MonoCell.vue +3 -2
  35. package/frontend-vue/layers/dms-ui/app/build/components/table/Table.vue +24 -13
  36. package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +1 -1
  37. package/frontend-vue/layers/dms-ui/app/build/composables/blocks/useBlockItems.ts +12 -5
  38. package/frontend-vue/layers/dms-ui/app/build/composables/blocks/useRefreshTriggers.ts +40 -0
  39. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/metricCells.ts +15 -4
  40. package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +85 -22
  41. package/frontend-vue/layers/dms-ui/app/build/composables/table/utils/clearTableFilters.ts +10 -2
  42. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/usePersistedPagination.ts +79 -0
  43. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableListData.ts +94 -0
  44. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +41 -5
  45. package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/tableQuery.ts +3 -0
  46. package/frontend-vue/layers/dms-ui/app/build/utils/codeEditor.ts +2 -13
  47. package/frontend-vue/layers/dms-ui/app/build/utils/codeHighlight.ts +59 -0
  48. package/frontend-vue/layers/dms-ui/app/build/utils/codeLanguages.ts +36 -0
  49. package/frontend-vue/layers/dms-ui/app/build/utils/codePalette.ts +32 -0
  50. package/frontend-vue/layers/dms-ui/app/build/utils/shellParser.ts +55 -0
  51. package/frontend-vue/layers/dms-ui/app/build/utils/urlVariables.ts +77 -20
  52. package/frontend-vue/layers/dms-ui/app/components/activity-feed/ActivityFeed.vue +15 -4
  53. package/frontend-vue/layers/dms-ui/app/components/blocks/BannerBlock.vue +111 -25
  54. package/frontend-vue/layers/dms-ui/app/components/blocks/CodeBlock.vue +95 -0
  55. package/frontend-vue/layers/dms-ui/app/components/blocks/EmptyStateBlock.vue +18 -0
  56. package/frontend-vue/layers/dms-ui/app/components/blocks/KeyValueListBlock.vue +8 -0
  57. package/frontend-vue/layers/dms-ui/app/components/blocks/NavCardGridBlock.vue +21 -9
  58. package/frontend-vue/layers/dms-ui/app/components/blocks/StatGroupBlock.vue +8 -0
  59. package/frontend-vue/layers/dms-ui/app/components/code-snippet/CodeSnippet.vue +97 -0
  60. package/frontend-vue/layers/dms-ui/app/components/empty-state/EmptyState.vue +13 -4
  61. package/frontend-vue/layers/dms-ui/app/components/flow-canvas/FlowCanvas.vue +14 -3
  62. package/frontend-vue/layers/dms-ui/app/components/form/components/InputCode.vue +2 -8
  63. package/frontend-vue/layers/dms-ui/app/components/key-value-list/KeyValueList.vue +19 -0
  64. package/frontend-vue/layers/dms-ui/app/components/master-detail/MasterDetail.vue +54 -48
  65. package/frontend-vue/layers/dms-ui/app/components/meter/MeterBlock.vue +24 -9
  66. package/frontend-vue/layers/dms-ui/app/components/tab/Tab.vue +4 -1
  67. package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +66 -74
  68. package/frontend-vue/layers/dms-ui/app/components/top-list/internal/TopListRow.vue +4 -3
  69. package/frontend-vue/layers/dms-ui/app/composables/chart/types.ts +5 -2
  70. package/frontend-vue/layers/dms-ui/app/composables/chart/useChartFetch.ts +2 -29
  71. package/frontend-vue/layers/dms-ui/app/composables/form/useForm.ts +2 -1
  72. package/frontend-vue/layers/dms-ui/app/composables/tab/types/props.ts +2 -0
  73. package/frontend-vue/layers/dms-ui/app/composables/table-view/types/action-target.ts +4 -0
  74. package/frontend-vue/layers/dms-ui/app/utils/blockRefresh.ts +2 -2
  75. package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +1 -0
  76. package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +1 -0
  77. package/frontend-vue/package.json +4 -3
  78. package/frontend-vue/pnpm-lock.yaml +8 -5
  79. package/frontend-vue/tests/activity-feed-days.test.ts +26 -12
  80. package/frontend-vue/tests/banner-block-fetch.test.ts +162 -0
  81. package/frontend-vue/tests/block-fetch-options-forwarding.test.ts +100 -0
  82. package/frontend-vue/tests/block-items-period-realtime.test.ts +147 -0
  83. package/frontend-vue/tests/block-route-params.test.ts +48 -0
  84. package/frontend-vue/tests/code-block.test.ts +155 -0
  85. package/frontend-vue/tests/code-highlight.test.ts +72 -0
  86. package/frontend-vue/tests/composed-text-blocks.test.ts +5 -0
  87. package/frontend-vue/tests/composed-text-more-blocks.test.ts +391 -0
  88. package/frontend-vue/tests/drawer-target-direction.test.ts +205 -0
  89. package/frontend-vue/tests/empty-state-late-slots.test.ts +53 -0
  90. package/frontend-vue/tests/flow-canvas-delete-badge.test.ts +88 -0
  91. package/frontend-vue/tests/form-redirect-placeholders.test.ts +11 -0
  92. package/frontend-vue/tests/key-value-list-copy.test.ts +114 -0
  93. package/frontend-vue/tests/master-detail-layout.test.ts +53 -0
  94. package/frontend-vue/tests/mono-cell-truncate.test.ts +50 -0
  95. package/frontend-vue/tests/nav-card-grid-empty-category.test.ts +1 -0
  96. package/frontend-vue/tests/page-fill-height.test.ts +20 -3
  97. package/frontend-vue/tests/route-param-occurrences.test.ts +9 -0
  98. package/frontend-vue/tests/table-actions-search-label.test.ts +147 -0
  99. package/frontend-vue/tests/table-clear-filters.test.ts +22 -0
  100. package/frontend-vue/tests/table-empty-body-viewport.test.ts +25 -0
  101. package/frontend-vue/tests/table-metric-cells.test.ts +22 -0
  102. package/frontend-vue/tests/table-pills-cell.test.ts +126 -0
  103. package/frontend-vue/tests/table-source-navigation.test.ts +123 -0
  104. package/frontend-vue/tests/table-view-hidden-filters-page.test.ts +137 -0
  105. package/frontend-vue/tests/table-view-refresh.test.ts +84 -0
  106. package/frontend-vue/tests/table-view-sources.test.ts +67 -0
  107. package/frontend-vue/tests/vue-inventory.test.ts +1 -1
  108. package/package.json +2 -2
  109. package/skills/dms/SKILL.md +14 -3
  110. package/skills/dms-pages/REFERENCE.md +17 -1
@@ -31,6 +31,10 @@ interface StatGroupBlockProps extends DefaultComponentProps {
31
31
  label?: string;
32
32
  fetchUrl?: string;
33
33
  fetchUrlMethod?: string;
34
+ /** Id of the PeriodSelector the data follows. */
35
+ periodScope?: string;
36
+ /** Topics whose events make the block read `fetchUrl` again. */
37
+ realtimeTopic?: string | string[];
34
38
  /** Shown when there is nothing to list. */
35
39
  empty?: BlockEmptyText;
36
40
  /**
@@ -47,6 +51,8 @@ const props = withDefaults(defineProps<StatGroupBlockProps>(), {
47
51
  label: undefined,
48
52
  fetchUrl: undefined,
49
53
  fetchUrlMethod: undefined,
54
+ periodScope: undefined,
55
+ realtimeTopic: undefined,
50
56
  empty: undefined,
51
57
  skeletonCount: undefined,
52
58
  });
@@ -60,6 +66,8 @@ const { items, isPending, hasError, refresh } =
60
66
  items: () => props.items,
61
67
  fetchUrl: props.fetchUrl,
62
68
  fetchUrlMethod: props.fetchUrlMethod,
69
+ periodScope: props.periodScope,
70
+ realtimeTopic: props.realtimeTopic,
63
71
  routeParams: () => props.routeParams,
64
72
  watchActions: props.watchActions,
65
73
  componentId: props.componentId,
@@ -0,0 +1,97 @@
1
+ <script setup lang="ts">
2
+ import { computed, onMounted, shallowRef } from "vue";
3
+ import DmsCopyButton from "../copy/CopyButton.vue";
4
+ import {
5
+ codeLanguageLabel,
6
+ PLAIN_CODE_LANGUAGE,
7
+ resolveCodeLanguage,
8
+ } from "../../build/utils/codeLanguages";
9
+ import type { CodeToken, highlightCode } from "../../build/utils/codeHighlight";
10
+
11
+ // Read-only code in the editor's look: a head with the title, the language
12
+ // and a copy button, then the code highlighted by the editor's languages.
13
+ // Until the highlighter loads (and on the server) the code shows as is.
14
+ interface CodeSnippetProps {
15
+ /** The code, verbatim. */
16
+ code: string;
17
+ /**
18
+ * `json`, `javascript`, `typescript`, `html`, `sql`, `shell` or `text`;
19
+ * `js`, `ts`, `bash`, `sh` and `plain` are read as theirs.
20
+ */
21
+ language?: string;
22
+ /** A button copies the code. */
23
+ copy?: boolean;
24
+ /** Shown in the head, as written. */
25
+ title?: string;
26
+ /** Lines shown before the code scrolls; all of them when unset. */
27
+ maxLines?: number;
28
+ /** Wraps long lines instead of scrolling sideways. */
29
+ wrap?: boolean;
30
+ }
31
+
32
+ const props = withDefaults(defineProps<CodeSnippetProps>(), {
33
+ language: undefined,
34
+ copy: true,
35
+ title: undefined,
36
+ maxLines: undefined,
37
+ wrap: false,
38
+ });
39
+
40
+ const LINE_HEIGHT_PX = 19;
41
+ // The code's top and bottom padding (`py-2`), around its lines.
42
+ const CODE_PADDING_PX = 16;
43
+
44
+ const highlight = shallowRef<typeof highlightCode | null>(null);
45
+
46
+ const language = computed(() => resolveCodeLanguage(props.language));
47
+ const hasHead = computed(
48
+ () => !!props.title || props.copy || language.value !== PLAIN_CODE_LANGUAGE,
49
+ );
50
+ const tokens = computed<CodeToken[]>(() =>
51
+ highlight.value
52
+ ? highlight.value(props.code, language.value)
53
+ : [{ text: props.code }],
54
+ );
55
+ const sizeStyle = computed(() =>
56
+ props.maxLines
57
+ ? {
58
+ maxHeight: `${props.maxLines * LINE_HEIGHT_PX + CODE_PADDING_PX}px`,
59
+ }
60
+ : undefined,
61
+ );
62
+
63
+ onMounted(async () => {
64
+ const module = await import("../../build/utils/codeHighlight");
65
+ highlight.value = module.highlightCode;
66
+ });
67
+ </script>
68
+
69
+ <template>
70
+ <div
71
+ class="border-default min-w-0 overflow-hidden rounded-[10px] border bg-(--ui-bg)"
72
+ >
73
+ <div
74
+ v-if="hasHead"
75
+ class="border-default flex min-h-9 items-center gap-2 border-b bg-(--dms-bg-muted) py-1 ps-3.5 pe-1"
76
+ >
77
+ <span
78
+ v-if="props.title"
79
+ class="text-highlighted min-w-0 truncate text-[12.5px] font-semibold"
80
+ >
81
+ {{ props.title }}
82
+ </span>
83
+ <span class="text-dimmed ms-auto font-mono text-[11px]">
84
+ {{ codeLanguageLabel(language) }}
85
+ </span>
86
+ <DmsCopyButton v-if="props.copy" :value="props.code" />
87
+ </div>
88
+ <!-- Focusable: a keyboard scrolls the code when it overflows. -->
89
+ <pre
90
+ class="text-highlighted m-0 overflow-auto px-3.5 py-2 font-mono text-[12.5px] leading-[19px]"
91
+ :class="props.wrap ? 'break-words whitespace-pre-wrap' : 'whitespace-pre'"
92
+ :style="sizeStyle"
93
+ :data-language="language"
94
+ tabindex="0"
95
+ ><code><span v-for="(token, index) in tokens" :key="index" :style="token.style">{{ token.text }}</span></code></pre>
96
+ </div>
97
+ </template>
@@ -35,6 +35,8 @@ interface EmptyStateProps {
35
35
  interface EmptyStateSlots {
36
36
  /** Rich description, replaces `description`. */
37
37
  default?: () => unknown;
38
+ /** Under the text, above the actions: a snippet to start with. */
39
+ content?: () => unknown;
38
40
  /** Buttons or links under the text. */
39
41
  actions?: () => unknown;
40
42
  }
@@ -71,6 +73,7 @@ const theme = tv({
71
73
  well: "mb-1.5",
72
74
  title: "text-highlighted",
73
75
  description: "text-muted",
76
+ content: "mt-2.5 w-full max-w-[60ch] text-start",
74
77
  actions: "mt-2.5 flex flex-wrap items-center justify-center gap-2",
75
78
  },
76
79
  variants: {
@@ -106,8 +109,11 @@ const resolvedIcon = computed(() => props.icon ?? VARIANT_ICONS[props.variant]);
106
109
  const resolvedTone = computed<Tone>(
107
110
  () => props.tone ?? (props.variant === "error" ? "error" : "neutral"),
108
111
  );
109
- const hasDescription = computed(() => !!props.description || !!slots.default);
110
- const hasActions = computed(() => !!slots.actions || !!props.actions?.length);
112
+ // Read at each render rather than in a computed: the slots object is not
113
+ // reactive, so a computed would keep the slots of the first render and miss
114
+ // a slot the parent adds or removes later.
115
+ const hasDescription = (): boolean => !!props.description || !!slots.default;
116
+ const hasActions = (): boolean => !!slots.actions || !!props.actions?.length;
111
117
  </script>
112
118
 
113
119
  <template>
@@ -122,10 +128,13 @@ const hasActions = computed(() => !!slots.actions || !!props.actions?.length);
122
128
  :class="ui.well()"
123
129
  />
124
130
  <p :class="ui.title()">{{ props.title }}</p>
125
- <p v-if="hasDescription" :class="ui.description()">
131
+ <p v-if="hasDescription()" :class="ui.description()">
126
132
  <slot>{{ props.description }}</slot>
127
133
  </p>
128
- <div v-if="hasActions" :class="ui.actions()">
134
+ <div v-if="slots.content" :class="ui.content()">
135
+ <slot name="content" />
136
+ </div>
137
+ <div v-if="hasActions()" :class="ui.actions()">
129
138
  <slot name="actions">
130
139
  <UButton
131
140
  v-for="(action, index) in props.actions"
@@ -29,7 +29,9 @@ import { computed, useId, useSlots } from "vue";
29
29
  // Delete/Backspace remove the selection. Both paths only *emit* the
30
30
  // `delete-nodes` intent — the badge for its own node, Delete/Backspace for
31
31
  // the whole selection — the wrapper never mutates the consumer's
32
- // `nodes`/`edges`; the consumer applies the removal.
32
+ // `nodes`/`edges`; the consumer applies the removal. `deleteBadge: false`
33
+ // hides the badge and keeps the keyboard path, for a consumer that draws
34
+ // its own delete control.
33
35
  // - `zoomReadout` adds a live zoom percentage to the control bar that doubles
34
36
  // as a reset-to-100% button.
35
37
  //
@@ -119,6 +121,11 @@ interface Props {
119
121
  * Delete + Backspace.
120
122
  */
121
123
  deleteKeyCode?: DeleteKeyCode;
124
+ /**
125
+ * Draw the delete badge on selected nodes while `deletableNodes` is on.
126
+ * `false` leaves the keyboard path alone. Defaults to `true`.
127
+ */
128
+ deleteBadge?: boolean;
122
129
  /**
123
130
  * Add a live zoom percentage / reset-to-100% control to the control bar.
124
131
  * It renders inside the bar, so it requires `controls` to be enabled.
@@ -141,6 +148,7 @@ const props = withDefaults(defineProps<Props>(), {
141
148
  height: "100%",
142
149
  deletableNodes: false,
143
150
  deleteKeyCode: undefined,
151
+ deleteBadge: true,
144
152
  zoomReadout: false,
145
153
  });
146
154
 
@@ -198,7 +206,7 @@ const zoomPercent = computed(() =>
198
206
  // Project each selected node's top-right corner from flow space to screen
199
207
  // space through the live viewport transform, so badges track pan/zoom/drag.
200
208
  const deleteAffordances = computed<DeleteAffordance[]>(() => {
201
- if (!props.deletableNodes) return [];
209
+ if (!props.deletableNodes || !props.deleteBadge) return [];
202
210
  const {
203
211
  x: panX,
204
212
  y: panY,
@@ -295,7 +303,10 @@ defineExpose(flow);
295
303
  <slot />
296
304
  </VueFlow>
297
305
 
298
- <div v-if="deletableNodes" class="dms-flow-canvas__delete-layer">
306
+ <div
307
+ v-if="deletableNodes && deleteBadge"
308
+ class="dms-flow-canvas__delete-layer"
309
+ >
299
310
  <button
300
311
  v-for="affordance in deleteAffordances"
301
312
  :key="affordance.id"
@@ -3,6 +3,7 @@ import { getCurrentInstance, h, render } from "vue";
3
3
  import { useFormField } from "@nuxt/ui/composables/useFormField";
4
4
  import UIcon from "@nuxt/ui/runtime/vue/components/Icon.vue";
5
5
  import { useControlError } from "../../../build/composables/form/useControlError";
6
+ import { codeLanguageLabel } from "../../../build/utils/codeLanguages";
6
7
  import type {
7
8
  CodeEditor,
8
9
  CodeEditorCompletion,
@@ -47,13 +48,6 @@ const LINE_HEIGHT_PX = 19;
47
48
  // The editor's top and bottom padding, around its lines.
48
49
  const EDITOR_PADDING_PX = 16;
49
50
  const INVALID_JSON = "$dms.field_errors.invalid_json";
50
- const LANGUAGE_LABELS: Record<string, string> = {
51
- json: "JSON",
52
- sql: "SQL",
53
- html: "HTML",
54
- javascript: "JavaScript",
55
- text: "Text",
56
- };
57
51
 
58
52
  const host = useTemplateRef<HTMLElement>("host");
59
53
  const position = ref<CodeEditorPosition>({ line: 1, column: 1 });
@@ -188,7 +182,7 @@ onBeforeUnmount(() => {
188
182
  {{ t("dms.form.code.complete") }}
189
183
  </span>
190
184
  <span class="ms-auto">
191
- {{ LANGUAGE_LABELS[props.language] ?? props.language }} ·
185
+ {{ codeLanguageLabel(props.language) }} ·
192
186
  {{ t("dms.form.code.position", position) }}
193
187
  </span>
194
188
  </div>
@@ -3,6 +3,7 @@ import { computed } from "vue";
3
3
  import { regionalDateTimeFormat } from "#dms-core/app/utils/regional";
4
4
  import DmsStatusPill from "../status-pill/StatusPill.vue";
5
5
  import DmsAutoLink from "../link/AutoLink.vue";
6
+ import DmsCopyButton from "../copy/CopyButton.vue";
6
7
  import { DMS_TONE_TEXT } from "../../build/utils/tone";
7
8
  import type { Tone } from "../../types/tone";
8
9
 
@@ -36,6 +37,10 @@ export interface KeyValueItem {
36
37
  currency?: string;
37
38
  /** This value alone is still loading: a skeleton stands in for it. */
38
39
  loading?: boolean;
40
+ /** Draws a button copying the value as it is shown. */
41
+ copy?: boolean;
42
+ /** Text the copy button copies instead of the shown value; implies `copy`. */
43
+ copyValue?: string;
39
44
  }
40
45
 
41
46
  interface KeyValueListProps {
@@ -156,6 +161,14 @@ function valueClass(item: KeyValueItem): string[] {
156
161
  return ["wrap-anywhere", color, TYPE_CLASSES[type] ?? ""];
157
162
  }
158
163
 
164
+ // An empty value shows a dash, which is not worth copying.
165
+ function copyTextOf(item: KeyValueItem): string | undefined {
166
+ if (item.copyValue) return item.copyValue;
167
+ if (!item.copy) return undefined;
168
+ const formatted = format(item);
169
+ return formatted === EMPTY_VALUE ? undefined : formatted;
170
+ }
171
+
159
172
  function keyOf(item: KeyValueItem, index: number): string {
160
173
  return item.id ?? `${index}-${item.label}`;
161
174
  }
@@ -216,6 +229,12 @@ function keyOf(item: KeyValueItem, index: number): string {
216
229
  {{ item.detail }}
217
230
  </span>
218
231
  </template>
232
+ <!-- -my-1: the button is taller than a dense row's text line. -->
233
+ <DmsCopyButton
234
+ v-if="!props.loading && !item.loading && copyTextOf(item)"
235
+ :value="copyTextOf(item)!"
236
+ class="-my-1"
237
+ />
219
238
  </dd>
220
239
  </div>
221
240
  </template>
@@ -3,6 +3,10 @@
3
3
  // left (380px) and a detail panel on the right. The list is driven by `items`
4
4
  // + v-model; the detail is rendered by the default slot, which receives the
5
5
  // currently selected value.
6
+ //
7
+ // The panes sit side by side from 64rem of the layout's own width (the former
8
+ // `lg` viewport breakpoint), not the viewport's: a docked side panel or a
9
+ // narrow grid cell leaves less room than the screen suggests.
6
10
  interface MdItem {
7
11
  value: string | number;
8
12
  label: string;
@@ -23,59 +27,61 @@ defineSlots<{
23
27
  </script>
24
28
 
25
29
  <template>
26
- <div
27
- class="grid grid-cols-1 items-start gap-5 lg:grid-cols-[380px_minmax(0,1fr)]"
28
- >
29
- <!-- List pane -->
30
- <div class="dms-card p-2">
31
- <p
32
- v-if="listLabel"
33
- class="text-dimmed px-2.5 pt-2.5 pb-1 font-mono text-[10px] tracking-widest uppercase"
34
- >
35
- {{ listLabel }}
36
- </p>
37
- <ul class="flex flex-col gap-1.5">
38
- <li v-for="item in items" :key="item.value">
39
- <button
40
- type="button"
41
- class="flex w-full items-start gap-3 rounded-md border border-transparent px-3.5 py-3 text-left transition-colors"
42
- :class="
43
- selected === item.value
44
- ? 'border-primary bg-primary/10'
45
- : 'hover:bg-elevated'
46
- "
47
- @click="selected = item.value"
48
- >
49
- <span
50
- v-if="item.icon"
51
- class="grid size-[26px] shrink-0 place-items-center"
52
- :class="selected === item.value ? 'text-primary' : 'text-toned'"
30
+ <div class="@container">
31
+ <div
32
+ class="grid grid-cols-1 items-start gap-5 @5xl:grid-cols-[380px_minmax(0,1fr)]"
33
+ >
34
+ <!-- List pane -->
35
+ <div class="dms-card p-2">
36
+ <p
37
+ v-if="listLabel"
38
+ class="text-dimmed px-2.5 pt-2.5 pb-1 font-mono text-[10px] tracking-widest uppercase"
39
+ >
40
+ {{ listLabel }}
41
+ </p>
42
+ <ul class="flex flex-col gap-1.5">
43
+ <li v-for="item in items" :key="item.value">
44
+ <button
45
+ type="button"
46
+ class="flex w-full items-start gap-3 rounded-md border border-transparent px-3.5 py-3 text-left transition-colors"
47
+ :class="
48
+ selected === item.value
49
+ ? 'border-primary bg-primary/10'
50
+ : 'hover:bg-elevated'
51
+ "
52
+ @click="selected = item.value"
53
53
  >
54
- <UIcon
55
- :name="item.icon"
56
- class="size-[18px]"
57
- :aria-hidden="true"
58
- />
59
- </span>
60
- <span class="min-w-0">
61
- <span class="text-highlighted block text-sm font-semibold">
62
- {{ item.label }}
63
- </span>
64
54
  <span
65
- v-if="item.sublabel"
66
- class="text-muted mt-0.5 block truncate text-xs"
55
+ v-if="item.icon"
56
+ class="grid size-[26px] shrink-0 place-items-center"
57
+ :class="selected === item.value ? 'text-primary' : 'text-toned'"
67
58
  >
68
- {{ item.sublabel }}
59
+ <UIcon
60
+ :name="item.icon"
61
+ class="size-[18px]"
62
+ :aria-hidden="true"
63
+ />
69
64
  </span>
70
- </span>
71
- </button>
72
- </li>
73
- </ul>
74
- </div>
65
+ <span class="min-w-0">
66
+ <span class="text-highlighted block text-sm font-semibold">
67
+ {{ item.label }}
68
+ </span>
69
+ <span
70
+ v-if="item.sublabel"
71
+ class="text-muted mt-0.5 block truncate text-xs"
72
+ >
73
+ {{ item.sublabel }}
74
+ </span>
75
+ </span>
76
+ </button>
77
+ </li>
78
+ </ul>
79
+ </div>
75
80
 
76
- <!-- Detail pane -->
77
- <div class="dms-card overflow-hidden">
78
- <slot :selected="selected" />
81
+ <!-- Detail pane -->
82
+ <div class="dms-card overflow-hidden">
83
+ <slot :selected="selected" />
84
+ </div>
79
85
  </div>
80
86
  </div>
81
87
  </template>
@@ -11,28 +11,35 @@ import DmsBlockActions, {
11
11
  } from "../../build/components/blocks/BlockActions.vue";
12
12
  import { useChartFetch } from "../../composables/chart/useChartFetch";
13
13
  import { useWatch } from "../../../../dms-core/app/composables/watch/useWatch";
14
+ import { useComposedText } from "../../../../dms-core/app/composables/translation/useComposedText";
14
15
  import type { DefaultComponentProps } from "../../../../dms-core/app/types/component";
16
+ import type { BlockText } from "../../../../dms-core/app/types/composed-text";
17
+
18
+ /** A segment as the page or the route sends it, its legend still to write. */
19
+ interface MeterBlockSegment extends Omit<MeterSegment, "label"> {
20
+ label?: BlockText;
21
+ }
15
22
 
16
23
  /** What a meter's data source answers: any subset of its figures. */
17
24
  interface MeterResponse {
18
25
  value?: number;
19
26
  max?: number;
20
- segments?: MeterSegment[];
21
- hint?: string;
22
- valueLabel?: string;
27
+ segments?: MeterBlockSegment[];
28
+ hint?: BlockText;
29
+ valueLabel?: BlockText;
23
30
  }
24
31
 
25
32
  // The page block behind the backend `Meter` builder: static figures or a
26
33
  // data source, optionally in a card. DmsMeter draws it.
27
34
  interface MeterBlockProps extends DefaultComponentProps {
28
35
  label?: string;
29
- hint?: string;
36
+ hint?: BlockText;
30
37
  value?: number;
31
38
  max?: number;
32
- segments?: MeterSegment[];
39
+ segments?: MeterBlockSegment[];
33
40
  legend?: boolean;
34
41
  format?: MeterFormat;
35
- valueLabel?: string;
42
+ valueLabel?: BlockText;
36
43
  tone?: MeterTone;
37
44
  warnAt?: number;
38
45
  errorAt?: number;
@@ -40,6 +47,10 @@ interface MeterBlockProps extends DefaultComponentProps {
40
47
  /** Endpoint answering `{ value?, max?, segments?, hint?, valueLabel? }`. */
41
48
  fetchUrl?: string;
42
49
  fetchUrlMethod?: string;
50
+ /** Id of the PeriodSelector the data follows. */
51
+ periodScope?: string;
52
+ /** Topics whose events make the block read `fetchUrl` again. */
53
+ realtimeTopic?: string | string[];
43
54
  /** Wraps the meter in a padded card. */
44
55
  card?: boolean;
45
56
  /** Link buttons pushed right of the legend ("Manage members"). */
@@ -61,11 +72,13 @@ const props = withDefaults(defineProps<MeterBlockProps>(), {
61
72
  size: "sm",
62
73
  fetchUrl: undefined,
63
74
  fetchUrlMethod: undefined,
75
+ periodScope: undefined,
76
+ realtimeTopic: undefined,
64
77
  card: false,
65
78
  actions: () => [],
66
79
  });
67
80
 
68
- const { processI18n } = useTranslation();
81
+ const { processText } = useComposedText();
69
82
  const { t } = useI18n();
70
83
 
71
84
  const { state: watchState } = useWatch(
@@ -76,12 +89,14 @@ const { state: watchState } = useWatch(
76
89
  const { data, isLoading, error, refresh } = useChartFetch<MeterResponse>({
77
90
  fetchUrl: props.fetchUrl,
78
91
  fetchUrlMethod: props.fetchUrlMethod,
92
+ periodScope: props.periodScope,
93
+ realtimeTopic: props.realtimeTopic,
79
94
  routeParams: () => props.routeParams,
80
95
  watchSource: () => JSON.stringify(watchState.value),
81
96
  });
82
97
 
83
- const translate = (text: string | undefined): string | undefined =>
84
- text ? processI18n(text) : undefined;
98
+ const translate = (text: BlockText | undefined): string | undefined =>
99
+ text ? processText(text) : undefined;
85
100
 
86
101
  const figures = computed(() => {
87
102
  const fetched = data.value ?? {};
@@ -20,16 +20,19 @@ const props = withDefaults(defineProps<TabProps>(), {
20
20
  persistState: false,
21
21
  stateKey: "tab",
22
22
  badgesUrl: undefined,
23
+ realtimeTopic: undefined,
23
24
  });
24
25
 
25
26
  // The badges a module counts for a record's tabs, in one request, again when
26
- // a watch action fires: each replaces its tab's static badge.
27
+ // a watch action or a realtime topic fires: each replaces its tab's static
28
+ // badge.
27
29
  const { state: watchState } = useWatch(
28
30
  props.watchActions || [],
29
31
  props.componentId,
30
32
  );
31
33
  const { data: fetchedBadges } = useChartFetch<TabBadges>({
32
34
  fetchUrl: props.badgesUrl,
35
+ realtimeTopic: props.realtimeTopic,
33
36
  routeParams: () => props.routeParams,
34
37
  watchSource: () => JSON.stringify(watchState.value),
35
38
  });