@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
@@ -1,6 +1,7 @@
1
1
  import { translate } from '@nextcloud/l10n';
2
2
  import { generateUrl } from '@nextcloud/router';
3
3
  import { NcAppNavigation, NcAppNavigationCaption, NcAppNavigationItem, NcAppNavigationList, NcAppNavigationNew, NcAppNavigationSettings, NcButton, NcCounterBubble } from '@nextcloud/vue';
4
+ import { toRaw } from 'vue';
4
5
  import BookOpenVariant from 'vue-material-design-icons/BookOpenVariant.vue';
5
6
  import Cog from 'vue-material-design-icons/Cog.vue';
6
7
  import HelpCircleOutline from 'vue-material-design-icons/HelpCircleOutline.vue';
@@ -44,6 +45,44 @@ function byManifestOrder(a, b) {
44
45
  return a.order - b.order
45
46
  }
46
47
 
48
+ /** A `:name` page-path segment: name, optional `(regexp)`, optional modifier. */
49
+ const PARAM_SEGMENT = /^:(\w+)(\([^)]*\))?([?*+])?$/;
50
+
51
+ /**
52
+ * Whether a `PARAM_SEGMENT` match may be left out (`?` or `*`).
53
+ *
54
+ * @param {Array<string>} param The match.
55
+ * @return {boolean} True for an optional segment.
56
+ */
57
+ function isOptionalParam(param) {
58
+ return param[3] === '?' || param[3] === '*'
59
+ }
60
+
61
+ /**
62
+ * Whether a route param value fills its segment.
63
+ *
64
+ * @param {unknown} value The value.
65
+ * @return {boolean} False for undefined, null and the empty string.
66
+ */
67
+ function isFilled(value) {
68
+ return value !== undefined && value !== null && value !== ''
69
+ }
70
+
71
+ /**
72
+ * Decode one percent-encoded path segment, or keep it as is when it is not
73
+ * valid percent-encoding.
74
+ *
75
+ * @param {string} segment The segment.
76
+ * @return {string} The decoded segment.
77
+ */
78
+ function decodeSegment(segment) {
79
+ try {
80
+ return decodeURIComponent(segment)
81
+ } catch {
82
+ return segment
83
+ }
84
+ }
85
+
47
86
  var script = {
48
87
  name: 'CnAppNav',
49
88
 
@@ -227,6 +266,13 @@ var script = {
227
266
 
228
267
  emits: ['primary-action', 'primary-action-click'],
229
268
 
269
+ /**
270
+ * The navigation's own state: which groups are open, and which entry
271
+ * of a route the reader last used.
272
+ *
273
+ * @return {object} The state.
274
+ * @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-a-page-below-a-list-marks-one-menu-entry
275
+ */
230
276
  data() {
231
277
  return {
232
278
  /**
@@ -240,6 +286,15 @@ var script = {
240
286
  */
241
287
  openState: {},
242
288
 
289
+ /**
290
+ * Per route name, the key of the entry that was last active on
291
+ * that route in this session. Read by `subRouteParent` to keep
292
+ * "the list I came from" marked on a page below it. In memory
293
+ * only: a reload starts empty and falls back to menu order.
294
+ *
295
+ * @type {Record<string, string>}
296
+ */
297
+ lastActiveEntryByRoute: {},
243
298
  }
244
299
  },
245
300
 
@@ -558,7 +613,9 @@ var script = {
558
613
  * `/expenses/:id`) light up — and auto-expand — their index entry
559
614
  * even though the detail route name is not in the menu. Longest
560
615
  * prefix disambiguates nested namespaces (e.g. `/pos` vs
561
- * `/pos/refunds`). Returns null when nothing matches.
616
+ * `/pos/refunds`). An entry with `params` matches by its page path
617
+ * with those params filled in (see `pageMatchDepth`). Returns null
618
+ * when nothing matches.
562
619
  *
563
620
  * @return {string|null}
564
621
  */
@@ -579,24 +636,84 @@ var script = {
579
636
  return routeName ?? null
580
637
  }
581
638
  let best = null;
582
- let bestLen = -1;
639
+ let bestDepth = 0;
583
640
  for (const it of flat) {
584
641
  if (!it.route) {
585
642
  continue
586
643
  }
587
- const pagePath = this.pageForItem(it)?.route;
588
- if (!pagePath || pagePath === '/' || pagePath.includes(':')) {
589
- continue
590
- }
591
- if (path === pagePath || path.startsWith(pagePath + '/')) {
592
- if (pagePath.length > bestLen) {
593
- best = it.route;
594
- bestLen = pagePath.length;
595
- }
644
+ const depth = this.pageMatchDepth(it, path);
645
+ if (depth > bestDepth) {
646
+ best = it.route;
647
+ bestDepth = depth;
596
648
  }
597
649
  }
598
650
  return best ?? routeName ?? null
599
651
  },
652
+
653
+ /**
654
+ * Every visible entry and child, flat, in menu order.
655
+ *
656
+ * @return {Array<object>} The entries.
657
+ * @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-a-page-below-a-list-marks-one-menu-entry
658
+ */
659
+ flatEntries() {
660
+ const flat = [];
661
+ for (const item of this.visibleItems) {
662
+ flat.push(item, ...this.visibleChildren(item));
663
+ }
664
+ return flat
665
+ },
666
+
667
+ /**
668
+ * The entry the query rule marks while the reader is on a menu
669
+ * route itself (not on a page below it). Watched, so the nav can
670
+ * remember which entry of a route the reader last used.
671
+ *
672
+ * @return {object|null} The entry, or null on a page below a route.
673
+ * @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-a-page-below-a-list-marks-one-menu-entry
674
+ */
675
+ entryActiveOnOwnRoute() {
676
+ const routeName = this.$route?.name;
677
+ if (!routeName || routeName !== this.activeRouteName) {
678
+ return null
679
+ }
680
+ return this.flatEntries.find((entry) => this.isActiveByRule(entry)) ?? null
681
+ },
682
+
683
+ /**
684
+ * The one entry to mark on a page BELOW a menu route when the query
685
+ * rule marks none.
686
+ *
687
+ * On a detail page (`/cases/123`) the active route is found by path
688
+ * prefix, and the address carries the detail page's query, not the
689
+ * list's. When every entry on that list has a `query` ("My work",
690
+ * "Queue"), none of them matched and the menu showed no place at
691
+ * all. Marking all of them is the defect the query rule ended, so
692
+ * this picks exactly one: the entry the reader last had active on
693
+ * that route, else the first in menu order.
694
+ *
695
+ * Null whenever the existing rule already marks an entry, so a list
696
+ * with an entry without a `query` behaves as it did. An entry with
697
+ * `params` is a candidate only when the current path sits below its
698
+ * own filled-in page path, so another param value is never marked.
699
+ *
700
+ * @return {object|null} The entry to mark, or null.
701
+ * @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-a-page-below-a-list-marks-one-menu-entry
702
+ */
703
+ subRouteParent() {
704
+ const active = this.activeRouteName;
705
+ if (!active || this.$route?.name === active) {
706
+ return null
707
+ }
708
+ const path = this.$route?.path;
709
+ const entries = this.flatEntries.filter((entry) => !entry.href && entry.route === active
710
+ && (!this.hasParams(entry) || this.pageMatchDepth(entry, path) > 0));
711
+ if (entries.length === 0 || entries.some((entry) => this.isActiveByRule(entry))) {
712
+ return null
713
+ }
714
+ const remembered = this.lastActiveEntryByRoute[active];
715
+ return entries.find((entry) => this.entryKey(entry) === remembered) ?? entries[0]
716
+ },
600
717
  },
601
718
 
602
719
  watch: {
@@ -611,6 +728,23 @@ var script = {
611
728
  visibleItems() {
612
729
  this.pinGroupsEntered(this.activeRouteName);
613
730
  },
731
+
732
+ entryActiveOnOwnRoute: {
733
+ immediate: true,
734
+ /**
735
+ * Remember which entry of a route the reader used, for
736
+ * `subRouteParent`.
737
+ *
738
+ * @param {object|null} entry The entry active on its own route.
739
+ * @return {void}
740
+ * @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-a-page-below-a-list-marks-one-menu-entry
741
+ */
742
+ handler(entry) {
743
+ if (entry && entry.route) {
744
+ this.lastActiveEntryByRoute[entry.route] = this.entryKey(entry);
745
+ }
746
+ },
747
+ },
614
748
  },
615
749
 
616
750
  created() {
@@ -623,6 +757,8 @@ var script = {
623
757
  // stays out of `data()`: a reactive Set mutated inside render would
624
758
  // re-trigger the render effect.
625
759
  this._autoCountWarned = new Set();
760
+ // Same latch, for warnUnfilledRouteParam() (`itemTo()` runs during render).
761
+ this._unfilledParamWarned = new Set();
626
762
  },
627
763
 
628
764
  methods: {
@@ -831,7 +967,19 @@ var script = {
831
967
  .sort(byManifestOrder)
832
968
  },
833
969
 
970
+ /**
971
+ * The label an entry shows: run through the translate function, or
972
+ * as written when the entry sets `translateLabel: false` (a
973
+ * user-authored title that may equal a translation key). Every
974
+ * place the nav shows an entry's label reads it from here.
975
+ *
976
+ * @param {object} item Menu entry.
977
+ * @return {string} The label to render as text.
978
+ */
834
979
  resolveLabel(item) {
980
+ if (item.translateLabel === false) {
981
+ return item.label
982
+ }
835
983
  return this.effectiveTranslate(item.label)
836
984
  },
837
985
 
@@ -842,30 +990,204 @@ var script = {
842
990
  * work" and "Queue" on one list). The route name alone lit both. An
843
991
  * entry with a `query` is active only when the address carries that
844
992
  * query; an entry without one steps back when a sibling's query
845
- * matches, so exactly one of them is marked.
993
+ * matches, so exactly one of them is marked. `params` work the
994
+ * same way against the route's params, so of several entries on a
995
+ * `/items/:slug` route only the one whose `slug` is in the address
996
+ * is marked.
997
+ *
998
+ * On a page below a list where that rule marks nothing, the one
999
+ * entry `subRouteParent` picks is active instead.
846
1000
  *
847
1001
  * @param {object} item Menu entry.
848
1002
  * @return {boolean} True when the entry is the current page.
849
1003
  * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-menu-entries-that-differ-in-query
1004
+ * @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-a-page-below-a-list-marks-one-menu-entry
850
1005
  */
851
1006
  isActive(item) {
1007
+ if (this.isActiveByRule(item)) {
1008
+ return true
1009
+ }
1010
+ const parent = this.subRouteParent;
1011
+ return parent !== null && toRaw(parent) === toRaw(item)
1012
+ },
1013
+
1014
+ /**
1015
+ * The route, query and params rule on its own, without the fallback
1016
+ * for a page below a list. `subRouteParent` asks this to see whether
1017
+ * the rule already marks an entry.
1018
+ *
1019
+ * @param {object} item Menu entry.
1020
+ * @return {boolean} True when the rule marks the entry.
1021
+ * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-menu-entries-that-differ-in-query
1022
+ * @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-a-page-below-a-list-marks-one-menu-entry
1023
+ */
1024
+ isActiveByRule(item) {
852
1025
  if (item.href || !item.route) {
853
1026
  return false
854
1027
  }
855
1028
  if (item.route !== this.activeRouteName) {
856
1029
  return false
857
1030
  }
858
- if (item.query && typeof item.query === 'object') {
859
- return this.queryMatches(item)
860
- }
861
- const siblings = [];
862
- for (const entry of this.visibleItems) {
863
- siblings.push(entry, ...this.visibleChildren(entry));
1031
+ if (this.isNarrowed(item)) {
1032
+ return this.narrowingMatches(item)
864
1033
  }
865
- return !siblings.some((entry) => entry !== item
1034
+ return !this.flatEntries.some((entry) => entry !== item
866
1035
  && entry.route === item.route
867
- && entry.query && typeof entry.query === 'object'
868
- && this.queryMatches(entry))
1036
+ && this.isNarrowed(entry)
1037
+ && this.narrowingMatches(entry))
1038
+ },
1039
+
1040
+ /**
1041
+ * Whether an entry narrows its route with a `query` or `params`.
1042
+ *
1043
+ * @param {object} item Menu entry.
1044
+ * @return {boolean} True when the entry declares either.
1045
+ */
1046
+ isNarrowed(item) {
1047
+ return Boolean(item.query && typeof item.query === 'object') || this.hasParams(item)
1048
+ },
1049
+
1050
+ /**
1051
+ * Whether the address carries every `query` key and every `params`
1052
+ * key the entry declares.
1053
+ *
1054
+ * @param {object} item Menu entry that narrows its route.
1055
+ * @return {boolean} True when everything it declares matches.
1056
+ */
1057
+ narrowingMatches(item) {
1058
+ const queryOk = !(item.query && typeof item.query === 'object') || this.queryMatches(item);
1059
+ const paramsOk = !this.hasParams(item) || this.paramsMatch(item);
1060
+ return queryOk && paramsOk
1061
+ },
1062
+
1063
+ /**
1064
+ * Whether an entry declares a non-empty `params` object.
1065
+ *
1066
+ * @param {object} item Menu entry.
1067
+ * @return {boolean} True when it has route params.
1068
+ */
1069
+ hasParams(item) {
1070
+ return Boolean(item?.params) && typeof item.params === 'object' && Object.keys(item.params).length > 0
1071
+ },
1072
+
1073
+ /**
1074
+ * Whether the route's params carry every key of an entry's
1075
+ * `params`. Values are compared as strings.
1076
+ *
1077
+ * @param {object} item Menu entry with `params`.
1078
+ * @return {boolean} True when every declared key matches.
1079
+ */
1080
+ paramsMatch(item) {
1081
+ const current = this.$route?.params ?? {};
1082
+ return Object.keys(item.params).every((key) => String(current[key] ?? '') === String(item.params[key]))
1083
+ },
1084
+
1085
+ /**
1086
+ * The segments of an entry's page path with its `params` filled in,
1087
+ * as plain values. A missing optional `:name?` segment is dropped;
1088
+ * null when a required one has no param, or without a page.
1089
+ *
1090
+ * @param {object} item Menu entry with `params`.
1091
+ * @return {Array<string>|null} The segments, or null.
1092
+ */
1093
+ filledPageSegments(item) {
1094
+ const pagePath = this.pageForItem(item)?.route;
1095
+ if (!pagePath) {
1096
+ return null
1097
+ }
1098
+ const segments = [];
1099
+ for (const part of pagePath.split('/').filter(Boolean)) {
1100
+ const param = PARAM_SEGMENT.exec(part);
1101
+ if (!param) {
1102
+ segments.push(part);
1103
+ continue
1104
+ }
1105
+ const value = item.params?.[param[1]];
1106
+ if (isFilled(value)) {
1107
+ segments.push(String(value));
1108
+ } else if (!isOptionalParam(param)) {
1109
+ return null
1110
+ }
1111
+ }
1112
+ return segments
1113
+ },
1114
+
1115
+ /**
1116
+ * The required route param the router cannot fill when it resolves
1117
+ * an entry's link on its manifest page. vue-router fills a named
1118
+ * link's required params from the current route, then overlays every
1119
+ * key the entry declares, empty or null included. So a declared key
1120
+ * decides on its own value, and only an undeclared one falls back
1121
+ * to the current route. Null without a known page, or when every
1122
+ * required segment is filled. Reads `$route`, so it follows
1123
+ * navigation.
1124
+ *
1125
+ * @param {object} item Menu entry.
1126
+ * @return {string|null} The param name, or null.
1127
+ */
1128
+ unfilledRouteParam(item) {
1129
+ const pagePath = this.pageForItem(item)?.route;
1130
+ if (!pagePath) {
1131
+ return null
1132
+ }
1133
+ const declared = item.params && typeof item.params === 'object' ? item.params : {};
1134
+ const current = this.$route?.params ?? {};
1135
+ for (const part of pagePath.split('/')) {
1136
+ const param = PARAM_SEGMENT.exec(part);
1137
+ if (!param || isOptionalParam(param)) {
1138
+ continue
1139
+ }
1140
+ const value = param[1] in declared ? declared[param[1]] : current[param[1]];
1141
+ if (!isFilled(value)) {
1142
+ return param[1]
1143
+ }
1144
+ }
1145
+ return null
1146
+ },
1147
+
1148
+ /**
1149
+ * How deep a path sits on an entry's page path: the page path's
1150
+ * segment count when the path is that page or below it, else 0.
1151
+ * A `/` page path, or one with an unfilled `:name`, never matches.
1152
+ *
1153
+ * Without `params` the page path is compared as written. With
1154
+ * `params`, segments are compared decoded, because the router
1155
+ * percent-encodes param values in the path and the entry holds them
1156
+ * plain.
1157
+ *
1158
+ * @param {object} item Menu entry.
1159
+ * @param {string|undefined} path The path to test.
1160
+ * @return {number} The depth of the match, or 0.
1161
+ */
1162
+ pageMatchDepth(item, path) {
1163
+ if (!path) {
1164
+ return 0
1165
+ }
1166
+ if (!this.hasParams(item)) {
1167
+ const pagePath = this.pageForItem(item)?.route;
1168
+ if (!pagePath || pagePath === '/' || pagePath.includes(':')) {
1169
+ return 0
1170
+ }
1171
+ const below = path === pagePath || path.startsWith(pagePath + '/');
1172
+ return below ? pagePath.split('/').filter(Boolean).length : 0
1173
+ }
1174
+ const segments = this.filledPageSegments(item);
1175
+ if (!segments || segments.length === 0) {
1176
+ return 0
1177
+ }
1178
+ const current = path.split('/').filter(Boolean).map(decodeSegment);
1179
+ return segments.every((segment, index) => current[index] === segment) ? segments.length : 0
1180
+ },
1181
+
1182
+ /**
1183
+ * What an entry is remembered by: its `id`, else its label.
1184
+ *
1185
+ * @param {object} item Menu entry.
1186
+ * @return {string} The key.
1187
+ * @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-a-page-below-a-list-marks-one-menu-entry
1188
+ */
1189
+ entryKey(item) {
1190
+ return String(item.id ?? item.label ?? '')
869
1191
  },
870
1192
 
871
1193
  /**
@@ -977,6 +1299,27 @@ var script = {
977
1299
  console.warn(`[CnAppNav] Menu entry "${item.id}" declares count: "auto" but has no resolvable index-type page with register + schema config — no badge will render.`);
978
1300
  },
979
1301
 
1302
+ /**
1303
+ * One-shot `console.warn` per menu entry whose link cannot fill a
1304
+ * required route param. Keyed by `id`, else route and label, so
1305
+ * entries without an id each get their own warning.
1306
+ *
1307
+ * @param {{ id?: string, route: string, label?: string }} item Menu entry descriptor.
1308
+ * @param {string} param The unfilled param name.
1309
+ * @return {void}
1310
+ * @private
1311
+ */
1312
+ warnUnfilledRouteParam(item, param) {
1313
+ const key = item.id ?? `${item.route}|${item.label ?? ''}`;
1314
+ if (this._unfilledParamWarned.has(key)) {
1315
+ return
1316
+ }
1317
+ this._unfilledParamWarned.add(key);
1318
+ const name = item.id ?? item.label ?? item.route;
1319
+ // eslint-disable-next-line no-console
1320
+ console.warn(`[CnAppNav] Menu entry "${name}" cannot fill the required route param "${param}" of page "${item.route}" from its params or the current route — the entry renders without a link.`);
1321
+ },
1322
+
980
1323
  /**
981
1324
  * Look up an item's resolved page (`pages[]` entry whose `id`
982
1325
  * matches the menu item's `route`) — used to decide whether the
@@ -999,11 +1342,17 @@ var script = {
999
1342
  * items (`action: "user-settings"`) and `href` items return
1000
1343
  * `null` so the entry is NOT a vue-router link: action items
1001
1344
  * fall through to the click handler, `href` items render a real
1002
- * anchor via `itemHref`. Route items return a named route.
1345
+ * anchor via `itemHref`. Route items return a named route that
1346
+ * carries the item's `params` and `query` when it declares them.
1347
+ *
1348
+ * An item whose manifest page has a required segment that neither
1349
+ * its `params` nor the current route fill also returns null, with a
1350
+ * one-time warning: the router throws resolving such a link, which
1351
+ * would take the whole navigation down rather than that one entry.
1003
1352
  *
1004
1353
  * @param {object} item Menu item being rendered.
1005
- * @return {object|null} A `{ name }` route object, or null for
1006
- * action / href / route-less items.
1354
+ * @return {object|null} A `{ name, params?, query? }` route object,
1355
+ * or null for action / href / route-less / unfillable items.
1007
1356
  */
1008
1357
  itemTo(item) {
1009
1358
  if (item.action) {
@@ -1015,9 +1364,22 @@ var script = {
1015
1364
  if (!item.route) {
1016
1365
  return null
1017
1366
  }
1018
- // Carry optional query params so a nav entry can deep-link to a
1019
- // pre-filtered index page (e.g. one entry per case type → Cases?caseType=…).
1020
- return item.query ? { name: item.route, query: item.query } : { name: item.route }
1367
+ const unfilled = this.unfilledRouteParam(item);
1368
+ if (unfilled) {
1369
+ this.warnUnfilledRouteParam(item, unfilled);
1370
+ return null
1371
+ }
1372
+ const to = { name: item.route };
1373
+ // Params fill a parameterised route (one entry per catalog →
1374
+ // /catalogs/:slug); query deep-links to a pre-filtered index page
1375
+ // (one entry per case type → Cases?caseType=…).
1376
+ if (this.hasParams(item)) {
1377
+ to.params = item.params;
1378
+ }
1379
+ if (item.query) {
1380
+ to.query = item.query;
1381
+ }
1382
+ return to
1021
1383
  },
1022
1384
 
1023
1385
  /**
@@ -1169,6 +1531,11 @@ var script = {
1169
1531
  this.cnReplayWalkthrough(item.tourId);
1170
1532
  return
1171
1533
  }
1534
+ // An entry left without a link (see itemTo) renders a `#` anchor;
1535
+ // following it would change the address, so the click goes nowhere.
1536
+ if (!item.href && this.unfilledRouteParam(item) && event && typeof event.preventDefault === 'function') {
1537
+ event.preventDefault();
1538
+ }
1172
1539
  if (this.visibleChildren(item).length === 0) {
1173
1540
  return
1174
1541
  }