@conduction/nextcloud-vue 2.69.0 → 2.71.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 (153) hide show
  1. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js +25 -0
  2. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
  3. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +155 -152
  4. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
  5. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +77 -0
  6. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
  7. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +8 -8
  8. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
  9. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +151 -13
  10. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  11. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +3 -1
  12. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  13. package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue.js +1 -0
  14. package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue.js.map +1 -1
  15. package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue2.js +28 -0
  16. package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue2.js.map +1 -1
  17. package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue3.js +4 -2
  18. package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue3.js.map +1 -1
  19. package/dist/esm/components/CnCalendarWidget/CnCalendarWidget.vue2.js +25 -1
  20. package/dist/esm/components/CnCalendarWidget/CnCalendarWidget.vue2.js.map +1 -1
  21. package/dist/esm/components/CnCalendarWidget/CnCalendarWidget.vue3.js +32 -17
  22. package/dist/esm/components/CnCalendarWidget/CnCalendarWidget.vue3.js.map +1 -1
  23. package/dist/esm/components/CnCapabilityTable/CnCapabilityTable.vue2.js +14 -1
  24. package/dist/esm/components/CnCapabilityTable/CnCapabilityTable.vue2.js.map +1 -1
  25. package/dist/esm/components/CnCapabilityTable/CnCapabilityTable.vue3.js.map +1 -1
  26. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js +43 -2
  27. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js.map +1 -1
  28. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js +3 -3
  29. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js.map +1 -1
  30. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +25 -1
  31. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  32. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +40 -43
  33. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  34. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +35 -1
  35. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  36. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  37. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +54 -3
  38. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  39. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +6 -3
  40. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  41. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +28 -0
  42. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  43. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +3 -2
  44. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  45. package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue2.js +12 -0
  46. package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue2.js.map +1 -1
  47. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js +9 -0
  48. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js.map +1 -1
  49. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js.map +1 -1
  50. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +29 -2
  51. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  52. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +71 -34
  53. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  54. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js +54 -7
  55. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js.map +1 -1
  56. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js +26 -8
  57. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js.map +1 -1
  58. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +165 -0
  59. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  60. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +69 -26
  61. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  62. package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue2.js.map +1 -1
  63. package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue3.js.map +1 -1
  64. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +4 -1
  65. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  66. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  67. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue2.js +1 -1
  68. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue2.js.map +1 -1
  69. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue3.js.map +1 -1
  70. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
  71. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +46 -32
  72. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
  73. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue2.js +17 -3
  74. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue2.js.map +1 -1
  75. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue3.js +25 -24
  76. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue3.js.map +1 -1
  77. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
  78. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
  79. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +27 -0
  80. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  81. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +7 -0
  82. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  83. package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue2.js +24 -1
  84. package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue2.js.map +1 -1
  85. package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue3.js +1 -1
  86. package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue3.js.map +1 -1
  87. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js +11 -0
  88. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js.map +1 -1
  89. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js +4 -2
  90. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js.map +1 -1
  91. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js +30 -9
  92. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js.map +1 -1
  93. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js.map +1 -1
  94. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js +15 -2
  95. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js.map +1 -1
  96. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue3.js +31 -32
  97. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue3.js.map +1 -1
  98. package/dist/esm/composables/useWalkthrough.js +11 -3
  99. package/dist/esm/composables/useWalkthrough.js.map +1 -1
  100. package/dist/esm/nextcloud-vue.css +198 -0
  101. package/dist/esm/utils/capabilityComparison.js +29 -1
  102. package/dist/esm/utils/capabilityComparison.js.map +1 -1
  103. package/dist/esm/utils/columnFilters.js +54 -4
  104. package/dist/esm/utils/columnFilters.js.map +1 -1
  105. package/dist/esm/utils/userAutocomplete.js +69 -3
  106. package/dist/esm/utils/userAutocomplete.js.map +1 -1
  107. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  108. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  109. package/dist/nextcloud-vue.cjs.js +2778 -1630
  110. package/dist/nextcloud-vue.cjs.js.map +2 -2
  111. package/dist/nextcloud-vue.css +198 -0
  112. package/package.json +1 -1
  113. package/src/components/CnActionsBar/CnActionsBar.md +2 -0
  114. package/src/components/CnActionsBar/CnActionsBar.vue +28 -2
  115. package/src/components/CnAppNav/CnAppNav.vue +85 -8
  116. package/src/components/CnAppRoot/CnAppRoot.vue +153 -13
  117. package/src/components/CnBreadcrumbs/CnBreadcrumbs.vue +45 -0
  118. package/src/components/CnCalendarWidget/CnCalendarWidget.vue +32 -4
  119. package/src/components/CnCapabilityTable/CnCapabilityTable.vue +14 -0
  120. package/src/components/CnDashboardGrid/CnDashboardGrid.md +2 -0
  121. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +54 -2
  122. package/src/components/CnDashboardPage/CnDashboardPage.vue +46 -19
  123. package/src/components/CnDataTable/CnDataTable.vue +35 -1
  124. package/src/components/CnDetailPage/CnDetailPage.md +2 -0
  125. package/src/components/CnDetailPage/CnDetailPage.vue +57 -3
  126. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +30 -1
  127. package/src/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue +12 -0
  128. package/src/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue +9 -0
  129. package/src/components/CnFormDialog/CnFormDialog.vue +62 -2
  130. package/src/components/CnHeaderWidget/CnHeaderWidget.vue +95 -8
  131. package/src/components/CnIndexPage/CnIndexPage.vue +194 -7
  132. package/src/components/CnNextStepCard/CnNextStepCard.vue +11 -0
  133. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +4 -1
  134. package/src/components/CnObjectSidebar/CnNotesTab.vue +1 -1
  135. package/src/components/CnSetupWizard/CnSetupWizard.vue +7 -0
  136. package/src/components/CnStackedBarWidget/CnStackedBarWidget.vue +34 -7
  137. package/src/components/CnStagesWidget/CnStagesWidget.vue +19 -0
  138. package/src/components/CnStatWidget/CnStatWidget.vue +36 -0
  139. package/src/components/CnStatsBlock/CnStatsBlock.md +2 -0
  140. package/src/components/CnStatsBlock/CnStatsBlock.vue +25 -2
  141. package/src/components/CnStatsBlockWidget/CnStatsBlockWidget.vue +13 -0
  142. package/src/components/CnWalkthrough/CnWalkthrough.vue +30 -9
  143. package/src/components/CnWeekStripWidget/CnWeekStripWidget.vue +24 -3
  144. package/src/composables/useWalkthrough.js +11 -3
  145. package/src/css/dashboard.css +44 -0
  146. package/src/css/detail-page.css +10 -0
  147. package/src/css/index-page.css +12 -0
  148. package/src/css/kpi-card.css +15 -0
  149. package/src/schemas/app-manifest-v2.schema.json +47 -5
  150. package/src/utils/capabilityComparison.js +28 -0
  151. package/src/utils/columnFilters.js +54 -4
  152. package/src/utils/userAutocomplete.js +68 -2
  153. package/src/utils/validateManifestV2.compiled.js +2 -2
@@ -513,8 +513,10 @@
513
513
  :manifest="manifest"
514
514
  :seenVersion="walkthroughSeenVersion"
515
515
  :resume="walkthroughResume"
516
+ :autoStart="!walkthroughPaused"
516
517
  :translate="translate"
517
518
  @complete="onWalkthroughComplete"
519
+ @pause="onWalkthroughPause"
518
520
  @progress="onWalkthroughProgress" />
519
521
  </slot>
520
522
  <!--
@@ -934,8 +936,11 @@ export default {
934
936
  * Restart entry for the product walkthrough (ADR-043). Descendants
935
937
  * (a menu/settings "Replay walkthrough" entry, or a manifest menu
936
938
  * `action: "replay-walkthrough"`) call this to re-run a tour. With no
937
- * `tourId` the first declared tour is used.
939
+ * `tourId` the first declared tour is used. An unfinished tour
940
+ * continues where the user was (a paused tour, else the saved
941
+ * step); "Start over" in the user settings goes back to step 1.
938
942
  *
943
+ * @spec openspec/changes/audit-round-lib-fixes/specs/cn-walkthrough/spec.md
939
944
  * @param {string} [tourId] The tour to restart.
940
945
  * @return {void}
941
946
  */
@@ -945,9 +950,18 @@ export default {
945
950
  }
946
951
  const wt = useWalkthrough(this.appId, this.manifest)
947
952
  const id = tourId || (this.manifest.walkthrough.tours[0] && this.manifest.walkthrough.tours[0].id)
948
- if (id) {
949
- wt.restart(id)
953
+ if (!id) {
954
+ return
955
+ }
956
+ const progress = this.walkthroughProgressValue
957
+ this.unpauseWalkthroughProgress()
958
+ if (wt.paused.value && wt.activeTour.value && wt.activeTour.value.id === id && wt.resumePaused()) {
959
+ return
960
+ }
961
+ if (progress && progress.tourId === id && wt.resumeAt(id, progress.stepId)) {
962
+ return
950
963
  }
964
+ wt.restart(id)
951
965
  },
952
966
 
953
967
  /**
@@ -1528,7 +1542,7 @@ export default {
1528
1542
  },
1529
1543
  },
1530
1544
 
1531
- emits: ['setup-complete', 'walkthrough-complete'],
1545
+ emits: ['setup-complete', 'setup-wizard-dismissed', 'walkthrough-complete'],
1532
1546
 
1533
1547
  /**
1534
1548
  * Component-instance state for the capabilities guard.
@@ -2599,9 +2613,21 @@ export default {
2599
2613
  } catch {
2600
2614
  // No URL token; fall through to the remembered progress.
2601
2615
  }
2602
- // An unfinished tour continues at the step the user reached.
2616
+ // An unfinished tour continues at the step the user reached,
2617
+ // unless the user paused it: then it waits for "Continue".
2603
2618
  const progress = this.walkthroughProgressValue
2604
- return progress ? { tourId: progress.tourId, stepId: progress.stepId } : null
2619
+ return progress && !progress.paused ? { tourId: progress.tourId, stepId: progress.stepId } : null
2620
+ },
2621
+
2622
+ /**
2623
+ * Whether the user paused the tour (X, ESC or the dim). A paused tour
2624
+ * stays hidden across page loads until the user picks "Continue".
2625
+ *
2626
+ * @spec openspec/changes/audit-round-lib-fixes/specs/cn-walkthrough/spec.md
2627
+ * @return {boolean} True while the saved progress is paused.
2628
+ */
2629
+ walkthroughPaused() {
2630
+ return !!(this.walkthroughProgressValue && this.walkthroughProgressValue.paused)
2605
2631
  },
2606
2632
 
2607
2633
  phase() {
@@ -3281,7 +3307,7 @@ export default {
3281
3307
  // the server keeps reporting it unmet — so without this the wizard
3282
3308
  // auto-opens again on every visit. Cancel must not be the only way to
3283
3309
  // get peace.
3284
- this.persistSetupWizardDismissal()
3310
+ this.persistSetupWizardDismissal(true)
3285
3311
  if (this.setupState && typeof this.setupState.refresh === 'function') {
3286
3312
  this.setupState.refresh()
3287
3313
  }
@@ -3302,7 +3328,7 @@ export default {
3302
3328
  * @return {void}
3303
3329
  */
3304
3330
  dismissSetupWizard() {
3305
- this.persistSetupWizardDismissal()
3331
+ this.persistSetupWizardDismissal(false)
3306
3332
  this.setupWizardOpen = false
3307
3333
  },
3308
3334
 
@@ -3325,7 +3351,7 @@ export default {
3325
3351
  * @return {boolean}
3326
3352
  */
3327
3353
  isSetupWizardDismissed() {
3328
- if (this.setupWizardDismissed) {
3354
+ if (this.setupWizardDismissed || this.isSetupWizardDismissedOnServer()) {
3329
3355
  return true
3330
3356
  }
3331
3357
  try {
@@ -3336,18 +3362,87 @@ export default {
3336
3362
  },
3337
3363
 
3338
3364
  /**
3339
- * Record the non-gating setup wizard as dismissed for this manifest
3340
- * `setup.version` so it doesn't auto-open again.
3365
+ * Whether the app's setup status says the wizard was closed, so it
3366
+ * stays closed in every browser and on every device. The status
3367
+ * (`GET /api/setup/status`) may carry `dismissed: true`, or the
3368
+ * `setup.version` it was closed at (`dismissed: 2`), which re-opens the
3369
+ * wizard after a version bump. An app whose status carries neither
3370
+ * keeps today's per-browser behaviour. An app may also answer the
3371
+ * outstanding steps itself when it records the close (OpenRegister's
3372
+ * `dismiss-setup`), which needs nothing here.
3373
+ *
3374
+ * @spec openspec/changes/setup-wizard-close-on-server/specs/cn-setup-wizard/spec.md
3375
+ * @return {boolean} True when the server recorded the close for this version.
3376
+ */
3377
+ isSetupWizardDismissedOnServer() {
3378
+ const s = this.setupState
3379
+ const status = s && s.status && s.status.value
3380
+ const dismissed = status && typeof status === 'object' ? status.dismissed : undefined
3381
+ if (dismissed === true) {
3382
+ return true
3383
+ }
3384
+ if (typeof dismissed === 'number' && Number.isFinite(dismissed)) {
3385
+ const version = (this.manifest && this.manifest.setup && this.manifest.setup.version) || 0
3386
+ return dismissed >= version
3387
+ }
3388
+ return false
3389
+ },
3390
+
3391
+ /**
3392
+ * The setup action that records a closed wizard on the server, from
3393
+ * `manifest.setup.dismissAction`, or '' when the app declares none.
3394
+ *
3395
+ * @spec openspec/changes/setup-wizard-close-on-server/specs/cn-setup-wizard/spec.md
3396
+ * @return {string} The action id.
3397
+ */
3398
+ setupDismissAction() {
3399
+ const action = this.manifest && this.manifest.setup && this.manifest.setup.dismissAction
3400
+ return typeof action === 'string' && /^[a-z0-9][a-z0-9-]*$/.test(action) ? action : ''
3401
+ },
3402
+
3403
+ /**
3404
+ * Record the setup wizard as closed for this manifest `setup.version`
3405
+ * so it doesn't auto-open again: in this browser (localStorage) and,
3406
+ * when the manifest declares `setup.dismissAction`, on the server
3407
+ * through the app's own setup action endpoint
3408
+ * (`POST /apps/{appId}/api/setup/action/{dismissAction}`, admin-only
3409
+ * like every setup endpoint, so the record is per instance). Then
3410
+ * emits `setup-wizard-dismissed`. Once per page load: finishing
3411
+ * records it, and the result screen's Close that follows does not
3412
+ * post again.
3341
3413
  *
3414
+ * @spec openspec/changes/setup-wizard-close-on-server/specs/cn-setup-wizard/spec.md
3415
+ * @param {boolean} [finished] True when the user finished the wizard, false when they closed it.
3342
3416
  * @return {void}
3343
3417
  */
3344
- persistSetupWizardDismissal() {
3418
+ persistSetupWizardDismissal(finished = false) {
3345
3419
  try {
3346
3420
  window.localStorage.setItem(this.setupWizardDismissKey(), '1')
3347
3421
  } catch {
3348
3422
  // Best-effort persistence (private mode / no storage).
3349
3423
  }
3424
+ if (this.setupWizardDismissed) {
3425
+ return
3426
+ }
3350
3427
  this.setupWizardDismissed = true
3428
+ const action = this.setupDismissAction()
3429
+ if (action) {
3430
+ try {
3431
+ Promise.resolve(axios.post(generateUrl(`/apps/${this.appId}/api/setup/action/${action}`), { finished: finished === true }))
3432
+ .catch(() => { /* the local record still holds in this browser */ })
3433
+ } catch {
3434
+ // Never let a failed record break the close.
3435
+ }
3436
+ }
3437
+ /**
3438
+ * @event setup-wizard-dismissed Emitted once when the user closes or finishes the setup wizard.
3439
+ * @type {{ appId: string, version: number, finished: boolean }}
3440
+ */
3441
+ this.$emit('setup-wizard-dismissed', {
3442
+ appId: this.appId,
3443
+ version: (this.manifest && this.manifest.setup && this.manifest.setup.version) || 0,
3444
+ finished: finished === true,
3445
+ })
3351
3446
  },
3352
3447
 
3353
3448
  /**
@@ -3434,6 +3529,49 @@ export default {
3434
3529
  persistWalkthroughProgress(this.appId, this.walkthroughConfigKey, value)
3435
3530
  },
3436
3531
 
3532
+ /**
3533
+ * Remember that the user paused the tour, at the step they were on,
3534
+ * so the next page load keeps it hidden until they pick "Continue".
3535
+ *
3536
+ * @spec openspec/changes/audit-round-lib-fixes/specs/cn-walkthrough/spec.md
3537
+ * @param {object} [where] `{ tourId, stepId, index }` from CnWalkthrough.
3538
+ * @return {void}
3539
+ */
3540
+ onWalkthroughPause(where) {
3541
+ const current = this.walkthroughProgressValue || {}
3542
+ const tourId = (where && where.tourId) || current.tourId
3543
+ if (!tourId) {
3544
+ return
3545
+ }
3546
+ const value = {
3547
+ tourId,
3548
+ stepId: (where && where.tourId) ? (where.stepId || '') : (current.stepId || ''),
3549
+ index: (where && where.tourId && Number.isInteger(where.index)) ? where.index : (current.index || 0),
3550
+ version: String((this.manifest && this.manifest.version) || ''),
3551
+ paused: true,
3552
+ }
3553
+ this.walkthroughProgressValue = value
3554
+ persistWalkthroughProgress(this.appId, this.walkthroughConfigKey, value)
3555
+ },
3556
+
3557
+ /**
3558
+ * Clear the paused mark when the user continues the tour. The step
3559
+ * stays; the next step change writes fresh progress anyway.
3560
+ *
3561
+ * @spec openspec/changes/audit-round-lib-fixes/specs/cn-walkthrough/spec.md
3562
+ * @return {void}
3563
+ */
3564
+ unpauseWalkthroughProgress() {
3565
+ const progress = this.walkthroughProgressValue
3566
+ if (!progress || !progress.paused) {
3567
+ return
3568
+ }
3569
+ const value = { ...progress }
3570
+ delete value.paused
3571
+ this.walkthroughProgressValue = value
3572
+ persistWalkthroughProgress(this.appId, this.walkthroughConfigKey, value)
3573
+ },
3574
+
3437
3575
  /**
3438
3576
  * Continue an unfinished tour from the user-settings dialog: show a
3439
3577
  * paused tour again, or start the remembered tour at its step.
@@ -3448,10 +3586,11 @@ export default {
3448
3586
  }
3449
3587
  setTimeout(() => {
3450
3588
  const wt = useWalkthrough(this.appId, this.manifest)
3589
+ const progress = this.walkthroughProgressValue
3590
+ this.unpauseWalkthroughProgress()
3451
3591
  if (wt.resumePaused()) {
3452
3592
  return
3453
3593
  }
3454
- const progress = this.walkthroughProgressValue
3455
3594
  if (progress) {
3456
3595
  wt.resumeAt(progress.tourId, progress.stepId)
3457
3596
  }
@@ -3478,6 +3617,7 @@ export default {
3478
3617
  setTimeout(() => {
3479
3618
  const id = this.manifest.walkthrough.tours[0] && this.manifest.walkthrough.tours[0].id
3480
3619
  if (id) {
3620
+ this.unpauseWalkthroughProgress()
3481
3621
  useWalkthrough(this.appId, this.manifest).restart(id)
3482
3622
  }
3483
3623
  }, 50)
@@ -19,6 +19,8 @@
19
19
  v-if="crumbs.length > 0"
20
20
  :aria-label="ariaLabel"
21
21
  :rootIcon="rootText ? '' : undefined"
22
+ :class="{ 'cn-breadcrumbs--text-separator': separator !== '' }"
23
+ :style="separatorStyle"
22
24
  data-testid="cn-breadcrumbs">
23
25
  <!-- `name` falls back to '' because NcBreadcrumb declares it a REQUIRED
24
26
  String and warns per render on the label-less icon-only crumb. The
@@ -111,6 +113,34 @@ export default {
111
113
  type: Boolean,
112
114
  default: false,
113
115
  },
116
+
117
+ /**
118
+ * Draw this text between the crumbs (for example `/`) instead of
119
+ * NcBreadcrumbs' chevron. Empty (the default) keeps the chevron. The
120
+ * separator is decoration: it is not read out.
121
+ *
122
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-breadcrumb-can-name-the-record-by-a-field-and-use-a-text-separator
123
+ */
124
+ separator: {
125
+ type: String,
126
+ default: '',
127
+ },
128
+ },
129
+
130
+ computed: {
131
+ /**
132
+ * The separator handed to the stylesheet as a CSS string, or nothing
133
+ * without a `separator`.
134
+ *
135
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-breadcrumb-can-name-the-record-by-a-field-and-use-a-text-separator
136
+ * @return {object|undefined}
137
+ */
138
+ separatorStyle() {
139
+ if (this.separator === '') {
140
+ return undefined
141
+ }
142
+ return { '--cn-breadcrumbs-separator': JSON.stringify(this.separator) }
143
+ },
114
144
  },
115
145
 
116
146
  methods: {
@@ -127,3 +157,18 @@ export default {
127
157
  },
128
158
  }
129
159
  </script>
160
+
161
+ <style scoped>
162
+ /* `separator`: a text separator in place of the chevron. The chevron's own
163
+ element stays (it carries NcBreadcrumbs' spacing and its "no separator after
164
+ the last crumb" rule); only its icon gives way to the text. */
165
+ .cn-breadcrumbs--text-separator :deep(.vue-crumb__separator svg) {
166
+ display: none;
167
+ }
168
+
169
+ .cn-breadcrumbs--text-separator :deep(.vue-crumb__separator)::before {
170
+ content: var(--cn-breadcrumbs-separator);
171
+ padding-inline: var(--cn-breadcrumbs-separator-gap, 0);
172
+ color: var(--color-text-maxcontrast);
173
+ }
174
+ </style>
@@ -5,9 +5,13 @@
5
5
 
6
6
  <template>
7
7
  <div class="cn-calendar-widget">
8
- <header class="cn-calendar-widget__header">
9
- <span class="cn-calendar-widget__title">{{ t('nextcloud-vue', 'Calendar') }}</span>
10
- <div class="cn-calendar-widget__modes">
8
+ <!-- `content.showTitle: false` and `content.showViewModes: false` drop
9
+ the sub-heading and the Month / Week / Agenda buttons, for an agenda
10
+ placement whose card already names it; without both the header
11
+ row goes. -->
12
+ <header v-if="showsTitle || showsViewModes" class="cn-calendar-widget__header">
13
+ <span v-if="showsTitle" class="cn-calendar-widget__title">{{ t('nextcloud-vue', 'Calendar') }}</span>
14
+ <div v-if="showsViewModes" class="cn-calendar-widget__modes">
11
15
  <button
12
16
  v-for="mode in viewModes"
13
17
  :key="mode"
@@ -160,7 +164,10 @@ export default {
160
164
 
161
165
  props: {
162
166
  /**
163
- * Persisted widget content `{viewMode, daysAhead, colorByCalendar}`.
167
+ * Persisted widget content `{viewMode, daysAhead, colorByCalendar,
168
+ * showTitle, showViewModes}`. `showTitle: false` drops the "Calendar"
169
+ * sub-heading and `showViewModes: false` the Month / Week / Agenda
170
+ * buttons (both shown by default).
164
171
  *
165
172
  * @type {object}
166
173
  */
@@ -197,6 +204,27 @@ export default {
197
204
  },
198
205
 
199
206
  computed: {
207
+ /**
208
+ * Whether the "Calendar" sub-heading renders (`content.showTitle`).
209
+ *
210
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-an-agenda-placement-can-drop-the-calendar-chrome
211
+ * @return {boolean}
212
+ */
213
+ showsTitle() {
214
+ return !(this.content && this.content.showTitle === false)
215
+ },
216
+
217
+ /**
218
+ * Whether the Month / Week / Agenda buttons render
219
+ * (`content.showViewModes`).
220
+ *
221
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-an-agenda-placement-can-drop-the-calendar-chrome
222
+ * @return {boolean}
223
+ */
224
+ showsViewModes() {
225
+ return !(this.content && this.content.showViewModes === false)
226
+ },
227
+
200
228
  /**
201
229
  * The available view modes.
202
230
  *
@@ -187,6 +187,7 @@ import {
187
187
  hasProviders,
188
188
  RATINGS,
189
189
  resolveProvider,
190
+ warnCapabilityComparisonDeprecated,
190
191
  } from '../../utils/capabilityComparison.js'
191
192
 
192
193
  /**
@@ -212,6 +213,13 @@ export default {
212
213
  * document without them renders the table this component's consumers
213
214
  * already had.
214
215
  *
216
+ * Deprecated, with the whole component: publish the comparison on the
217
+ * app's docs site and link to it (as dossiq #3312 and pipelinq #2195
218
+ * did). Still rendered; removal waits for a major version. A
219
+ * development build warns once when a comparison is given.
220
+ *
221
+ * @deprecated since 2.66.0, publish the comparison on the app's docs site and link to it.
222
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-the-in-app-capability-comparison-is-deprecated
215
223
  * @type {object}
216
224
  */
217
225
  comparison: {
@@ -437,6 +445,12 @@ export default {
437
445
  }
438
446
  },
439
447
 
448
+ mounted() {
449
+ if (this.comparison) {
450
+ warnCapabilityComparisonDeprecated('CnCapabilityTable')
451
+ }
452
+ },
453
+
440
454
  methods: {
441
455
  /**
442
456
  * Debounce the reader's typing into `query`.
@@ -158,3 +158,5 @@ explicit event when you are writing new code:
158
158
  @item-activate="({ item, clientX, clientY }) => openMenu(item, clientX, clientY)"
159
159
  @layout-change="onLayoutChange" />
160
160
  ```
161
+
162
+ `float` (Boolean, default `true`): GridStack's `float`. `false` packs items upward, so a size-to-content cell that shrinks, or a widget that is not shown, closes its gap.
@@ -31,6 +31,7 @@
31
31
  v-for="(item, index) in layout"
32
32
  :key="resolveItemKey(item)"
33
33
  class="grid-stack-item"
34
+ :class="{ 'cn-dashboard-grid__item--fit': sizeToContentAttr(item) !== undefined }"
34
35
  role="group"
35
36
  :aria-label="resolveItemLabel(item, index)"
36
37
  :aria-describedby="itemDescribedBy"
@@ -255,6 +256,19 @@ export default {
255
256
  type: Boolean,
256
257
  default: true,
257
258
  },
259
+
260
+ /**
261
+ * GridStack's `float`. True (the default) keeps every item on the row
262
+ * it was placed on, with empty rows above it if need be. False packs
263
+ * items upward, so a size-to-content cell that shrinks, or a widget
264
+ * that is not shown, closes its gap instead of leaving a band.
265
+ *
266
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-dashboard-closes-the-gap-a-shrinking-or-hidden-widget-leaves
267
+ */
268
+ float: {
269
+ type: Boolean,
270
+ default: true,
271
+ },
258
272
  },
259
273
 
260
274
  emits: [
@@ -278,6 +292,8 @@ export default {
278
292
  return {
279
293
  grid: null,
280
294
  contentObserver: null,
295
+ /** True while the grid moves an item to where the layout put it. */
296
+ applyingLayout: false,
281
297
  domId: `cn-dashboard-grid-${++domIdCounter}`,
282
298
  /** Text currently held in the polite live region. */
283
299
  announcement: '',
@@ -485,7 +501,7 @@ export default {
485
501
  column: this.columns,
486
502
  cellHeight: this.cellHeight,
487
503
  margin: this.margin,
488
- float: true,
504
+ float: this.float,
489
505
  animate: true,
490
506
  disableDrag: !this.editable,
491
507
  disableResize: !this.editable,
@@ -558,6 +574,10 @@ export default {
558
574
  // switched off as the grid narrows, and back on as it widens.
559
575
  this.syncInteractivity()
560
576
 
577
+ if (this.applyingLayout) {
578
+ return
579
+ }
580
+
561
581
  // GridStack fires `change` for its own responsive rescale too, and at a
562
582
  // 12 → 1 breakpoint every item reads `gridX: 0, gridWidth: 1`. Emitting
563
583
  // that has the host persist the reflow AS the authored layout, which
@@ -957,7 +977,29 @@ export default {
957
977
  const node = this.grid.engine.nodes.find((n) => String(n.id) === String(item.id))
958
978
  if (!node) {
959
979
  this.grid.makeWidget(el)
960
- } else if (node.el !== el) {
980
+ } else if (node.el === el) {
981
+ // The layout MOVED an item the grid already tracks: the
982
+ // page re-compacts its display layout when a widget's
983
+ // condition hides it, and the item below was given a new
984
+ // gridY that never reached GridStack, so a band stayed
985
+ // where the hidden widget had been. A drag reports the
986
+ // grid's own position back, which matches and does
987
+ // nothing here.
988
+ const x = item.gridX ?? node.x
989
+ const y = item.gridY ?? node.y
990
+ if (x !== node.x || y !== node.y) {
991
+ // The layout said where it goes; GridStack's `change`
992
+ // for it is not a user's arrangement and is not
993
+ // reported back (it would write the display-only
994
+ // compaction into the authored layout).
995
+ this.applyingLayout = true
996
+ try {
997
+ this.grid.update(el, { x, y })
998
+ } finally {
999
+ this.applyingLayout = false
1000
+ }
1001
+ }
1002
+ } else {
961
1003
  // Drop the stale node without touching its detached DOM,
962
1004
  // then adopt the new element so GridStack sizes/places it.
963
1005
  this.grid.removeWidget(node.el, false, false)
@@ -1016,6 +1058,16 @@ export default {
1016
1058
  border-radius: var(--border-radius-container-large, 16px);
1017
1059
  }
1018
1060
 
1061
+ /* A size-to-content cell (`sizeToContent`) is measured by GridStack from the
1062
+ content's first child, and a widget wrapper there filled the cell
1063
+ (`height: 100%`), so the measure returned the cell's own height and the
1064
+ cell never shrank: "Waiting for me" stayed a tall card around one row. In
1065
+ such a cell the child takes its own height. A cell without the key keeps
1066
+ the filling child. */
1067
+ .cn-dashboard-grid__item--fit > :deep(.grid-stack-item-content > *) {
1068
+ height: auto;
1069
+ }
1070
+
1019
1071
  :deep(.grid-stack-item-content) {
1020
1072
  background: var(--color-main-background);
1021
1073
  border: none;
@@ -305,6 +305,7 @@
305
305
  :columns="columns"
306
306
  :cellHeight="cellHeight"
307
307
  :margin="gridMargin"
308
+ :float="gridFloat"
308
309
  @layoutChange="section.view ? onViewLayoutChange($event) : onLayoutChange($event)">
309
310
  <template #widget="{ item }">
310
311
  <!-- In-app edit overlay (ADR-041): a single launchpad-style
@@ -329,24 +330,26 @@
329
330
  v-if="missingRequiredApp(item)"
330
331
  :title="getWidgetTitle(item)"
331
332
  :showTitle="widgetShowTitle(item)">
332
- <NcEmptyContent
333
- :name="installAppLabel(missingRequiredApp(item))"
334
- :description="t('nextcloud-vue', 'This widget shows data from another app that isn\'t installed yet.')"
335
- class="cn-dashboard-page__requires-app cn-requires-app">
336
- <template #icon>
337
- <Download :size="24" />
338
- </template>
339
- <template #action>
340
- <NcButton
341
- variant="primary"
342
- :href="appInstallUrl(missingRequiredApp(item))">
343
- <template #icon>
344
- <Download :size="18" />
345
- </template>
346
- {{ installAppLabel(missingRequiredApp(item)) }}
347
- </NcButton>
348
- </template>
349
- </NcEmptyContent>
333
+ <!-- Compact on purpose: a two-row tile is about 150px, less
334
+ a title row. NcEmptyContent's 64px icon, 20px name,
335
+ description and button did not fit, so the icon ran out
336
+ at the top and the button at the bottom. The button
337
+ already says "Install {app}", so there is no separate
338
+ name, and the text keeps to two lines. -->
339
+ <div class="cn-dashboard-page__requires-app cn-requires-app cn-requires-app--compact"
340
+ data-testid="cn-requires-app">
341
+ <p class="cn-requires-app__description">
342
+ {{ t('nextcloud-vue', 'This widget shows data from another app that isn\'t installed yet.') }}
343
+ </p>
344
+ <NcButton
345
+ variant="primary"
346
+ :href="appInstallUrl(missingRequiredApp(item))">
347
+ <template #icon>
348
+ <Download :size="18" />
349
+ </template>
350
+ {{ installAppLabel(missingRequiredApp(item)) }}
351
+ </NcButton>
352
+ </div>
350
353
  </CnWidgetWrapper>
351
354
 
352
355
  <!-- Tile widget -->
@@ -1030,6 +1033,19 @@ export default {
1030
1033
  default: true,
1031
1034
  },
1032
1035
 
1036
+ /**
1037
+ * GridStack's `float` for this dashboard (manifest
1038
+ * `config.gridFloat`). True (the default) keeps every widget on its
1039
+ * row. False packs widgets upward, so a size-to-content widget that
1040
+ * shrinks closes the gap under it instead of leaving an empty band.
1041
+ *
1042
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-dashboard-closes-the-gap-a-shrinking-or-hidden-widget-leaves
1043
+ */
1044
+ gridFloat: {
1045
+ type: Boolean,
1046
+ default: true,
1047
+ },
1048
+
1033
1049
  /**
1034
1050
  * Widget definitions array. Each widget defines metadata for rendering.
1035
1051
  *
@@ -3602,6 +3618,12 @@ export default {
3602
3618
 
3603
3619
  getWidgetButtons(item) {
3604
3620
  const def = this.getWidgetDef(item.widgetId)
3621
+ // `showButtons: false` on the placement or the definition drops
3622
+ // the footer links ("More events"), for a placement whose card
3623
+ // says enough without them (zuiddrecht-pixel-gaps-3).
3624
+ if (item?.showButtons === false || def?.showButtons === false) {
3625
+ return []
3626
+ }
3605
3627
  return def?.buttons || []
3606
3628
  },
3607
3629
 
@@ -3905,11 +3927,16 @@ export default {
3905
3927
  // library's own defaults, so a manifest need not set them — but a
3906
3928
  // manifest that DOES set them must reach the component, or the
3907
3929
  // declaration is a silent no-op that reads like configuration.
3908
- for (const key of ['countLabel', 'variant', 'showZeroCount', 'horizontal', 'vertical', 'filled', 'route', 'iconClass', 'format']) {
3930
+ for (const key of ['countLabel', 'variant', 'showZeroCount', 'horizontal', 'vertical', 'filled', 'route', 'iconClass', 'format', 'layout']) {
3909
3931
  if (props[key] !== undefined) {
3910
3932
  out[key] = props[key]
3911
3933
  }
3912
3934
  }
3935
+ // `content.layout: "stacked"`, the key a stat tile takes, works on
3936
+ // a stats block too (zuiddrecht-pixel-gaps-3).
3937
+ if (out.layout === undefined && typeof content.layout === 'string') {
3938
+ out.layout = content.layout
3939
+ }
3913
3940
  // `countLabel` is the unit beside the number ("0 cases", "0 tasks").
3914
3941
  // It is manifest-authored prose and was being forwarded raw, so a
3915
3942
  // Dutch account read "0 cases" beside a translated widget title.
@@ -361,6 +361,40 @@ import { CnLockIndicator } from '../CnLockIndicator/index.js'
361
361
  // consuming app does not pull in the library's global css/index.css.
362
362
  import '../../css/table.css'
363
363
 
364
+ /**
365
+ * Fill a `secondary` template such as `"{identifier} · {requester}"`.
366
+ *
367
+ * A field without a value takes its separator with it: the text between two
368
+ * fields belongs to the field after it, so an empty requester leaves
369
+ * "2026-0002", not "2026-0002 ·", and an empty first field does not leave a
370
+ * leading separator either. Text before the first field and after the last
371
+ * one is kept as written. A template whose fields all have values fills
372
+ * exactly as before.
373
+ *
374
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-an-empty-field-takes-its-separator-with-it
375
+ * @param {string} template The template, with `{field}` placeholders.
376
+ * @param {(key: string) => string} valueOf The text of a field ('' when empty).
377
+ * @return {string} The filled template.
378
+ */
379
+ function fillSecondaryTemplate(template, valueOf) {
380
+ // split() with a capture group alternates literal, field, literal, …
381
+ const parts = template.split(/\{([^}]+)\}/)
382
+ const lead = parts[0]
383
+ const tail = parts[parts.length - 1]
384
+ let body = ''
385
+ for (let i = 1; i < parts.length; i += 2) {
386
+ const value = valueOf(parts[i].trim())
387
+ if (value === '') {
388
+ continue
389
+ }
390
+ // The literal before this field: the separator, unless nothing has
391
+ // been written yet (then there is nothing to separate from).
392
+ const separator = i === 1 ? '' : parts[i - 1]
393
+ body += (body === '' ? '' : separator) + value
394
+ }
395
+ return lead + body + tail
396
+ }
397
+
364
398
  /**
365
399
  * CnDataTable — Generic sortable data table for list views.
366
400
  *
@@ -1451,7 +1485,7 @@ export default {
1451
1485
  }
1452
1486
  const asText = (v) => (v === null || v === undefined ? '' : String(v))
1453
1487
  if (spec.includes('{')) {
1454
- const filled = spec.replace(/\{([^}]+)\}/g, (_, key) => asText(this.getCellValue(row, key.trim())))
1488
+ const filled = fillSecondaryTemplate(spec, (key) => asText(this.getCellValue(row, key)))
1455
1489
  // A template whose every field is empty leaves only separators:
1456
1490
  // nothing worth a line.
1457
1491
  return /[\p{L}\p{N}]/u.test(filled) ? filled.trim() : ''