@conduction/nextcloud-vue 2.56.0-beta.34 → 2.56.0-beta.36

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 (155) hide show
  1. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue.js +1 -1
  2. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +1 -1
  3. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
  4. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
  5. package/dist/esm/components/CnBodySections/CnBodySections.vue.js +1 -1
  6. package/dist/esm/components/CnBodySections/CnBodySections.vue2.js +22 -1
  7. package/dist/esm/components/CnBodySections/CnBodySections.vue2.js.map +1 -1
  8. package/dist/esm/components/CnBodySections/CnBodySections.vue3.js.map +1 -1
  9. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue.js +1 -1
  10. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js +47 -4
  11. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js.map +1 -1
  12. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue3.js.map +1 -1
  13. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue.js +1 -1
  14. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js +239 -3
  15. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js.map +1 -1
  16. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js +5 -5
  17. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js.map +1 -1
  18. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  19. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +5 -0
  20. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  21. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  22. package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue.js +1 -1
  23. package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue2.js +1 -1
  24. package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue2.js.map +1 -1
  25. package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue3.js.map +1 -1
  26. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +203 -4
  27. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  28. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  29. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js +1 -1
  30. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +38 -1
  31. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
  32. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +45 -5
  33. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
  34. package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue.js +1 -1
  35. package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue2.js +1 -1
  36. package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue2.js.map +1 -1
  37. package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue3.js.map +1 -1
  38. package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue2.js +29 -6
  39. package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue2.js.map +1 -1
  40. package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue3.js.map +1 -1
  41. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue.js +1 -1
  42. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +126 -2
  43. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
  44. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js +24 -2
  45. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
  46. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  47. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +37 -2
  48. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  49. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  50. package/dist/esm/components/CnReportsPage/CnReportsPage.vue.js +1 -1
  51. package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js +135 -2
  52. package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js.map +1 -1
  53. package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js +95 -43
  54. package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js.map +1 -1
  55. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue.js +1 -1
  56. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js +72 -5
  57. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
  58. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js +33 -8
  59. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
  60. package/dist/esm/components/CnStatWidget/CnStatWidget.vue.js +1 -1
  61. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +1 -1
  62. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  63. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  64. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue.js +1 -1
  65. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js +17 -2
  66. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js.map +1 -1
  67. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js +1 -1
  68. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js.map +1 -1
  69. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  70. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +33 -3
  71. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  72. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  73. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue.js +1 -1
  74. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js +16 -0
  75. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js.map +1 -1
  76. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js.map +1 -1
  77. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue.js +1 -1
  78. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js +2 -1
  79. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js.map +1 -1
  80. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue3.js.map +1 -1
  81. package/dist/esm/dialogs/CnTransitionInputDialog.vue.js +1 -1
  82. package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js +68 -6
  83. package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js.map +1 -1
  84. package/dist/esm/dialogs/CnTransitionInputDialog.vue3.js +133 -105
  85. package/dist/esm/dialogs/CnTransitionInputDialog.vue3.js.map +1 -1
  86. package/dist/esm/index.js +1 -0
  87. package/dist/esm/index.js.map +1 -1
  88. package/dist/esm/l10n/en.json.js +16 -1
  89. package/dist/esm/l10n/en.json.js.map +1 -1
  90. package/dist/esm/l10n/nl.json.js +38 -1
  91. package/dist/esm/l10n/nl.json.js.map +1 -1
  92. package/dist/esm/library-style.js +1 -1
  93. package/dist/esm/nextcloud-vue.css +525 -318
  94. package/dist/esm/utils/buildManifest.js +37 -3
  95. package/dist/esm/utils/buildManifest.js.map +1 -1
  96. package/dist/esm/utils/detailWidgetVisibility.js +149 -0
  97. package/dist/esm/utils/detailWidgetVisibility.js.map +1 -0
  98. package/dist/esm/utils/formatMetric.js +39 -3
  99. package/dist/esm/utils/formatMetric.js.map +1 -1
  100. package/dist/esm/utils/gridStack.js +40 -1
  101. package/dist/esm/utils/gridStack.js.map +1 -1
  102. package/dist/esm/utils/humaniseKey.js +41 -0
  103. package/dist/esm/utils/humaniseKey.js.map +1 -0
  104. package/dist/esm/utils/notificationRuleLabel.js +6 -23
  105. package/dist/esm/utils/notificationRuleLabel.js.map +1 -1
  106. package/dist/esm/utils/resolveManifestTokens.js +1 -0
  107. package/dist/esm/utils/resolveManifestTokens.js.map +1 -1
  108. package/dist/esm/utils/sentinelTokens.js +9 -0
  109. package/dist/esm/utils/sentinelTokens.js.map +1 -1
  110. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  111. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  112. package/dist/nextcloud-vue.cjs.js +2250 -708
  113. package/dist/nextcloud-vue.cjs.js.map +2 -2
  114. package/dist/nextcloud-vue.css +525 -318
  115. package/l10n/en.json +16 -1
  116. package/l10n/nl.json +38 -1
  117. package/package.json +1 -1
  118. package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +1 -1
  119. package/src/components/CnBodySections/CnBodySections.vue +22 -1
  120. package/src/components/CnCountdownWidget/CnCountdownWidget.vue +47 -4
  121. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +244 -8
  122. package/src/components/CnDashboardPage/CnDashboardPage.vue +5 -0
  123. package/src/components/CnDeltaWidget/CnDeltaWidget.vue +1 -1
  124. package/src/components/CnDetailPage/CnDetailPage.md +2 -0
  125. package/src/components/CnDetailPage/CnDetailPage.vue +203 -4
  126. package/src/components/CnFieldListWidget/CnFieldListWidget.vue +63 -1
  127. package/src/components/CnGaugeWidget/CnGaugeWidget.vue +1 -1
  128. package/src/components/CnInteractionFormWidget/CnInteractionFormWidget.vue +29 -6
  129. package/src/components/CnObjectDataWidget/CnObjectDataWidget.md +1 -1
  130. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +154 -5
  131. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +37 -2
  132. package/src/components/CnReportsPage/CnReportsPage.vue +179 -1
  133. package/src/components/CnStagesWidget/CnStagesWidget.vue +103 -6
  134. package/src/components/CnStatWidget/CnStatWidget.vue +1 -1
  135. package/src/components/CnStatsBlockWidget/CnStatsBlockWidget.vue +34 -3
  136. package/src/components/CnTabsWidget/CnTabsWidget.vue +33 -3
  137. package/src/components/CnWalkthrough/CnWalkthrough.vue +16 -0
  138. package/src/components/CnWeekStripWidget/CnWeekStripWidget.vue +2 -1
  139. package/src/css/form-field.css +14 -0
  140. package/src/css/form-widget.css +11 -0
  141. package/src/css/kpi-card.css +13 -0
  142. package/src/css/look-board-reports.css +58 -0
  143. package/src/css/look-board.css +31 -0
  144. package/src/dialogs/CnTransitionInputDialog.vue +87 -7
  145. package/src/index.js +2 -0
  146. package/src/schemas/app-manifest-v2.schema.json +23 -2
  147. package/src/utils/buildManifest.js +36 -2
  148. package/src/utils/detailWidgetVisibility.js +145 -0
  149. package/src/utils/formatMetric.js +38 -2
  150. package/src/utils/gridStack.js +39 -0
  151. package/src/utils/humaniseKey.js +38 -0
  152. package/src/utils/notificationRuleLabel.js +5 -23
  153. package/src/utils/resolveManifestTokens.js +1 -0
  154. package/src/utils/sentinelTokens.js +9 -0
  155. package/src/utils/validateManifestV2.compiled.js +3 -3
@@ -59,8 +59,15 @@
59
59
  class="cn-stages-widget__bar-label"
60
60
  :title="stage.hint || stage.label"
61
61
  :aria-disabled="stage.disabled && stage.clickable ? 'true' : null"
62
+ :aria-busy="movingTo === stage.id ? 'true' : null"
62
63
  :data-testid="`cn-stages-widget-stage-${stage.id}`"
63
64
  @click="onBarClick(stage)">
65
+ <NcLoadingIcon
66
+ v-if="movingTo === stage.id"
67
+ class="cn-stages-widget__moving"
68
+ :size="16"
69
+ :name="movingMessage"
70
+ data-testid="cn-stages-widget-moving" />
64
71
  {{ stage.label }}<span v-if="stage.state === 'current'" class="cn-stages-widget__sr-only"> ({{ tr('current step') }})</span>
65
72
  </component>
66
73
  <span v-if="stage.date" class="cn-stages-widget__bar-date">{{ stage.date }}</span>
@@ -83,7 +90,19 @@
83
90
  @stageClick="onStageClick"
84
91
  @stageBlocked="onStageBlocked">
85
92
  <template #label="{ stage }">
86
- <span class="cn-timeline-stages__label" :data-testid="`cn-stages-widget-stage-${stage.id}`">
93
+ <span
94
+ class="cn-timeline-stages__label"
95
+ :aria-busy="movingTo === stage.id ? 'true' : null"
96
+ :data-testid="`cn-stages-widget-stage-${stage.id}`">
97
+ <!-- The move in flight. A transition can take seconds, and
98
+ a strip that shows nothing until it returns reads as
99
+ one that never moved. -->
100
+ <NcLoadingIcon
101
+ v-if="movingTo === stage.id"
102
+ class="cn-stages-widget__moving"
103
+ :size="16"
104
+ :name="movingMessage"
105
+ data-testid="cn-stages-widget-moving" />
87
106
  {{ stage.label }}
88
107
  </span>
89
108
  <span v-if="stage.subtitle" class="cn-timeline-stages__subtitle">
@@ -156,8 +175,11 @@
156
175
  v-if="pendingAction"
157
176
  :transition="pendingAction"
158
177
  :schema="schema"
178
+ :error="inputError"
179
+ :fieldErrors="inputFieldErrors"
180
+ :busy="busy"
159
181
  @confirm="onInputConfirm"
160
- @close="pendingAction = null" />
182
+ @close="closeInputDialog" />
161
183
  </div>
162
184
  </template>
163
185
 
@@ -177,6 +199,7 @@ import {
177
199
  performTransition,
178
200
  readAvailableActions,
179
201
  transitionError,
202
+ transitionFieldErrors,
180
203
  } from '../../composables/useLifecycleTransitions.js'
181
204
  import { normalizeLook } from '../../composables/useLook.js'
182
205
  import { useObjectStore } from '../../store/useObjectStore.js'
@@ -534,6 +557,17 @@ export default {
534
557
  pendingAction: null,
535
558
  /** @type {boolean} Whether a move is running. */
536
559
  busy: false,
560
+ /**
561
+ * @type {string} The stage a running move leads to, or ''. Drives
562
+ * the loading indicator on that one stage.
563
+ */
564
+ movingTo: '',
565
+ /** @type {string} A refusal shown inside the open input dialog. */
566
+ inputError: '',
567
+ /** @type {string[]} The input keys that refusal named. */
568
+ inputFieldErrors: [],
569
+ /** @type {string[]} The input keys the last refused move named. */
570
+ lastFieldErrors: [],
537
571
  /** @type {string} Why the last move failed, or ''. */
538
572
  moveError: '',
539
573
  /**
@@ -766,6 +800,20 @@ export default {
766
800
  return this.interactive && !this.busy
767
801
  },
768
802
 
803
+ /**
804
+ * What the stage a move is running to says while it runs: the
805
+ * loading indicator's name and the live region's sentence.
806
+ *
807
+ * @return {string} The sentence, or '' when nothing is moving.
808
+ */
809
+ movingMessage() {
810
+ if (!this.movingTo) {
811
+ return ''
812
+ }
813
+ const stage = this.stages.find((s) => s.id === this.movingTo)
814
+ return this.tr('Moving to {stage}', { stage: stage ? stage.label : this.movingTo })
815
+ },
816
+
769
817
  /**
770
818
  * The allowed moves by the stage they lead to, or null while they are
771
819
  * unknown.
@@ -1225,13 +1273,44 @@ export default {
1225
1273
  * @param {object} data The collected input values.
1226
1274
  * @return {void}
1227
1275
  */
1228
- onInputConfirm(data) {
1276
+ async onInputConfirm(data) {
1229
1277
  const pending = this.pendingAction
1230
- this.pendingAction = null
1231
1278
  if (!pending || !pending.__request) {
1279
+ this.closeInputDialog()
1280
+ return
1281
+ }
1282
+ this.inputError = ''
1283
+ this.inputFieldErrors = []
1284
+ // THE DIALOG STAYS OPEN WHILE THE MOVE RUNS, with its confirm
1285
+ // button busy. Closing it first left the person looking at an
1286
+ // unchanged strip for as long as the transition took, which on a
1287
+ // live case was long enough to reload the page thinking it had
1288
+ // not been sent.
1289
+ const moved = await this.performMove(pending.__request, data)
1290
+ if (this.pendingAction !== pending) {
1291
+ // Closed while the move ran; nothing left to update.
1232
1292
  return
1233
1293
  }
1234
- this.performMove(pending.__request, data)
1294
+ if (moved) {
1295
+ this.closeInputDialog()
1296
+ return
1297
+ }
1298
+ // Refused. The sentence goes into the dialog the person is looking
1299
+ // at, with what they typed still in it.
1300
+ this.inputError = this.moveError
1301
+ this.inputFieldErrors = this.lastFieldErrors
1302
+ this.moveError = ''
1303
+ },
1304
+
1305
+ /**
1306
+ * Close the input dialog and forget the refusal it was showing.
1307
+ *
1308
+ * @return {void}
1309
+ */
1310
+ closeInputDialog() {
1311
+ this.pendingAction = null
1312
+ this.inputError = ''
1313
+ this.inputFieldErrors = []
1235
1314
  },
1236
1315
 
1237
1316
  /**
@@ -1239,11 +1318,17 @@ export default {
1239
1318
  *
1240
1319
  * @param {{stage: {id: string, label: string}, action: string}} request The move.
1241
1320
  * @param {object} [data] The collected transition inputs, when any.
1242
- * @return {Promise<void>}
1321
+ * @return {Promise<boolean>} Whether the move was made.
1243
1322
  */
1244
1323
  async performMove(request, data) {
1245
1324
  this.busy = true
1325
+ this.movingTo = request.stage.id
1246
1326
  this.moveError = ''
1327
+ this.lastFieldErrors = []
1328
+ // Said as the move starts, not only when it lands: a screen reader
1329
+ // user otherwise hears nothing for as long as the server takes.
1330
+ this.statusMessage = this.movingMessage
1331
+ let moved = false
1247
1332
  try {
1248
1333
  if (this.lifecycleMode) {
1249
1334
  await performTransition(this.recordId, request.action, data)
@@ -1270,11 +1355,16 @@ export default {
1270
1355
  if (this.lifecycleMode) {
1271
1356
  await this.loadActions()
1272
1357
  }
1358
+ moved = true
1273
1359
  } catch (error) {
1274
1360
  this.moveError = transitionError(error, this.tr('The move could not be made'))
1361
+ this.lastFieldErrors = transitionFieldErrors(error)
1362
+ this.statusMessage = this.moveError
1275
1363
  } finally {
1276
1364
  this.busy = false
1365
+ this.movingTo = ''
1277
1366
  }
1367
+ return moved
1278
1368
  },
1279
1369
 
1280
1370
  /**
@@ -1501,6 +1591,13 @@ export default {
1501
1591
  padding: 8px 0;
1502
1592
  }
1503
1593
 
1594
+ /* The stage a running move leads to: a small spinner before its label. */
1595
+ .cn-stages-widget__moving {
1596
+ display: inline-flex;
1597
+ vertical-align: middle;
1598
+ margin-inline-end: 4px;
1599
+ }
1600
+
1504
1601
  .cn-stages-widget__notice {
1505
1602
  margin: 0;
1506
1603
  color: var(--color-text-maxcontrast);
@@ -1460,7 +1460,7 @@ export default {
1460
1460
  * @return {string}
1461
1461
  */
1462
1462
  formattedValue() {
1463
- return formatMetricValue(this.displayValue, this.content.format, this.configCtx, this.objectCtx?.object || null)
1463
+ return formatMetricValue(this.displayValue, this.content.format, this.configCtx, this.objectCtx?.object || null, this.effectiveTranslate)
1464
1464
  },
1465
1465
 
1466
1466
  /**
@@ -17,7 +17,7 @@
17
17
  <template>
18
18
  <div
19
19
  class="cn-stats-block-widget"
20
- :class="[iconClass, { 'cn-stats-block-widget--multi': hasEntries && !isRows && !isGrid, 'cn-stats-block-widget--rows': isRows, 'cn-stats-block-widget--grid': isGrid }]">
20
+ :class="[iconClass, { 'cn-stats-block-widget--multi': hasEntries && !isRows && !isGrid, 'cn-stats-block-widget--rows': isRows, 'cn-stats-block-widget--grid': isGrid, 'cn-stats-block-widget--tiles-box': isGrid && tiles === 'box' }]">
21
21
  <!-- Grid layout: the `stats` (each a stat tile's content, so every
22
22
  source kind a stat takes: ratio, elapsed, computed...) in `columns`
23
23
  columns, inside this one card (the board's "Je week tot nu toe"
@@ -288,6 +288,21 @@ export default {
288
288
  default: 2,
289
289
  },
290
290
 
291
+ /**
292
+ * How the grid layout draws its tiles: `'box'` gives each tile a
293
+ * surface of its own (a grey ground, 10px radius, 14px 16px padding),
294
+ * as board DcRapportages draws its efficiency figures. Empty (the
295
+ * default) keeps the tiles as parts of the one card, without a box
296
+ * (board PqMijnWerk "Je week tot nu toe").
297
+ *
298
+ * @type {'box'|null}
299
+ * @spec openspec/changes/screens-stats-block-tile-box/specs/stats-block-grid/spec.md#requirement-a-stats-block-grid-can-box-its-tiles
300
+ */
301
+ tiles: {
302
+ type: String,
303
+ default: null,
304
+ },
305
+
291
306
  /**
292
307
  * How the single-source value is shown: a style name (`'currency'`,
293
308
  * `'number'`, `'percent'`, `'decimal'`, `'duration-hours'`) or
@@ -566,7 +581,7 @@ export default {
566
581
  if (!fmt || this.loading) {
567
582
  return null
568
583
  }
569
- return formatMetricValue(this.resolvedCount, fmt, this.configCtx, this.pageObject)
584
+ return formatMetricValue(this.resolvedCount, fmt, this.configCtx, this.pageObject, this.tl)
570
585
  },
571
586
 
572
587
  /**
@@ -598,7 +613,7 @@ export default {
598
613
  return {
599
614
  // The value as the entry's `format` says, money in the
600
615
  // currency the entry names; null keeps the plain count.
601
- formatted: fmt && resolved ? formatMetricValue(raw, fmt, this.configCtx, this.pageObject) : null,
616
+ formatted: fmt && resolved ? formatMetricValue(raw, fmt, this.configCtx, this.pageObject, this.tl) : null,
602
617
  entry: entry || {},
603
618
  key: `${(entry && entry.register) || ''}/${(entry && entry.schema) || ''}/${i}`,
604
619
  count: resolved ? raw : 0,
@@ -961,7 +976,10 @@ export default {
961
976
  color: var(--color-text-maxcontrast);
962
977
  }
963
978
 
979
+ /* `width: 100%` plus this padding overflowed the card by 48px: the last
980
+ column ran past the card's right edge (visible once the tiles have a box). */
964
981
  .cn-stats-block-widget--grid {
982
+ box-sizing: border-box;
965
983
  padding: 4px 24px 20px;
966
984
  }
967
985
 
@@ -977,6 +995,19 @@ export default {
977
995
  min-width: 0;
978
996
  }
979
997
 
998
+ /* `tiles: "box"`: each tile on a grey ground of its own (board DcRapportages,
999
+ "Vergaderingen en efficiëntie"), 12px apart. */
1000
+ .cn-stats-block-widget--tiles-box .cn-stats-block-widget__grid {
1001
+ gap: 12px;
1002
+ }
1003
+
1004
+ .cn-stats-block-widget--tiles-box .cn-stats-block-widget__grid-stat {
1005
+ box-sizing: border-box;
1006
+ padding: 14px 16px;
1007
+ border-radius: 10px;
1008
+ background: var(--color-background-hover);
1009
+ }
1010
+
980
1011
  .cn-stats-block-widget__rows {
981
1012
  display: flex;
982
1013
  flex-direction: column;
@@ -311,6 +311,12 @@ export default {
311
311
  cnLook: { default: 'nextcloud' },
312
312
  /** The tab list's name under the board look, provided by CnDetailPage (`config.tabsLabel`). */
313
313
  cnDetailTabsLabel: { default: null },
314
+ /**
315
+ * The records the detail page's `visibilityContext` names, keyed
316
+ * by name (a case's case type), provided by CnDetailPage. A tab's
317
+ * `visibleWhen` reads them as `@context.<name>`. Null on any other page.
318
+ */
319
+ cnDetailVisibilityRows: { default: null },
314
320
  /**
315
321
  * The host's translate (CnAppRoot provides `cnTranslate`), so a tab
316
322
  * label written in the manifest reads in the user's language.
@@ -475,6 +481,29 @@ export default {
475
481
  },
476
482
 
477
483
  computed: {
484
+ /**
485
+ * What a tab's `visibleWhen` reads: the record, with the records the
486
+ * detail page's `visibilityContext` names under `@context.<name>`. Null while
487
+ * the record, or one of those records, is not there yet.
488
+ *
489
+ * @return {object|null}
490
+ * @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-page-can-load-records-a-condition-names
491
+ */
492
+ conditionData() {
493
+ if (!this.objectData) {
494
+ return null
495
+ }
496
+ const holder = this.cnDetailVisibilityRows
497
+ if (!holder) {
498
+ return this.objectData
499
+ }
500
+ const rows = holder.value
501
+ if (!rows) {
502
+ return null
503
+ }
504
+ return { ...this.objectData, '@context': rows }
505
+ },
506
+
478
507
  /**
479
508
  * The open panel's own menu items, or an empty list.
480
509
  *
@@ -859,10 +888,11 @@ export default {
859
888
  return 'visible'
860
889
  }
861
890
  if (isLocallyDecidableVisibleWhen(cond)) {
862
- if (!this.objectData) {
891
+ const data = this.conditionData
892
+ if (!data) {
863
893
  return 'pending'
864
894
  }
865
- return evaluateVisibleWhenLocal(cond, this.objectData) ? 'visible' : 'hidden'
895
+ return evaluateVisibleWhenLocal(cond, data) ? 'visible' : 'hidden'
866
896
  }
867
897
  const answer = this.sourceVisibility[index]
868
898
  if (answer === undefined) {
@@ -883,7 +913,7 @@ export default {
883
913
  async refreshSourceVisibility() {
884
914
  const tabs = Array.isArray(this.content?.tabs) ? this.content.tabs : []
885
915
  const seq = ++this.visibilitySeq
886
- const ctx = { objectId: this.objectId, object: this.objectData || undefined }
916
+ const ctx = { objectId: this.objectId, object: this.conditionData || this.objectData || undefined }
887
917
  await Promise.all(tabs.map(async (tab, index) => {
888
918
  const cond = tab && typeof tab === 'object' ? tab.visibleWhen : null
889
919
  if (!cond || isLocallyDecidableVisibleWhen(cond)) {
@@ -683,11 +683,27 @@ export default {
683
683
  * deferred tour) go through here — so a centered step cannot be
684
684
  * swallowed by either.
685
685
  *
686
+ * A tour that declares `startOn` opens only on one of those route
687
+ * names, whatever its first step looks like.
688
+ *
686
689
  * @param {object} tour The tour definition.
687
690
  * @param {string} [routeName] The current route name.
688
691
  * @return {boolean} True when the tour may open on this route.
692
+ * @spec openspec/changes/a-tour-starts-on-the-pages-it-names/specs/walkthrough/spec.md
689
693
  */
690
694
  routeMatchesTour(tour, routeName) {
695
+ // A tour that names where it starts (`startOn`) opens only there.
696
+ // It wins over the centered-step rule below: a welcome card floats
697
+ // in the middle of the screen, but a user who opens a case from a
698
+ // mail link lands on that case, and the card then blocks the page
699
+ // they came for. Measured 13 October on dossiq: a staff user's first
700
+ // visit was a deep link to a Woo request, and "Welcome to Dossiq"
701
+ // opened over it. The tour waits in `_pendingAutoTour` and opens when
702
+ // the user reaches a named route.
703
+ const startOn = Array.isArray(tour && tour.startOn) ? tour.startOn.map(String) : []
704
+ if (startOn.length > 0) {
705
+ return !!routeName && startOn.includes(String(routeName))
706
+ }
691
707
  const page = this.firstStepPage(tour)
692
708
  if (!page) {
693
709
  return true
@@ -89,7 +89,8 @@ const DEFAULT_LIMIT = 100
89
89
  *
90
90
  * It renders the five working days by default (`days: 7` adds the weekend),
91
91
  * highlights today, marks items that are late, and says so when a day has
92
- * nothing. On a narrow screen the strip scrolls sideways.
92
+ * nothing. On a narrow screen the strip scrolls sideways; under the board
93
+ * look (`.cn-look-board`) the days wrap into rows instead.
93
94
  *
94
95
  * Items come from an OpenRegister source (one request for the whole week) or
95
96
  * from static `items`. Resolved by its registry type key `week-strip`.
@@ -183,3 +183,17 @@
183
183
  .cn-look-board .cn-form-page__footer .cn-form-page__primary {
184
184
  margin-inline-start: auto;
185
185
  }
186
+
187
+ /* A Nextcloud text area or text field with its label inside the field hides
188
+ its placeholder until the field takes focus, so the label and the
189
+ placeholder are never drawn over each other. A theme that forces every
190
+ placeholder visible (thematiq's nldesign system sets
191
+ `::placeholder { opacity: 1 !important }`) drew both: the dossiq DqTaak
192
+ notes field read "Een notitie toevoegen" over "Wat heb je gevonden, wie heb
193
+ je gebeld?". Under the board look the placeholder stays hidden at rest, as
194
+ Nextcloud intends; a field with its label outside keeps its placeholder.
195
+ @spec openspec/changes/board-field-label-over-placeholder/specs/form-field-board-look/spec.md#requirement-a-field-label-and-its-placeholder-are-never-drawn-over-each-other */
196
+ .cn-look-board .textarea__input:not(:focus, .textarea__input--label-outside)::placeholder,
197
+ .cn-look-board .input-field:not(.input-field--label-outside) .input-field__input:not(:focus)::placeholder {
198
+ opacity: 0 !important;
199
+ }
@@ -37,6 +37,7 @@
37
37
  }
38
38
 
39
39
  .cn-form-widget__textarea {
40
+ box-sizing: border-box;
40
41
  width: 100%;
41
42
  min-height: 80px;
42
43
  padding: 8px;
@@ -58,3 +59,13 @@
58
59
  color: var(--color-error);
59
60
  margin: 0;
60
61
  }
62
+
63
+ /* A select takes its field's width, however narrow the card. NcSelect is at
64
+ least 260px wide on its own, so in pipelinq's 234px "Nieuw contactmoment"
65
+ card at 1100 wide the channel and client selects ran 40px past the card's
66
+ edge. The client picker (CnResourceSelect) is an NcSelect too.
67
+ @spec openspec/changes/interaction-form-fits-and-reads-dutch/specs/interaction-form-widget/spec.md#requirement-the-interaction-form-fits-its-card-and-reads-in-the-readers-language */
68
+ .cn-form-widget .v-select.select {
69
+ min-width: 0;
70
+ width: 100%;
71
+ }
@@ -444,6 +444,19 @@ a.cn-kpi-card--stacked:focus-visible {
444
444
  color: var(--color-main-text);
445
445
  }
446
446
 
447
+ /* A board tile whose label wraps grows into its cell instead of clipping its
448
+ own last line. dossiq's "Afgehandeld deze maand" at 1100 wide held 129px of
449
+ content in a 114px tile and lost its caption's bottom. The tile still fills
450
+ a cell it fits (`min-height: 100%`), so every tile that fits keeps its
451
+ size; one that does not takes the height of its content, and the card-fit
452
+ cell's `safe center` keeps its top in view.
453
+ @spec openspec/changes/board-stat-tile-grows/specs/stat-tile-board-look/spec.md#requirement-a-board-stat-tile-shows-all-its-lines */
454
+ .cn-kpi-card--board.cn-kpi-card--stacked {
455
+ flex: 0 0 auto;
456
+ height: auto;
457
+ min-height: 100%;
458
+ }
459
+
447
460
  .cn-kpi-card--board.cn-kpi-card--stacked .cn-kpi-card__caption-line,
448
461
  .cn-kpi-card--board.cn-kpi-card--stacked .cn-stats-block__count-label {
449
462
  font-size: 13px;
@@ -115,3 +115,61 @@
115
115
  .cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) .cn-dashboard-page {
116
116
  padding: 0;
117
117
  }
118
+
119
+ /* The widget area draws no panel of its own (board DcRapportages: the
120
+ highlighted report's card sits straight on the page ground). A theme's
121
+ catch-all for anything named like a widget (nldesign's element overrides:
122
+ `[class*="widget"]:not([class*="widget__"]):not(#header *)`, an id
123
+ specificity no class rule outranks) painted `__widgets` white, hence the
124
+ `!important`. */
125
+ .cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) .cn-reports-page__widgets {
126
+ background: transparent !important;
127
+ }
128
+
129
+ /* The grid insets every card by its 12px margin (CnDashboardPage's default
130
+ `gridMargin`); the page padding already places the area, so the card's
131
+ edges line up with the report cards above it. */
132
+ .cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) .cn-reports-page__widgets .cn-dashboard-grid {
133
+ width: calc(100% + 24px);
134
+ margin: -12px;
135
+ }
136
+
137
+ /* A report card is a link, but its title and description read as text, as on
138
+ the board: no underline, whatever a theme does to links. */
139
+ .cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) .cn-reports-page__card,
140
+ .cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) .cn-reports-page__card * {
141
+ text-decoration: none !important;
142
+ }
143
+
144
+ /* The header buttons beside the title (screens-reports-header-buttons, board
145
+ DcRapportages "Downloaden"): the board's 40px button with a grey border on
146
+ the main background, the primary one in the primary fill, as on the index
147
+ header. `!important` holds them under a theme that restyles every button
148
+ (nldesign). The buildiq square keeps its own rule. */
149
+ .cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) .cn-reports-page__header-buttons > .button-vue--secondary {
150
+ height: var(--cn-board-control-height) !important;
151
+ min-height: var(--cn-board-control-height) !important;
152
+ padding: 0 14px !important;
153
+ border: 1px solid var(--color-border-dark) !important;
154
+ border-radius: var(--cn-board-control-radius) !important;
155
+ background: var(--color-main-background) !important;
156
+ color: var(--color-main-text) !important;
157
+ }
158
+
159
+ .cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) .cn-reports-page__header-buttons > .button-vue--secondary:hover {
160
+ background: var(--color-background-hover) !important;
161
+ }
162
+
163
+ .cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) .cn-reports-page__header-buttons > .button-vue--primary {
164
+ height: var(--cn-board-control-height) !important;
165
+ min-height: var(--cn-board-control-height) !important;
166
+ padding: 0 16px !important;
167
+ border: 0 !important;
168
+ border-radius: var(--cn-board-control-radius) !important;
169
+ background: var(--color-primary-element) !important;
170
+ color: var(--color-primary-element-text) !important;
171
+ }
172
+
173
+ .cn-look-board .cn-reports-page:not(.cn-look-nextcloud *) .cn-reports-page__header-buttons > .button-vue--primary:hover {
174
+ background: var(--color-primary-element-hover) !important;
175
+ }
@@ -616,6 +616,18 @@
616
616
  font-weight: 600;
617
617
  }
618
618
 
619
+ /* ---- Dashboard card titles --------------------------------------------- */
620
+ /* A card's title on a dashboard (and on the widgets under a reports page) is
621
+ the board's card h2: 17px weight 700 (boards PqMijnWerk, DcRapportages and
622
+ the app dashboards; `--cn-board-heading-size`, as on a detail page's cards).
623
+ The wrapper's own 14px weight 600 stays for the Nextcloud look, and the
624
+ native-dashboard chrome keeps its 20px. */
625
+ .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-widget-wrapper:not(.cn-widget-wrapper--nc-dashboard) .cn-widget-wrapper__title {
626
+ font-size: var(--cn-board-heading-size, 17px);
627
+ font-weight: 700;
628
+ line-height: 1.3;
629
+ }
630
+
619
631
  /* ---- Record list on a dashboard card (PtDashboard) ---------------------- */
620
632
  /* The dashboard's card body is flush, so the rows carry their own inset: the
621
633
  board's card pads 24px at the sides and each row 14px, a soft hairline
@@ -702,3 +714,22 @@
702
714
  .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-record-list__chevron {
703
715
  display: none;
704
716
  }
717
+
718
+ /* ---- Week strip (DqMijnWerk "Termijnen deze week") ---------------------- */
719
+ /* The days wrap inside the card instead of scrolling sideways: a row holds
720
+ as many days of at least 120px as the card fits, and the rest go to the
721
+ next row (dossiq at 1100 wide held 680px of days in a 494px card and
722
+ scrolled; now three days and two). 120px keeps a case title's words whole;
723
+ five 88px days fitted one row but broke "Bezwaar" mid-word. A card wide
724
+ enough for the week draws it as before, one row of equal columns. The
725
+ day's head wraps its "today" badge under the label when the day is
726
+ narrow.
727
+ @spec openspec/changes/board-week-strip-fits/specs/week-strip-board-look/spec.md#requirement-a-board-week-strip-fits-its-card */
728
+ .cn-look-board .cn-week-strip:not(.cn-look-nextcloud *) .cn-week-strip__days {
729
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 120px), 1fr));
730
+ min-width: 0;
731
+ }
732
+
733
+ .cn-look-board .cn-week-strip:not(.cn-look-nextcloud *) .cn-week-strip__head {
734
+ flex-wrap: wrap;
735
+ }