@conduction/nextcloud-vue 2.92.0 → 2.94.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 (199) hide show
  1. package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue2.js +1 -1
  2. package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue2.js.map +1 -1
  3. package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue3.js +2 -1
  4. package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue3.js.map +1 -1
  5. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue.js +1 -1
  6. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js +28 -8
  7. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
  8. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +37 -2
  9. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
  10. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +57 -4
  11. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  12. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +44 -17
  13. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  14. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue.js +1 -1
  15. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js +44 -2
  16. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
  17. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js +140 -59
  18. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
  19. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue.js +1 -1
  20. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js +139 -3
  21. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js.map +1 -1
  22. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue3.js +60 -12
  23. package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue3.js.map +1 -1
  24. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  25. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +111 -42
  26. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  27. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +117 -73
  28. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  29. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +34 -39
  30. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  31. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +7 -7
  32. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  33. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +5 -3
  34. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  35. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  36. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  37. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +6 -4
  38. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  39. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  40. package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue2.js +46 -0
  41. package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue2.js.map +1 -1
  42. package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue3.js +6 -2
  43. package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue3.js.map +1 -1
  44. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue.js +1 -1
  45. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js +82 -9
  46. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js.map +1 -1
  47. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js +104 -90
  48. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js.map +1 -1
  49. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue.js +9 -0
  50. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue.js.map +1 -0
  51. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js +355 -0
  52. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js.map +1 -0
  53. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js +462 -0
  54. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js.map +1 -0
  55. package/dist/esm/components/CnFormDialog/CnFormDialog.vue.js +1 -1
  56. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +21 -0
  57. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  58. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +402 -391
  59. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  60. package/dist/esm/components/CnIcon/semanticIcons.js +2 -2
  61. package/dist/esm/components/CnIcon/semanticIcons.js.map +1 -1
  62. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +141 -7
  63. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  64. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +5 -2
  65. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  66. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue.js +1 -1
  67. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js +22 -1
  68. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js.map +1 -1
  69. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js +20 -14
  70. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js.map +1 -1
  71. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue.js +1 -1
  72. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue2.js +2 -0
  73. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue2.js.map +1 -1
  74. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue3.js +15 -5
  75. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue3.js.map +1 -1
  76. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue.js +8 -0
  77. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue.js.map +1 -0
  78. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue2.js +114 -0
  79. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue2.js.map +1 -0
  80. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue3.js +45 -0
  81. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue3.js.map +1 -0
  82. package/dist/esm/components/CnPageHeader/CnPageHeader.vue2.js +38 -2
  83. package/dist/esm/components/CnPageHeader/CnPageHeader.vue2.js.map +1 -1
  84. package/dist/esm/components/CnPageHeader/CnPageHeader.vue3.js +12 -2
  85. package/dist/esm/components/CnPageHeader/CnPageHeader.vue3.js.map +1 -1
  86. package/dist/esm/components/CnPagination/CnPagination.vue2.js.map +1 -1
  87. package/dist/esm/components/CnPagination/CnPagination.vue3.js +56 -51
  88. package/dist/esm/components/CnPagination/CnPagination.vue3.js.map +1 -1
  89. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  90. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +3 -3
  91. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  92. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  93. package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue.js +1 -1
  94. package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue2.js +8 -30
  95. package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue2.js.map +1 -1
  96. package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue3.js.map +1 -1
  97. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue.js +1 -1
  98. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js +24 -0
  99. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js.map +1 -1
  100. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue3.js +3 -1
  101. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue3.js.map +1 -1
  102. package/dist/esm/components/CnTabs/CnTabs.vue.js +1 -1
  103. package/dist/esm/components/CnTabs/CnTabs.vue2.js +39 -2
  104. package/dist/esm/components/CnTabs/CnTabs.vue2.js.map +1 -1
  105. package/dist/esm/components/CnTabs/CnTabs.vue3.js.map +1 -1
  106. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  107. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +85 -8
  108. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  109. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +37 -9
  110. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  111. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue.js +9 -0
  112. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue.js.map +1 -0
  113. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js +164 -0
  114. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js.map +1 -0
  115. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js +93 -0
  116. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js.map +1 -0
  117. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue.js +1 -1
  118. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue2.js.map +1 -1
  119. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js +4 -3
  120. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js.map +1 -1
  121. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue.js +1 -1
  122. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js +32 -0
  123. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js.map +1 -1
  124. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js +31 -20
  125. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js.map +1 -1
  126. package/dist/esm/composables/indexSources.js +9 -0
  127. package/dist/esm/composables/indexSources.js.map +1 -1
  128. package/dist/esm/l10n/en.json.js +24 -1
  129. package/dist/esm/l10n/en.json.js.map +1 -1
  130. package/dist/esm/l10n/nl.json.js +24 -1
  131. package/dist/esm/l10n/nl.json.js.map +1 -1
  132. package/dist/esm/library-style.js +1 -1
  133. package/dist/esm/nextcloud-vue.css +1190 -410
  134. package/dist/esm/utils/dueText.js +49 -8
  135. package/dist/esm/utils/dueText.js.map +1 -1
  136. package/dist/esm/utils/featureCatalog.js +135 -0
  137. package/dist/esm/utils/featureCatalog.js.map +1 -0
  138. package/dist/esm/utils/fillTemplate.js +74 -3
  139. package/dist/esm/utils/fillTemplate.js.map +1 -1
  140. package/dist/esm/utils/headerMeta.js +62 -12
  141. package/dist/esm/utils/headerMeta.js.map +1 -1
  142. package/dist/esm/utils/roadmapIssues.js +86 -0
  143. package/dist/esm/utils/roadmapIssues.js.map +1 -0
  144. package/dist/esm/utils/userDisplayName.js +47 -0
  145. package/dist/esm/utils/userDisplayName.js.map +1 -0
  146. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  147. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  148. package/dist/nextcloud-vue.cjs.js +16229 -13423
  149. package/dist/nextcloud-vue.cjs.js.map +2 -2
  150. package/dist/nextcloud-vue.css +1190 -410
  151. package/l10n/en.json +24 -1
  152. package/l10n/nl.json +24 -1
  153. package/package.json +1 -1
  154. package/src/components/CnActionsMenu/CnActionsMenu.vue +3 -1
  155. package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +39 -1
  156. package/src/components/CnAppRoot/CnAppRoot.md +2 -0
  157. package/src/components/CnAppRoot/CnAppRoot.vue +66 -5
  158. package/src/components/CnChoiceCards/CnChoiceCards.md +27 -0
  159. package/src/components/CnChoiceCards/CnChoiceCards.vue +168 -3
  160. package/src/components/CnCountdownWidget/CnCountdownWidget.vue +222 -6
  161. package/src/components/CnDashboardPage/CnDashboardPage.vue +141 -8
  162. package/src/components/CnDataTable/CnDataTable.md +1 -0
  163. package/src/components/CnDataTable/CnDataTable.vue +41 -46
  164. package/src/components/CnDetailPage/CnDetailPage.vue +5 -3
  165. package/src/components/CnFactsWidget/CnFactsWidget.vue +6 -4
  166. package/src/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue +51 -1
  167. package/src/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue +98 -13
  168. package/src/components/CnFeaturesBoard/CnFeaturesBoard.vue +1053 -0
  169. package/src/components/CnFormDialog/CnFormDialog.vue +606 -562
  170. package/src/components/CnIndexPage/CnIndexPage.vue +144 -7
  171. package/src/components/CnNcWidgetWidget/CnNcWidgetWidget.vue +23 -2
  172. package/src/components/CnNotificationPreferences/CnNotificationPreferences.vue +5 -0
  173. package/src/components/CnPageHeader/CnHeaderTrail.vue +140 -0
  174. package/src/components/CnPageHeader/CnPageHeader.md +11 -0
  175. package/src/components/CnPageHeader/CnPageHeader.vue +42 -0
  176. package/src/components/CnPagination/CnPagination.vue +10 -2
  177. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +3 -3
  178. package/src/components/CnRoadmapTab/CnRoadmapTab.vue +6 -28
  179. package/src/components/CnSettingsPage/CnSettingsPage.vue +26 -0
  180. package/src/components/CnTabs/CnTabs.vue +39 -2
  181. package/src/components/CnTabsWidget/CnTabsWidget.vue +106 -13
  182. package/src/components/CnUserSettingsDialog/CnUserSettingsDialog.vue +305 -0
  183. package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +4 -3
  184. package/src/components/CnWidgetWrapper/CnWidgetWrapper.md +8 -0
  185. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +43 -2
  186. package/src/composables/indexSources.js +9 -0
  187. package/src/css/dialog.css +61 -0
  188. package/src/css/look-board-detail.css +8 -0
  189. package/src/css/look-board-index.css +27 -1
  190. package/src/css/look-board.css +60 -0
  191. package/src/css/page-header.css +16 -0
  192. package/src/schemas/app-manifest-v2.schema.json +30 -17
  193. package/src/utils/dueText.js +49 -8
  194. package/src/utils/featureCatalog.js +132 -0
  195. package/src/utils/fillTemplate.js +73 -2
  196. package/src/utils/headerMeta.js +62 -12
  197. package/src/utils/roadmapIssues.js +82 -0
  198. package/src/utils/userDisplayName.js +54 -0
  199. package/src/utils/validateManifestV2.compiled.js +3 -3
@@ -0,0 +1,1053 @@
1
+ <!--
2
+ - SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ - SPDX-License-Identifier: EUPL-1.2
4
+ -->
5
+ <template>
6
+ <div class="cn-features-board" :class="`cn-features-board--${layout}`" data-testid="cn-features-board">
7
+ <header class="cn-features-board__header">
8
+ <div class="cn-features-board__title-row">
9
+ <h1 class="cn-features-board__title">
10
+ {{ resolvedTitle }}
11
+ </h1>
12
+ <div class="cn-features-board__header-actions">
13
+ <NcButton
14
+ v-if="documentationButton && documentationUrl"
15
+ variant="secondary"
16
+ :href="documentationUrl"
17
+ target="_blank"
18
+ rel="noopener noreferrer"
19
+ data-testid="cn-features-board-docs">
20
+ <template #icon>
21
+ <BookOpenVariant :size="20" />
22
+ </template>
23
+ {{ t('nextcloud-vue', 'Documentation') }}
24
+ </NcButton>
25
+ <CnBuildiqEditButton />
26
+ <NcButton
27
+ v-if="suggestUrl && suggestButton"
28
+ variant="primary"
29
+ :href="suggestUrl"
30
+ target="_blank"
31
+ rel="noopener noreferrer"
32
+ data-testid="cn-features-board-suggest">
33
+ <template #icon>
34
+ <Plus :size="20" />
35
+ </template>
36
+ {{ t('nextcloud-vue', 'Suggest feature') }}
37
+ </NcButton>
38
+ <NcActions
39
+ v-if="!tabs"
40
+ :menuName="t('nextcloud-vue', 'Actions')"
41
+ :forceName="true"
42
+ data-testid="cn-features-board-actions">
43
+ <template #icon>
44
+ <DotsHorizontal :size="20" />
45
+ </template>
46
+ <NcActionLink
47
+ v-for="action in menuLinks"
48
+ :key="action.key"
49
+ :href="action.href"
50
+ target="_blank">
51
+ {{ action.label }}
52
+ </NcActionLink>
53
+ <NcActionButton @click="onSupport">
54
+ {{ t('nextcloud-vue', 'Support this app') }}
55
+ </NcActionButton>
56
+ </NcActions>
57
+ </div>
58
+ </div>
59
+ <p v-if="resolvedSubtitle" class="cn-features-board__subtitle" data-testid="cn-features-board-subtitle">
60
+ {{ resolvedSubtitle }}
61
+ </p>
62
+ </header>
63
+
64
+ <div v-if="tabs" class="cn-features-board__tab-row">
65
+ <div
66
+ class="cn-features-board__tabs"
67
+ role="tablist"
68
+ :aria-label="resolvedTitle"
69
+ @keydown="onTabKeydown">
70
+ <button
71
+ v-for="tab in tabList"
72
+ :id="`${uid}-tab-${tab.key}`"
73
+ :key="tab.key"
74
+ ref="tabButtons"
75
+ type="button"
76
+ role="tab"
77
+ class="cn-features-board__tab"
78
+ :class="{ 'cn-features-board__tab--active': activeTab === tab.key }"
79
+ :aria-selected="activeTab === tab.key ? 'true' : 'false'"
80
+ :aria-controls="`${uid}-panel`"
81
+ :tabindex="activeTab === tab.key ? 0 : -1"
82
+ :data-testid="`cn-features-board-tab-${tab.key}`"
83
+ @click="activeTab = tab.key">
84
+ {{ tab.label }}
85
+ </button>
86
+ </div>
87
+ <NcActions
88
+ :menuName="t('nextcloud-vue', 'Actions')"
89
+ :forceName="true"
90
+ class="cn-features-board__tab-actions"
91
+ data-testid="cn-features-board-actions">
92
+ <template #icon>
93
+ <DotsHorizontal :size="20" />
94
+ </template>
95
+ <NcActionLink
96
+ v-for="action in menuLinks"
97
+ :key="action.key"
98
+ :href="action.href"
99
+ target="_blank">
100
+ {{ action.label }}
101
+ </NcActionLink>
102
+ <NcActionButton @click="onSupport">
103
+ {{ t('nextcloud-vue', 'Support this app') }}
104
+ </NcActionButton>
105
+ </NcActions>
106
+ </div>
107
+
108
+ <div
109
+ :id="`${uid}-panel`"
110
+ class="cn-features-board__panel"
111
+ :class="{ 'cn-features-board__panel--tabbed': tabs }"
112
+ :role="tabs ? 'tabpanel' : null"
113
+ :aria-labelledby="tabs ? `${uid}-tab-${activeTab}` : null">
114
+ <CnRoadmapTab v-if="tabs && activeTab === 'roadmap'" :repo="repo" />
115
+ <template v-else>
116
+ <div
117
+ class="cn-features-board__filter"
118
+ :class="`cn-features-board__filter--${filter}`"
119
+ role="group"
120
+ :aria-label="t('nextcloud-vue', 'Status')"
121
+ data-testid="cn-features-board-filter">
122
+ <button
123
+ v-for="option in filterOptions"
124
+ :key="option.key"
125
+ type="button"
126
+ class="cn-features-board__filter-option"
127
+ :class="{ 'cn-features-board__filter-option--active': status === option.key }"
128
+ :aria-pressed="status === option.key ? 'true' : 'false'"
129
+ :data-status="option.key"
130
+ @click="status = option.key">
131
+ {{ option.label }}<span
132
+ v-if="filter === 'chips'"
133
+ class="cn-features-board__filter-count"
134
+ data-testid="cn-features-board-filter-count">{{ ' ' + option.count }}</span>
135
+ </button>
136
+ </div>
137
+
138
+ <div class="cn-features-board__body">
139
+ <div class="cn-features-board__groups" :class="`cn-features-board__groups--${layout}`">
140
+ <p v-if="shownGroups.length === 0" class="cn-features-board__empty" data-testid="cn-features-board-empty">
141
+ {{ t('nextcloud-vue', 'No features with this status') }}
142
+ </p>
143
+ <section
144
+ v-for="group in shownGroups"
145
+ :key="group.key"
146
+ class="cn-features-board__group"
147
+ :aria-labelledby="group.label ? `${uid}-group-${group.key}` : null"
148
+ :aria-label="group.label ? null : resolvedTitle"
149
+ data-testid="cn-features-board-group"
150
+ :data-group="group.key">
151
+ <div v-if="group.label" class="cn-features-board__group-head">
152
+ <h2 :id="`${uid}-group-${group.key}`" class="cn-features-board__group-title">
153
+ {{ group.label }}
154
+ </h2>
155
+ <span
156
+ v-if="layout === 'cards'"
157
+ class="cn-features-board__group-caption"
158
+ data-testid="cn-features-board-group-caption">{{ groupCaption(group) }}</span>
159
+ </div>
160
+ <div v-if="layout === 'cards'" class="cn-features-board__cards">
161
+ <article
162
+ v-for="feature in group.shown"
163
+ :key="feature.slug"
164
+ class="cn-features-board__card"
165
+ data-testid="cn-features-board-feature">
166
+ <div class="cn-features-board__card-head">
167
+ <h3 class="cn-features-board__card-title">
168
+ {{ feature.title }}
169
+ </h3>
170
+ <CnStatusBadge
171
+ :label="statusText(feature)"
172
+ :variant="variantOf(feature.status)"
173
+ data-testid="cn-features-board-status" />
174
+ </div>
175
+ <p v-if="feature.summary" class="cn-features-board__card-summary">
176
+ {{ feature.summary }}
177
+ </p>
178
+ <div v-if="readMoreHref(feature)" class="cn-features-board__card-footer">
179
+ <a
180
+ class="cn-features-board__read-more"
181
+ :href="readMoreHref(feature)"
182
+ target="_blank"
183
+ rel="noopener noreferrer"
184
+ :aria-label="t('nextcloud-vue', 'Learn more about {title}', { title: feature.title })">
185
+ {{ t('nextcloud-vue', 'Learn more') }}
186
+ </a>
187
+ </div>
188
+ </article>
189
+ </div>
190
+ <ul v-else class="cn-features-board__rows">
191
+ <li
192
+ v-for="feature in group.shown"
193
+ :key="feature.slug"
194
+ class="cn-features-board__row"
195
+ data-testid="cn-features-board-feature">
196
+ <span class="cn-features-board__row-text">
197
+ <span class="cn-features-board__row-title">{{ feature.title }}</span>
198
+ <span v-if="feature.summary" class="cn-features-board__row-summary">{{ feature.summary }}</span>
199
+ </span>
200
+ <CnStatusBadge
201
+ :label="statusText(feature)"
202
+ :variant="variantOf(feature.status)"
203
+ data-testid="cn-features-board-status" />
204
+ </li>
205
+ </ul>
206
+ </section>
207
+ </div>
208
+
209
+ <aside
210
+ v-if="next > 0 && repo"
211
+ class="cn-features-board__next"
212
+ :aria-label="t('nextcloud-vue', 'Next on the roadmap')"
213
+ data-testid="cn-features-board-next">
214
+ <section class="cn-features-board__next-card">
215
+ <div class="cn-features-board__next-head">
216
+ <RoadVariant :size="20" />
217
+ <h2 class="cn-features-board__next-title">
218
+ {{ t('nextcloud-vue', 'Coming next') }}
219
+ </h2>
220
+ </div>
221
+ <p class="cn-features-board__next-lead">
222
+ {{ t('nextcloud-vue', 'The most requested items from the issue tracker.') }}
223
+ </p>
224
+ <ol v-if="nextIssues.length > 0" class="cn-features-board__next-list">
225
+ <li
226
+ v-for="issue in nextIssues"
227
+ :key="issue.number"
228
+ class="cn-features-board__next-item"
229
+ data-testid="cn-features-board-next-item">
230
+ <a
231
+ class="cn-features-board__next-link"
232
+ :href="safeHref(issue.html_url)"
233
+ target="_blank"
234
+ rel="noopener noreferrer">{{ issue.title }}</a>
235
+ <div class="cn-features-board__next-meta">
236
+ <CnStatusBadge
237
+ v-if="issue.status"
238
+ :label="statusLabel(issue.status)"
239
+ :variant="variantOf(issue.status)" />
240
+ <span v-if="issue.opened" class="cn-features-board__next-opened">
241
+ <CalendarBlankOutline :size="14" />{{ t('nextcloud-vue', 'Opened {date}', { date: issue.opened }) }}
242
+ </span>
243
+ <span>#{{ issue.number }}</span>
244
+ </div>
245
+ </li>
246
+ </ol>
247
+ <p v-else-if="!issuesLoading" class="cn-features-board__next-lead" data-testid="cn-features-board-next-empty">
248
+ {{ t('nextcloud-vue', 'No roadmap items yet') }}
249
+ </p>
250
+ <a
251
+ v-if="tabs"
252
+ href="#"
253
+ class="cn-features-board__next-more"
254
+ data-testid="cn-features-board-whole-roadmap"
255
+ @click.prevent="activeTab = 'roadmap'">
256
+ {{ t('nextcloud-vue', 'See the whole roadmap') }}<ChevronRight :size="16" />
257
+ </a>
258
+ <a
259
+ v-if="documentationUrl"
260
+ :href="documentationUrl"
261
+ target="_blank"
262
+ rel="noopener noreferrer"
263
+ class="cn-features-board__next-more">
264
+ {{ t('nextcloud-vue', 'Documentation') }}<ChevronRight :size="16" />
265
+ </a>
266
+ </section>
267
+ </aside>
268
+ </div>
269
+
270
+ <NcNoteCard v-if="notice"
271
+ type="info"
272
+ class="cn-features-board__notice"
273
+ data-testid="cn-features-board-notice">
274
+ {{ tr(notice) }}
275
+ </NcNoteCard>
276
+ </template>
277
+ </div>
278
+ </div>
279
+ </template>
280
+
281
+ <script>
282
+ import { getLanguage, translatePlural as n, translate as t } from '@nextcloud/l10n'
283
+ import { NcActionButton, NcActionLink, NcActions, NcButton, NcNoteCard } from '@nextcloud/vue'
284
+ import BookOpenVariant from 'vue-material-design-icons/BookOpenVariant.vue'
285
+ import CalendarBlankOutline from 'vue-material-design-icons/CalendarBlankOutline.vue'
286
+ import ChevronRight from 'vue-material-design-icons/ChevronRight.vue'
287
+ import DotsHorizontal from 'vue-material-design-icons/DotsHorizontal.vue'
288
+ import Plus from 'vue-material-design-icons/Plus.vue'
289
+ import RoadVariant from 'vue-material-design-icons/RoadVariant.vue'
290
+ import CnBuildiqEditButton from '../CnBuildiqEditButton/CnBuildiqEditButton.vue'
291
+ import CnRoadmapTab from '../CnRoadmapTab/CnRoadmapTab.vue'
292
+ import CnStatusBadge from '../CnStatusBadge/CnStatusBadge.vue'
293
+ import { formatBoardDate } from '../../utils/boardDate.js'
294
+ import { countByStatus, FEATURE_STATUS_VARIANTS, FEATURE_STATUSES, groupFeatures } from '../../utils/featureCatalog.js'
295
+ import { fetchRoadmapIssues, issueStatus } from '../../utils/roadmapIssues.js'
296
+ import { safeHref } from '../../utils/safeHref.js'
297
+
298
+ let featuresBoardUid = 0
299
+
300
+ /**
301
+ * Read the reader's language, which @nextcloud/l10n throws on outside a
302
+ * Nextcloud page.
303
+ *
304
+ * @return {string} The language tag, or ''.
305
+ */
306
+ function readerLanguage() {
307
+ try {
308
+ return getLanguage() || ''
309
+ } catch {
310
+ return ''
311
+ }
312
+ }
313
+
314
+ /**
315
+ * CnFeaturesBoard - the Features and roadmap page in the board look.
316
+ *
317
+ * A title with a subtitle, optional folder tabs (Features, Roadmap), a status
318
+ * filter (count chips or a segmented control), the features grouped by
319
+ * category (as two-up cards or as rows inside one card per group), an
320
+ * optional "Next" card with the most requested forge issues, and an optional
321
+ * notice under the groups. The links the default look puts in its right-hand
322
+ * sidebar (suggest, buildiq, AI skills, support) sit in an Actions menu.
323
+ *
324
+ * Rendered by CnFeaturesAndRoadmapView when the page is in the board look.
325
+ *
326
+ * @spec openspec/changes/screens-roadmap-board-look/specs/features-roadmap-board-look/spec.md
327
+ *
328
+ * @event support Emitted when the reader picks "Support this app" in the Actions menu.
329
+ */
330
+ export default {
331
+ name: 'CnFeaturesBoard',
332
+
333
+ components: {
334
+ BookOpenVariant,
335
+ CalendarBlankOutline,
336
+ ChevronRight,
337
+ CnBuildiqEditButton,
338
+ CnRoadmapTab,
339
+ CnStatusBadge,
340
+ DotsHorizontal,
341
+ NcActionButton,
342
+ NcActionLink,
343
+ NcActions,
344
+ NcButton,
345
+ NcNoteCard,
346
+ Plus,
347
+ RoadVariant,
348
+ },
349
+
350
+ inject: {
351
+ /** The app's translate function, provided by CnAppRoot. Identity outside one. */
352
+ cnTranslate: { default: () => (key) => key },
353
+ },
354
+
355
+ props: {
356
+ /** The features: `{ slug, title, summary, status, category, expected, docsUrl, title_<lang>, summary_<lang> }`. */
357
+ features: { type: Array, default: () => [] },
358
+ /** Category key to caption; sets the order of the groups. */
359
+ categories: { type: Object, default: () => ({}) },
360
+ /** `cards`: a section per category with two-up feature cards. `rows`: one card per category with a row per feature. */
361
+ layout: {
362
+ type: String,
363
+ default: 'cards',
364
+ validator: (value) => ['cards', 'rows'].includes(value),
365
+ },
366
+
367
+ /** Show the Features and Roadmap folder tabs. */
368
+ tabs: { type: Boolean, default: true },
369
+ /** `chips`: pill buttons with a count. `segmented`: a segmented control without counts. */
370
+ filter: {
371
+ type: String,
372
+ default: 'chips',
373
+ validator: (value) => ['chips', 'segmented'].includes(value),
374
+ },
375
+
376
+ /** Overrides of the status labels, by key (`all`, `available`, `beta`, `in_progress`, `planned`). */
377
+ statusLabels: { type: Object, default: () => ({}) },
378
+ /** How many issues the Next card lists; 0 hides the card. */
379
+ next: { type: Number, default: 3 },
380
+ /** Forge label to issue status (`in_progress`, `planned`) for the badges in the Next card. */
381
+ issueLabels: { type: Object, default: () => ({}) },
382
+ /** `<owner>/<repo>` on the forge, for the Roadmap tab and the Next card. */
383
+ repo: { type: String, default: '' },
384
+ /** Page title. */
385
+ title: { type: String, default: '' },
386
+ /** Subtitle template; `{count}` is the number of features and `{appVersion}` the app version. */
387
+ subtitle: { type: String, default: '' },
388
+ /** The app version, for `{appVersion}` in the subtitle. */
389
+ appVersion: { type: String, default: '' },
390
+ /** An info notice under the groups. */
391
+ notice: { type: String, default: '' },
392
+ /** The app's documentation site: the Next card's link and the target of "Learn more". */
393
+ documentationUrl: { type: String, default: '' },
394
+ /** Show a Documentation button in the header. */
395
+ documentationButton: { type: Boolean, default: false },
396
+ /** The feature-request form; the header's primary button. */
397
+ suggestUrl: { type: String, default: '' },
398
+ /** Show the feature-request form as the header's primary button; without it, it is an entry in the Actions menu. */
399
+ suggestButton: { type: Boolean, default: true },
400
+ /** Target of the OpenBuilt entry in the Actions menu. */
401
+ openbuiltUrl: { type: String, default: '' },
402
+ /** Target of the AI skills entry in the Actions menu. */
403
+ llmSkillsUrl: { type: String, default: '' },
404
+ },
405
+
406
+ emits: ['support'],
407
+
408
+ data() {
409
+ featuresBoardUid += 1
410
+ return {
411
+ uid: `cn-features-board-${featuresBoardUid}`,
412
+ activeTab: 'features',
413
+ status: 'all',
414
+ issues: [],
415
+ issuesLoading: false,
416
+ }
417
+ },
418
+
419
+ computed: {
420
+ language() {
421
+ return readerLanguage()
422
+ },
423
+
424
+ groups() {
425
+ return groupFeatures(this.features, this.categories, this.language)
426
+ },
427
+
428
+ allFeatures() {
429
+ return this.groups.flatMap((group) => group.features)
430
+ },
431
+
432
+ counts() {
433
+ return countByStatus(this.allFeatures)
434
+ },
435
+
436
+ /** The groups with the features the filter lets through; an emptied group is left out. */
437
+ shownGroups() {
438
+ return this.groups
439
+ .map((group) => ({
440
+ ...group,
441
+ shown: this.status === 'all' ? group.features : group.features.filter((feature) => feature.status === this.status),
442
+ }))
443
+ .filter((group) => group.shown.length > 0)
444
+ },
445
+
446
+ /** All, then each status; `in_progress` only when a feature has it. */
447
+ filterOptions() {
448
+ const options = [{ key: 'all', label: this.statusLabel('all'), count: this.counts.all }]
449
+ for (const key of FEATURE_STATUSES) {
450
+ if (key === 'in_progress' && this.counts.in_progress === 0) {
451
+ continue
452
+ }
453
+ options.push({ key, label: this.statusLabel(key), count: this.counts[key] })
454
+ }
455
+ return options
456
+ },
457
+
458
+ tabList() {
459
+ return [
460
+ { key: 'features', label: t('nextcloud-vue', 'Features') },
461
+ { key: 'roadmap', label: t('nextcloud-vue', 'Roadmap') },
462
+ ]
463
+ },
464
+
465
+ resolvedTitle() {
466
+ return this.title ? this.tr(this.title) : t('nextcloud-vue', 'Features and roadmap')
467
+ },
468
+
469
+ resolvedSubtitle() {
470
+ if (!this.subtitle) {
471
+ return ''
472
+ }
473
+ return this.tr(this.subtitle)
474
+ .replace(/\{count\}/g, String(this.counts.all))
475
+ .replace(/\{appVersion\}/g, this.appVersion)
476
+ },
477
+
478
+ menuLinks() {
479
+ const links = []
480
+ if (this.suggestUrl && !this.suggestButton) {
481
+ links.push({ key: 'suggest', href: this.suggestUrl, label: t('nextcloud-vue', 'Suggest feature') })
482
+ }
483
+ if (this.openbuiltUrl) {
484
+ links.push({ key: 'openbuilt', href: this.openbuiltUrl, label: t('nextcloud-vue', 'Build on this app') })
485
+ }
486
+ if (this.llmSkillsUrl) {
487
+ links.push({ key: 'llm', href: this.llmSkillsUrl, label: t('nextcloud-vue', 'AI skills') })
488
+ }
489
+ return links
490
+ },
491
+
492
+ nextIssues() {
493
+ return this.issues.slice(0, this.next).map((issue) => ({
494
+ number: issue.number,
495
+ title: issue.title,
496
+ html_url: issue.html_url,
497
+ status: issueStatus(issue, this.issueLabels),
498
+ opened: issue.created_at ? formatBoardDate(issue.created_at) : '',
499
+ }))
500
+ },
501
+ },
502
+
503
+ mounted() {
504
+ if (this.next > 0 && this.repo) {
505
+ this.loadIssues()
506
+ }
507
+ },
508
+
509
+ methods: {
510
+ t,
511
+ safeHref,
512
+
513
+ /**
514
+ * Run an app-supplied string through the app's translate function.
515
+ *
516
+ * @param {string} key The string.
517
+ * @return {string} The translated string.
518
+ */
519
+ tr(key) {
520
+ return this.cnTranslate(key)
521
+ },
522
+
523
+ /**
524
+ * The label of a status, or of `all`: the app's override, else the library's.
525
+ *
526
+ * @param {string} key The status key.
527
+ * @return {string} The label.
528
+ */
529
+ statusLabel(key) {
530
+ if (typeof this.statusLabels[key] === 'string' && this.statusLabels[key] !== '') {
531
+ return this.tr(this.statusLabels[key])
532
+ }
533
+ return {
534
+ all: t('nextcloud-vue', 'All'),
535
+ available: t('nextcloud-vue', 'Available'),
536
+ beta: t('nextcloud-vue', 'Beta'),
537
+ in_progress: t('nextcloud-vue', 'In progress'),
538
+ planned: t('nextcloud-vue', 'Planned'),
539
+ }[key] || key
540
+ },
541
+
542
+ /**
543
+ * The badge text of a feature: its status, with the expected moment
544
+ * when it has one ("Planned, Q4 2026").
545
+ *
546
+ * @param {{status: string, expected: string}} feature The feature.
547
+ * @return {string} The badge text.
548
+ */
549
+ statusText(feature) {
550
+ const label = this.statusLabel(feature.status)
551
+ return feature.expected ? t('nextcloud-vue', '{status}, {expected}', { status: label, expected: feature.expected }) : label
552
+ },
553
+
554
+ /**
555
+ * The badge variant of a status.
556
+ *
557
+ * @param {string} status The status key.
558
+ * @return {string} The CnStatusBadge variant.
559
+ */
560
+ variantOf(status) {
561
+ return FEATURE_STATUS_VARIANTS[status] || 'default'
562
+ },
563
+
564
+ /**
565
+ * "14 features, 14 available": the group's size, then each status it holds.
566
+ *
567
+ * @param {{features: Array<{status: string}>}} group The group.
568
+ * @return {string} The caption.
569
+ */
570
+ groupCaption(group) {
571
+ const counts = countByStatus(group.features)
572
+ const parts = [n('nextcloud-vue', '%n feature', '%n features', counts.all)]
573
+ for (const key of FEATURE_STATUSES) {
574
+ if (counts[key] > 0) {
575
+ parts.push(`${counts[key]} ${this.statusLabel(key).toLocaleLowerCase()}`)
576
+ }
577
+ }
578
+ return parts.join(', ')
579
+ },
580
+
581
+ /**
582
+ * Where "Learn more" goes: the feature's own page when it is a web
583
+ * address, else the documentation site, else nowhere.
584
+ *
585
+ * @param {{docsUrl: string}} feature The feature.
586
+ * @return {string} The address, or ''.
587
+ */
588
+ readMoreHref(feature) {
589
+ if (/^https?:\/\//i.test(feature.docsUrl)) {
590
+ return safeHref(feature.docsUrl)
591
+ }
592
+ return this.documentationUrl ? safeHref(this.documentationUrl) : ''
593
+ },
594
+
595
+ /** Ask the host to open the support dialog. */
596
+ onSupport() {
597
+ /**
598
+ * @event support Emitted when the reader picks "Support this app" in the Actions menu.
599
+ */
600
+ this.$emit('support')
601
+ },
602
+
603
+ /** Fetch the forge issues for the Next card. */
604
+ async loadIssues() {
605
+ this.issuesLoading = true
606
+ const { items } = await fetchRoadmapIssues(this.repo)
607
+ this.issues = items
608
+ this.issuesLoading = false
609
+ },
610
+
611
+ /**
612
+ * Arrow keys, Home and End move between the folder tabs.
613
+ *
614
+ * @param {KeyboardEvent} event The key press.
615
+ */
616
+ onTabKeydown(event) {
617
+ const keys = this.tabList.map((tab) => tab.key)
618
+ const index = keys.indexOf(this.activeTab)
619
+ const target = {
620
+ ArrowRight: (index + 1) % keys.length,
621
+ ArrowLeft: (index - 1 + keys.length) % keys.length,
622
+ Home: 0,
623
+ End: keys.length - 1,
624
+ }[event.key]
625
+ if (target === undefined) {
626
+ return
627
+ }
628
+ event.preventDefault()
629
+ this.activeTab = keys[target]
630
+ this.$nextTick(() => this.$refs.tabButtons?.[target]?.focus())
631
+ },
632
+ },
633
+ }
634
+ </script>
635
+
636
+ <style scoped>
637
+ .cn-features-board {
638
+ box-sizing: border-box;
639
+ display: flex;
640
+ flex-direction: column;
641
+ gap: var(--cn-board-section-gap, 20px);
642
+ padding: var(--cn-board-content-padding, 24px 28px);
643
+ max-width: 1240px;
644
+ width: 100%;
645
+ }
646
+
647
+ .cn-features-board__header {
648
+ display: flex;
649
+ flex-direction: column;
650
+ gap: 6px;
651
+ padding: 4px 0 8px;
652
+ }
653
+
654
+ .cn-features-board__title-row {
655
+ display: flex;
656
+ flex-wrap: wrap;
657
+ justify-content: space-between;
658
+ align-items: flex-start;
659
+ gap: 12px 24px;
660
+ }
661
+
662
+ .cn-features-board__title {
663
+ flex: 1 1 320px;
664
+ min-width: 0;
665
+ margin: 0;
666
+ font-size: 28px;
667
+ font-weight: 700;
668
+ line-height: 1.2;
669
+ }
670
+
671
+ .cn-features-board__header-actions {
672
+ flex: none;
673
+ display: flex;
674
+ flex-wrap: wrap;
675
+ align-items: center;
676
+ gap: 10px;
677
+ }
678
+
679
+ .cn-features-board__subtitle {
680
+ margin: 0;
681
+ font-size: 15px;
682
+ color: var(--color-text-maxcontrast);
683
+ }
684
+
685
+ .cn-features-board__tab-row {
686
+ display: flex;
687
+ align-items: flex-end;
688
+ gap: 8px;
689
+ margin-bottom: calc(-1 * var(--cn-board-section-gap, 20px));
690
+ }
691
+
692
+ .cn-features-board__tabs {
693
+ display: flex;
694
+ flex-wrap: wrap;
695
+ gap: 4px;
696
+ flex: 1 1 auto;
697
+ min-width: 0;
698
+ }
699
+
700
+ .cn-features-board__tab {
701
+ display: inline-flex;
702
+ align-items: center;
703
+ box-sizing: border-box;
704
+ min-height: 34px;
705
+ margin: 0 0 -1px;
706
+ padding: 8px 12px;
707
+ border: 1px solid var(--cn-board-hairline, var(--color-border));
708
+ border-bottom: none;
709
+ border-radius: 8px 8px 0 0;
710
+ background: var(--color-background-dark);
711
+ color: var(--color-main-text);
712
+ font-size: 14px;
713
+ font-weight: 400;
714
+ cursor: pointer;
715
+ white-space: nowrap;
716
+ }
717
+
718
+ .cn-features-board__tab--active {
719
+ position: relative;
720
+ background: var(--color-main-background);
721
+ border-bottom: 1px solid var(--color-main-background);
722
+ font-weight: 700;
723
+ }
724
+
725
+ .cn-features-board__tab-actions {
726
+ flex: none;
727
+ align-self: flex-start;
728
+ margin: 0 8px 8px 0;
729
+ }
730
+
731
+ .cn-features-board__panel {
732
+ display: flex;
733
+ flex-direction: column;
734
+ gap: 22px;
735
+ }
736
+
737
+ .cn-features-board__panel--tabbed {
738
+ padding: 22px;
739
+ border: 1px solid var(--color-border);
740
+ border-radius: 0 var(--cn-board-card-radius, 12px) var(--cn-board-card-radius, 12px) var(--cn-board-card-radius, 12px);
741
+ background: var(--color-main-background);
742
+ }
743
+
744
+ .cn-features-board__filter {
745
+ display: flex;
746
+ flex-wrap: wrap;
747
+ align-items: center;
748
+ gap: 8px;
749
+ }
750
+
751
+ .cn-features-board__filter-option {
752
+ display: inline-flex;
753
+ align-items: center;
754
+ gap: 8px;
755
+ margin: 0;
756
+ font-size: 14px;
757
+ font-weight: 600;
758
+ cursor: pointer;
759
+ }
760
+
761
+ .cn-features-board__filter--chips .cn-features-board__filter-option {
762
+ height: 36px;
763
+ padding: 0 14px;
764
+ border: 1px solid var(--color-border-dark);
765
+ border-radius: 18px;
766
+ background: var(--color-main-background);
767
+ color: var(--color-main-text);
768
+ }
769
+
770
+ .cn-features-board__filter--chips .cn-features-board__filter-option--active {
771
+ border-color: var(--color-main-text);
772
+ background: var(--color-main-text);
773
+ color: var(--color-main-background);
774
+ }
775
+
776
+ .cn-features-board__filter-count {
777
+ font-weight: 400;
778
+ color: var(--color-text-maxcontrast);
779
+ }
780
+
781
+ .cn-features-board__filter--chips .cn-features-board__filter-option--active .cn-features-board__filter-count {
782
+ color: var(--color-border-dark);
783
+ }
784
+
785
+ .cn-features-board__filter--segmented {
786
+ display: inline-flex;
787
+ align-self: flex-start;
788
+ gap: 2px;
789
+ padding: 3px;
790
+ border-radius: 8px;
791
+ background: var(--color-background-dark);
792
+ }
793
+
794
+ .cn-features-board__filter--segmented .cn-features-board__filter-option {
795
+ height: 34px;
796
+ padding: 0 14px;
797
+ border: 0;
798
+ border-radius: 6px;
799
+ background: transparent;
800
+ color: var(--color-main-text);
801
+ }
802
+
803
+ .cn-features-board__filter--segmented .cn-features-board__filter-option--active {
804
+ background: var(--color-main-background);
805
+ box-shadow: 0 1px 2px rgba(27, 28, 29, 0.12);
806
+ }
807
+
808
+ .cn-features-board__body {
809
+ display: flex;
810
+ flex-wrap: wrap;
811
+ align-items: flex-start;
812
+ gap: 24px;
813
+ }
814
+
815
+ .cn-features-board__groups {
816
+ flex: 999 1 520px;
817
+ min-width: 0;
818
+ display: flex;
819
+ flex-direction: column;
820
+ gap: 26px;
821
+ }
822
+
823
+ .cn-features-board__groups--rows {
824
+ display: grid;
825
+ grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
826
+ gap: 20px;
827
+ }
828
+
829
+ .cn-features-board__group {
830
+ display: flex;
831
+ flex-direction: column;
832
+ gap: 12px;
833
+ max-width: none;
834
+ margin: 0;
835
+ }
836
+
837
+ .cn-features-board__groups--rows .cn-features-board__group {
838
+ padding: var(--cn-board-card-padding, 20px 22px);
839
+ border: 1px solid var(--color-border);
840
+ border-radius: var(--cn-board-card-radius, 12px);
841
+ background: var(--color-main-background);
842
+ }
843
+
844
+ .cn-features-board__group-head {
845
+ display: flex;
846
+ flex-wrap: wrap;
847
+ align-items: baseline;
848
+ gap: 10px;
849
+ }
850
+
851
+ .cn-features-board__group-title {
852
+ margin: 0;
853
+ font-size: 18px;
854
+ font-weight: 700;
855
+ }
856
+
857
+ .cn-features-board__groups--rows .cn-features-board__group-title {
858
+ font-size: 17px;
859
+ }
860
+
861
+ .cn-features-board__group-caption {
862
+ font-size: 13px;
863
+ color: var(--color-text-maxcontrast);
864
+ }
865
+
866
+ .cn-features-board__cards {
867
+ display: grid;
868
+ grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
869
+ gap: 16px;
870
+ }
871
+
872
+ .cn-features-board__card {
873
+ display: flex;
874
+ flex-direction: column;
875
+ gap: 10px;
876
+ padding: 18px;
877
+ border: 1px solid var(--color-border);
878
+ border-radius: var(--cn-board-card-radius, 12px);
879
+ background: var(--color-main-background);
880
+ }
881
+
882
+ .cn-features-board__card-head {
883
+ display: flex;
884
+ flex-direction: column;
885
+ align-items: flex-start;
886
+ gap: 4px;
887
+ }
888
+
889
+ .cn-features-board__card-title {
890
+ margin: 0;
891
+ font-size: 17px;
892
+ font-weight: 700;
893
+ line-height: 1.3;
894
+ }
895
+
896
+ .cn-features-board__card-summary {
897
+ margin: 0;
898
+ font-size: 14px;
899
+ color: var(--color-main-text);
900
+ }
901
+
902
+ .cn-features-board__card-footer {
903
+ display: flex;
904
+ justify-content: flex-end;
905
+ align-items: center;
906
+ min-height: 34px;
907
+ margin-top: auto;
908
+ padding-top: 10px;
909
+ border-top: 1px solid var(--color-background-dark);
910
+ }
911
+
912
+ .cn-features-board__read-more {
913
+ display: inline-flex;
914
+ align-items: center;
915
+ height: 34px;
916
+ padding: 0 12px;
917
+ border: 1px solid var(--color-border-dark);
918
+ border-radius: 8px;
919
+ background: var(--color-main-background);
920
+ color: var(--color-main-text);
921
+ font-size: 14px;
922
+ font-weight: 600;
923
+ text-decoration: none;
924
+ }
925
+
926
+ .cn-features-board__rows {
927
+ display: flex;
928
+ flex-direction: column;
929
+ margin: 0;
930
+ padding: 0;
931
+ list-style: none;
932
+ }
933
+
934
+ .cn-features-board__row {
935
+ display: flex;
936
+ flex-wrap: wrap;
937
+ align-items: center;
938
+ gap: 6px 16px;
939
+ padding: 10px 0;
940
+ border-top: 1px solid var(--color-background-dark);
941
+ }
942
+
943
+ .cn-features-board__row-text {
944
+ flex: 1 1 240px;
945
+ min-width: 0;
946
+ display: flex;
947
+ flex-direction: column;
948
+ gap: 2px;
949
+ }
950
+
951
+ .cn-features-board__row-title {
952
+ font-size: 15px;
953
+ font-weight: 500;
954
+ }
955
+
956
+ .cn-features-board__row-summary {
957
+ font-size: 13px;
958
+ color: var(--color-text-maxcontrast);
959
+ }
960
+
961
+ .cn-features-board__next {
962
+ flex: 1 1 280px;
963
+ min-width: 0;
964
+ max-width: 340px;
965
+ }
966
+
967
+ .cn-features-board__next-card {
968
+ display: flex;
969
+ flex-direction: column;
970
+ gap: 12px;
971
+ padding: 18px;
972
+ border: 1px solid var(--color-border);
973
+ border-radius: var(--cn-board-card-radius, 12px);
974
+ background: var(--color-background-hover);
975
+ }
976
+
977
+ .cn-features-board__next-head {
978
+ display: flex;
979
+ align-items: center;
980
+ gap: 8px;
981
+ }
982
+
983
+ .cn-features-board__next-title {
984
+ margin: 0;
985
+ font-size: 18px;
986
+ font-weight: 700;
987
+ }
988
+
989
+ .cn-features-board__next-lead {
990
+ margin: 0;
991
+ font-size: 14px;
992
+ color: var(--color-text-maxcontrast);
993
+ }
994
+
995
+ .cn-features-board__next-list {
996
+ display: flex;
997
+ flex-direction: column;
998
+ gap: 10px;
999
+ margin: 0;
1000
+ padding: 0;
1001
+ list-style: none;
1002
+ }
1003
+
1004
+ .cn-features-board__next-item {
1005
+ display: flex;
1006
+ flex-direction: column;
1007
+ gap: 6px;
1008
+ padding: 12px 14px;
1009
+ border: 1px solid var(--color-border);
1010
+ border-radius: 10px;
1011
+ background: var(--color-main-background);
1012
+ }
1013
+
1014
+ .cn-features-board__next-link {
1015
+ font-size: 15px;
1016
+ font-weight: 600;
1017
+ line-height: 1.3;
1018
+ color: var(--color-main-text);
1019
+ text-decoration: none;
1020
+ }
1021
+
1022
+ .cn-features-board__next-meta {
1023
+ display: flex;
1024
+ flex-wrap: wrap;
1025
+ align-items: center;
1026
+ gap: 8px;
1027
+ font-size: 13px;
1028
+ color: var(--color-text-maxcontrast);
1029
+ }
1030
+
1031
+ .cn-features-board__next-opened {
1032
+ display: inline-flex;
1033
+ align-items: center;
1034
+ gap: 4px;
1035
+ }
1036
+
1037
+ .cn-features-board__next-more {
1038
+ display: inline-flex;
1039
+ align-items: center;
1040
+ gap: 4px;
1041
+ font-size: 14px;
1042
+ font-weight: 600;
1043
+ }
1044
+
1045
+ .cn-features-board__empty {
1046
+ margin: 0;
1047
+ color: var(--color-text-maxcontrast);
1048
+ }
1049
+
1050
+ .cn-features-board__notice {
1051
+ margin: 0;
1052
+ }
1053
+ </style>