@conduction/nextcloud-vue 2.91.0 → 2.93.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 (149) hide show
  1. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +57 -4
  2. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  3. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +44 -17
  4. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  5. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue.js +1 -1
  6. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js +44 -2
  7. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
  8. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js +140 -59
  9. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
  10. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  11. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +20 -6
  12. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  13. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +3 -2
  14. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  15. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +10 -39
  16. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  17. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  18. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +5 -3
  19. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  20. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  21. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  22. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +6 -4
  23. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  24. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  25. package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue2.js +46 -0
  26. package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue2.js.map +1 -1
  27. package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue3.js +6 -2
  28. package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue3.js.map +1 -1
  29. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue.js +1 -1
  30. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js +82 -9
  31. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js.map +1 -1
  32. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js +104 -90
  33. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js.map +1 -1
  34. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue.js +9 -0
  35. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue.js.map +1 -0
  36. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js +355 -0
  37. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js.map +1 -0
  38. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js +462 -0
  39. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js.map +1 -0
  40. package/dist/esm/components/CnFormDialog/CnFormDialog.vue.js +1 -1
  41. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +39 -10
  42. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  43. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +418 -399
  44. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  45. package/dist/esm/components/CnIcon/semanticIcons.js +2 -2
  46. package/dist/esm/components/CnIcon/semanticIcons.js.map +1 -1
  47. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue.js +1 -1
  48. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue2.js +156 -3
  49. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue2.js.map +1 -1
  50. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue3.js +168 -52
  51. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue3.js.map +1 -1
  52. package/dist/esm/components/CnModuleCard/CnModuleCard.vue.js +9 -0
  53. package/dist/esm/components/CnModuleCard/CnModuleCard.vue.js.map +1 -0
  54. package/dist/esm/components/CnModuleCard/CnModuleCard.vue2.js +83 -0
  55. package/dist/esm/components/CnModuleCard/CnModuleCard.vue2.js.map +1 -0
  56. package/dist/esm/components/CnModuleCard/CnModuleCard.vue3.js +119 -0
  57. package/dist/esm/components/CnModuleCard/CnModuleCard.vue3.js.map +1 -0
  58. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue.js +1 -1
  59. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js +22 -1
  60. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js.map +1 -1
  61. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js +20 -14
  62. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js.map +1 -1
  63. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue.js +1 -1
  64. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue2.js +2 -0
  65. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue2.js.map +1 -1
  66. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue3.js +15 -5
  67. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue3.js.map +1 -1
  68. package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue.js +1 -1
  69. package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue2.js +8 -30
  70. package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue2.js.map +1 -1
  71. package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue3.js.map +1 -1
  72. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  73. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +33 -2
  74. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  75. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  76. package/dist/esm/components/CnTextWidget/CnTextWidget.vue.js +1 -1
  77. package/dist/esm/components/CnTextWidget/CnTextWidget.vue2.js +63 -4
  78. package/dist/esm/components/CnTextWidget/CnTextWidget.vue2.js.map +1 -1
  79. package/dist/esm/components/CnTextWidget/CnTextWidget.vue3.js +19 -2
  80. package/dist/esm/components/CnTextWidget/CnTextWidget.vue3.js.map +1 -1
  81. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue.js +9 -0
  82. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue.js.map +1 -0
  83. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js +164 -0
  84. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js.map +1 -0
  85. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js +93 -0
  86. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js.map +1 -0
  87. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue.js +1 -1
  88. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js +12 -0
  89. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js.map +1 -1
  90. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js.map +1 -1
  91. package/dist/esm/l10n/en.json.js +45 -1
  92. package/dist/esm/l10n/en.json.js.map +1 -1
  93. package/dist/esm/l10n/nl.json.js +44 -1
  94. package/dist/esm/l10n/nl.json.js.map +1 -1
  95. package/dist/esm/library-style.js +1 -1
  96. package/dist/esm/nextcloud-vue.css +1182 -328
  97. package/dist/esm/utils/boardDate.js +35 -1
  98. package/dist/esm/utils/boardDate.js.map +1 -1
  99. package/dist/esm/utils/featureCatalog.js +135 -0
  100. package/dist/esm/utils/featureCatalog.js.map +1 -0
  101. package/dist/esm/utils/fillTemplate.js +84 -4
  102. package/dist/esm/utils/fillTemplate.js.map +1 -1
  103. package/dist/esm/utils/headerMeta.js +62 -12
  104. package/dist/esm/utils/headerMeta.js.map +1 -1
  105. package/dist/esm/utils/roadmapIssues.js +86 -0
  106. package/dist/esm/utils/roadmapIssues.js.map +1 -0
  107. package/dist/esm/utils/userDisplayName.js +47 -0
  108. package/dist/esm/utils/userDisplayName.js.map +1 -0
  109. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  110. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  111. package/dist/nextcloud-vue.cjs.js +15618 -13097
  112. package/dist/nextcloud-vue.cjs.js.map +2 -2
  113. package/dist/nextcloud-vue.css +1182 -328
  114. package/l10n/en.json +45 -1
  115. package/l10n/nl.json +44 -1
  116. package/package.json +1 -1
  117. package/src/components/CnAppRoot/CnAppRoot.md +2 -0
  118. package/src/components/CnAppRoot/CnAppRoot.vue +66 -5
  119. package/src/components/CnChoiceCards/CnChoiceCards.md +27 -0
  120. package/src/components/CnChoiceCards/CnChoiceCards.vue +168 -3
  121. package/src/components/CnDashboardPage/CnDashboardPage.vue +45 -6
  122. package/src/components/CnDataTable/CnDataTable.vue +10 -39
  123. package/src/components/CnDetailPage/CnDetailPage.vue +5 -3
  124. package/src/components/CnFactsWidget/CnFactsWidget.vue +6 -4
  125. package/src/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue +51 -1
  126. package/src/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue +98 -13
  127. package/src/components/CnFeaturesBoard/CnFeaturesBoard.vue +1053 -0
  128. package/src/components/CnFormDialog/CnFormDialog.md +1 -0
  129. package/src/components/CnFormDialog/CnFormDialog.vue +619 -562
  130. package/src/components/CnLinkCardsPage/CnLinkCardsPage.vue +338 -5
  131. package/src/components/CnModuleCard/CnModuleCard.vue +258 -0
  132. package/src/components/CnNcWidgetWidget/CnNcWidgetWidget.vue +23 -2
  133. package/src/components/CnNotificationPreferences/CnNotificationPreferences.vue +5 -0
  134. package/src/components/CnRoadmapTab/CnRoadmapTab.vue +6 -28
  135. package/src/components/CnTabsWidget/CnTabsWidget.vue +33 -2
  136. package/src/components/CnTextWidget/CnTextWidget.vue +89 -4
  137. package/src/components/CnUserSettingsDialog/CnUserSettingsDialog.vue +305 -0
  138. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +12 -0
  139. package/src/css/dialog.css +47 -0
  140. package/src/css/look-board-detail.css +8 -0
  141. package/src/css/look-board.css +33 -0
  142. package/src/schemas/app-manifest-v2.schema.json +67 -4
  143. package/src/utils/boardDate.js +34 -0
  144. package/src/utils/featureCatalog.js +132 -0
  145. package/src/utils/fillTemplate.js +83 -3
  146. package/src/utils/headerMeta.js +62 -12
  147. package/src/utils/roadmapIssues.js +82 -0
  148. package/src/utils/userDisplayName.js +54 -0
  149. package/src/utils/validateManifestV2.compiled.js +3 -3
@@ -1,4 +1,4 @@
1
- import { resolveComponent, openBlock, createElementBlock, createElementVNode, toDisplayString, createCommentVNode, Fragment, renderList, createBlock } from 'vue';
1
+ import { resolveComponent, openBlock, createElementBlock, createElementVNode, toDisplayString, createCommentVNode, Fragment, renderList, createBlock, createVNode, normalizeClass } from 'vue';
2
2
 
3
3
  const _hoisted_1 = {
4
4
  class: "cn-link-cards-page",
@@ -13,21 +13,54 @@ const _hoisted_4 = {
13
13
  key: 1,
14
14
  class: "cn-link-cards-page__description"
15
15
  };
16
- const _hoisted_5 = {
16
+ const _hoisted_5 = ["aria-labelledby"];
17
+ const _hoisted_6 = { class: "cn-link-cards-page__modules-head" };
18
+ const _hoisted_7 = ["id"];
19
+ const _hoisted_8 = {
20
+ class: "cn-link-cards-page__modules-count",
21
+ "data-testid": "cn-link-cards-modules-count"
22
+ };
23
+ const _hoisted_9 = { class: "cn-link-cards-page__modules-grid" };
24
+ const _hoisted_10 = {
17
25
  key: 0,
26
+ class: "cn-link-cards-page__modules-note",
27
+ "data-testid": "cn-link-cards-modules-note"
28
+ };
29
+ const _hoisted_11 = {
30
+ key: 1,
18
31
  class: "cn-link-cards-page__empty",
19
32
  "data-testid": "cn-link-cards-empty"
20
33
  };
21
- const _hoisted_6 = ["aria-labelledby", "aria-label", "data-group"];
22
- const _hoisted_7 = ["id"];
23
- const _hoisted_8 = { class: "cn-link-cards-page__grid" };
24
- const _hoisted_9 = ["data-card-id", "href", "target", "rel", "aria-describedby", "onClick"];
25
- const _hoisted_10 = { class: "cn-link-cards-page__body" };
26
- const _hoisted_11 = { class: "cn-link-cards-page__label" };
27
- const _hoisted_12 = ["id"];
34
+ const _hoisted_12 = ["aria-labelledby", "aria-label", "data-group"];
35
+ const _hoisted_13 = ["id"];
36
+ const _hoisted_14 = {
37
+ key: 1,
38
+ class: "cn-link-cards-page__list"
39
+ };
40
+ const _hoisted_15 = ["data-card-id", "href", "target", "rel", "aria-describedby", "onClick"];
41
+ const _hoisted_16 = {
42
+ key: 0,
43
+ class: "cn-link-cards-page__row-icon",
44
+ "aria-hidden": "true"
45
+ };
46
+ const _hoisted_17 = { class: "cn-link-cards-page__row-body" };
47
+ const _hoisted_18 = { class: "cn-link-cards-page__row-label" };
48
+ const _hoisted_19 = ["id"];
49
+ const _hoisted_20 = {
50
+ key: 2,
51
+ class: "cn-link-cards-page__grid"
52
+ };
53
+ const _hoisted_21 = ["data-card-id", "href", "target", "rel", "aria-describedby", "onClick"];
54
+ const _hoisted_22 = { class: "cn-link-cards-page__body" };
55
+ const _hoisted_23 = { class: "cn-link-cards-page__label" };
56
+ const _hoisted_24 = ["id"];
28
57
 
29
58
  function render(_ctx, _cache, $props, $setup, $data, $options) {
59
+ const _component_CnModuleCard = resolveComponent("CnModuleCard");
60
+ const _component_InformationOutline = resolveComponent("InformationOutline");
30
61
  const _component_CnIcon = resolveComponent("CnIcon");
62
+ const _component_OpenInNew = resolveComponent("OpenInNew");
63
+ const _component_ChevronRight = resolveComponent("ChevronRight");
31
64
 
32
65
  return (openBlock(), createElementBlock("div", _hoisted_1, [
33
66
  createElementVNode("div", _hoisted_2, [
@@ -38,13 +71,47 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
38
71
  ? (openBlock(), createElementBlock("p", _hoisted_4, toDisplayString($options.resolvedDescription), 1 /* TEXT */))
39
72
  : createCommentVNode("v-if", true)
40
73
  ]),
41
- ($options.groups.length === 0)
42
- ? (openBlock(), createElementBlock("p", _hoisted_5, toDisplayString($options.resolvedEmptyLabel), 1 /* TEXT */))
74
+ ($options.visibleModules.length > 0)
75
+ ? (openBlock(), createElementBlock("section", {
76
+ key: 0,
77
+ class: "cn-link-cards-page__modules",
78
+ "aria-labelledby": `cn-link-cards-modules-${$data.uid}`,
79
+ "data-testid": "cn-link-cards-modules"
80
+ }, [
81
+ createElementVNode("div", _hoisted_6, [
82
+ createElementVNode("h3", {
83
+ id: `cn-link-cards-modules-${$data.uid}`,
84
+ class: "cn-link-cards-page__modules-title"
85
+ }, toDisplayString($options.resolvedModulesTitle), 9 /* TEXT, PROPS */, _hoisted_7),
86
+ createElementVNode("span", _hoisted_8, toDisplayString($options.modulesCount), 1 /* TEXT */)
87
+ ]),
88
+ createElementVNode("div", _hoisted_9, [
89
+ (openBlock(true), createElementBlock(Fragment, null, renderList($options.visibleModules, (module) => {
90
+ return (openBlock(), createBlock(_component_CnModuleCard, {
91
+ key: module.id,
92
+ module: module,
93
+ headingId: `cn-link-cards-module-${$data.uid}-${module.key}`
94
+ }, null, 8 /* PROPS */, ["module", "headingId"]))
95
+ }), 128 /* KEYED_FRAGMENT */))
96
+ ]),
97
+ ($options.resolvedFootnote)
98
+ ? (openBlock(), createElementBlock("p", _hoisted_10, [
99
+ createVNode(_component_InformationOutline, {
100
+ size: 18,
101
+ class: "cn-link-cards-page__modules-note-icon"
102
+ }),
103
+ createElementVNode("span", null, toDisplayString($options.resolvedFootnote), 1 /* TEXT */)
104
+ ]))
105
+ : createCommentVNode("v-if", true)
106
+ ], 8 /* PROPS */, _hoisted_5))
107
+ : createCommentVNode("v-if", true),
108
+ ($options.groups.length === 0 && $options.visibleModules.length === 0)
109
+ ? (openBlock(), createElementBlock("p", _hoisted_11, toDisplayString($options.resolvedEmptyLabel), 1 /* TEXT */))
43
110
  : createCommentVNode("v-if", true),
44
111
  (openBlock(true), createElementBlock(Fragment, null, renderList($options.groups, (group) => {
45
112
  return (openBlock(), createElementBlock("section", {
46
113
  key: group.key,
47
- class: "cn-link-cards-page__group",
114
+ class: normalizeClass(["cn-link-cards-page__group", { 'cn-link-cards-page__group--list': $options.isListStyle }]),
48
115
  "aria-labelledby": group.label ? group.headingId : null,
49
116
  "aria-label": group.label ? null : $options.resolvedTitle || null,
50
117
  "data-testid": "cn-link-cards-group",
@@ -55,48 +122,97 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
55
122
  key: 0,
56
123
  id: group.headingId,
57
124
  class: "cn-link-cards-page__caption"
58
- }, toDisplayString(group.label), 9 /* TEXT, PROPS */, _hoisted_7))
125
+ }, toDisplayString(group.label), 9 /* TEXT, PROPS */, _hoisted_13))
59
126
  : createCommentVNode("v-if", true),
60
- createElementVNode("ul", _hoisted_8, [
61
- (openBlock(true), createElementBlock(Fragment, null, renderList(group.cards, (card) => {
62
- return (openBlock(), createElementBlock("li", {
63
- key: card.id,
64
- class: "cn-link-cards-page__cell"
65
- }, [
66
- createElementVNode("a", {
67
- class: "cn-link-cards-page__card",
68
- "data-testid": "cn-link-card",
69
- "data-card-id": card.id,
70
- href: card.url,
71
- target: card.external ? '_blank' : null,
72
- rel: card.external ? 'noopener noreferrer' : null,
73
- "aria-describedby": card.description ? card.descriptionId : null,
74
- onClick: $event => ($options.open(card, $event))
75
- }, [
76
- (card.icon)
77
- ? (openBlock(), createBlock(_component_CnIcon, {
78
- key: 0,
79
- name: card.icon,
80
- size: 24,
81
- class: "cn-link-cards-page__icon",
82
- "aria-hidden": "true"
83
- }, null, 8 /* PROPS */, ["name"]))
84
- : createCommentVNode("v-if", true),
85
- createElementVNode("span", _hoisted_10, [
86
- createElementVNode("span", _hoisted_11, toDisplayString(card.label), 1 /* TEXT */),
87
- (card.description)
88
- ? (openBlock(), createElementBlock("span", {
89
- key: 0,
90
- id: card.descriptionId,
91
- class: "cn-link-cards-page__card-description"
92
- }, toDisplayString(card.description), 9 /* TEXT, PROPS */, _hoisted_12))
93
- : createCommentVNode("v-if", true)
94
- ])
95
- ], 8 /* PROPS */, _hoisted_9)
127
+ ($options.isListStyle)
128
+ ? (openBlock(), createElementBlock("ul", _hoisted_14, [
129
+ (openBlock(true), createElementBlock(Fragment, null, renderList(group.cards, (card) => {
130
+ return (openBlock(), createElementBlock("li", {
131
+ key: card.id
132
+ }, [
133
+ createElementVNode("a", {
134
+ class: "cn-link-cards-page__row",
135
+ "data-testid": "cn-link-card",
136
+ "data-card-id": card.id,
137
+ href: card.url,
138
+ target: card.external ? '_blank' : null,
139
+ rel: card.external ? 'noopener noreferrer' : null,
140
+ "aria-describedby": card.description ? card.descriptionId : null,
141
+ onClick: $event => ($options.open(card, $event))
142
+ }, [
143
+ (card.icon)
144
+ ? (openBlock(), createElementBlock("span", _hoisted_16, [
145
+ createVNode(_component_CnIcon, {
146
+ name: card.icon,
147
+ size: 20
148
+ }, null, 8 /* PROPS */, ["name"])
149
+ ]))
150
+ : createCommentVNode("v-if", true),
151
+ createElementVNode("span", _hoisted_17, [
152
+ createElementVNode("span", _hoisted_18, toDisplayString(card.label), 1 /* TEXT */),
153
+ (card.description)
154
+ ? (openBlock(), createElementBlock("span", {
155
+ key: 0,
156
+ id: card.descriptionId,
157
+ class: "cn-link-cards-page__row-description"
158
+ }, toDisplayString(card.description), 9 /* TEXT, PROPS */, _hoisted_19))
159
+ : createCommentVNode("v-if", true)
160
+ ]),
161
+ ($options.trailingOf(card) === 'external')
162
+ ? (openBlock(), createBlock(_component_OpenInNew, {
163
+ key: 1,
164
+ size: 18,
165
+ class: "cn-link-cards-page__row-trailing"
166
+ }))
167
+ : (openBlock(), createBlock(_component_ChevronRight, {
168
+ key: 2,
169
+ size: 20,
170
+ class: "cn-link-cards-page__row-trailing"
171
+ }))
172
+ ], 8 /* PROPS */, _hoisted_15)
173
+ ]))
174
+ }), 128 /* KEYED_FRAGMENT */))
175
+ ]))
176
+ : (openBlock(), createElementBlock("ul", _hoisted_20, [
177
+ (openBlock(true), createElementBlock(Fragment, null, renderList(group.cards, (card) => {
178
+ return (openBlock(), createElementBlock("li", {
179
+ key: card.id,
180
+ class: "cn-link-cards-page__cell"
181
+ }, [
182
+ createElementVNode("a", {
183
+ class: "cn-link-cards-page__card",
184
+ "data-testid": "cn-link-card",
185
+ "data-card-id": card.id,
186
+ href: card.url,
187
+ target: card.external ? '_blank' : null,
188
+ rel: card.external ? 'noopener noreferrer' : null,
189
+ "aria-describedby": card.description ? card.descriptionId : null,
190
+ onClick: $event => ($options.open(card, $event))
191
+ }, [
192
+ (card.icon)
193
+ ? (openBlock(), createBlock(_component_CnIcon, {
194
+ key: 0,
195
+ name: card.icon,
196
+ size: 24,
197
+ class: "cn-link-cards-page__icon",
198
+ "aria-hidden": "true"
199
+ }, null, 8 /* PROPS */, ["name"]))
200
+ : createCommentVNode("v-if", true),
201
+ createElementVNode("span", _hoisted_22, [
202
+ createElementVNode("span", _hoisted_23, toDisplayString(card.label), 1 /* TEXT */),
203
+ (card.description)
204
+ ? (openBlock(), createElementBlock("span", {
205
+ key: 0,
206
+ id: card.descriptionId,
207
+ class: "cn-link-cards-page__card-description"
208
+ }, toDisplayString(card.description), 9 /* TEXT, PROPS */, _hoisted_24))
209
+ : createCommentVNode("v-if", true)
210
+ ])
211
+ ], 8 /* PROPS */, _hoisted_21)
212
+ ]))
213
+ }), 128 /* KEYED_FRAGMENT */))
96
214
  ]))
97
- }), 128 /* KEYED_FRAGMENT */))
98
- ])
99
- ], 8 /* PROPS */, _hoisted_6))
215
+ ], 10 /* CLASS, PROPS */, _hoisted_12))
100
216
  }), 128 /* KEYED_FRAGMENT */))
101
217
  ]))
102
218
  }
@@ -1 +1 @@
1
- {"version":3,"file":"CnLinkCardsPage.vue3.js","sources":["../../../../src/components/CnLinkCardsPage/CnLinkCardsPage.vue?vue&type=template&id=4d8cf2f8&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-link-cards-page\" data-testid=\"cn-link-cards-page\">\n\t\t<div class=\"cn-link-cards-page__header\">\n\t\t\t<h2 v-if=\"resolvedTitle\" class=\"cn-link-cards-page__title\">\n\t\t\t\t{{ resolvedTitle }}\n\t\t\t</h2>\n\t\t\t<p v-if=\"resolvedDescription\" class=\"cn-link-cards-page__description\">\n\t\t\t\t{{ resolvedDescription }}\n\t\t\t</p>\n\t\t</div>\n\n\t\t<p\n\t\t\tv-if=\"groups.length === 0\"\n\t\t\tclass=\"cn-link-cards-page__empty\"\n\t\t\tdata-testid=\"cn-link-cards-empty\">\n\t\t\t{{ resolvedEmptyLabel }}\n\t\t</p>\n\n\t\t<section\n\t\t\tv-for=\"group in groups\"\n\t\t\t:key=\"group.key\"\n\t\t\tclass=\"cn-link-cards-page__group\"\n\t\t\t:aria-labelledby=\"group.label ? group.headingId : null\"\n\t\t\t:aria-label=\"group.label ? null : resolvedTitle || null\"\n\t\t\tdata-testid=\"cn-link-cards-group\"\n\t\t\t:data-group=\"group.key\">\n\t\t\t<h3\n\t\t\t\tv-if=\"group.label\"\n\t\t\t\t:id=\"group.headingId\"\n\t\t\t\tclass=\"cn-link-cards-page__caption\">\n\t\t\t\t{{ group.label }}\n\t\t\t</h3>\n\t\t\t<ul class=\"cn-link-cards-page__grid\">\n\t\t\t\t<li v-for=\"card in group.cards\" :key=\"card.id\" class=\"cn-link-cards-page__cell\">\n\t\t\t\t\t<!-- A real <a href>, so the card can be middle-clicked, copied\n\t\t\t\t\t and announced as a link. A plain click on an in-app card\n\t\t\t\t\t is routed, which keeps the page from reloading. -->\n\t\t\t\t\t<a\n\t\t\t\t\t\tclass=\"cn-link-cards-page__card\"\n\t\t\t\t\t\tdata-testid=\"cn-link-card\"\n\t\t\t\t\t\t:data-card-id=\"card.id\"\n\t\t\t\t\t\t:href=\"card.url\"\n\t\t\t\t\t\t:target=\"card.external ? '_blank' : null\"\n\t\t\t\t\t\t:rel=\"card.external ? 'noopener noreferrer' : null\"\n\t\t\t\t\t\t:aria-describedby=\"card.description ? card.descriptionId : null\"\n\t\t\t\t\t\t@click=\"open(card, $event)\">\n\t\t\t\t\t\t<CnIcon\n\t\t\t\t\t\t\tv-if=\"card.icon\"\n\t\t\t\t\t\t\t:name=\"card.icon\"\n\t\t\t\t\t\t\t:size=\"24\"\n\t\t\t\t\t\t\tclass=\"cn-link-cards-page__icon\"\n\t\t\t\t\t\t\taria-hidden=\"true\" />\n\t\t\t\t\t\t<span class=\"cn-link-cards-page__body\">\n\t\t\t\t\t\t\t<span class=\"cn-link-cards-page__label\">{{ card.label }}</span>\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tv-if=\"card.description\"\n\t\t\t\t\t\t\t\t:id=\"card.descriptionId\"\n\t\t\t\t\t\t\t\tclass=\"cn-link-cards-page__card-description\">\n\t\t\t\t\t\t\t\t{{ card.description }}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</a>\n\t\t\t\t</li>\n\t\t\t</ul>\n\t\t</section>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { isAppInstalled } from '../../utils/appInstalled.js'\nimport { followLinkClick, resolveHref } from '../../utils/linkNavigation.js'\nimport { passesContextPredicates } from '../../utils/visibleIfContext.js'\nimport { CnIcon } from '../CnIcon/index.js'\n\nlet linkCardsPageUid = 0\n\n/**\n * CnLinkCardsPage: the `type: \"links\"` page. Link cards, grouped under captions.\n *\n * One page that opens every page a short menu leaves out. A card has a\n * label, a one-line description, an optional icon and a target. Groups come\n * from `categories`, in the order they are declared; a card names its group\n * with `category`.\n *\n * ```json\n * { \"id\": \"Modules\", \"route\": \"/modules\", \"type\": \"links\", \"title\": \"Modules and more\",\n * \"config\": {\n * \"description\": \"Every page that is not in the daily menu.\",\n * \"categories\": { \"sales\": \"Sales\", \"marketing\": \"Marketing\" },\n * \"cards\": [\n * { \"id\": \"Leads\", \"label\": \"Leads\", \"description\": \"Every deal you are working on.\",\n * \"icon\": \"CashMultiple\", \"category\": \"sales\", \"route\": \"Leads\" }\n * ]\n * } }\n * ```\n *\n * Every string goes through the app's translate function. A card is hidden\n * by the same `visibleIf` and `permission` a menu entry takes, and a group\n * with no visible card is not drawn.\n */\nexport default {\n\tname: 'CnLinkCardsPage',\n\n\tcomponents: { CnIcon },\n\n\tinject: {\n\t\t/** The app's translate function, provided by CnAppRoot. Identity outside one. */\n\t\tcnTranslate: { default: () => (key) => key },\n\t\t/** The effective manifest, for `visibleIf` predicates against `runtime`. */\n\t\tcnManifest: { default: null },\n\t\t/** The permissions the user holds. Empty means the app did not say, and allows. */\n\t\tcnPermissions: { default: () => [] },\n\t},\n\n\tprops: {\n\t\t/** The whole manifest page, for a host that mounts this component itself. CnPageRenderer passes the flattened props below instead. */\n\t\tpage: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * The cards: `{ id, label, description?, icon?, category?, route?, params?, query?, href?, visibleIf?, permission? }`.\n\t\t * `route` is a route name, or a path when it starts with `/`.\n\t\t *\n\t\t * @type {Array<object>|null}\n\t\t */\n\t\tcards: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Group key to caption. Sets the order of the groups.\n\t\t *\n\t\t * @type {Record<string, string>|null}\n\t\t */\n\t\tcategories: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The heading. */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The lead paragraph under the heading. */\n\t\tdescription: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Text shown when no card is visible. */\n\t\temptyLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Translate function. Falls back to the injected `cnTranslate`.\n\t\t *\n\t\t * @type {((key: string) => string)|null}\n\t\t */\n\t\ttranslate: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tdata() {\n\t\tlinkCardsPageUid += 1\n\t\treturn {\n\t\t\tuid: linkCardsPageUid,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The config, from the flattened props or from `page.config`. Props win.\n\t\t *\n\t\t * @return {object} The config.\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-links-page-renders-grouped-link-cards\n\t\t */\n\t\tconfig() {\n\t\t\tconst fromPage = (this.page && this.page.config) || {}\n\t\t\tconst fromProps = {}\n\t\t\tfor (const key of ['cards', 'categories', 'description', 'title', 'emptyLabel']) {\n\t\t\t\tif (this[key] !== null && this[key] !== undefined) {\n\t\t\t\t\tfromProps[key] = this[key]\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn { ...fromPage, ...fromProps }\n\t\t},\n\n\t\t/**\n\t\t * The heading, translated.\n\t\t *\n\t\t * @return {string} The title, or '' when the page declares none.\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-links-page-renders-grouped-link-cards\n\t\t */\n\t\tresolvedTitle() {\n\t\t\treturn this.tr(this.config.title || (this.page && this.page.title) || '')\n\t\t},\n\n\t\t/**\n\t\t * The lead paragraph, translated.\n\t\t *\n\t\t * @return {string} The description.\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-links-page-renders-grouped-link-cards\n\t\t */\n\t\tresolvedDescription() {\n\t\t\treturn this.tr(this.config.description || '')\n\t\t},\n\n\t\t/**\n\t\t * The empty-state text.\n\t\t *\n\t\t * @return {string} The text.\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-links-page-renders-grouped-link-cards\n\t\t */\n\t\tresolvedEmptyLabel() {\n\t\t\treturn this.config.emptyLabel ? this.tr(this.config.emptyLabel) : t('nextcloud-vue', 'Nothing to open here.')\n\t\t},\n\n\t\t/**\n\t\t * The cards a reader may see and can open, with text translated and\n\t\t * the link resolved. A card that is gated away, or has no target that\n\t\t * resolves, is left out: a card that does nothing is worse than none.\n\t\t *\n\t\t * @return {Array<object>} The cards.\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-card-is-a-real-link\n\t\t */\n\t\tvisibleCards() {\n\t\t\tconst declared = Array.isArray(this.config.cards) ? this.config.cards : []\n\t\t\tconst runtime = this.cnManifest?.runtime ?? null\n\t\t\treturn declared\n\t\t\t\t.filter((card) => card && card.id && card.label)\n\t\t\t\t.filter((card) => this.passesPermission(card) && this.passesVisibleIf(card, runtime))\n\t\t\t\t.map((card) => {\n\t\t\t\t\tconst target = this.targetOf(card)\n\t\t\t\t\treturn {\n\t\t\t\t\t\tid: String(card.id),\n\t\t\t\t\t\tlabel: this.tr(card.label),\n\t\t\t\t\t\tdescription: this.tr(card.description || ''),\n\t\t\t\t\t\tdescriptionId: `cn-link-card-desc-${this.uid}-${String(card.id).replace(/[^A-Za-z0-9_-]/g, '-')}`,\n\t\t\t\t\t\ticon: card.icon || '',\n\t\t\t\t\t\tcategory: card.category || '',\n\t\t\t\t\t\texternal: target.external,\n\t\t\t\t\t\tlocation: target.location,\n\t\t\t\t\t\turl: target.url,\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.filter((card) => card.url !== '')\n\t\t},\n\n\t\t/**\n\t\t * The groups to draw: ungrouped cards first under no caption, then\n\t\t * one group per category in declaration order. A group without a\n\t\t * visible card is left out.\n\t\t *\n\t\t * @return {Array<{key: string, label: string, headingId: string, cards: Array<object>}>} The groups.\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-links-page-renders-grouped-link-cards\n\t\t */\n\t\tgroups() {\n\t\t\tconst declared = (this.config.categories && typeof this.config.categories === 'object') ? this.config.categories : {}\n\t\t\tconst keys = Object.keys(declared)\n\t\t\tconst loose = this.visibleCards.filter((card) => !keys.includes(card.category))\n\t\t\tconst groups = loose.length > 0 ? [{ key: '', label: '', headingId: '', cards: loose }] : []\n\t\t\tkeys.forEach((key, index) => {\n\t\t\t\tconst cards = this.visibleCards.filter((card) => card.category === key)\n\t\t\t\tif (cards.length > 0) {\n\t\t\t\t\tgroups.push({\n\t\t\t\t\t\tkey,\n\t\t\t\t\t\tlabel: this.tr(declared[key]),\n\t\t\t\t\t\theadingId: `cn-link-cards-group-${this.uid}-${index}`,\n\t\t\t\t\t\tcards,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t})\n\t\t\treturn groups\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Translate a manifest string through the app's translate function.\n\t\t *\n\t\t * @param {string} value The declared string.\n\t\t * @return {string} The translation, or the input unchanged.\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-links-page-renders-grouped-link-cards\n\t\t */\n\t\ttr(value) {\n\t\t\tif (typeof value !== 'string' || value === '') {\n\t\t\t\treturn value\n\t\t\t}\n\t\t\treturn (this.translate ?? this.cnTranslate)(value)\n\t\t},\n\n\t\t/**\n\t\t * Whether the user holds the card's `permission`. Same rule as the\n\t\t * menu: no permission declared, or no permissions known, allows.\n\t\t *\n\t\t * @param {object} card The card.\n\t\t * @return {boolean} True when the card may render.\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-cards-honour-menu-visibility-conditions\n\t\t */\n\t\tpassesPermission(card) {\n\t\t\tconst held = Array.isArray(this.cnPermissions) ? this.cnPermissions : []\n\t\t\tif (!card.permission || held.length === 0) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn held.includes(card.permission)\n\t\t},\n\n\t\t/**\n\t\t * Evaluate a card's `visibleIf` the way CnAppNav evaluates a menu\n\t\t * entry's: `appInstalled` first, then dot-path predicates against\n\t\t * the manifest's `runtime`.\n\t\t *\n\t\t * @param {object} card The card.\n\t\t * @param {object|null} runtime `manifest.runtime`, or null.\n\t\t * @return {boolean} True when the card may render.\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-cards-honour-menu-visibility-conditions\n\t\t */\n\t\tpassesVisibleIf(card, runtime) {\n\t\t\tconst condition = card.visibleIf\n\t\t\tif (!condition || typeof condition !== 'object') {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tif (condition.appInstalled && !isAppInstalled(condition.appInstalled)) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn passesContextPredicates(condition, runtime)\n\t\t},\n\n\t\t/**\n\t\t * Where a card goes. An `href` is an external URL. A `route` is a\n\t\t * route name, or a path when it starts with `/`, resolved by the\n\t\t * router so the link carries the app's base with or without\n\t\t * `/index.php`.\n\t\t *\n\t\t * @param {object} card The card.\n\t\t * @return {{external: boolean, location: object|null, url: string}} The target; `url` is '' when it cannot be resolved.\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-card-is-a-real-link\n\t\t */\n\t\ttargetOf(card) {\n\t\t\tif (typeof card.route === 'string' && card.route !== '') {\n\t\t\t\tconst location = card.route.startsWith('/') ? { path: card.route } : { name: card.route }\n\t\t\t\tif (card.params && typeof card.params === 'object') {\n\t\t\t\t\tlocation.params = card.params\n\t\t\t\t}\n\t\t\t\tif (card.query && typeof card.query === 'object') {\n\t\t\t\t\tlocation.query = card.query\n\t\t\t\t}\n\t\t\t\treturn { external: false, location, url: resolveHref(location, this.$router) }\n\t\t\t}\n\t\t\tif (typeof card.href === 'string' && card.href !== '') {\n\t\t\t\treturn { external: true, location: null, url: card.href }\n\t\t\t}\n\t\t\treturn { external: false, location: null, url: '' }\n\t\t},\n\n\t\t/**\n\t\t * Route a plain click on an in-app card. A modified or middle click,\n\t\t * and every click on an external card, is left to the browser.\n\t\t *\n\t\t * @param {object} card The resolved card.\n\t\t * @param {MouseEvent} event The click.\n\t\t * @return {void}\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-card-is-a-real-link\n\t\t */\n\t\topen(card, event) {\n\t\t\tif (card.external || !card.location) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfollowLinkClick(event, card.location, this.$router)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-link-cards-page {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: calc(6 * var(--default-grid-baseline));\n\tpadding: calc(5 * var(--default-grid-baseline));\n}\n\n/* Clear the Nextcloud navigation toggle button (44px wide, absolutely\n positioned at the left edge of .app-content) plus 12px breathing room, as\n CnPageHeader does. Only the header shifts; the cards keep the full width. */\n.cn-link-cards-page__header {\n\tpadding-inline-start: 56px;\n}\n\n.cn-link-cards-page__title {\n\tmargin: 0 0 var(--default-grid-baseline);\n}\n\n.cn-link-cards-page__description,\n.cn-link-cards-page__empty {\n\tcolor: var(--color-text-maxcontrast);\n\tmargin: 0;\n\tmax-width: 70ch;\n}\n\n.cn-link-cards-page__group {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: calc(3 * var(--default-grid-baseline));\n}\n\n.cn-link-cards-page__caption {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n\tfont-weight: 600;\n\tletter-spacing: 0.06em;\n\tmargin: 0;\n\ttext-transform: uppercase;\n}\n\n.cn-link-cards-page__grid {\n\tdisplay: grid;\n\tgap: calc(3 * var(--default-grid-baseline));\n\tgrid-template-columns: repeat(auto-fill, minmax(240px, 1fr));\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-link-cards-page__cell {\n\tdisplay: flex;\n}\n\n.cn-link-cards-page__card {\n\talign-items: flex-start;\n\tbackground: var(--color-main-background);\n\tborder: 1px solid var(--color-border-dark);\n\tborder-radius: var(--border-radius-large);\n\tcolor: var(--color-main-text);\n\tdisplay: flex;\n\tflex: 1;\n\tgap: calc(3 * var(--default-grid-baseline));\n\tmin-width: 0;\n\tpadding: calc(4 * var(--default-grid-baseline));\n\ttransition: background-color var(--animation-quick, 100ms) ease, border-color var(--animation-quick, 100ms) ease, box-shadow var(--animation-quick, 100ms) ease;\n}\n\n/* A card is a link, and it must not look like a line of prose. A theme may\n underline every link with `!important` (thematiq: `a { text-decoration:\n underline !important }`, specificity 0,0,1). Among `!important` rules the\n higher specificity wins, so this class rule has to carry it too; without it\n the label and the description both rendered underlined on a live Nextcloud. */\n.cn-link-cards-page__card,\n.cn-link-cards-page__card:hover,\n.cn-link-cards-page__card:focus,\n.cn-link-cards-page__card:active {\n\ttext-decoration: none !important;\n}\n\n/* Hover moves the whole card: border, background and a soft lift. */\n.cn-link-cards-page__card:hover {\n\tbackground: var(--color-background-hover);\n\tborder-color: var(--color-primary-element);\n\tbox-shadow: 0 2px 6px var(--color-box-shadow);\n}\n\n@media (prefers-reduced-motion: reduce) {\n\t.cn-link-cards-page__card {\n\t\ttransition: none;\n\t}\n}\n\n.cn-link-cards-page__card:focus-visible {\n\tborder-color: var(--color-primary-element);\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 2px;\n}\n\n.cn-link-cards-page__icon {\n\tcolor: var(--color-primary-element);\n\tflex: none;\n}\n\n.cn-link-cards-page__body {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: var(--default-grid-baseline);\n\tmin-width: 0;\n}\n\n/* The label is the card's title. Its colour is set here, on the span, because\n a theme may force its link colour on the `a` itself; an inherited link\n colour made the title read as a hyperlink. */\n.cn-link-cards-page__label {\n\tcolor: var(--color-main-text);\n\tfont-size: 1.05em;\n\tfont-weight: 700;\n\tline-height: 1.3;\n\toverflow-wrap: anywhere;\n}\n\n.cn-link-cards-page__card-description {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n\tline-height: 1.4;\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_toDisplayString","_renderList","_createElementVNode","_createBlock"],"mappings":"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"}
1
+ {"version":3,"file":"CnLinkCardsPage.vue3.js","sources":["../../../../src/components/CnLinkCardsPage/CnLinkCardsPage.vue?vue&type=template&id=1a155b6c&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-link-cards-page\" data-testid=\"cn-link-cards-page\">\n\t\t<div class=\"cn-link-cards-page__header\">\n\t\t\t<h2 v-if=\"resolvedTitle\" class=\"cn-link-cards-page__title\">\n\t\t\t\t{{ resolvedTitle }}\n\t\t\t</h2>\n\t\t\t<p v-if=\"resolvedDescription\" class=\"cn-link-cards-page__description\">\n\t\t\t\t{{ resolvedDescription }}\n\t\t\t</p>\n\t\t</div>\n\n\t\t<section\n\t\t\tv-if=\"visibleModules.length > 0\"\n\t\t\tclass=\"cn-link-cards-page__modules\"\n\t\t\t:aria-labelledby=\"`cn-link-cards-modules-${uid}`\"\n\t\t\tdata-testid=\"cn-link-cards-modules\">\n\t\t\t<div class=\"cn-link-cards-page__modules-head\">\n\t\t\t\t<h3 :id=\"`cn-link-cards-modules-${uid}`\" class=\"cn-link-cards-page__modules-title\">\n\t\t\t\t\t{{ resolvedModulesTitle }}\n\t\t\t\t</h3>\n\t\t\t\t<span class=\"cn-link-cards-page__modules-count\" data-testid=\"cn-link-cards-modules-count\">\n\t\t\t\t\t{{ modulesCount }}\n\t\t\t\t</span>\n\t\t\t</div>\n\t\t\t<div class=\"cn-link-cards-page__modules-grid\">\n\t\t\t\t<CnModuleCard\n\t\t\t\t\tv-for=\"module in visibleModules\"\n\t\t\t\t\t:key=\"module.id\"\n\t\t\t\t\t:module=\"module\"\n\t\t\t\t\t:headingId=\"`cn-link-cards-module-${uid}-${module.key}`\" />\n\t\t\t</div>\n\t\t\t<p v-if=\"resolvedFootnote\" class=\"cn-link-cards-page__modules-note\" data-testid=\"cn-link-cards-modules-note\">\n\t\t\t\t<InformationOutline :size=\"18\" class=\"cn-link-cards-page__modules-note-icon\" />\n\t\t\t\t<span>{{ resolvedFootnote }}</span>\n\t\t\t</p>\n\t\t</section>\n\n\t\t<p\n\t\t\tv-if=\"groups.length === 0 && visibleModules.length === 0\"\n\t\t\tclass=\"cn-link-cards-page__empty\"\n\t\t\tdata-testid=\"cn-link-cards-empty\">\n\t\t\t{{ resolvedEmptyLabel }}\n\t\t</p>\n\n\t\t<section\n\t\t\tv-for=\"group in groups\"\n\t\t\t:key=\"group.key\"\n\t\t\tclass=\"cn-link-cards-page__group\"\n\t\t\t:class=\"{ 'cn-link-cards-page__group--list': isListStyle }\"\n\t\t\t:aria-labelledby=\"group.label ? group.headingId : null\"\n\t\t\t:aria-label=\"group.label ? null : resolvedTitle || null\"\n\t\t\tdata-testid=\"cn-link-cards-group\"\n\t\t\t:data-group=\"group.key\">\n\t\t\t<h3\n\t\t\t\tv-if=\"group.label\"\n\t\t\t\t:id=\"group.headingId\"\n\t\t\t\tclass=\"cn-link-cards-page__caption\">\n\t\t\t\t{{ group.label }}\n\t\t\t</h3>\n\t\t\t<!-- groupStyle \"list\": the group is one card of rows, each an icon\n\t\t\t chip, the label over its description, and a trailing chevron\n\t\t\t (an external-link icon for a URL). -->\n\t\t\t<ul v-if=\"isListStyle\" class=\"cn-link-cards-page__list\">\n\t\t\t\t<li v-for=\"card in group.cards\" :key=\"card.id\">\n\t\t\t\t\t<a\n\t\t\t\t\t\tclass=\"cn-link-cards-page__row\"\n\t\t\t\t\t\tdata-testid=\"cn-link-card\"\n\t\t\t\t\t\t:data-card-id=\"card.id\"\n\t\t\t\t\t\t:href=\"card.url\"\n\t\t\t\t\t\t:target=\"card.external ? '_blank' : null\"\n\t\t\t\t\t\t:rel=\"card.external ? 'noopener noreferrer' : null\"\n\t\t\t\t\t\t:aria-describedby=\"card.description ? card.descriptionId : null\"\n\t\t\t\t\t\t@click=\"open(card, $event)\">\n\t\t\t\t\t\t<span v-if=\"card.icon\" class=\"cn-link-cards-page__row-icon\" aria-hidden=\"true\">\n\t\t\t\t\t\t\t<CnIcon :name=\"card.icon\" :size=\"20\" />\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<span class=\"cn-link-cards-page__row-body\">\n\t\t\t\t\t\t\t<span class=\"cn-link-cards-page__row-label\">{{ card.label }}</span>\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tv-if=\"card.description\"\n\t\t\t\t\t\t\t\t:id=\"card.descriptionId\"\n\t\t\t\t\t\t\t\tclass=\"cn-link-cards-page__row-description\">\n\t\t\t\t\t\t\t\t{{ card.description }}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<OpenInNew v-if=\"trailingOf(card) === 'external'\" :size=\"18\" class=\"cn-link-cards-page__row-trailing\" />\n\t\t\t\t\t\t<ChevronRight v-else :size=\"20\" class=\"cn-link-cards-page__row-trailing\" />\n\t\t\t\t\t</a>\n\t\t\t\t</li>\n\t\t\t</ul>\n\t\t\t<ul v-else class=\"cn-link-cards-page__grid\">\n\t\t\t\t<li v-for=\"card in group.cards\" :key=\"card.id\" class=\"cn-link-cards-page__cell\">\n\t\t\t\t\t<!-- A real <a href>, so the card can be middle-clicked, copied\n\t\t\t\t\t and announced as a link. A plain click on an in-app card\n\t\t\t\t\t is routed, which keeps the page from reloading. -->\n\t\t\t\t\t<a\n\t\t\t\t\t\tclass=\"cn-link-cards-page__card\"\n\t\t\t\t\t\tdata-testid=\"cn-link-card\"\n\t\t\t\t\t\t:data-card-id=\"card.id\"\n\t\t\t\t\t\t:href=\"card.url\"\n\t\t\t\t\t\t:target=\"card.external ? '_blank' : null\"\n\t\t\t\t\t\t:rel=\"card.external ? 'noopener noreferrer' : null\"\n\t\t\t\t\t\t:aria-describedby=\"card.description ? card.descriptionId : null\"\n\t\t\t\t\t\t@click=\"open(card, $event)\">\n\t\t\t\t\t\t<CnIcon\n\t\t\t\t\t\t\tv-if=\"card.icon\"\n\t\t\t\t\t\t\t:name=\"card.icon\"\n\t\t\t\t\t\t\t:size=\"24\"\n\t\t\t\t\t\t\tclass=\"cn-link-cards-page__icon\"\n\t\t\t\t\t\t\taria-hidden=\"true\" />\n\t\t\t\t\t\t<span class=\"cn-link-cards-page__body\">\n\t\t\t\t\t\t\t<span class=\"cn-link-cards-page__label\">{{ card.label }}</span>\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tv-if=\"card.description\"\n\t\t\t\t\t\t\t\t:id=\"card.descriptionId\"\n\t\t\t\t\t\t\t\tclass=\"cn-link-cards-page__card-description\">\n\t\t\t\t\t\t\t\t{{ card.description }}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</a>\n\t\t\t\t</li>\n\t\t\t</ul>\n\t\t</section>\n\t</div>\n</template>\n\n<script>\nimport { translatePlural as n, translate as t } from '@nextcloud/l10n'\nimport ChevronRight from 'vue-material-design-icons/ChevronRight.vue'\nimport InformationOutline from 'vue-material-design-icons/InformationOutline.vue'\nimport OpenInNew from 'vue-material-design-icons/OpenInNew.vue'\nimport CnModuleCard from '../CnModuleCard/CnModuleCard.vue'\nimport { isAppInstalled } from '../../utils/appInstalled.js'\nimport { followLinkClick, resolveHref } from '../../utils/linkNavigation.js'\nimport { passesContextPredicates } from '../../utils/visibleIfContext.js'\nimport { CnIcon } from '../CnIcon/index.js'\n\nlet linkCardsPageUid = 0\n\n/**\n * CnLinkCardsPage: the `type: \"links\"` page. Link cards, grouped under captions.\n *\n * One page that opens every page a short menu leaves out. A card has a\n * label, a one-line description, an optional icon and a target. Groups come\n * from `categories`, in the order they are declared; a card names its group\n * with `category`.\n *\n * ```json\n * { \"id\": \"Modules\", \"route\": \"/modules\", \"type\": \"links\", \"title\": \"Modules and more\",\n * \"config\": {\n * \"description\": \"Every page that is not in the daily menu.\",\n * \"categories\": { \"sales\": \"Sales\", \"marketing\": \"Marketing\" },\n * \"cards\": [\n * { \"id\": \"Leads\", \"label\": \"Leads\", \"description\": \"Every deal you are working on.\",\n * \"icon\": \"CashMultiple\", \"category\": \"sales\", \"route\": \"Leads\" }\n * ]\n * } }\n * ```\n *\n * Every string goes through the app's translate function. A card is hidden\n * by the same `visibleIf` and `permission` a menu entry takes, and a group\n * with no visible card is not drawn.\n */\nexport default {\n\tname: 'CnLinkCardsPage',\n\n\tcomponents: { ChevronRight, CnIcon, CnModuleCard, InformationOutline, OpenInNew },\n\n\tinject: {\n\t\t/** The app's translate function, provided by CnAppRoot. Identity outside one. */\n\t\tcnTranslate: { default: () => (key) => key },\n\t\t/** The effective manifest, for `visibleIf` predicates against `runtime`. */\n\t\tcnManifest: { default: null },\n\t\t/** The permissions the user holds. Empty means the app did not say, and allows. */\n\t\tcnPermissions: { default: () => [] },\n\t},\n\n\tprops: {\n\t\t/** The whole manifest page, for a host that mounts this component itself. CnPageRenderer passes the flattened props below instead. */\n\t\tpage: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * The cards: `{ id, label, description?, icon?, category?, route?, params?, query?, href?, visibleIf?, permission? }`.\n\t\t * `route` is a route name, or a path when it starts with `/`.\n\t\t *\n\t\t * @type {Array<object>|null}\n\t\t */\n\t\tcards: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Group key to caption. Sets the order of the groups.\n\t\t *\n\t\t * @type {Record<string, string>|null}\n\t\t */\n\t\tcategories: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The heading. */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The lead paragraph under the heading. */\n\t\tdescription: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The modules: `{ id, label, description?, icon?, enabled?, enabledIf?,\n\t\t * route?, pages: [{ label, route, highlight? }], settings?: { label?, prefix?,\n\t\t * route?, href?, permission?, visibleIf? }, permission?, visibleIf? }`.\n\t\t * Drawn as module cards above the link cards.\n\t\t *\n\t\t * @type {Array<object>|null}\n\t\t */\n\t\tmodules: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** `cards` (default): a grid of link cards per group. `list`: one card per group with a row per link. */\n\t\tgroupStyle: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Heading of the modules section. Defaults to \"Modules\". */\n\t\tmodulesTitle: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** A note under the module cards. */\n\t\tfootnote: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Text shown when no card is visible. */\n\t\temptyLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Translate function. Falls back to the injected `cnTranslate`.\n\t\t *\n\t\t * @type {((key: string) => string)|null}\n\t\t */\n\t\ttranslate: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\tdata() {\n\t\tlinkCardsPageUid += 1\n\t\treturn {\n\t\t\tuid: linkCardsPageUid,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The config, from the flattened props or from `page.config`. Props win.\n\t\t *\n\t\t * @return {object} The config.\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-links-page-renders-grouped-link-cards\n\t\t */\n\t\tconfig() {\n\t\t\tconst fromPage = (this.page && this.page.config) || {}\n\t\t\tconst fromProps = {}\n\t\t\tfor (const key of ['cards', 'categories', 'description', 'title', 'emptyLabel', 'modules', 'modulesTitle', 'footnote', 'groupStyle']) {\n\t\t\t\tif (this[key] !== null && this[key] !== undefined) {\n\t\t\t\t\tfromProps[key] = this[key]\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn { ...fromPage, ...fromProps }\n\t\t},\n\n\t\t/**\n\t\t * The heading, translated.\n\t\t *\n\t\t * @return {string} The title, or '' when the page declares none.\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-links-page-renders-grouped-link-cards\n\t\t */\n\t\tresolvedTitle() {\n\t\t\treturn this.tr(this.config.title || (this.page && this.page.title) || '')\n\t\t},\n\n\t\t/**\n\t\t * The lead paragraph, translated.\n\t\t *\n\t\t * @return {string} The description.\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-links-page-renders-grouped-link-cards\n\t\t */\n\t\tresolvedDescription() {\n\t\t\treturn this.tr(this.config.description || '')\n\t\t},\n\n\t\t/**\n\t\t * The empty-state text.\n\t\t *\n\t\t * @return {string} The text.\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-links-page-renders-grouped-link-cards\n\t\t */\n\t\tresolvedEmptyLabel() {\n\t\t\treturn this.config.emptyLabel ? this.tr(this.config.emptyLabel) : t('nextcloud-vue', 'Nothing to open here.')\n\t\t},\n\n\t\t/**\n\t\t * The cards a reader may see and can open, with text translated and\n\t\t * the link resolved. A card that is gated away, or has no target that\n\t\t * resolves, is left out: a card that does nothing is worse than none.\n\t\t *\n\t\t * @return {Array<object>} The cards.\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-card-is-a-real-link\n\t\t */\n\t\tvisibleCards() {\n\t\t\tconst declared = Array.isArray(this.config.cards) ? this.config.cards : []\n\t\t\tconst runtime = this.cnManifest?.runtime ?? null\n\t\t\treturn declared\n\t\t\t\t.filter((card) => card && card.id && card.label)\n\t\t\t\t.filter((card) => this.passesPermission(card) && this.passesVisibleIf(card, runtime))\n\t\t\t\t.map((card) => {\n\t\t\t\t\tconst target = this.targetOf(card)\n\t\t\t\t\treturn {\n\t\t\t\t\t\tid: String(card.id),\n\t\t\t\t\t\tlabel: this.tr(card.label),\n\t\t\t\t\t\tdescription: this.tr(card.description || ''),\n\t\t\t\t\t\tdescriptionId: `cn-link-card-desc-${this.uid}-${String(card.id).replace(/[^A-Za-z0-9_-]/g, '-')}`,\n\t\t\t\t\t\ticon: card.icon || '',\n\t\t\t\t\t\tcategory: card.category || '',\n\t\t\t\t\t\texternal: target.external,\n\t\t\t\t\t\tlocation: target.location,\n\t\t\t\t\t\turl: target.url,\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.filter((card) => card.url !== '')\n\t\t},\n\n\t\t/**\n\t\t * The modules a reader may see, with text translated and links\n\t\t * resolved. `enabled` is the declared boolean, or the result of\n\t\t * `enabledIf` (a visibleIf condition against `runtime`). A page that\n\t\t * resolves to nothing is dropped; the admin-settings link only shows when\n\t\t * its own `permission` and `visibleIf` pass.\n\t\t *\n\t\t * @return {Array<object>} The modules.\n\t\t * @spec openspec/changes/screens-links-module-cards/specs/link-cards-page/spec.md#requirement-a-links-page-can-show-modules\n\t\t */\n\t\tvisibleModules() {\n\t\t\tconst declared = Array.isArray(this.config.modules) ? this.config.modules : []\n\t\t\tconst runtime = this.cnManifest?.runtime ?? null\n\t\t\treturn declared\n\t\t\t\t.filter((module) => module && module.id && module.label)\n\t\t\t\t.filter((module) => this.passesPermission(module) && this.passesVisibleIf(module, runtime))\n\t\t\t\t.map((module) => {\n\t\t\t\t\tconst pages = (Array.isArray(module.pages) ? module.pages : [])\n\t\t\t\t\t\t.filter((page) => page && page.label)\n\t\t\t\t\t\t.map((page, index) => ({ key: String(index), label: this.tr(page.label), highlight: page.highlight === true, ...this.targetOf(page) }))\n\t\t\t\t\t\t.filter((page) => page.url !== '')\n\t\t\t\t\tconst own = this.targetOf(module)\n\t\t\t\t\tconst open = own.url !== '' ? own : (pages[0] || null)\n\t\t\t\t\tconst settingsDeclared = module.settings && typeof module.settings === 'object' ? module.settings : null\n\t\t\t\t\tconst settingsTarget = settingsDeclared && this.passesPermission(settingsDeclared) && this.passesVisibleIf(settingsDeclared, runtime)\n\t\t\t\t\t\t? this.targetOf(settingsDeclared)\n\t\t\t\t\t\t: null\n\t\t\t\t\treturn {\n\t\t\t\t\t\tid: String(module.id),\n\t\t\t\t\t\tkey: String(module.id).replace(/[^A-Za-z0-9_-]/g, '-'),\n\t\t\t\t\t\tlabel: this.tr(module.label),\n\t\t\t\t\t\tdescription: this.tr(module.description || ''),\n\t\t\t\t\t\ticon: module.icon || '',\n\t\t\t\t\t\tenabled: this.moduleEnabled(module, runtime),\n\t\t\t\t\t\tpages,\n\t\t\t\t\t\topen,\n\t\t\t\t\t\tsettings: settingsTarget && settingsTarget.url !== ''\n\t\t\t\t\t\t\t? { ...settingsTarget, label: this.tr(settingsDeclared.label || ''), prefix: this.tr(settingsDeclared.prefix || '') }\n\t\t\t\t\t\t\t: null,\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Whether the groups are drawn as list cards (`config.groupStyle: \"list\"`).\n\t\t *\n\t\t * @return {boolean} True for list cards.\n\t\t * @spec openspec/changes/screens-links-module-cards/specs/link-cards-page/spec.md#requirement-a-group-can-be-a-list-card\n\t\t */\n\t\tisListStyle() {\n\t\t\treturn this.config.groupStyle === 'list'\n\t\t},\n\n\t\t/**\n\t\t * The modules heading, translated.\n\t\t *\n\t\t * @return {string} The heading.\n\t\t * @spec openspec/changes/screens-links-module-cards/specs/link-cards-page/spec.md#requirement-a-links-page-can-show-modules\n\t\t */\n\t\tresolvedModulesTitle() {\n\t\t\treturn this.config.modulesTitle ? this.tr(this.config.modulesTitle) : t('nextcloud-vue', 'Modules')\n\t\t},\n\n\t\t/**\n\t\t * \"3 of 6 are in the menu\".\n\t\t *\n\t\t * @return {string} The count.\n\t\t * @spec openspec/changes/screens-links-module-cards/specs/link-cards-page/spec.md#requirement-a-links-page-can-show-modules\n\t\t */\n\t\tmodulesCount() {\n\t\t\tconst on = this.visibleModules.filter((module) => module.enabled).length\n\t\t\treturn n('nextcloud-vue', '{on} of %n is in the menu', '{on} of %n are in the menu', this.visibleModules.length, { on })\n\t\t},\n\n\t\t/**\n\t\t * The note under the module cards, translated.\n\t\t *\n\t\t * @return {string} The note, or ''.\n\t\t * @spec openspec/changes/screens-links-module-cards/specs/link-cards-page/spec.md#requirement-a-links-page-can-show-modules\n\t\t */\n\t\tresolvedFootnote() {\n\t\t\treturn this.tr(this.config.footnote || '')\n\t\t},\n\n\t\t/**\n\t\t * The groups to draw: ungrouped cards first under no caption, then\n\t\t * one group per category in declaration order. A group without a\n\t\t * visible card is left out.\n\t\t *\n\t\t * @return {Array<{key: string, label: string, headingId: string, cards: Array<object>}>} The groups.\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-links-page-renders-grouped-link-cards\n\t\t */\n\t\tgroups() {\n\t\t\tconst declared = (this.config.categories && typeof this.config.categories === 'object') ? this.config.categories : {}\n\t\t\tconst keys = Object.keys(declared)\n\t\t\tconst loose = this.visibleCards.filter((card) => !keys.includes(card.category))\n\t\t\tconst groups = loose.length > 0 ? [{ key: '', label: '', headingId: '', cards: loose }] : []\n\t\t\tkeys.forEach((key, index) => {\n\t\t\t\tconst cards = this.visibleCards.filter((card) => card.category === key)\n\t\t\t\tif (cards.length > 0) {\n\t\t\t\t\tgroups.push({\n\t\t\t\t\t\tkey,\n\t\t\t\t\t\tlabel: this.tr(declared[key]),\n\t\t\t\t\t\theadingId: `cn-link-cards-group-${this.uid}-${index}`,\n\t\t\t\t\t\tcards,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t})\n\t\t\treturn groups\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Whether a module is in the menu: its `enabled` boolean, else its\n\t\t * `enabledIf` condition evaluated as a `visibleIf`, else false.\n\t\t *\n\t\t * @param {object} module The declared module.\n\t\t * @param {object|null} runtime `manifest.runtime`, or null.\n\t\t * @return {boolean} True when the module is in the menu.\n\t\t * @spec openspec/changes/screens-links-module-cards/specs/link-cards-page/spec.md#requirement-a-links-page-can-show-modules\n\t\t */\n\t\t/**\n\t\t * The icon at the end of a list row: an external-link icon for a URL,\n\t\t * a chevron for a page in the app. A resolved `trailing` wins.\n\t\t *\n\t\t * @param {object} card The resolved card.\n\t\t * @return {string} `external` or `chevron`.\n\t\t * @spec openspec/changes/screens-links-module-cards/specs/link-cards-page/spec.md#requirement-a-group-can-be-a-list-card\n\t\t */\n\t\ttrailingOf(card) {\n\t\t\tif (card.trailing === 'external' || card.trailing === 'chevron') {\n\t\t\t\treturn card.trailing\n\t\t\t}\n\t\t\treturn card.external ? 'external' : 'chevron'\n\t\t},\n\n\t\tmoduleEnabled(module, runtime) {\n\t\t\tif (typeof module.enabled === 'boolean') {\n\t\t\t\treturn module.enabled\n\t\t\t}\n\t\t\tif (module.enabledIf && typeof module.enabledIf === 'object') {\n\t\t\t\treturn this.passesVisibleIf({ visibleIf: module.enabledIf }, runtime)\n\t\t\t}\n\t\t\treturn false\n\t\t},\n\n\t\t/**\n\t\t * Translate a manifest string through the app's translate function.\n\t\t *\n\t\t * @param {string} value The declared string.\n\t\t * @return {string} The translation, or the input unchanged.\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-links-page-renders-grouped-link-cards\n\t\t */\n\t\ttr(value) {\n\t\t\tif (typeof value !== 'string' || value === '') {\n\t\t\t\treturn value\n\t\t\t}\n\t\t\treturn (this.translate ?? this.cnTranslate)(value)\n\t\t},\n\n\t\t/**\n\t\t * Whether the user holds the card's `permission`. Same rule as the\n\t\t * menu: no permission declared, or no permissions known, allows.\n\t\t *\n\t\t * @param {object} card The card.\n\t\t * @return {boolean} True when the card may render.\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-cards-honour-menu-visibility-conditions\n\t\t */\n\t\tpassesPermission(card) {\n\t\t\tconst held = Array.isArray(this.cnPermissions) ? this.cnPermissions : []\n\t\t\tif (!card.permission || held.length === 0) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn held.includes(card.permission)\n\t\t},\n\n\t\t/**\n\t\t * Evaluate a card's `visibleIf` the way CnAppNav evaluates a menu\n\t\t * entry's: `appInstalled` first, then dot-path predicates against\n\t\t * the manifest's `runtime`.\n\t\t *\n\t\t * @param {object} card The card.\n\t\t * @param {object|null} runtime `manifest.runtime`, or null.\n\t\t * @return {boolean} True when the card may render.\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-cards-honour-menu-visibility-conditions\n\t\t */\n\t\tpassesVisibleIf(card, runtime) {\n\t\t\tconst condition = card.visibleIf\n\t\t\tif (!condition || typeof condition !== 'object') {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tif (condition.appInstalled && !isAppInstalled(condition.appInstalled)) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn passesContextPredicates(condition, runtime)\n\t\t},\n\n\t\t/**\n\t\t * Where a card goes. An `href` is an external URL. A `route` is a\n\t\t * route name, or a path when it starts with `/`, resolved by the\n\t\t * router so the link carries the app's base with or without\n\t\t * `/index.php`.\n\t\t *\n\t\t * @param {object} card The card.\n\t\t * @return {{external: boolean, location: object|null, url: string}} The target; `url` is '' when it cannot be resolved.\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-card-is-a-real-link\n\t\t */\n\t\ttargetOf(card) {\n\t\t\tif (typeof card.route === 'string' && card.route !== '') {\n\t\t\t\tconst location = card.route.startsWith('/') ? { path: card.route } : { name: card.route }\n\t\t\t\tif (card.params && typeof card.params === 'object') {\n\t\t\t\t\tlocation.params = card.params\n\t\t\t\t}\n\t\t\t\tif (card.query && typeof card.query === 'object') {\n\t\t\t\t\tlocation.query = card.query\n\t\t\t\t}\n\t\t\t\treturn { external: false, location, url: resolveHref(location, this.$router) }\n\t\t\t}\n\t\t\tif (typeof card.href === 'string' && card.href !== '') {\n\t\t\t\treturn { external: true, location: null, url: card.href }\n\t\t\t}\n\t\t\treturn { external: false, location: null, url: '' }\n\t\t},\n\n\t\t/**\n\t\t * Route a plain click on an in-app card. A modified or middle click,\n\t\t * and every click on an external card, is left to the browser.\n\t\t *\n\t\t * @param {object} card The resolved card.\n\t\t * @param {MouseEvent} event The click.\n\t\t * @return {void}\n\t\t * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-a-card-is-a-real-link\n\t\t */\n\t\topen(card, event) {\n\t\t\tif (card.external || !card.location) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfollowLinkClick(event, card.location, this.$router)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-link-cards-page {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: calc(6 * var(--default-grid-baseline));\n\tpadding: calc(5 * var(--default-grid-baseline));\n}\n\n/* Clear the Nextcloud navigation toggle button (44px wide, absolutely\n positioned at the left edge of .app-content) plus 12px breathing room, as\n CnPageHeader does. Only the header shifts; the cards keep the full width. */\n.cn-link-cards-page__header {\n\tpadding-inline-start: 56px;\n}\n\n.cn-link-cards-page__title {\n\tmargin: 0 0 var(--default-grid-baseline);\n}\n\n.cn-link-cards-page__description,\n.cn-link-cards-page__empty {\n\tcolor: var(--color-text-maxcontrast);\n\tmargin: 0;\n\tmax-width: 70ch;\n}\n\n.cn-link-cards-page__group {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: calc(3 * var(--default-grid-baseline));\n}\n\n.cn-link-cards-page__caption {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n\tfont-weight: 600;\n\tletter-spacing: 0.06em;\n\tmargin: 0;\n\ttext-transform: uppercase;\n}\n\n.cn-link-cards-page__grid {\n\tdisplay: grid;\n\tgap: calc(3 * var(--default-grid-baseline));\n\tgrid-template-columns: repeat(auto-fill, minmax(240px, 1fr));\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-link-cards-page__cell {\n\tdisplay: flex;\n}\n\n.cn-link-cards-page__card {\n\talign-items: flex-start;\n\tbackground: var(--color-main-background);\n\tborder: 1px solid var(--color-border-dark);\n\tborder-radius: var(--border-radius-large);\n\tcolor: var(--color-main-text);\n\tdisplay: flex;\n\tflex: 1;\n\tgap: calc(3 * var(--default-grid-baseline));\n\tmin-width: 0;\n\tpadding: calc(4 * var(--default-grid-baseline));\n\ttransition: background-color var(--animation-quick, 100ms) ease, border-color var(--animation-quick, 100ms) ease, box-shadow var(--animation-quick, 100ms) ease;\n}\n\n/* A card is a link, and it must not look like a line of prose. A theme may\n underline every link with `!important` (thematiq: `a { text-decoration:\n underline !important }`, specificity 0,0,1). Among `!important` rules the\n higher specificity wins, so this class rule has to carry it too; without it\n the label and the description both rendered underlined on a live Nextcloud. */\n.cn-link-cards-page__card,\n.cn-link-cards-page__card:hover,\n.cn-link-cards-page__card:focus,\n.cn-link-cards-page__card:active {\n\ttext-decoration: none !important;\n}\n\n/* Hover moves the whole card: border, background and a soft lift. */\n.cn-link-cards-page__card:hover {\n\tbackground: var(--color-background-hover);\n\tborder-color: var(--color-primary-element);\n\tbox-shadow: 0 2px 6px var(--color-box-shadow);\n}\n\n@media (prefers-reduced-motion: reduce) {\n\t.cn-link-cards-page__card {\n\t\ttransition: none;\n\t}\n}\n\n.cn-link-cards-page__card:focus-visible {\n\tborder-color: var(--color-primary-element);\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 2px;\n}\n\n.cn-link-cards-page__icon {\n\tcolor: var(--color-primary-element);\n\tflex: none;\n}\n\n.cn-link-cards-page__body {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: var(--default-grid-baseline);\n\tmin-width: 0;\n}\n\n/* The label is the card's title. Its colour is set here, on the span, because\n a theme may force its link colour on the `a` itself; an inherited link\n colour made the title read as a hyperlink. */\n.cn-link-cards-page__label {\n\tcolor: var(--color-main-text);\n\tfont-size: 1.05em;\n\tfont-weight: 700;\n\tline-height: 1.3;\n\toverflow-wrap: anywhere;\n}\n\n.cn-link-cards-page__card-description {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n\tline-height: 1.4;\n}\n\n.cn-link-cards-page__group--list {\n\tpadding: 20px 22px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-container-large, 12px);\n\tbackground: var(--color-main-background);\n\tgap: 6px;\n}\n\n.cn-link-cards-page__group--list .cn-link-cards-page__caption {\n\tmargin: 0 0 8px;\n\tcolor: var(--color-main-text);\n\tfont-size: 17px;\n\tfont-weight: 700;\n\tletter-spacing: normal;\n\ttext-transform: none;\n}\n\n.cn-link-cards-page__list {\n\tmargin: 0;\n\tpadding: 0;\n\tlist-style: none;\n}\n\n.cn-link-cards-page__row {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 12px;\n\tpadding: 10px 0;\n\tborder-top: 1px solid var(--color-background-dark);\n\tcolor: var(--color-main-text);\n}\n\n.cn-link-cards-page__row,\n.cn-link-cards-page__row:hover,\n.cn-link-cards-page__row:focus,\n.cn-link-cards-page__row:active {\n\ttext-decoration: none !important;\n}\n\n.cn-link-cards-page__row:focus-visible {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 2px;\n}\n\n.cn-link-cards-page__row-icon {\n\tdisplay: flex;\n\tflex: none;\n\talign-items: center;\n\tjustify-content: center;\n\twidth: 34px;\n\theight: 34px;\n\tborder-radius: var(--border-radius-element, 8px);\n\tbackground: var(--color-background-dark);\n}\n\n.cn-link-cards-page__row-body {\n\tdisplay: flex;\n\tflex: 1 1 auto;\n\tflex-direction: column;\n\tgap: 2px;\n\tmin-width: 0;\n}\n\n.cn-link-cards-page__row-label {\n\tfont-size: 15px;\n\tfont-weight: 600;\n}\n\n.cn-link-cards-page__row-description {\n\tfont-size: 13px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-link-cards-page__row-trailing {\n\tflex: none;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-link-cards-page__modules {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 14px;\n}\n\n.cn-link-cards-page__modules-head {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tjustify-content: space-between;\n\talign-items: baseline;\n\tgap: 8px;\n}\n\n.cn-link-cards-page__modules-title {\n\tmargin: 0;\n\tfont-size: 20px;\n\tfont-weight: 700;\n}\n\n.cn-link-cards-page__modules-count {\n\tfont-size: 14px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-link-cards-page__modules-grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fill, minmax(260px, 1fr));\n\tgap: 16px;\n}\n\n.cn-link-cards-page__modules-note {\n\tdisplay: flex;\n\talign-items: flex-start;\n\tgap: 8px;\n\tmargin: 0;\n\tfont-size: 14px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-link-cards-page__modules-note-icon {\n\tflex: none;\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_toDisplayString","_createElementVNode","_Fragment","_renderList","_createBlock","_createVNode","_normalizeClass"],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import script from './CnModuleCard.vue2.js';
2
+ import { render } from './CnModuleCard.vue3.js';
3
+
4
+ script.render = render;
5
+ script.__scopeId = "data-v-7cd47118";
6
+ const __sfc_main = script;
7
+
8
+ export { __sfc_main as default };
9
+ //# sourceMappingURL=CnModuleCard.vue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CnModuleCard.vue.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;"}
@@ -0,0 +1,83 @@
1
+ import { translatePlural, translate } from '@nextcloud/l10n';
2
+ import ChevronRight from 'vue-material-design-icons/ChevronRight.vue';
3
+ import __sfc_main$1 from '../CnStatusBadge/CnStatusBadge.vue.js';
4
+ import { followLinkClick } from '../../utils/linkNavigation.js';
5
+ import __sfc_main from '../CnIcon/CnIcon.vue.js';
6
+
7
+ /** A module with more pages than this names its highlights instead of every page. */
8
+ const LISTED_PAGES = 4;
9
+
10
+ /**
11
+ * CnModuleCard - one module on a `type: "links"` page (board PqModules): an
12
+ * icon, the title and a state badge (in the menu, or off), a description,
13
+ * the module's pages as a line and as links, an Open button, and for a
14
+ * module that is off a link to where an admin puts it in the menu.
15
+ *
16
+ * Rendered by CnLinkCardsPage for each entry of `config.modules`; the page
17
+ * resolves the links and the text before handing the module over.
18
+ *
19
+ * @spec openspec/changes/screens-links-module-cards/specs/link-cards-page/spec.md#requirement-a-links-page-can-show-modules
20
+ */
21
+ var script = {
22
+ name: 'CnModuleCard',
23
+
24
+ components: { ChevronRight, CnIcon: __sfc_main, CnStatusBadge: __sfc_main$1 },
25
+
26
+ props: {
27
+ /**
28
+ * The resolved module: `{ id, label, description, icon, enabled,
29
+ * pages: [{ key, label, url, location, highlight }], open: { url,
30
+ * location }|null, settings: { url, location, external, label, prefix }|null }`.
31
+ */
32
+ module: {
33
+ type: Object,
34
+ required: true,
35
+ },
36
+
37
+ /** Id of the card's heading, unique on the page. */
38
+ headingId: {
39
+ type: String,
40
+ required: true,
41
+ },
42
+ },
43
+
44
+ computed: {
45
+ /**
46
+ * "4 pages: Overview, Leads, Prospects, Pipeline", or for a larger
47
+ * module "15 pages, including Campaigns, Blasts and Mailing lists"
48
+ * from the pages marked `highlight` (else the first three).
49
+ *
50
+ * @return {string} The line.
51
+ */
52
+ pagesLine() {
53
+ const pages = this.module.pages;
54
+ if (pages.length <= LISTED_PAGES) {
55
+ return translatePlural('nextcloud-vue', '%n page: {pages}', '%n pages: {pages}', pages.length, { pages: pages.map((page) => page.label).join(', ') })
56
+ }
57
+ const marked = pages.filter((page) => page.highlight);
58
+ const named = (marked.length > 0 ? marked : pages).slice(0, 3).map((page) => page.label);
59
+ const last = named.pop();
60
+ const list = named.length > 0 ? translate('nextcloud-vue', '{list} and {last}', { list: named.join(', '), last }) : last;
61
+ return translatePlural('nextcloud-vue', '%n page, including {pages}', '%n pages, including {pages}', pages.length, { pages: list })
62
+ },
63
+ },
64
+
65
+ methods: {
66
+ t: translate,
67
+
68
+ /**
69
+ * Route a plain click on an in-app link, so the page does not reload.
70
+ *
71
+ * @param {{location: (object|null)}} target The resolved link.
72
+ * @param {MouseEvent} event The click.
73
+ */
74
+ follow(target, event) {
75
+ if (target && target.location) {
76
+ followLinkClick(event, target.location, this.$router);
77
+ }
78
+ },
79
+ },
80
+ };
81
+
82
+ export { script as default };
83
+ //# sourceMappingURL=CnModuleCard.vue2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CnModuleCard.vue2.js","sources":["../../../../src/components/CnModuleCard/CnModuleCard.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<article\n\t\tclass=\"cn-module-card\"\n\t\t:aria-labelledby=\"headingId\"\n\t\tdata-testid=\"cn-module-card\"\n\t\t:data-module-id=\"module.id\">\n\t\t<div class=\"cn-module-card__head\">\n\t\t\t<span\n\t\t\t\tv-if=\"module.icon\"\n\t\t\t\tclass=\"cn-module-card__icon\"\n\t\t\t\t:class=\"{ 'cn-module-card__icon--off': !module.enabled }\"\n\t\t\t\taria-hidden=\"true\">\n\t\t\t\t<CnIcon :name=\"module.icon\" :size=\"22\" />\n\t\t\t</span>\n\t\t\t<h3 :id=\"headingId\" class=\"cn-module-card__title\">\n\t\t\t\t{{ module.label }}\n\t\t\t</h3>\n\t\t\t<CnStatusBadge\n\t\t\t\tclass=\"cn-module-card__state\"\n\t\t\t\t:label=\"module.enabled ? t('nextcloud-vue', 'In the menu') : t('nextcloud-vue', 'Off')\"\n\t\t\t\t:variant=\"module.enabled ? 'success' : 'default'\"\n\t\t\t\tdata-testid=\"cn-module-card-state\" />\n\t\t</div>\n\t\t<p v-if=\"module.description\" class=\"cn-module-card__description\">\n\t\t\t{{ module.description }}\n\t\t</p>\n\t\t<template v-if=\"module.pages.length > 0\">\n\t\t\t<span class=\"cn-module-card__pages-line\" data-testid=\"cn-module-card-pages-line\">{{ pagesLine }}</span>\n\t\t\t<ul class=\"cn-module-card__pages\" :aria-label=\"t('nextcloud-vue', 'Pages')\">\n\t\t\t\t<li v-for=\"page in module.pages\" :key=\"page.key\">\n\t\t\t\t\t<a\n\t\t\t\t\t\tclass=\"cn-module-card__page\"\n\t\t\t\t\t\t:href=\"page.url\"\n\t\t\t\t\t\t@click=\"follow(page, $event)\">{{ page.label }}</a>\n\t\t\t\t</li>\n\t\t\t</ul>\n\t\t</template>\n\t\t<div v-if=\"module.open || (!module.enabled && module.settings)\" class=\"cn-module-card__footer\">\n\t\t\t<a\n\t\t\t\tv-if=\"module.open\"\n\t\t\t\tclass=\"cn-module-card__open\"\n\t\t\t\t:href=\"module.open.url\"\n\t\t\t\t:aria-label=\"t('nextcloud-vue', 'Open {module}', { module: module.label })\"\n\t\t\t\tdata-testid=\"cn-module-card-open\"\n\t\t\t\t@click=\"follow(module.open, $event)\">\n\t\t\t\t{{ t('nextcloud-vue', 'Open') }}\n\t\t\t\t<ChevronRight :size=\"18\" />\n\t\t\t</a>\n\t\t\t<span v-if=\"!module.enabled && module.settings\" class=\"cn-module-card__settings\" data-testid=\"cn-module-card-settings\">\n\t\t\t\t{{ module.settings.prefix || t('nextcloud-vue', 'Put it in the menu:') }}\n\t\t\t\t<a\n\t\t\t\t\t:href=\"module.settings.url\"\n\t\t\t\t\t:target=\"module.settings.external ? '_blank' : null\"\n\t\t\t\t\t:rel=\"module.settings.external ? 'noopener noreferrer' : null\"\n\t\t\t\t\t@click=\"follow(module.settings, $event)\">{{ module.settings.label || t('nextcloud-vue', 'Admin settings') }}</a>\n\t\t\t</span>\n\t\t</div>\n\t</article>\n</template>\n\n<script>\nimport { translatePlural as n, translate as t } from '@nextcloud/l10n'\nimport ChevronRight from 'vue-material-design-icons/ChevronRight.vue'\nimport CnStatusBadge from '../CnStatusBadge/CnStatusBadge.vue'\nimport { followLinkClick } from '../../utils/linkNavigation.js'\nimport { CnIcon } from '../CnIcon/index.js'\n\n/** A module with more pages than this names its highlights instead of every page. */\nconst LISTED_PAGES = 4\n\n/**\n * CnModuleCard - one module on a `type: \"links\"` page (board PqModules): an\n * icon, the title and a state badge (in the menu, or off), a description,\n * the module's pages as a line and as links, an Open button, and for a\n * module that is off a link to where an admin puts it in the menu.\n *\n * Rendered by CnLinkCardsPage for each entry of `config.modules`; the page\n * resolves the links and the text before handing the module over.\n *\n * @spec openspec/changes/screens-links-module-cards/specs/link-cards-page/spec.md#requirement-a-links-page-can-show-modules\n */\nexport default {\n\tname: 'CnModuleCard',\n\n\tcomponents: { ChevronRight, CnIcon, CnStatusBadge },\n\n\tprops: {\n\t\t/**\n\t\t * The resolved module: `{ id, label, description, icon, enabled,\n\t\t * pages: [{ key, label, url, location, highlight }], open: { url,\n\t\t * location }|null, settings: { url, location, external, label, prefix }|null }`.\n\t\t */\n\t\tmodule: {\n\t\t\ttype: Object,\n\t\t\trequired: true,\n\t\t},\n\n\t\t/** Id of the card's heading, unique on the page. */\n\t\theadingId: {\n\t\t\ttype: String,\n\t\t\trequired: true,\n\t\t},\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * \"4 pages: Overview, Leads, Prospects, Pipeline\", or for a larger\n\t\t * module \"15 pages, including Campaigns, Blasts and Mailing lists\"\n\t\t * from the pages marked `highlight` (else the first three).\n\t\t *\n\t\t * @return {string} The line.\n\t\t */\n\t\tpagesLine() {\n\t\t\tconst pages = this.module.pages\n\t\t\tif (pages.length <= LISTED_PAGES) {\n\t\t\t\treturn n('nextcloud-vue', '%n page: {pages}', '%n pages: {pages}', pages.length, { pages: pages.map((page) => page.label).join(', ') })\n\t\t\t}\n\t\t\tconst marked = pages.filter((page) => page.highlight)\n\t\t\tconst named = (marked.length > 0 ? marked : pages).slice(0, 3).map((page) => page.label)\n\t\t\tconst last = named.pop()\n\t\t\tconst list = named.length > 0 ? t('nextcloud-vue', '{list} and {last}', { list: named.join(', '), last }) : last\n\t\t\treturn n('nextcloud-vue', '%n page, including {pages}', '%n pages, including {pages}', pages.length, { pages: list })\n\t\t},\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * Route a plain click on an in-app link, so the page does not reload.\n\t\t *\n\t\t * @param {{location: (object|null)}} target The resolved link.\n\t\t * @param {MouseEvent} event The click.\n\t\t */\n\t\tfollow(target, event) {\n\t\t\tif (target && target.location) {\n\t\t\t\tfollowLinkClick(event, target.location, this.$router)\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-module-card {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n\tpadding: 20px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-container-large, 12px);\n\tbackground: var(--color-main-background);\n}\n\n.cn-module-card__head {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 12px;\n}\n\n.cn-module-card__icon {\n\tdisplay: flex;\n\tflex: none;\n\talign-items: center;\n\tjustify-content: center;\n\twidth: 42px;\n\theight: 42px;\n\tborder-radius: var(--border-radius-large, 10px);\n\tbackground: var(--color-primary-element-light);\n\tcolor: var(--color-primary-element-light-text, var(--color-primary-element));\n}\n\n.cn-module-card__title {\n\tflex: 1 1 auto;\n\tmargin: 0;\n\tfont-size: 17px;\n\tfont-weight: 700;\n}\n\n.cn-module-card__state {\n\tflex: none;\n}\n\n.cn-module-card__description {\n\tmargin: 0;\n\tfont-size: 15px;\n\tline-height: 1.5;\n}\n\n.cn-module-card__pages-line {\n\tfont-size: 13px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-module-card__pages {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 6px 12px;\n\tmargin: 0;\n\tpadding: 0;\n\tlist-style: none;\n\tfont-size: 14px;\n}\n\n.cn-module-card__page {\n\tfont-weight: 600;\n\tcolor: var(--color-primary-element);\n}\n\n.cn-module-card__footer {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 8px 14px;\n\tmargin-top: auto;\n\tpadding-top: 4px;\n}\n\n.cn-module-card__open {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tbox-sizing: border-box;\n\theight: 36px;\n\tpadding: 0 14px;\n\tfont-size: 14px;\n\tfont-weight: 600;\n\ttext-decoration: none;\n}\n\n.cn-module-card__open {\n\tborder: 1px solid var(--color-border-dark);\n\tborder-radius: var(--border-radius-element, 8px);\n\tbackground: var(--color-main-background);\n\tcolor: var(--color-main-text);\n}\n\n.cn-module-card__icon--off {\n\tbackground: var(--color-background-dark);\n\tcolor: var(--color-main-text);\n}\n\n.cn-module-card__settings {\n\tfont-size: 13px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-module-card__open:focus-visible,\n.cn-module-card__settings a:focus-visible,\n.cn-module-card__page:focus-visible {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 2px;\n}\n</style>\n"],"names":["CnIcon","CnStatusBadge","n","t"],"mappings":";;;;;;AAuEA;AACA,MAAM,eAAe,EAAA;;AAErB;AACC;;;;AAIA;AACA;;AAEA;;;AAGD,aAAe;AACd,CAAA,IAAI,EAAE,cAAc;;CAEpB,UAAU,EAAE,EAAE,YAAY,UAAEA,UAAM,iBAAEC,cAAe;;AAEnD,CAAA,KAAK,EAAE;AACN;;;AAGC;;AAED,EAAA,MAAM,EAAE;GACP,IAAI,EAAE,MAAM;GACZ,QAAQ,EAAE,IAAI;GACd;;AAED;EACA,SAAS,EAAE;GACV,IAAI,EAAE,MAAM;GACZ,QAAQ,EAAE,IAAI;GACd;EACD;;AAED,CAAA,QAAQ,EAAE;AACT;;;;AAIC;;;EAGD,SAAS,GAAG;GACX,MAAM,KAAM,GAAE,IAAI,CAAC,MAAM,CAAC,MAAI;AAC9B,GAAA,IAAI,KAAK,CAAC,MAAK,IAAK,YAAY,EAAE;IACjC,OAAOC,eAAC,CAAC,eAAe,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,KAAK,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAA,EAAG,CAAA;AACvI,IAAA;GACA,MAAM,MAAK,GAAI,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,SAAS,EAAA;AACpD,GAAA,MAAM,KAAI,GAAI,CAAC,MAAM,CAAC,SAAS,CAAA,GAAI,MAAK,GAAI,KAAK,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,KAAK,EAAA;AACvF,GAAA,MAAM,IAAK,GAAE,KAAK,CAAC,GAAG,GAAC;AACvB,GAAA,MAAM,IAAK,GAAE,KAAK,CAAC,MAAK,GAAI,CAAE,GAAEC,SAAC,CAAC,eAAe,EAAE,mBAAmB,EAAE,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,IAAK,EAAC,IAAI,KAAG;GAC/G,OAAOD,eAAC,CAAC,eAAe,EAAE,4BAA4B,EAAE,6BAA6B,EAAE,KAAK,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,IAAG,EAAG,CAAA;GACpH;EACD;;AAED,CAAA,OAAO,EAAE;KACRC,SAAC;;AAED;;AAEC;;AAEA;;AAED,EAAA,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE;GACrB,IAAI,MAAK,IAAK,MAAM,CAAC,QAAQ,EAAE;IAC9B,eAAe,CAAC,KAAK,EAAE,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,OAAO,EAAA;AACrD,IAAA;GACA;EACD;AACF;;;;"}