@conduction/nextcloud-vue 2.61.0 → 2.63.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 (186) hide show
  1. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue.js +10 -0
  2. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue.js.map +1 -0
  3. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue2.js +189 -0
  4. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue2.js.map +1 -0
  5. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue3.js +72 -0
  6. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue3.js.map +1 -0
  7. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +394 -27
  8. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
  9. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
  10. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +99 -9
  11. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  12. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +39 -18
  13. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  14. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +79 -2
  15. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
  16. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js +38 -25
  17. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
  18. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue2.js +21 -1
  19. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue2.js.map +1 -1
  20. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue3.js +4 -1
  21. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue3.js.map +1 -1
  22. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
  23. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js +53 -37
  24. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
  25. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +30 -6
  26. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  27. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +2 -2
  28. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  29. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue.js +10 -0
  30. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue.js.map +1 -0
  31. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js +293 -0
  32. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js.map +1 -0
  33. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js +189 -0
  34. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js.map +1 -0
  35. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +256 -23
  36. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  37. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +119 -51
  38. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  39. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  40. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +2 -1
  41. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  42. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue2.js +3 -1
  43. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue2.js.map +1 -1
  44. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue3.js +1 -1
  45. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue3.js.map +1 -1
  46. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +234 -9
  47. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  48. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +96 -57
  49. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  50. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +51 -2
  51. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  52. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +5 -1
  53. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  54. package/dist/esm/components/CnIndexPage/selfModeActions.js +3 -3
  55. package/dist/esm/components/CnIndexPage/selfModeActions.js.map +1 -1
  56. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue2.js.map +1 -1
  57. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue3.js.map +1 -1
  58. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +36 -1
  59. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  60. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +4 -1
  61. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  62. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +53 -0
  63. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  64. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +22 -5
  65. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  66. package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js.map +1 -1
  67. package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js.map +1 -1
  68. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue2.js +121 -12
  69. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue2.js.map +1 -1
  70. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue3.js +3 -1
  71. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue3.js.map +1 -1
  72. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +326 -1
  73. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
  74. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +112 -26
  75. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
  76. package/dist/esm/components/CnStorePage/CnStorePage.vue2.js.map +1 -1
  77. package/dist/esm/components/CnStorePage/CnStorePage.vue3.js.map +1 -1
  78. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js +10 -0
  79. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js.map +1 -0
  80. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js +337 -0
  81. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js.map +1 -0
  82. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js +102 -0
  83. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js.map +1 -0
  84. package/dist/esm/components/CnTimelineWidget/dashboardRegistration.js +34 -0
  85. package/dist/esm/components/CnTimelineWidget/dashboardRegistration.js.map +1 -0
  86. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js +131 -8
  87. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js.map +1 -1
  88. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js +40 -38
  89. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js.map +1 -1
  90. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +8 -6
  91. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js.map +1 -1
  92. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  93. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  94. package/dist/esm/components/CnWikiPage/CnWikiPage.vue2.js.map +1 -1
  95. package/dist/esm/components/CnWikiPage/CnWikiPage.vue3.js.map +1 -1
  96. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js +19 -0
  97. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js.map +1 -1
  98. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js +1 -1
  99. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js.map +1 -1
  100. package/dist/esm/composables/useDependencyCheck.js +65 -0
  101. package/dist/esm/composables/useDependencyCheck.js.map +1 -0
  102. package/dist/esm/composables/useWalkthrough.js +218 -6
  103. package/dist/esm/composables/useWalkthrough.js.map +1 -1
  104. package/dist/esm/index.js +2 -0
  105. package/dist/esm/index.js.map +1 -1
  106. package/dist/esm/l10n/en.json.js +48 -0
  107. package/dist/esm/l10n/en.json.js.map +1 -1
  108. package/dist/esm/l10n/nl.json.js +48 -0
  109. package/dist/esm/l10n/nl.json.js.map +1 -1
  110. package/dist/esm/nextcloud-vue.css +557 -9
  111. package/dist/esm/store/useObjectStore.js +8 -0
  112. package/dist/esm/store/useObjectStore.js.map +1 -1
  113. package/dist/esm/utils/columnFilters.js +311 -0
  114. package/dist/esm/utils/columnFilters.js.map +1 -0
  115. package/dist/esm/utils/groupAutocomplete.js +123 -0
  116. package/dist/esm/utils/groupAutocomplete.js.map +1 -0
  117. package/dist/esm/utils/libraryWidgetKeys.js +2 -0
  118. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  119. package/dist/esm/utils/pickerOptions.js +198 -0
  120. package/dist/esm/utils/pickerOptions.js.map +1 -0
  121. package/dist/esm/utils/schema.js +83 -7
  122. package/dist/esm/utils/schema.js.map +1 -1
  123. package/dist/esm/utils/timelineEvents.js +174 -0
  124. package/dist/esm/utils/timelineEvents.js.map +1 -0
  125. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  126. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  127. package/dist/esm/utils/walkthroughSignals.js +88 -0
  128. package/dist/esm/utils/walkthroughSignals.js.map +1 -0
  129. package/dist/nextcloud-vue.cjs.js +9779 -5236
  130. package/dist/nextcloud-vue.cjs.js.map +2 -2
  131. package/dist/nextcloud-vue.css +557 -9
  132. package/l10n/en.json +48 -0
  133. package/l10n/nl.json +48 -0
  134. package/package.json +3 -2
  135. package/scripts/manifest-schema-hash.js +41 -0
  136. package/scripts/record-manifest-schema-hash.js +52 -0
  137. package/src/components/CnAdminActionCard/CnAdminActionCard.vue +234 -0
  138. package/src/components/CnAdminActionCard/index.js +4 -0
  139. package/src/components/CnAppNav/CnAppNav.vue +394 -27
  140. package/src/components/CnAppRoot/CnAppRoot.vue +129 -15
  141. package/src/components/CnBannerWidget/CnBannerWidget.vue +113 -2
  142. package/src/components/CnBrandStripe/CnBrandStripe.md +16 -0
  143. package/src/components/CnBrandStripe/CnBrandStripe.vue +58 -5
  144. package/src/components/CnChoiceCards/CnChoiceCards.vue +56 -31
  145. package/src/components/CnDashboardPage/CnDashboardPage.vue +32 -8
  146. package/src/components/CnDataTable/CnColumnFilterPopover.vue +524 -0
  147. package/src/components/CnDataTable/CnDataTable.md +4 -1
  148. package/src/components/CnDataTable/CnDataTable.vue +302 -21
  149. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +2 -2
  150. package/src/components/CnFieldHelper/CnFieldHelper.vue +4 -2
  151. package/src/components/CnFormDialog/CnFormDialog.vue +290 -31
  152. package/src/components/CnIndexPage/CnIndexPage.vue +55 -2
  153. package/src/components/CnIndexPage/selfModeActions.js +3 -3
  154. package/src/components/CnLinkCardsPage/CnLinkCardsPage.vue +35 -2
  155. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +39 -1
  156. package/src/components/CnObjectSidebar/CnFilesTab.vue +65 -0
  157. package/src/components/CnReportsPage/CnReportsPage.vue +7 -0
  158. package/src/components/CnResourceSelect/CnResourceSelect.vue +123 -12
  159. package/src/components/CnSetupWizard/CnSetupWizard.vue +392 -3
  160. package/src/components/CnStorePage/CnStorePage.vue +7 -0
  161. package/src/components/CnTimelineWidget/CnTimelineWidget.vue +477 -0
  162. package/src/components/CnTimelineWidget/dashboardRegistration.js +32 -0
  163. package/src/components/CnTimelineWidget/index.js +4 -0
  164. package/src/components/CnWalkthrough/CnWalkthrough.vue +143 -10
  165. package/src/components/CnWidgetGrid/builtInWidgets.js +2 -0
  166. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  167. package/src/components/CnWikiPage/CnWikiPage.vue +7 -0
  168. package/src/components/CnWizardDialog/CnWizardDialog.vue +20 -1
  169. package/src/components/index.js +2 -0
  170. package/src/composables/useDependencyCheck.js +61 -0
  171. package/src/composables/useWalkthrough.js +217 -5
  172. package/src/css/dashboard.css +49 -0
  173. package/src/css/detail-page.css +19 -0
  174. package/src/css/table.css +82 -2
  175. package/src/index.js +2 -0
  176. package/src/public/index.js +5 -0
  177. package/src/schemas/app-manifest-v2.schema.json +23 -1
  178. package/src/store/useObjectStore.js +8 -0
  179. package/src/utils/columnFilters.js +324 -0
  180. package/src/utils/groupAutocomplete.js +119 -0
  181. package/src/utils/libraryWidgetKeys.js +2 -0
  182. package/src/utils/pickerOptions.js +194 -0
  183. package/src/utils/schema.js +83 -7
  184. package/src/utils/timelineEvents.js +171 -0
  185. package/src/utils/validateManifestV2.compiled.js +2 -2
  186. package/src/utils/walkthroughSignals.js +94 -0
@@ -509,7 +509,7 @@
509
509
  :resume="walkthroughResume"
510
510
  :translate="translate"
511
511
  @complete="onWalkthroughComplete"
512
- @dismiss="onWalkthroughComplete" />
512
+ @progress="onWalkthroughProgress" />
513
513
  </slot>
514
514
  <!--
515
515
  User-settings modal. Always mounted so descendants can
@@ -554,12 +554,27 @@
554
554
  <p class="cn-app-root__walkthrough-hint">
555
555
  {{ restartWalkthroughHint }}
556
556
  </p>
557
- <NcButton variant="secondary" @click="restartWalkthroughFromSettings">
558
- <template #icon>
559
- <Restart :size="20" />
560
- </template>
561
- {{ restartWalkthroughLabel }}
562
- </NcButton>
557
+ <div class="cn-app-root__walkthrough-actions">
558
+ <NcButton
559
+ v-if="walkthroughProgressValue"
560
+ variant="primary"
561
+ data-testid="cn-walkthrough-continue"
562
+ @click="continueWalkthroughFromSettings">
563
+ <template #icon>
564
+ <Play :size="20" />
565
+ </template>
566
+ {{ continueWalkthroughLabel }}
567
+ </NcButton>
568
+ <NcButton
569
+ variant="secondary"
570
+ data-testid="cn-walkthrough-restart"
571
+ @click="restartWalkthroughFromSettings">
572
+ <template #icon>
573
+ <Restart :size="20" />
574
+ </template>
575
+ {{ walkthroughProgressValue ? startOverWalkthroughLabel : restartWalkthroughLabel }}
576
+ </NcButton>
577
+ </div>
563
578
  </NcAppSettingsSection>
564
579
  </slot>
565
580
  <!--
@@ -638,11 +653,13 @@
638
653
  import { getCurrentUser } from '@nextcloud/auth'
639
654
  import axios from '@nextcloud/axios'
640
655
  import { loadState } from '@nextcloud/initial-state'
656
+ import { translate as t } from '@nextcloud/l10n'
641
657
  import { generateUrl } from '@nextcloud/router'
642
658
  import { NcAppContent, NcAppSettingsDialog, NcAppSettingsSection, NcButton, NcContent, NcEmptyContent, NcLoadingIcon, NcNoteCard } from '@nextcloud/vue'
643
659
  import { computed, reactive, shallowRef, watch } from 'vue'
644
660
  import DatabaseSearchOutline from 'vue-material-design-icons/DatabaseSearchOutline.vue'
645
661
  import OpenInNew from 'vue-material-design-icons/OpenInNew.vue'
662
+ import Play from 'vue-material-design-icons/Play.vue'
646
663
  import Restart from 'vue-material-design-icons/Restart.vue'
647
664
  import CnAiCompanion from '../CnAiCompanion/CnAiCompanion.vue'
648
665
  import CnAppLoading from '../CnAppLoading/CnAppLoading.vue'
@@ -667,9 +684,12 @@ import { useSupportDialog } from '../../composables/useSupportDialog.js'
667
684
  import { provideTenantContext } from '../../composables/useTenantContext.js'
668
685
  import { useUserPreferences } from '../../composables/useUserPreferences.js'
669
686
  import {
687
+ loadWalkthroughProgress,
670
688
  loadWalkthroughSeenVersion,
671
689
  normaliseSeenVersion,
690
+ persistWalkthroughProgress,
672
691
  persistWalkthroughSeenVersion,
692
+ readLocalWalkthroughProgress,
673
693
  readLocalWalkthroughSeenVersion,
674
694
  useWalkthrough,
675
695
  } from '../../composables/useWalkthrough.js'
@@ -749,6 +769,7 @@ export default {
749
769
  NcNoteCard,
750
770
  DatabaseSearchOutline,
751
771
  OpenInNew,
772
+ Play,
752
773
  Restart,
753
774
  CnAppNav,
754
775
  CnAppLoading,
@@ -1776,6 +1797,15 @@ export default {
1776
1797
  * @type {string}
1777
1798
  */
1778
1799
  walkthroughSeenVersionValue: readLocalWalkthroughSeenVersion(this.appId),
1800
+ /**
1801
+ * Where the user is in an unfinished tour (`{ tourId, stepId,
1802
+ * index, version }`), or null. Seeded from the local mirror, then
1803
+ * replaced by the per-user `<completionConfigKey>-progress`
1804
+ * preference. The tour resumes at this step on the next visit.
1805
+ *
1806
+ * @type {object|null}
1807
+ */
1808
+ walkthroughProgressValue: readLocalWalkthroughProgress(this.appId),
1779
1809
  /**
1780
1810
  * Whether `walkthroughSeenVersionValue` is settled. Starts `true`
1781
1811
  * when the manifest declares no `completionConfigKey` (the local
@@ -2529,13 +2559,15 @@ export default {
2529
2559
  try {
2530
2560
  const p = new URLSearchParams(window.location.search)
2531
2561
  const tourId = p.get('cn_resume_tour')
2532
- if (!tourId) {
2533
- return null
2562
+ if (tourId) {
2563
+ return { tourId, stepId: p.get('cn_resume_step') || '' }
2534
2564
  }
2535
- return { tourId, stepId: p.get('cn_resume_step') || '' }
2536
2565
  } catch {
2537
- return null
2566
+ // No URL token; fall through to the remembered progress.
2538
2567
  }
2568
+ // An unfinished tour continues at the step the user reached.
2569
+ const progress = this.walkthroughProgressValue
2570
+ return progress ? { tourId: progress.tourId, stepId: progress.stepId } : null
2539
2571
  },
2540
2572
 
2541
2573
  phase() {
@@ -2778,6 +2810,24 @@ export default {
2778
2810
  return this.translate('Restart walkthrough')
2779
2811
  },
2780
2812
 
2813
+ /**
2814
+ * Label for continuing an unfinished tour at the remembered step.
2815
+ *
2816
+ * @return {string}
2817
+ */
2818
+ continueWalkthroughLabel() {
2819
+ return t('nextcloud-vue', 'Continue where you left off')
2820
+ },
2821
+
2822
+ /**
2823
+ * Label for restarting an unfinished tour from the first step.
2824
+ *
2825
+ * @return {string}
2826
+ */
2827
+ startOverWalkthroughLabel() {
2828
+ return t('nextcloud-vue', 'Start over')
2829
+ },
2830
+
2781
2831
  /**
2782
2832
  * Resolve the active modal's Vue component from the registry.
2783
2833
  * Returns null when no modal is open or the key no longer resolves.
@@ -3281,10 +3331,12 @@ export default {
3281
3331
  return
3282
3332
  }
3283
3333
  try {
3284
- this.walkthroughSeenVersionValue = await loadWalkthroughSeenVersion(
3285
- this.appId,
3286
- this.walkthroughConfigKey,
3287
- )
3334
+ const [seen, progress] = await Promise.all([
3335
+ loadWalkthroughSeenVersion(this.appId, this.walkthroughConfigKey),
3336
+ loadWalkthroughProgress(this.appId, this.walkthroughConfigKey),
3337
+ ])
3338
+ this.walkthroughSeenVersionValue = seen
3339
+ this.walkthroughProgressValue = progress
3288
3340
  } finally {
3289
3341
  this.walkthroughSeenResolved = true
3290
3342
  }
@@ -3308,12 +3360,68 @@ export default {
3308
3360
  // Fire-and-forget: the loader never rejects and the local mirror is
3309
3361
  // already written, so a failed PUT must not surface as an error.
3310
3362
  persistWalkthroughSeenVersion(this.appId, this.walkthroughConfigKey, v)
3363
+ // A finished or skipped tour has nothing to continue.
3364
+ if (this.walkthroughProgressValue) {
3365
+ this.walkthroughProgressValue = null
3366
+ persistWalkthroughProgress(this.appId, this.walkthroughConfigKey, null)
3367
+ }
3311
3368
  /**
3312
3369
  * @event walkthrough-complete Emitted when the walkthrough finishes or is dismissed.
3313
3370
  */
3314
3371
  this.$emit('walkthrough-complete')
3315
3372
  },
3316
3373
 
3374
+ /**
3375
+ * Remember where the user is in the tour, so a reload or a restart
3376
+ * entry continues there (local mirror + per-user preference).
3377
+ *
3378
+ * @spec openspec/changes/walkthrough-advance-pause-resume/specs/cn-walkthrough/spec.md
3379
+ * @param {object} progress `{ tourId, stepId, index }` from CnWalkthrough.
3380
+ * @return {void}
3381
+ */
3382
+ onWalkthroughProgress(progress) {
3383
+ if (!progress || !progress.tourId) {
3384
+ return
3385
+ }
3386
+ // The first step of a fresh tour is where a restart lands anyway,
3387
+ // so it costs no request.
3388
+ if (progress.index === 0 && !this.walkthroughProgressValue) {
3389
+ return
3390
+ }
3391
+ const value = {
3392
+ tourId: progress.tourId,
3393
+ stepId: progress.stepId || '',
3394
+ index: progress.index || 0,
3395
+ version: String((this.manifest && this.manifest.version) || ''),
3396
+ }
3397
+ this.walkthroughProgressValue = value
3398
+ persistWalkthroughProgress(this.appId, this.walkthroughConfigKey, value)
3399
+ },
3400
+
3401
+ /**
3402
+ * Continue an unfinished tour from the user-settings dialog: show a
3403
+ * paused tour again, or start the remembered tour at its step.
3404
+ *
3405
+ * @spec openspec/changes/walkthrough-advance-pause-resume/specs/cn-walkthrough/spec.md
3406
+ * @return {void}
3407
+ */
3408
+ continueWalkthroughFromSettings() {
3409
+ this.userSettingsOpen = false
3410
+ if (!this.walkthroughEnabled) {
3411
+ return
3412
+ }
3413
+ setTimeout(() => {
3414
+ const wt = useWalkthrough(this.appId, this.manifest)
3415
+ if (wt.resumePaused()) {
3416
+ return
3417
+ }
3418
+ const progress = this.walkthroughProgressValue
3419
+ if (progress) {
3420
+ wt.resumeAt(progress.tourId, progress.stepId)
3421
+ }
3422
+ }, 50)
3423
+ },
3424
+
3317
3425
  /**
3318
3426
  * Replay the product tour from the user-settings dialog. Closes the
3319
3427
  * dialog first, then restarts the first declared tour on the next tick
@@ -3646,6 +3754,12 @@ export default {
3646
3754
  color: var(--color-text-maxcontrast);
3647
3755
  }
3648
3756
 
3757
+ .cn-app-root__walkthrough-actions {
3758
+ display: flex;
3759
+ flex-wrap: wrap;
3760
+ gap: 8px;
3761
+ }
3762
+
3649
3763
  .cn-app-root__integrations-hint {
3650
3764
  margin-bottom: 12px;
3651
3765
  color: var(--color-text-maxcontrast);
@@ -36,6 +36,16 @@
36
36
  </component>
37
37
  </div>
38
38
  </section>
39
+ <!-- The count could not be read. One quiet line, not the card and not an
40
+ alarm: hidden would read as "nothing to report", which is not known. -->
41
+ <p
42
+ v-else-if="checkFailed"
43
+ class="cn-banner-widget cn-banner-widget--unchecked"
44
+ :title="failureTooltip"
45
+ data-testid="cn-banner-widget-unchecked">
46
+ <span class="cn-banner-widget__unchecked-text">{{ uncheckedText }}</span>
47
+ <span class="cn-banner-widget__unchecked-reason">{{ failureTooltip }}</span>
48
+ </p>
39
49
  <div v-else-if="visible" class="cn-banner-widget">
40
50
  <NcNoteCard :type="resolvedVariant" class="cn-banner-widget__card">
41
51
  <component
@@ -52,6 +62,7 @@
52
62
  </template>
53
63
 
54
64
  <script>
65
+ import { translate as t } from '@nextcloud/l10n'
55
66
  import { NcNoteCard } from '@nextcloud/vue'
56
67
  import { followLinkClick, resolveHref } from '../../utils/linkNavigation.js'
57
68
  import { safeHref } from '../../utils/safeHref.js'
@@ -97,6 +108,12 @@ const MAX_ACTIONS = 2
97
108
  * condition evaluates true — a failed fetch never breaks (or spams) a
98
109
  * dashboard.
99
110
  *
111
+ * An attention card (`layout: "attention"`) whose `visibleWhen` request
112
+ * FAILS renders one quiet line instead, "Could not check", with the reason as
113
+ * a tooltip. A hidden card reads as "nothing needs attention", and a failed
114
+ * count does not know that. A request that succeeds with a value that does
115
+ * not meet the condition still renders nothing.
116
+ *
100
117
  * `layout: "attention"` turns the banner into an attention card: a card with
101
118
  * a coloured edge by severity, a `kicker` label, a `title`, a `reason` line
102
119
  * and up to two `actions` (the first is the primary one unless an action says
@@ -238,7 +255,9 @@ export default {
238
255
  * the banner renders from this verdict instead of fetching again —
239
256
  * no duplicate request and no hidden-until-self-evaluated flash —
240
257
  * and `value` feeds the `{value}` text placeholder. `null` (the
241
- * default) keeps the banner self-evaluating.
258
+ * default) keeps the banner self-evaluating. A host whose request
259
+ * failed passes `{ met: false, value: null, failed: true, reason }`,
260
+ * and an attention card then says it could not check.
242
261
  *
243
262
  * @type {object|null}
244
263
  */
@@ -259,6 +278,13 @@ export default {
259
278
  'action',
260
279
  ],
261
280
 
281
+ /**
282
+ * The banner's own state: the evaluated condition, the value it read,
283
+ * and whether the request for it failed.
284
+ *
285
+ * @return {object} The state.
286
+ * @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-an-attention-card-says-when-it-could-not-check
287
+ */
262
288
  data() {
263
289
  return {
264
290
  /** Id that ties the attention card's region to its title. */
@@ -271,6 +297,10 @@ export default {
271
297
  * the text wherever it says `{value}`.
272
298
  */
273
299
  conditionValue: null,
300
+ /** Whether the visibleWhen request itself failed (not: answered "no"). */
301
+ conditionFailed: false,
302
+ /** Why it failed, as the request reported it. */
303
+ conditionFailureReason: '',
274
304
  }
275
305
  },
276
306
 
@@ -419,6 +449,52 @@ export default {
419
449
  return this.conditionMet === true
420
450
  },
421
451
 
452
+ /**
453
+ * Whether to say the check failed: an attention card, with words to
454
+ * show, whose `visibleWhen` request failed. A plain banner stays
455
+ * hidden on failure, as it always did.
456
+ *
457
+ * @return {boolean}
458
+ * @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-an-attention-card-says-when-it-could-not-check
459
+ */
460
+ checkFailed() {
461
+ const title = this.title || (this.content && this.content.title) || ''
462
+ return this.isAttention
463
+ && !!this.resolvedVisibleWhen
464
+ && this.conditionFailed
465
+ && (title !== '' || this.resolvedText !== '')
466
+ },
467
+
468
+ /**
469
+ * The one line shown when the check failed. It names the card by its
470
+ * title, unless the title needs the value that could not be read.
471
+ *
472
+ * @return {string}
473
+ * @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-an-attention-card-says-when-it-could-not-check
474
+ */
475
+ uncheckedText() {
476
+ const title = this.title || (this.content && this.content.title) || this.resolvedText
477
+ if (!title || title.includes('{value}')) {
478
+ return t('nextcloud-vue', 'Could not check')
479
+ }
480
+ // `escape: false`: the result is rendered through `{{ }}`, which escapes.
481
+ return t('nextcloud-vue', 'Could not check: {subject}', { subject: title }, undefined, { escape: false })
482
+ },
483
+
484
+ /**
485
+ * The reason the check failed, for the tooltip and for assistive
486
+ * technology.
487
+ *
488
+ * @return {string}
489
+ * @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-an-attention-card-says-when-it-could-not-check
490
+ */
491
+ failureTooltip() {
492
+ if (!this.conditionFailureReason) {
493
+ return t('nextcloud-vue', 'The check failed.')
494
+ }
495
+ return t('nextcloud-vue', 'The check failed: {reason}', { reason: this.conditionFailureReason }, undefined, { escape: false })
496
+ },
497
+
422
498
  /** Whether the banner navigates on click (route set + router present). */
423
499
  clickable() {
424
500
  return !!this.resolvedRoute && !!this.$router
@@ -493,27 +569,36 @@ export default {
493
569
  * modes — the Wave-1 banner shape is the canonical one, extracted to
494
570
  * `utils/visibleWhen.js` in Wave 3 so manifest actions reuse it),
495
571
  * keeping the read VALUE for the `{value}` text placeholder.
496
- * Fail-safe: any fetch/shape error leaves the banner hidden.
572
+ * Fail-safe: any fetch/shape error leaves the banner hidden. The
573
+ * failure is kept (`conditionFailed`, with its reason) so an
574
+ * attention card can say it could not check.
497
575
  *
498
576
  * @return {Promise<void>}
577
+ * @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-an-attention-card-says-when-it-could-not-check
499
578
  */
500
579
  async evaluateCondition() {
501
580
  const outcome = this.resolvedConditionOutcome
502
581
  if (outcome) {
503
582
  this.conditionMet = outcome.met === true
504
583
  this.conditionValue = outcome.value !== undefined ? outcome.value : null
584
+ this.conditionFailed = outcome.failed === true
585
+ this.conditionFailureReason = outcome.failed === true && typeof outcome.reason === 'string' ? outcome.reason : ''
505
586
  return
506
587
  }
507
588
  const cond = this.resolvedVisibleWhen
508
589
  if (!cond) {
509
590
  this.conditionMet = null
510
591
  this.conditionValue = null
592
+ this.conditionFailed = false
593
+ this.conditionFailureReason = ''
511
594
  return
512
595
  }
513
596
  try {
514
597
  const value = await readVisibleWhenValue(cond)
515
598
  this.conditionValue = value
516
599
  this.conditionMet = compareVisibleWhen(value, cond.op || 'eq', cond.value)
600
+ this.conditionFailed = false
601
+ this.conditionFailureReason = ''
517
602
  } catch (error) {
518
603
  // Still hidden: a banner that cannot tell whether it applies
519
604
  // should not claim attention. But not silently, because a
@@ -522,6 +607,8 @@ export default {
522
607
  console.warn('[CnBannerWidget] visibleWhen could not be evaluated, banner hidden:', error?.message || error)
523
608
  this.conditionMet = false
524
609
  this.conditionValue = null
610
+ this.conditionFailed = true
611
+ this.conditionFailureReason = typeof error?.message === 'string' ? error.message : String(error ?? '')
525
612
  }
526
613
  },
527
614
 
@@ -557,6 +644,30 @@ export default {
557
644
  color: inherit;
558
645
  }
559
646
 
647
+ /* The check failed. Muted text, no fill, no severity colour: it is a note
648
+ that something is not known, not a warning. */
649
+ .cn-banner-widget--unchecked {
650
+ box-sizing: border-box;
651
+ margin: 0;
652
+ padding: 8px 12px;
653
+ color: var(--color-text-maxcontrast);
654
+ font-size: 0.9em;
655
+ cursor: help;
656
+ }
657
+
658
+ /* The reason, for assistive technology; sighted readers get the tooltip. */
659
+ .cn-banner-widget__unchecked-reason {
660
+ position: absolute;
661
+ width: 1px;
662
+ height: 1px;
663
+ margin: -1px;
664
+ padding: 0;
665
+ overflow: hidden;
666
+ clip-path: inset(50%);
667
+ white-space: nowrap;
668
+ border: 0;
669
+ }
670
+
560
671
  /* Attention card. The severity edge is an inset shadow, so it takes no width. */
561
672
  .cn-banner-widget--attention {
562
673
  --cn-banner-accent: var(--color-primary-element);
@@ -20,3 +20,19 @@ Down the side of a panel, with `orientation="vertical"`:
20
20
  <span>Content beside the stripe</span>
21
21
  </div>
22
22
  ```
23
+
24
+ A theme whose motif is not three bands names an image. Here the canal of a school: a blue line, its bank and the water:
25
+
26
+ ```vue
27
+ <div style="--cn-brand-stripe-image: linear-gradient(to bottom, #1f4fd8 0 4px, transparent 4px 7px, #1fb5a8 7px 9px); --cn-brand-stripe-height: 9px">
28
+ <CnBrandStripe />
29
+ </div>
30
+ ```
31
+
32
+ On a dark band, such as a footer, `variant="inverse"` draws the theme's `--cn-brand-stripe-image-inverse`, else the one image, else the bands:
33
+
34
+ ```vue
35
+ <div style="background: #14338f; padding-bottom: 16px; --cn-brand-stripe-image: linear-gradient(to bottom, #1f4fd8 0 4px, transparent 4px 7px, #1fb5a8 7px 9px); --cn-brand-stripe-image-inverse: linear-gradient(to bottom, #1f4fd8 0 4px, #ffffff 4px 7px, #1fb5a8 7px 9px); --cn-brand-stripe-height: 9px">
36
+ <CnBrandStripe variant="inverse" />
37
+ </div>
38
+ ```
@@ -5,7 +5,10 @@
5
5
  <template>
6
6
  <div
7
7
  class="cn-brand-stripe"
8
- :class="{ 'cn-brand-stripe--vertical': orientation === 'vertical' }"
8
+ :class="{
9
+ 'cn-brand-stripe--vertical': orientation === 'vertical',
10
+ 'cn-brand-stripe--inverse': variant === 'inverse',
11
+ }"
9
12
  aria-hidden="true"
10
13
  data-testid="cn-brand-stripe">
11
14
  <span class="cn-brand-stripe__band cn-brand-stripe__band--1" />
@@ -17,7 +20,8 @@
17
20
  <script>
18
21
  /**
19
22
  * CnBrandStripe draws the organisation's brand stripe: up to three coloured
20
- * bands next to each other, for the top of a page, a card or a sidebar.
23
+ * bands next to each other, or the organisation's own motif, for the top of a
24
+ * page, a card or a sidebar.
21
25
  *
22
26
  * The theme decides what it looks like, through the library's own custom
23
27
  * properties: `--cn-brand-stripe-color-1/2/3` (the colours, start to end),
@@ -26,6 +30,12 @@
26
30
  * tokens onto these; the library never reads a theme's tokens. Without them the
27
31
  * stripe is one band in the Nextcloud primary colour, 4px high.
28
32
  *
33
+ * A theme whose motif is not three bands (hanging twigs, a canal and its bank,
34
+ * a slanted cut) names it in `--cn-brand-stripe-image`, any background-image
35
+ * value; the stripe then draws that image in the same box instead of the
36
+ * bands. `--cn-brand-stripe-image-inverse` is the same motif for a dark band;
37
+ * the `inverse` variant draws it, else the one image, else the bands.
38
+ *
29
39
  * The stripe is decoration: it carries no meaning and is hidden from
30
40
  * assistive technology.
31
41
  *
@@ -49,6 +59,19 @@ export default {
49
59
  default: 'horizontal',
50
60
  validator: (v) => ['horizontal', 'vertical'].includes(v),
51
61
  },
62
+
63
+ /**
64
+ * The ground the stripe sits on: `default` (a light surface) or
65
+ * `inverse` (a dark band such as a footer), which draws the theme's
66
+ * `--cn-brand-stripe-image-inverse` when it names one.
67
+ *
68
+ * @type {'default'|'inverse'}
69
+ */
70
+ variant: {
71
+ type: String,
72
+ default: 'default',
73
+ validator: (v) => ['default', 'inverse'].includes(v),
74
+ },
52
75
  },
53
76
  }
54
77
  </script>
@@ -56,19 +79,52 @@ export default {
56
79
  <!--
57
80
  The stripe reads the library's own `--cn-brand-stripe-*` properties, each
58
81
  with a Nextcloud fallback. A theming app sets them from its own tokens.
82
+
83
+ THE ROOT PAINTS, NOT THE BANDS. The three bands are one background on the
84
+ root, a gradient with hard stops computed from the ratios, so a theme's
85
+ motif image can take its place with one `var()` fallback. A band painted on
86
+ its own element could not be switched off by the presence of an image: CSS
87
+ cannot ask whether a custom property is set. The band elements stay, empty,
88
+ so markup and tests that count them keep working.
59
89
  -->
60
90
  <style scoped>
61
91
  .cn-brand-stripe {
92
+ --cn-brand-stripe-first: var(--cn-brand-stripe-ratio-1, 1);
93
+ --cn-brand-stripe-second: var(--cn-brand-stripe-ratio-2, 1);
94
+ --cn-brand-stripe-total: calc(
95
+ var(--cn-brand-stripe-first) + var(--cn-brand-stripe-second) + var(--cn-brand-stripe-ratio-3, 1)
96
+ );
97
+ --cn-brand-stripe-stop-1: calc(var(--cn-brand-stripe-first) / var(--cn-brand-stripe-total) * 100%);
98
+ --cn-brand-stripe-stop-2: calc(
99
+ (var(--cn-brand-stripe-first) + var(--cn-brand-stripe-second)) / var(--cn-brand-stripe-total) * 100%
100
+ );
101
+ --cn-brand-stripe-direction: to right;
102
+ --cn-brand-stripe-bands: linear-gradient(
103
+ var(--cn-brand-stripe-direction),
104
+ var(--cn-brand-stripe-color-1, var(--color-primary-element)) 0 var(--cn-brand-stripe-stop-1),
105
+ var(--cn-brand-stripe-color-2, var(--color-primary-element)) var(--cn-brand-stripe-stop-1) var(--cn-brand-stripe-stop-2),
106
+ var(--cn-brand-stripe-color-3, var(--color-primary-element)) var(--cn-brand-stripe-stop-2) 100%
107
+ );
62
108
  display: flex;
63
109
  flex: none;
64
110
  width: 100%;
65
111
  height: var(--cn-brand-stripe-height, 4px);
112
+ background-image: var(--cn-brand-stripe-image, var(--cn-brand-stripe-bands));
113
+ background-repeat: repeat-x;
114
+ background-size: auto 100%;
115
+ }
116
+
117
+ .cn-brand-stripe--inverse {
118
+ background-image: var(--cn-brand-stripe-image-inverse, var(--cn-brand-stripe-image, var(--cn-brand-stripe-bands)));
66
119
  }
67
120
 
68
121
  .cn-brand-stripe--vertical {
122
+ --cn-brand-stripe-direction: to bottom;
69
123
  flex-direction: column;
70
124
  width: var(--cn-brand-stripe-height, 4px);
71
125
  height: 100%;
126
+ background-repeat: repeat-y;
127
+ background-size: 100% auto;
72
128
  }
73
129
 
74
130
  .cn-brand-stripe__band {
@@ -78,17 +134,14 @@ export default {
78
134
 
79
135
  .cn-brand-stripe__band--1 {
80
136
  flex-grow: var(--cn-brand-stripe-ratio-1, 1);
81
- background-color: var(--cn-brand-stripe-color-1, var(--color-primary-element));
82
137
  }
83
138
 
84
139
  .cn-brand-stripe__band--2 {
85
140
  flex-grow: var(--cn-brand-stripe-ratio-2, 1);
86
- background-color: var(--cn-brand-stripe-color-2, var(--color-primary-element));
87
141
  }
88
142
 
89
143
  .cn-brand-stripe__band--3 {
90
144
  flex-grow: var(--cn-brand-stripe-ratio-3, 1);
91
- background-color: var(--cn-brand-stripe-color-3, var(--color-primary-element));
92
145
  }
93
146
 
94
147
  @media print {
@@ -15,39 +15,51 @@
15
15
  </NcNoteCard>
16
16
 
17
17
  <div v-else class="cn-choice-cards__grid" :style="gridStyle">
18
- <label
18
+ <!-- One cell per option: the card's label, and below it whatever the
19
+ `#option-actions` slot adds. The slot sits OUTSIDE the label on
20
+ purpose: a button inside a label is interactive content inside
21
+ interactive content, and a click on it would toggle the radio. -->
22
+ <div
19
23
  v-for="option in normalizedOptions"
20
24
  :key="String(option.value)"
21
- class="cn-choice-cards__option"
22
- :class="{ 'cn-choice-cards__option--selected': isSelected(option) }">
23
- <!-- A REAL, VISIBLE input, not a styled div with a click handler.
24
- It carries the checked state to assistive technology and to
25
- Windows high-contrast mode for free, and it keeps the
26
- selection perceivable without colour (WCAG 1.4.1) — the
27
- card's active border alone would not be. -->
28
- <input
29
- class="cn-choice-cards__input"
30
- :type="multiple ? 'checkbox' : 'radio'"
31
- :name="groupName"
32
- :value="String(option.value)"
33
- :checked="isSelected(option)"
34
- :disabled="disabled"
35
- @change="onToggle(option)">
36
- <CnCard
37
- class="cn-choice-cards__card"
38
- :title="option.label"
39
- titleTag="span"
40
- :description="option.description"
41
- :stats="option.stats || []"
42
- :tags="option.tags || []"
43
- :active="isSelected(option)"
44
- activeVariant="primary"
45
- :descriptionLines="descriptionLines">
46
- <template v-if="option.icon" #icon>
47
- <CnIcon :name="option.icon" :size="20" />
48
- </template>
49
- </CnCard>
50
- </label>
25
+ class="cn-choice-cards__cell">
26
+ <label
27
+ class="cn-choice-cards__option"
28
+ :class="{ 'cn-choice-cards__option--selected': isSelected(option) }">
29
+ <!-- A REAL, VISIBLE input, not a styled div with a click handler.
30
+ It carries the checked state to assistive technology and to
31
+ Windows high-contrast mode for free, and it keeps the
32
+ selection perceivable without colour (WCAG 1.4.1) — the
33
+ card's active border alone would not be. -->
34
+ <input
35
+ class="cn-choice-cards__input"
36
+ :type="multiple ? 'checkbox' : 'radio'"
37
+ :name="groupName"
38
+ :value="String(option.value)"
39
+ :checked="isSelected(option)"
40
+ :disabled="disabled"
41
+ @change="onToggle(option)">
42
+ <CnCard
43
+ class="cn-choice-cards__card"
44
+ :title="option.label"
45
+ titleTag="span"
46
+ :description="option.description"
47
+ :stats="option.stats || []"
48
+ :tags="option.tags || []"
49
+ :active="isSelected(option)"
50
+ activeVariant="primary"
51
+ :descriptionLines="descriptionLines">
52
+ <template v-if="option.icon" #icon>
53
+ <CnIcon :name="option.icon" :size="20" />
54
+ </template>
55
+ </CnCard>
56
+ </label>
57
+ <div v-if="$slots['option-actions']" class="cn-choice-cards__actions">
58
+ <!-- @slot option-actions Content below one card, outside its label
59
+ (e.g. a per-option Load button). Scope: `{ option, selected }`. -->
60
+ <slot name="option-actions" :option="option" :selected="isSelected(option)" />
61
+ </div>
62
+ </div>
51
63
  </div>
52
64
  </fieldset>
53
65
  </template>
@@ -298,6 +310,19 @@ export default {
298
310
  padding: 12px;
299
311
  }
300
312
 
313
+ .cn-choice-cards__cell {
314
+ display: flex;
315
+ flex-direction: column;
316
+ gap: 8px;
317
+ min-inline-size: 0;
318
+ }
319
+
320
+ .cn-choice-cards__actions {
321
+ display: flex;
322
+ flex-direction: column;
323
+ gap: 4px;
324
+ }
325
+
301
326
  .cn-choice-cards__option {
302
327
  position: relative;
303
328
  display: block;