@conduction/nextcloud-vue 2.121.0 → 2.123.0

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 (102) hide show
  1. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +1 -0
  2. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  3. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  4. package/dist/esm/components/CnAttentionFlagWidget/CnAttentionFlagWidget.vue.js +1 -1
  5. package/dist/esm/components/CnAttentionFlagWidget/CnAttentionFlagWidget.vue2.js +18 -4
  6. package/dist/esm/components/CnAttentionFlagWidget/CnAttentionFlagWidget.vue2.js.map +1 -1
  7. package/dist/esm/components/CnAttentionFlagWidget/CnAttentionFlagWidget.vue3.js +63 -32
  8. package/dist/esm/components/CnAttentionFlagWidget/CnAttentionFlagWidget.vue3.js.map +1 -1
  9. package/dist/esm/components/CnChartWidget/CnChartWidget.vue.js +1 -1
  10. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
  11. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
  12. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +35 -0
  13. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  14. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +11 -5
  15. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  16. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
  17. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +13 -0
  18. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  19. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +37 -5
  20. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  21. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
  22. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +40 -0
  23. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  24. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +71 -65
  25. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  26. package/dist/esm/components/CnFlowDetail/CnFlowDetail.vue.js +1 -1
  27. package/dist/esm/components/CnFlowDetail/CnFlowDetail.vue2.js +20 -0
  28. package/dist/esm/components/CnFlowDetail/CnFlowDetail.vue2.js.map +1 -1
  29. package/dist/esm/components/CnFlowDetail/CnFlowDetail.vue3.js +2 -1
  30. package/dist/esm/components/CnFlowDetail/CnFlowDetail.vue3.js.map +1 -1
  31. package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue.js +1 -1
  32. package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue2.js +2 -0
  33. package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue2.js.map +1 -1
  34. package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue3.js +49 -44
  35. package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue3.js.map +1 -1
  36. package/dist/esm/components/CnFlowsPage/CnFlowEditorPage.vue2.js +10 -0
  37. package/dist/esm/components/CnFlowsPage/CnFlowEditorPage.vue2.js.map +1 -1
  38. package/dist/esm/components/CnFlowsPage/CnFlowEditorPage.vue3.js +2 -1
  39. package/dist/esm/components/CnFlowsPage/CnFlowEditorPage.vue3.js.map +1 -1
  40. package/dist/esm/components/CnGraphCanvas/CnGraphCanvas.vue2.js +31 -0
  41. package/dist/esm/components/CnGraphCanvas/CnGraphCanvas.vue2.js.map +1 -1
  42. package/dist/esm/components/CnGraphCanvas/CnGraphCanvas.vue3.js +4 -3
  43. package/dist/esm/components/CnGraphCanvas/CnGraphCanvas.vue3.js.map +1 -1
  44. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +34 -2
  45. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  46. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  47. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js.map +1 -1
  48. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js +1 -0
  49. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js.map +1 -1
  50. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue.js +1 -1
  51. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +2 -0
  52. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  53. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +2 -1
  54. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  55. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue.js +1 -1
  56. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue2.js +31 -1
  57. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue2.js.map +1 -1
  58. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue3.js +14 -4
  59. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue3.js.map +1 -1
  60. package/dist/esm/composables/flowGraphLayout.js +36 -7
  61. package/dist/esm/composables/flowGraphLayout.js.map +1 -1
  62. package/dist/esm/composables/useFlowStore.js +7 -3
  63. package/dist/esm/composables/useFlowStore.js.map +1 -1
  64. package/dist/esm/l10n/en.json.js +3 -1
  65. package/dist/esm/l10n/en.json.js.map +1 -1
  66. package/dist/esm/l10n/nl.json.js +3 -1
  67. package/dist/esm/l10n/nl.json.js.map +1 -1
  68. package/dist/esm/library-style.js +1 -1
  69. package/dist/esm/nextcloud-vue.css +670 -316
  70. package/dist/esm/utils/manifestTranslate.js +12 -3
  71. package/dist/esm/utils/manifestTranslate.js.map +1 -1
  72. package/dist/esm/utils/routeFilters.js +3 -3
  73. package/dist/esm/utils/routeFilters.js.map +1 -1
  74. package/dist/nextcloud-vue.cjs.js +1218 -847
  75. package/dist/nextcloud-vue.cjs.js.map +2 -2
  76. package/dist/nextcloud-vue.css +670 -316
  77. package/l10n/en.json +3 -1
  78. package/l10n/nl.json +3 -1
  79. package/package.json +1 -1
  80. package/src/components/CnAppRoot/CnAppRoot.vue +1 -0
  81. package/src/components/CnAttentionFlagWidget/CnAttentionFlagWidget.vue +50 -5
  82. package/src/components/CnChartWidget/CnChartWidget.vue +10 -8
  83. package/src/components/CnDetailPage/CnDetailPage.md +10 -0
  84. package/src/components/CnDetailPage/CnDetailPage.vue +42 -1
  85. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +39 -0
  86. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +95 -37
  87. package/src/components/CnFlowDetail/CnFlowDetail.vue +21 -0
  88. package/src/components/CnFlowsPage/CnFlowBoard.vue +35 -18
  89. package/src/components/CnFlowsPage/CnFlowEditorPage.vue +11 -0
  90. package/src/components/CnGraphCanvas/CnGraphCanvas.vue +34 -2
  91. package/src/components/CnIndexPage/CnIndexPage.vue +34 -2
  92. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +1 -0
  93. package/src/components/CnObjectSidebar/CnFilesTab.vue +3 -0
  94. package/src/components/CnSectionsWidget/CnSectionsWidget.vue +53 -2
  95. package/src/composables/flowGraphLayout.js +35 -6
  96. package/src/composables/useFlowStore.js +7 -3
  97. package/src/css/detail-page.css +6 -0
  98. package/src/css/look-board-detail.css +53 -6
  99. package/src/css/look-board-index.css +151 -12
  100. package/src/css/look-board.css +116 -4
  101. package/src/utils/manifestTranslate.js +12 -3
  102. package/src/utils/routeFilters.js +3 -3
package/l10n/en.json CHANGED
@@ -2219,7 +2219,9 @@
2219
2219
  "Nothing to check.": "Nothing to check.",
2220
2220
  "This could not be read: {reason}": "This could not be read: {reason}",
2221
2221
  "Still open": "Still open",
2222
- "The checklist could not be read.": "The checklist could not be read."
2222
+ "The checklist could not be read.": "The checklist could not be read.",
2223
+ "Flag it with a reason, so it stands out to everyone who works on it.": "Flag it with a reason, so it stands out to everyone who works on it.",
2224
+ "Clear the flag with a reason once it no longer needs attention.": "Clear the flag with a reason once it no longer needs attention."
2223
2225
  },
2224
2226
  "plurals": {
2225
2227
  "{count} member": [
package/l10n/nl.json CHANGED
@@ -2469,7 +2469,9 @@
2469
2469
  "Nothing to check.": "Er is niets te controleren.",
2470
2470
  "This could not be read: {reason}": "Dit kon niet worden gelezen: {reason}",
2471
2471
  "Still open": "Nog open",
2472
- "The checklist could not be read.": "De lijst kon niet worden gelezen."
2472
+ "The checklist could not be read.": "De lijst kon niet worden gelezen.",
2473
+ "Flag it with a reason, so it stands out to everyone who works on it.": "Markeer het met een reden, zodat het opvalt bij iedereen die eraan werkt.",
2474
+ "Clear the flag with a reason once it no longer needs attention.": "Haal de markering weg met een reden zodra het geen aandacht meer nodig heeft."
2473
2475
  },
2474
2476
  "plurals": {
2475
2477
  "{count} member": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conduction/nextcloud-vue",
3
- "version": "2.121.0",
3
+ "version": "2.123.0",
4
4
  "description": "Shared Vue component library for Conduction Nextcloud apps — complements @nextcloud/vue with higher-level components, OpenRegister integration, and NL Design System support",
5
5
  "homepage": "https://github.com/ConductionNL/nextcloud-vue#readme",
6
6
  "bugs": {
@@ -2278,6 +2278,7 @@ export default {
2278
2278
  getManifest: () => (this.manifestEditor ? this.manifestEditor.source.value : this.manifest),
2279
2279
  getLanguage: () => this.language || getLanguage(),
2280
2280
  translate: (text, vars) => this.translate(text, vars),
2281
+ library: (text, vars) => t('nextcloud-vue', text, vars),
2281
2282
  })
2282
2283
  },
2283
2284
 
@@ -40,7 +40,26 @@
40
40
  data-testid="cn-attention-flag-review-due">{{ label('reviewDue') }}</span>
41
41
  </p>
42
42
 
43
- <div class="cn-attention-flag__act">
43
+ <template v-if="behindButton && !composing">
44
+ <h4 class="cn-attention-flag__heading" data-testid="cn-attention-flag-heading">
45
+ {{ reasonLabel }}
46
+ </h4>
47
+ <p class="cn-attention-flag__hint" data-testid="cn-attention-flag-hint">
48
+ {{ raised ? label('hintClear') : label('hintRaise') }}
49
+ </p>
50
+ <NcButton
51
+ variant="secondary"
52
+ wide
53
+ data-testid="cn-attention-flag-compose"
54
+ @click="composing = true">
55
+ <template #icon>
56
+ <FlagOutline :size="20" />
57
+ </template>
58
+ {{ raised ? label('clear') : label('raise') }}
59
+ </NcButton>
60
+ </template>
61
+
62
+ <div v-else class="cn-attention-flag__act">
44
63
  <NcTextField
45
64
  :modelValue="reason"
46
65
  :label="reasonLabel"
@@ -84,6 +103,7 @@ import { emit } from '@nextcloud/event-bus'
84
103
  import { translate as t } from '@nextcloud/l10n'
85
104
  import { generateUrl } from '@nextcloud/router'
86
105
  import { NcButton, NcTextField } from '@nextcloud/vue'
106
+ import FlagOutline from 'vue-material-design-icons/FlagOutline.vue'
87
107
  import { interpolateUrlTokens } from '../../composables/useEndpointSource.js'
88
108
  import { dispatchObjectsChanged } from '../../utils/objectSignals.js'
89
109
  import { readPath } from '../../utils/readPath.js'
@@ -100,6 +120,8 @@ function defaultLabels() {
100
120
  reviewDue: t('nextcloud-vue', 'Due for review'),
101
121
  reasonRaise: t('nextcloud-vue', 'Why this needs attention'),
102
122
  reasonClear: t('nextcloud-vue', 'Why it no longer needs attention'),
123
+ hintRaise: t('nextcloud-vue', 'Flag it with a reason, so it stands out to everyone who works on it.'),
124
+ hintClear: t('nextcloud-vue', 'Clear the flag with a reason once it no longer needs attention.'),
103
125
  raise: t('nextcloud-vue', 'Flag for attention'),
104
126
  clear: t('nextcloud-vue', 'Clear the flag'),
105
127
  raised: t('nextcloud-vue', 'Raised'),
@@ -130,7 +152,8 @@ const LEVELS = {
130
152
  * "clearPath": "/clear",
131
153
  * "markersPath": "attentionMarkers",
132
154
  * "panelLabels": { "item-files": "Files" },
133
- * "assessment": { "path": "riskAssessment", "levelMap": { "high": "High" }, "reviewDateField": "reviewDate" }
155
+ * "assessment": { "path": "riskAssessment", "levelMap": { "high": "High" }, "reviewDateField": "reviewDate" },
156
+ * "behindButton": true
134
157
  * }
135
158
  * }
136
159
  * ```
@@ -142,7 +165,9 @@ const LEVELS = {
142
165
  * as written. Markers and the assessment are read off the record the page
143
166
  * already holds. An assessment the reader may not see is absent from the
144
167
  * record, so it is absent here rather than blank. A failed read draws
145
- * nothing. After a raise or a clear the widget emits `object-changed`, says
168
+ * nothing. With `behindButton` the strip shows the act as a heading, a hint
169
+ * (`labels.hintRaise` / `labels.hintClear`) and one button, and the reason
170
+ * field opens when that button is pressed. After a raise or a clear the widget emits `object-changed`, says
146
171
  * so to every list (`dispatchObjectsChanged`) and asks the page to refresh.
147
172
  *
148
173
  * @spec openspec/changes/dq-abstract-record-widgets/specs/record-widgets/spec.md#requirement-a-record-can-carry-an-attention-flag
@@ -150,7 +175,7 @@ const LEVELS = {
150
175
  export default {
151
176
  name: 'CnAttentionFlagWidget',
152
177
 
153
- components: { NcButton, NcTextField },
178
+ components: { FlagOutline, NcButton, NcTextField },
154
179
 
155
180
  inject: {
156
181
  cnTranslate: { default: () => (key) => key },
@@ -162,7 +187,8 @@ export default {
162
187
  * `raisePath` (default `/raise`), `clearPath` (default `/clear`),
163
188
  * `labels` (overrides of the default words), `markersPath`,
164
189
  * `panelLabels` (marker tab id to label) and `assessment`
165
- * (`{ path, levelMap?, levelField?, groundField?, reviewDateField? }`).
190
+ * (`{ path, levelMap?, levelField?, groundField?, reviewDateField? }`)
191
+ * and `behindButton` (the reason field opens from a button).
166
192
  */
167
193
  content: {
168
194
  type: Object,
@@ -205,6 +231,7 @@ export default {
205
231
  history: [],
206
232
  reason: '',
207
233
  refusal: '',
234
+ composing: false,
208
235
  }
209
236
  },
210
237
 
@@ -222,6 +249,11 @@ export default {
222
249
  return interpolateUrlTokens(raw, { objectId: this.objectId, object: this.objectData || {} })
223
250
  },
224
251
 
252
+ /** @return {boolean} Whether the reason field waits behind a button. */
253
+ behindButton() {
254
+ return this.content?.behindButton === true
255
+ },
256
+
225
257
  /** @return {string} What the raise or clear was for, in the flag's own words. */
226
258
  flagReason() {
227
259
  return String(this.flag?.reason ?? '')
@@ -370,6 +402,7 @@ export default {
370
402
  const response = await axios.post(generateUrl(this.endpoint + tail), { reason })
371
403
  this.apply(response?.data)
372
404
  this.reason = ''
405
+ this.composing = false
373
406
  this.announce()
374
407
  } catch (error) {
375
408
  const said = String(error?.response?.data?.error ?? error?.response?.data?.message ?? '')
@@ -450,6 +483,18 @@ function hasPassed(date) {
450
483
  font-weight: bold;
451
484
  }
452
485
 
486
+ .cn-attention-flag__heading {
487
+ margin: 0;
488
+ font-size: 14px;
489
+ font-weight: bold;
490
+ }
491
+
492
+ .cn-attention-flag__hint {
493
+ margin: 0 0 8px;
494
+ color: var(--color-text-maxcontrast);
495
+ font-size: 13px;
496
+ }
497
+
453
498
  .cn-attention-flag__muted {
454
499
  color: var(--color-text-maxcontrast);
455
500
  }
@@ -2908,9 +2908,11 @@ export default {
2908
2908
  }
2909
2909
 
2910
2910
  /* A cell is a span: its ink also goes to --nldesign-color-on-surface, which a
2911
- theme that forces the body ink onto every span (thematiq) reads. The light
2912
- levels (1 to 3) take a dark ink; white only on level 4, the full primary,
2913
- where it reaches 4.5:1 (on level 3's 65% tint it is about 2.9:1). */
2911
+ theme that forces the body ink onto every span (thematiq) reads. The four
2912
+ levels are the boards' tones (DqProcesanalyse): the light primary, the
2913
+ primary at 35% and at 70%, and level 1 inverted. Levels 1 to 3 take a dark
2914
+ ink; the board's white on level 3 is about 3.1:1, and 70% is as deep as the
2915
+ dark ink stays at 4.5:1 in Nextcloud's dark theme. */
2914
2916
  .cn-chart-widget__matrix-cell--1 {
2915
2917
  --nldesign-color-on-surface: var(--color-primary-element-light-text);
2916
2918
  background: var(--color-primary-element-light);
@@ -2918,20 +2920,20 @@ export default {
2918
2920
  }
2919
2921
 
2920
2922
  .cn-chart-widget__matrix-cell--2 {
2921
- background: color-mix(in srgb, var(--color-primary-element) 30%, var(--color-main-background));
2923
+ background: color-mix(in srgb, var(--color-primary-element) 35%, var(--color-main-background));
2922
2924
  color: var(--color-main-text);
2923
2925
  }
2924
2926
 
2925
2927
  .cn-chart-widget__matrix-cell--3 {
2926
2928
  --nldesign-color-on-surface: var(--color-main-text);
2927
- background: color-mix(in srgb, var(--color-primary-element) 65%, var(--color-main-background));
2929
+ background: color-mix(in srgb, var(--color-primary-element) 70%, var(--color-main-background));
2928
2930
  color: var(--color-main-text);
2929
2931
  }
2930
2932
 
2931
2933
  .cn-chart-widget__matrix-cell--4 {
2932
- --nldesign-color-on-surface: var(--color-primary-element-text);
2933
- background: var(--color-primary-element);
2934
- color: var(--color-primary-element-text);
2934
+ --nldesign-color-on-surface: var(--color-primary-element-light);
2935
+ background: var(--color-primary-element-light-text);
2936
+ color: var(--color-primary-element-light);
2935
2937
  }
2936
2938
 
2937
2939
  .cn-chart-widget__empty {
@@ -432,6 +432,16 @@ Four levels of action and the case surfaces around them. All opt in. See [the re
432
432
  | `accessPurpose` | Object | `null` | Ask for a purpose before the record opens (manifest `config.accessPurpose`): `{ when, options, url, title?, description? }`. `when` is a `visibleWhen` condition on the record and its `visibilityContext` records; `options` are the purposes offered (`{ value, label }`; "Other" with a written purpose is always added); `url` is the app endpoint that records the purpose (POST `{ purpose, note }`, `{objectId}` filled in). Nothing of the record shows until the endpoint accepts, and the same record is not asked again in the same browser session. A refusal keeps the record closed. `null` asks nothing. |
433
433
  | `isAdmin` | Boolean \| null | `null` | Shows `adminOnly` header actions. `null` reads it from Nextcloud. |
434
434
 
435
+ ## A body widget's own cards in the side column
436
+
437
+ A widget in the body can show cards about what is selected in it in the side column, in place of the generic side cards (DqDocumentenBeheer: "Wie mag dit zien: <file>" and "Controlegetal" while files are ticked). The page provides `cnDetailSideColumn`:
438
+
439
+ | Member | Meaning |
440
+ |---|---|
441
+ | `available` | Whether the page draws a side column. Without one the widget keeps its cards where they were. |
442
+ | `target` | A selector for the slot at the top of the side column, to `<Teleport>` the cards to. |
443
+ | `claim(owner, on)` | Start (`true`) or stop (`false`) showing cards there. While any owner claims, the generic side cards stand aside; they come back when the last one lets go. Release on unmount. |
444
+
435
445
  ## The record as a place
436
446
 
437
447
  `listNavigation`, `primaryAction` and `tabInAddress` make a record somewhere a handler stays. All three are off by default. See [the reference page](../../../docs/components/cn-detail-page.md#the-record-as-a-place).
@@ -1088,8 +1088,14 @@
1088
1088
  class="cn-detail-page__side"
1089
1089
  :aria-label="sideColumnLabel"
1090
1090
  data-testid="cn-detail-page-side">
1091
+ <!-- Where a body widget puts its own cards about what is selected in
1092
+ it (`cnDetailSideColumn`); while it does, the generic cards
1093
+ below stand aside. -->
1094
+ <div :id="sideTakeoverId"
1095
+ class="cn-detail-page__side-takeover"
1096
+ data-testid="cn-detail-page-side-takeover" />
1091
1097
  <div
1092
- v-for="widget in sideColumnWidgets"
1098
+ v-for="widget in sideClaims.length > 0 ? [] : sideColumnWidgets"
1093
1099
  :key="`side-${widget.id}`"
1094
1100
  class="cn-detail-page__side-item"
1095
1101
  :class="{ 'cn-detail-page__side-item--notice': isBoard && widget.type === 'banner', 'cn-detail-page__side-item--history': isBoard && widget.id === sideHistoryId, 'cn-detail-page__side-item--with-identifier': isBoard && widget.id === sideHistoryId && identifierLine !== '' }"
@@ -1421,6 +1427,9 @@ const INTEGRATION_SURFACES = ['user-dashboard', 'app-dashboard', 'detail-page',
1421
1427
  /** The session storage key that lists the records whose access purpose is recorded. */
1422
1428
  const ACCESS_PURPOSE_SESSION_KEY = 'cn-access-purposes'
1423
1429
 
1430
+ /** Counter for unique side column slot ids across instances. */
1431
+ let sideTakeoverCounter = 0
1432
+
1424
1433
  /**
1425
1434
  * The records whose access purpose this browser session recorded. Empty when
1426
1435
  * session storage cannot be read (a private window, blocked site data).
@@ -1612,7 +1621,22 @@ export default {
1612
1621
  const tabsLabel = computed(() => this.resolvedTabsLabel)
1613
1622
  const visibilityRows = computed(() => (this.visibilityRowsPending ? null : this.visibilityRows))
1614
1623
  const canCheckItems = computed(() => this.recordCanCheckItems)
1624
+ const sideAvailable = computed(() => this.showsSideColumn)
1615
1625
  return {
1626
+ // The side column, for a widget in the body that describes what is
1627
+ // selected in it (a files list's cards about the selected file).
1628
+ // `available` says whether the page draws a side column; `target`
1629
+ // is the selector to teleport the cards to; `claim(owner, on)`
1630
+ // puts the generic side cards aside while an owner shows its own.
1631
+ cnDetailSideColumn: {
1632
+ get available() {
1633
+ return sideAvailable.value
1634
+ },
1635
+
1636
+ target: `#${this.sideTakeoverId}`,
1637
+ claim: (owner, on) => this.claimSideColumn(owner, on),
1638
+ },
1639
+
1616
1640
  // The page's own look reaches the tabs, the cards and the activity
1617
1641
  // below it, also when it came from the `look` prop and not the app.
1618
1642
  cnLook: computed(() => (isBoard.value ? 'board' : 'nextcloud')),
@@ -3051,6 +3075,10 @@ export default {
3051
3075
 
3052
3076
  data() {
3053
3077
  return {
3078
+ /** The id of the side column's slot for a body widget's own cards. */
3079
+ sideTakeoverId: `cn-detail-side-takeover-${++sideTakeoverCounter}`,
3080
+ /** The body widgets that show their own cards in the side column now. */
3081
+ sideClaims: [],
3054
3082
  /** The session keys of the records whose access purpose is recorded. */
3055
3083
  accessPurposeGranted: readGrantedPurposes(),
3056
3084
  /** Whether the purpose dialog is open; closing it without a purpose keeps the record closed. */
@@ -5347,6 +5375,19 @@ export default {
5347
5375
  },
5348
5376
 
5349
5377
  methods: {
5378
+ /**
5379
+ * A body widget starts or stops showing its own cards in the side
5380
+ * column. While any does, the generic side cards stand aside.
5381
+ *
5382
+ * @param {string|number} owner Who claims, unique per widget.
5383
+ * @param {boolean} on Whether it shows its cards now.
5384
+ * @return {void}
5385
+ */
5386
+ claimSideColumn(owner, on) {
5387
+ const rest = this.sideClaims.filter((id) => id !== owner)
5388
+ this.sideClaims = on ? [...rest, owner] : rest
5389
+ },
5390
+
5350
5391
  /**
5351
5392
  * A text of the access purpose gate through the app's translate, '' when unset.
5352
5393
  *
@@ -305,6 +305,32 @@
305
305
  @selectObject="onSelectObject" />
306
306
  </CnWidgetWrapper>
307
307
 
308
+ <!-- A titled `sections` widget in the side column or a tab stack: its
309
+ sections stack inside one titled card, as the other cards there
310
+ are drawn. A grid cell has its own card, and `content.cards` makes
311
+ every section a card of its own, so both stay as they are. -->
312
+ <CnWidgetWrapper
313
+ v-else-if="renderer && wrapsSections"
314
+ :title="widget.title"
315
+ :subtitle="widgetSubtitle"
316
+ :showTitle="effectiveShowCardTitle"
317
+ titleIconPosition="left"
318
+ :showActions="false"
319
+ :showRefresh="false"
320
+ :showRequestFeature="false"
321
+ :headerLink="cardHeaderLink"
322
+ class="cn-detail-page__sections-card"
323
+ data-testid="cn-detail-widget-sections-card">
324
+ <template v-if="widget.icon" #title-icon>
325
+ <CnIcon :name="widget.icon" :size="20" />
326
+ </template>
327
+ <component
328
+ :is="renderer"
329
+ ref="renderer"
330
+ v-bind="rendererProps"
331
+ @selectObject="onSelectObject" />
332
+ </CnWidgetWrapper>
333
+
308
334
  <!-- Every other content-driven catalog widget, and every bare-mode
309
335
  catalog widget. These self-fetch from OpenRegister. The object
310
336
  context is forwarded too (the same shape CnWidgetGrid merges on the
@@ -857,6 +883,19 @@ export default {
857
883
  return typeof entry === 'object' && entry.chrome === 'card'
858
884
  },
859
885
 
886
+ /**
887
+ * Whether a `sections` widget gets one titled card: on a card surface
888
+ * outside a body grid cell, with a title, and not in `cards` mode.
889
+ *
890
+ * @return {boolean}
891
+ */
892
+ wrapsSections() {
893
+ if (this.isBare || this.inGridCell || !this.widget || this.widget.type !== 'sections' || !this.widget.title) {
894
+ return false
895
+ }
896
+ return !(this.widget.content && this.widget.content.cards)
897
+ },
898
+
860
899
  /** @return {boolean} true when the registry entry declares `card`. */
861
900
  isCard() {
862
901
  return isCardWidgetDef(this.widget)
@@ -280,7 +280,7 @@
280
280
  sidebar) is left out, and Show in Files covers it. -->
281
281
  <div v-else
282
282
  class="cn-files-browser__body"
283
- :class="{ 'cn-files-browser__body--with-side': sideNode !== null }">
283
+ :class="{ 'cn-files-browser__body--with-side': sideNode !== null && !sideInPage }">
284
284
  <table class="cn-files-browser__table" data-testid="cn-files-browser-table">
285
285
  <thead>
286
286
  <tr>
@@ -560,43 +560,50 @@
560
560
  </tbody>
561
561
  </table>
562
562
 
563
- <!-- `sideCards`: cards about the last selected file, beside the list. -->
564
- <aside v-if="sideNode !== null"
565
- class="cn-files-browser__side"
566
- :aria-label="t('nextcloud-vue', 'About {name}', { name: sideNode.basename })"
567
- data-testid="cn-files-browser-side">
568
- <section v-for="card in sideCardsFor(sideNode)"
569
- :key="card.id"
570
- class="cn-files-browser__side-card"
571
- :data-testid="`cn-files-browser-side-card-${card.id}`">
572
- <h3 class="cn-files-browser__side-title">
573
- {{ card.title }}
574
- </h3>
575
- <dl v-if="card.fields.length > 0" class="cn-files-browser__side-facts">
576
- <template v-for="field in card.fields" :key="field.key">
577
- <dt>{{ field.label }}</dt>
578
- <dd :class="field.tone ? `cn-files-browser__tone cn-files-browser__tone--${field.tone}` : ''">
579
- <CnCellRenderer
580
- :value="field.value"
581
- :formatter="field.formatter || null"
582
- :formatterOptions="field.formatterOptions || null"
583
- :row="sideNode" />
584
- </dd>
585
- </template>
586
- </dl>
587
- <div v-if="card.actions.length > 0" class="cn-files-browser__side-actions">
588
- <NcButton v-for="action in card.actions"
589
- :key="action.id"
590
- :data-testid="`cn-files-browser-side-action-${action.id}`"
591
- @click="runHostAction(action, sideNode)">
592
- <template v-if="action.icon" #icon>
593
- <CnIcon :name="action.icon" :size="20" />
563
+ <!-- `sideCards`: cards about the last selected file, beside the list,
564
+ or in the detail page's side column (`sideCardsPlacement`). -->
565
+ <Teleport v-if="sideNode !== null"
566
+ :to="pageSideTarget"
567
+ :disabled="!sideInPage"
568
+ defer>
569
+ <aside
570
+ class="cn-files-browser__side"
571
+ :class="{ 'cn-files-browser__side--in-page': sideInPage }"
572
+ :aria-label="t('nextcloud-vue', 'About {name}', { name: sideNode.basename })"
573
+ data-testid="cn-files-browser-side">
574
+ <section v-for="card in sideCardsFor(sideNode)"
575
+ :key="card.id"
576
+ class="cn-files-browser__side-card"
577
+ :data-testid="`cn-files-browser-side-card-${card.id}`">
578
+ <h3 class="cn-files-browser__side-title">
579
+ {{ card.title }}
580
+ </h3>
581
+ <dl v-if="card.fields.length > 0" class="cn-files-browser__side-facts">
582
+ <template v-for="field in card.fields" :key="field.key">
583
+ <dt>{{ field.label }}</dt>
584
+ <dd :class="field.tone ? `cn-files-browser__tone cn-files-browser__tone--${field.tone}` : ''">
585
+ <CnCellRenderer
586
+ :value="field.value"
587
+ :formatter="field.formatter || null"
588
+ :formatterOptions="field.formatterOptions || null"
589
+ :row="sideNode" />
590
+ </dd>
594
591
  </template>
595
- {{ hostLabel(action) }}
596
- </NcButton>
597
- </div>
598
- </section>
599
- </aside>
592
+ </dl>
593
+ <div v-if="card.actions.length > 0" class="cn-files-browser__side-actions">
594
+ <NcButton v-for="action in card.actions"
595
+ :key="action.id"
596
+ :data-testid="`cn-files-browser-side-action-${action.id}`"
597
+ @click="runHostAction(action, sideNode)">
598
+ <template v-if="action.icon" #icon>
599
+ <CnIcon :name="action.icon" :size="20" />
600
+ </template>
601
+ {{ hostLabel(action) }}
602
+ </NcButton>
603
+ </div>
604
+ </section>
605
+ </aside>
606
+ </Teleport>
600
607
  </div>
601
608
 
602
609
  <p v-if="dropHint && canChange && !loading && error === ''"
@@ -819,6 +826,8 @@ export default {
819
826
  // action or column the manifest declares is authored in English as
820
827
  // the catalogue key, and read through this in the user's language.
821
828
  cnTranslate: { default: () => (key) => key },
829
+ // The detail page's side column (CnDetailPage), for side cards placed there.
830
+ cnDetailSideColumn: { default: null },
822
831
  },
823
832
 
824
833
  props: {
@@ -1213,6 +1222,17 @@ export default {
1213
1222
  default: () => [],
1214
1223
  },
1215
1224
 
1225
+ /**
1226
+ * Where the side cards stand: `beside` (default) next to the list, or
1227
+ * `page-side` in the detail page's side column, in place of its
1228
+ * generic cards while a file is selected. Without a page side column
1229
+ * they stay beside the list.
1230
+ */
1231
+ sideCardsPlacement: {
1232
+ type: String,
1233
+ default: 'beside',
1234
+ },
1235
+
1216
1236
  /**
1217
1237
  * The key of a declared column to group the files on: a heading row
1218
1238
  * per value, in the order each is first met, with its count. Files
@@ -1487,6 +1507,21 @@ export default {
1487
1507
  * @return {object|null}
1488
1508
  * @spec openspec/changes/files-browser-side-cards/specs/files-browser/spec.md#requirement-cards-beside-the-list-describe-the-selected-file
1489
1509
  */
1510
+ /** @return {boolean} Whether the side cards fill the page's side column now. */
1511
+ sideClaimed() {
1512
+ return this.sideInPage && this.sideNode !== null
1513
+ },
1514
+
1515
+ /** @return {boolean} Whether the side cards stand in the page's side column. */
1516
+ sideInPage() {
1517
+ return this.sideCardsPlacement === 'page-side' && this.cnDetailSideColumn !== null && this.cnDetailSideColumn.available === true
1518
+ },
1519
+
1520
+ /** @return {string} Where the side cards are teleported to in the page. */
1521
+ pageSideTarget() {
1522
+ return this.cnDetailSideColumn?.target || 'body'
1523
+ },
1524
+
1490
1525
  sideNode() {
1491
1526
  if (!Array.isArray(this.sideCards) || this.sideCards.length === 0 || this.selectedIds.length === 0) {
1492
1527
  return null
@@ -1658,6 +1693,17 @@ export default {
1658
1693
  },
1659
1694
 
1660
1695
  watch: {
1696
+ /**
1697
+ * Tell the page whether the side cards stand in its side column now,
1698
+ * so its generic cards stand aside meanwhile.
1699
+ */
1700
+ sideClaimed: {
1701
+ immediate: true,
1702
+ handler(on) {
1703
+ this.cnDetailSideColumn?.claim?.(this.$.uid, on)
1704
+ },
1705
+ },
1706
+
1661
1707
  rootPath(next) {
1662
1708
  this.currentPath = next
1663
1709
  this.refresh()
@@ -1681,6 +1727,7 @@ export default {
1681
1727
  },
1682
1728
 
1683
1729
  beforeUnmount() {
1730
+ this.cnDetailSideColumn?.claim?.(this.$.uid, false)
1684
1731
  for (const channel of FOLDER_CHANGED_CHANNELS) {
1685
1732
  unsubscribe(channel, this.onFolderChanged)
1686
1733
  }
@@ -3072,6 +3119,17 @@ export default {
3072
3119
  gap: 12px;
3073
3120
  }
3074
3121
 
3122
+ /* In the detail page's side column the cards take its gap and its card
3123
+ anatomy, as the page's own side cards are drawn. */
3124
+ .cn-files-browser__side--in-page {
3125
+ gap: calc(5 * var(--default-grid-baseline));
3126
+ }
3127
+
3128
+ .cn-files-browser__side--in-page .cn-files-browser__side-card {
3129
+ padding: var(--cn-board-card-padding, 16px 18px);
3130
+ border-radius: var(--cn-board-card-radius, var(--border-radius-large));
3131
+ }
3132
+
3075
3133
  .cn-files-browser__side-card {
3076
3134
  display: flex;
3077
3135
  flex-direction: column;
@@ -202,6 +202,7 @@
202
202
  :edges="canvasEdgesWithRunState"
203
203
  :minZoom="minZoom"
204
204
  :maxZoom="maxZoom"
205
+ :fitMaxZoom="fitMaxZoom"
205
206
  @zoomChange="zoom = $event"
206
207
  @nodeSelect="onNodeSelect"
207
208
  @nodeActivate="onNodeActivate"
@@ -448,6 +449,25 @@ export default {
448
449
  default: '',
449
450
  },
450
451
 
452
+ /**
453
+ * Which way the automatic layout runs: `right` (default) left to
454
+ * right, `down` top to bottom with branches side by side.
455
+ */
456
+ layoutDirection: {
457
+ type: String,
458
+ default: 'right',
459
+ validator: (value) => ['right', 'down'].includes(value),
460
+ },
461
+
462
+ /**
463
+ * The most the first fit may zoom in. 0 lets it zoom to `maxZoom`; 1
464
+ * keeps a small graph at its own size instead of blowing it up.
465
+ */
466
+ fitMaxZoom: {
467
+ type: Number,
468
+ default: 0,
469
+ },
470
+
451
471
  /**
452
472
  * Show Save, Run and Check in the canvas toolbar. Off when the host
453
473
  * draws them itself, as the board look does in its page header.
@@ -1523,6 +1543,7 @@ export default {
1523
1543
  * @return {void}
1524
1544
  */
1525
1545
  created() {
1546
+ this.store.layoutDirection = this.layoutDirection
1526
1547
  this.store.beginBootstrap(this.run)
1527
1548
  },
1528
1549