@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.
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +57 -4
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +44 -17
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue.js +1 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js +44 -2
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js +140 -59
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +20 -6
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +3 -2
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +10 -39
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +5 -3
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +6 -4
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue2.js +46 -0
- package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue2.js.map +1 -1
- package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue3.js +6 -2
- package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue3.js.map +1 -1
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue.js +1 -1
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js +82 -9
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js.map +1 -1
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js +104 -90
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js.map +1 -1
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue.js +9 -0
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue.js.map +1 -0
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js +355 -0
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js.map +1 -0
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js +462 -0
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js.map +1 -0
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue.js +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +39 -10
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +418 -399
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnIcon/semanticIcons.js +2 -2
- package/dist/esm/components/CnIcon/semanticIcons.js.map +1 -1
- package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue.js +1 -1
- package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue2.js +156 -3
- package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue2.js.map +1 -1
- package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue3.js +168 -52
- package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue3.js.map +1 -1
- package/dist/esm/components/CnModuleCard/CnModuleCard.vue.js +9 -0
- package/dist/esm/components/CnModuleCard/CnModuleCard.vue.js.map +1 -0
- package/dist/esm/components/CnModuleCard/CnModuleCard.vue2.js +83 -0
- package/dist/esm/components/CnModuleCard/CnModuleCard.vue2.js.map +1 -0
- package/dist/esm/components/CnModuleCard/CnModuleCard.vue3.js +119 -0
- package/dist/esm/components/CnModuleCard/CnModuleCard.vue3.js.map +1 -0
- package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue.js +1 -1
- package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js +22 -1
- package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js +20 -14
- package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue.js +1 -1
- package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue2.js +2 -0
- package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue2.js.map +1 -1
- package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue3.js +15 -5
- package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue3.js.map +1 -1
- package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue.js +1 -1
- package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue2.js +8 -30
- package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue2.js.map +1 -1
- package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue3.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +33 -2
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnTextWidget/CnTextWidget.vue.js +1 -1
- package/dist/esm/components/CnTextWidget/CnTextWidget.vue2.js +63 -4
- package/dist/esm/components/CnTextWidget/CnTextWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTextWidget/CnTextWidget.vue3.js +19 -2
- package/dist/esm/components/CnTextWidget/CnTextWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue.js +9 -0
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue.js.map +1 -0
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js +164 -0
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js.map +1 -0
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js +93 -0
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js.map +1 -0
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue.js +1 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js +12 -0
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js.map +1 -1
- package/dist/esm/l10n/en.json.js +45 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +44 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/nextcloud-vue.css +1182 -328
- package/dist/esm/utils/boardDate.js +35 -1
- package/dist/esm/utils/boardDate.js.map +1 -1
- package/dist/esm/utils/featureCatalog.js +135 -0
- package/dist/esm/utils/featureCatalog.js.map +1 -0
- package/dist/esm/utils/fillTemplate.js +84 -4
- package/dist/esm/utils/fillTemplate.js.map +1 -1
- package/dist/esm/utils/headerMeta.js +62 -12
- package/dist/esm/utils/headerMeta.js.map +1 -1
- package/dist/esm/utils/roadmapIssues.js +86 -0
- package/dist/esm/utils/roadmapIssues.js.map +1 -0
- package/dist/esm/utils/userDisplayName.js +47 -0
- package/dist/esm/utils/userDisplayName.js.map +1 -0
- package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +15618 -13097
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +1182 -328
- package/l10n/en.json +45 -1
- package/l10n/nl.json +44 -1
- package/package.json +1 -1
- package/src/components/CnAppRoot/CnAppRoot.md +2 -0
- package/src/components/CnAppRoot/CnAppRoot.vue +66 -5
- package/src/components/CnChoiceCards/CnChoiceCards.md +27 -0
- package/src/components/CnChoiceCards/CnChoiceCards.vue +168 -3
- package/src/components/CnDashboardPage/CnDashboardPage.vue +45 -6
- package/src/components/CnDataTable/CnDataTable.vue +10 -39
- package/src/components/CnDetailPage/CnDetailPage.vue +5 -3
- package/src/components/CnFactsWidget/CnFactsWidget.vue +6 -4
- package/src/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue +51 -1
- package/src/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue +98 -13
- package/src/components/CnFeaturesBoard/CnFeaturesBoard.vue +1053 -0
- package/src/components/CnFormDialog/CnFormDialog.md +1 -0
- package/src/components/CnFormDialog/CnFormDialog.vue +619 -562
- package/src/components/CnLinkCardsPage/CnLinkCardsPage.vue +338 -5
- package/src/components/CnModuleCard/CnModuleCard.vue +258 -0
- package/src/components/CnNcWidgetWidget/CnNcWidgetWidget.vue +23 -2
- package/src/components/CnNotificationPreferences/CnNotificationPreferences.vue +5 -0
- package/src/components/CnRoadmapTab/CnRoadmapTab.vue +6 -28
- package/src/components/CnTabsWidget/CnTabsWidget.vue +33 -2
- package/src/components/CnTextWidget/CnTextWidget.vue +89 -4
- package/src/components/CnUserSettingsDialog/CnUserSettingsDialog.vue +305 -0
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +12 -0
- package/src/css/dialog.css +47 -0
- package/src/css/look-board-detail.css +8 -0
- package/src/css/look-board.css +33 -0
- package/src/schemas/app-manifest-v2.schema.json +67 -4
- package/src/utils/boardDate.js +34 -0
- package/src/utils/featureCatalog.js +132 -0
- package/src/utils/fillTemplate.js +83 -3
- package/src/utils/headerMeta.js +62 -12
- package/src/utils/roadmapIssues.js +82 -0
- package/src/utils/userDisplayName.js +54 -0
- package/src/utils/validateManifestV2.compiled.js +3 -3
|
@@ -14,8 +14,34 @@
|
|
|
14
14
|
</p>
|
|
15
15
|
</div>
|
|
16
16
|
|
|
17
|
+
<section
|
|
18
|
+
v-if="visibleModules.length > 0"
|
|
19
|
+
class="cn-link-cards-page__modules"
|
|
20
|
+
:aria-labelledby="`cn-link-cards-modules-${uid}`"
|
|
21
|
+
data-testid="cn-link-cards-modules">
|
|
22
|
+
<div class="cn-link-cards-page__modules-head">
|
|
23
|
+
<h3 :id="`cn-link-cards-modules-${uid}`" class="cn-link-cards-page__modules-title">
|
|
24
|
+
{{ resolvedModulesTitle }}
|
|
25
|
+
</h3>
|
|
26
|
+
<span class="cn-link-cards-page__modules-count" data-testid="cn-link-cards-modules-count">
|
|
27
|
+
{{ modulesCount }}
|
|
28
|
+
</span>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="cn-link-cards-page__modules-grid">
|
|
31
|
+
<CnModuleCard
|
|
32
|
+
v-for="module in visibleModules"
|
|
33
|
+
:key="module.id"
|
|
34
|
+
:module="module"
|
|
35
|
+
:headingId="`cn-link-cards-module-${uid}-${module.key}`" />
|
|
36
|
+
</div>
|
|
37
|
+
<p v-if="resolvedFootnote" class="cn-link-cards-page__modules-note" data-testid="cn-link-cards-modules-note">
|
|
38
|
+
<InformationOutline :size="18" class="cn-link-cards-page__modules-note-icon" />
|
|
39
|
+
<span>{{ resolvedFootnote }}</span>
|
|
40
|
+
</p>
|
|
41
|
+
</section>
|
|
42
|
+
|
|
17
43
|
<p
|
|
18
|
-
v-if="groups.length === 0"
|
|
44
|
+
v-if="groups.length === 0 && visibleModules.length === 0"
|
|
19
45
|
class="cn-link-cards-page__empty"
|
|
20
46
|
data-testid="cn-link-cards-empty">
|
|
21
47
|
{{ resolvedEmptyLabel }}
|
|
@@ -25,6 +51,7 @@
|
|
|
25
51
|
v-for="group in groups"
|
|
26
52
|
:key="group.key"
|
|
27
53
|
class="cn-link-cards-page__group"
|
|
54
|
+
:class="{ 'cn-link-cards-page__group--list': isListStyle }"
|
|
28
55
|
:aria-labelledby="group.label ? group.headingId : null"
|
|
29
56
|
:aria-label="group.label ? null : resolvedTitle || null"
|
|
30
57
|
data-testid="cn-link-cards-group"
|
|
@@ -35,7 +62,38 @@
|
|
|
35
62
|
class="cn-link-cards-page__caption">
|
|
36
63
|
{{ group.label }}
|
|
37
64
|
</h3>
|
|
38
|
-
|
|
65
|
+
<!-- groupStyle "list": the group is one card of rows, each an icon
|
|
66
|
+
chip, the label over its description, and a trailing chevron
|
|
67
|
+
(an external-link icon for a URL). -->
|
|
68
|
+
<ul v-if="isListStyle" class="cn-link-cards-page__list">
|
|
69
|
+
<li v-for="card in group.cards" :key="card.id">
|
|
70
|
+
<a
|
|
71
|
+
class="cn-link-cards-page__row"
|
|
72
|
+
data-testid="cn-link-card"
|
|
73
|
+
:data-card-id="card.id"
|
|
74
|
+
:href="card.url"
|
|
75
|
+
:target="card.external ? '_blank' : null"
|
|
76
|
+
:rel="card.external ? 'noopener noreferrer' : null"
|
|
77
|
+
:aria-describedby="card.description ? card.descriptionId : null"
|
|
78
|
+
@click="open(card, $event)">
|
|
79
|
+
<span v-if="card.icon" class="cn-link-cards-page__row-icon" aria-hidden="true">
|
|
80
|
+
<CnIcon :name="card.icon" :size="20" />
|
|
81
|
+
</span>
|
|
82
|
+
<span class="cn-link-cards-page__row-body">
|
|
83
|
+
<span class="cn-link-cards-page__row-label">{{ card.label }}</span>
|
|
84
|
+
<span
|
|
85
|
+
v-if="card.description"
|
|
86
|
+
:id="card.descriptionId"
|
|
87
|
+
class="cn-link-cards-page__row-description">
|
|
88
|
+
{{ card.description }}
|
|
89
|
+
</span>
|
|
90
|
+
</span>
|
|
91
|
+
<OpenInNew v-if="trailingOf(card) === 'external'" :size="18" class="cn-link-cards-page__row-trailing" />
|
|
92
|
+
<ChevronRight v-else :size="20" class="cn-link-cards-page__row-trailing" />
|
|
93
|
+
</a>
|
|
94
|
+
</li>
|
|
95
|
+
</ul>
|
|
96
|
+
<ul v-else class="cn-link-cards-page__grid">
|
|
39
97
|
<li v-for="card in group.cards" :key="card.id" class="cn-link-cards-page__cell">
|
|
40
98
|
<!-- A real <a href>, so the card can be middle-clicked, copied
|
|
41
99
|
and announced as a link. A plain click on an in-app card
|
|
@@ -72,7 +130,11 @@
|
|
|
72
130
|
</template>
|
|
73
131
|
|
|
74
132
|
<script>
|
|
75
|
-
import { translate as t } from '@nextcloud/l10n'
|
|
133
|
+
import { translatePlural as n, translate as t } from '@nextcloud/l10n'
|
|
134
|
+
import ChevronRight from 'vue-material-design-icons/ChevronRight.vue'
|
|
135
|
+
import InformationOutline from 'vue-material-design-icons/InformationOutline.vue'
|
|
136
|
+
import OpenInNew from 'vue-material-design-icons/OpenInNew.vue'
|
|
137
|
+
import CnModuleCard from '../CnModuleCard/CnModuleCard.vue'
|
|
76
138
|
import { isAppInstalled } from '../../utils/appInstalled.js'
|
|
77
139
|
import { followLinkClick, resolveHref } from '../../utils/linkNavigation.js'
|
|
78
140
|
import { passesContextPredicates } from '../../utils/visibleIfContext.js'
|
|
@@ -107,7 +169,7 @@ let linkCardsPageUid = 0
|
|
|
107
169
|
export default {
|
|
108
170
|
name: 'CnLinkCardsPage',
|
|
109
171
|
|
|
110
|
-
components: { CnIcon },
|
|
172
|
+
components: { ChevronRight, CnIcon, CnModuleCard, InformationOutline, OpenInNew },
|
|
111
173
|
|
|
112
174
|
inject: {
|
|
113
175
|
/** The app's translate function, provided by CnAppRoot. Identity outside one. */
|
|
@@ -158,6 +220,37 @@ export default {
|
|
|
158
220
|
default: null,
|
|
159
221
|
},
|
|
160
222
|
|
|
223
|
+
/**
|
|
224
|
+
* The modules: `{ id, label, description?, icon?, enabled?, enabledIf?,
|
|
225
|
+
* route?, pages: [{ label, route, highlight? }], settings?: { label?, prefix?,
|
|
226
|
+
* route?, href?, permission?, visibleIf? }, permission?, visibleIf? }`.
|
|
227
|
+
* Drawn as module cards above the link cards.
|
|
228
|
+
*
|
|
229
|
+
* @type {Array<object>|null}
|
|
230
|
+
*/
|
|
231
|
+
modules: {
|
|
232
|
+
type: Array,
|
|
233
|
+
default: null,
|
|
234
|
+
},
|
|
235
|
+
|
|
236
|
+
/** `cards` (default): a grid of link cards per group. `list`: one card per group with a row per link. */
|
|
237
|
+
groupStyle: {
|
|
238
|
+
type: String,
|
|
239
|
+
default: null,
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
/** Heading of the modules section. Defaults to "Modules". */
|
|
243
|
+
modulesTitle: {
|
|
244
|
+
type: String,
|
|
245
|
+
default: null,
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
/** A note under the module cards. */
|
|
249
|
+
footnote: {
|
|
250
|
+
type: String,
|
|
251
|
+
default: null,
|
|
252
|
+
},
|
|
253
|
+
|
|
161
254
|
/** Text shown when no card is visible. */
|
|
162
255
|
emptyLabel: {
|
|
163
256
|
type: String,
|
|
@@ -192,7 +285,7 @@ export default {
|
|
|
192
285
|
config() {
|
|
193
286
|
const fromPage = (this.page && this.page.config) || {}
|
|
194
287
|
const fromProps = {}
|
|
195
|
-
for (const key of ['cards', 'categories', 'description', 'title', 'emptyLabel']) {
|
|
288
|
+
for (const key of ['cards', 'categories', 'description', 'title', 'emptyLabel', 'modules', 'modulesTitle', 'footnote', 'groupStyle']) {
|
|
196
289
|
if (this[key] !== null && this[key] !== undefined) {
|
|
197
290
|
fromProps[key] = this[key]
|
|
198
291
|
}
|
|
@@ -261,6 +354,90 @@ export default {
|
|
|
261
354
|
.filter((card) => card.url !== '')
|
|
262
355
|
},
|
|
263
356
|
|
|
357
|
+
/**
|
|
358
|
+
* The modules a reader may see, with text translated and links
|
|
359
|
+
* resolved. `enabled` is the declared boolean, or the result of
|
|
360
|
+
* `enabledIf` (a visibleIf condition against `runtime`). A page that
|
|
361
|
+
* resolves to nothing is dropped; the admin-settings link only shows when
|
|
362
|
+
* its own `permission` and `visibleIf` pass.
|
|
363
|
+
*
|
|
364
|
+
* @return {Array<object>} The modules.
|
|
365
|
+
* @spec openspec/changes/screens-links-module-cards/specs/link-cards-page/spec.md#requirement-a-links-page-can-show-modules
|
|
366
|
+
*/
|
|
367
|
+
visibleModules() {
|
|
368
|
+
const declared = Array.isArray(this.config.modules) ? this.config.modules : []
|
|
369
|
+
const runtime = this.cnManifest?.runtime ?? null
|
|
370
|
+
return declared
|
|
371
|
+
.filter((module) => module && module.id && module.label)
|
|
372
|
+
.filter((module) => this.passesPermission(module) && this.passesVisibleIf(module, runtime))
|
|
373
|
+
.map((module) => {
|
|
374
|
+
const pages = (Array.isArray(module.pages) ? module.pages : [])
|
|
375
|
+
.filter((page) => page && page.label)
|
|
376
|
+
.map((page, index) => ({ key: String(index), label: this.tr(page.label), highlight: page.highlight === true, ...this.targetOf(page) }))
|
|
377
|
+
.filter((page) => page.url !== '')
|
|
378
|
+
const own = this.targetOf(module)
|
|
379
|
+
const open = own.url !== '' ? own : (pages[0] || null)
|
|
380
|
+
const settingsDeclared = module.settings && typeof module.settings === 'object' ? module.settings : null
|
|
381
|
+
const settingsTarget = settingsDeclared && this.passesPermission(settingsDeclared) && this.passesVisibleIf(settingsDeclared, runtime)
|
|
382
|
+
? this.targetOf(settingsDeclared)
|
|
383
|
+
: null
|
|
384
|
+
return {
|
|
385
|
+
id: String(module.id),
|
|
386
|
+
key: String(module.id).replace(/[^A-Za-z0-9_-]/g, '-'),
|
|
387
|
+
label: this.tr(module.label),
|
|
388
|
+
description: this.tr(module.description || ''),
|
|
389
|
+
icon: module.icon || '',
|
|
390
|
+
enabled: this.moduleEnabled(module, runtime),
|
|
391
|
+
pages,
|
|
392
|
+
open,
|
|
393
|
+
settings: settingsTarget && settingsTarget.url !== ''
|
|
394
|
+
? { ...settingsTarget, label: this.tr(settingsDeclared.label || ''), prefix: this.tr(settingsDeclared.prefix || '') }
|
|
395
|
+
: null,
|
|
396
|
+
}
|
|
397
|
+
})
|
|
398
|
+
},
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Whether the groups are drawn as list cards (`config.groupStyle: "list"`).
|
|
402
|
+
*
|
|
403
|
+
* @return {boolean} True for list cards.
|
|
404
|
+
* @spec openspec/changes/screens-links-module-cards/specs/link-cards-page/spec.md#requirement-a-group-can-be-a-list-card
|
|
405
|
+
*/
|
|
406
|
+
isListStyle() {
|
|
407
|
+
return this.config.groupStyle === 'list'
|
|
408
|
+
},
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* The modules heading, translated.
|
|
412
|
+
*
|
|
413
|
+
* @return {string} The heading.
|
|
414
|
+
* @spec openspec/changes/screens-links-module-cards/specs/link-cards-page/spec.md#requirement-a-links-page-can-show-modules
|
|
415
|
+
*/
|
|
416
|
+
resolvedModulesTitle() {
|
|
417
|
+
return this.config.modulesTitle ? this.tr(this.config.modulesTitle) : t('nextcloud-vue', 'Modules')
|
|
418
|
+
},
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* "3 of 6 are in the menu".
|
|
422
|
+
*
|
|
423
|
+
* @return {string} The count.
|
|
424
|
+
* @spec openspec/changes/screens-links-module-cards/specs/link-cards-page/spec.md#requirement-a-links-page-can-show-modules
|
|
425
|
+
*/
|
|
426
|
+
modulesCount() {
|
|
427
|
+
const on = this.visibleModules.filter((module) => module.enabled).length
|
|
428
|
+
return n('nextcloud-vue', '{on} of %n is in the menu', '{on} of %n are in the menu', this.visibleModules.length, { on })
|
|
429
|
+
},
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* The note under the module cards, translated.
|
|
433
|
+
*
|
|
434
|
+
* @return {string} The note, or ''.
|
|
435
|
+
* @spec openspec/changes/screens-links-module-cards/specs/link-cards-page/spec.md#requirement-a-links-page-can-show-modules
|
|
436
|
+
*/
|
|
437
|
+
resolvedFootnote() {
|
|
438
|
+
return this.tr(this.config.footnote || '')
|
|
439
|
+
},
|
|
440
|
+
|
|
264
441
|
/**
|
|
265
442
|
* The groups to draw: ungrouped cards first under no caption, then
|
|
266
443
|
* one group per category in declaration order. A group without a
|
|
@@ -290,6 +467,40 @@ export default {
|
|
|
290
467
|
},
|
|
291
468
|
|
|
292
469
|
methods: {
|
|
470
|
+
/**
|
|
471
|
+
* Whether a module is in the menu: its `enabled` boolean, else its
|
|
472
|
+
* `enabledIf` condition evaluated as a `visibleIf`, else false.
|
|
473
|
+
*
|
|
474
|
+
* @param {object} module The declared module.
|
|
475
|
+
* @param {object|null} runtime `manifest.runtime`, or null.
|
|
476
|
+
* @return {boolean} True when the module is in the menu.
|
|
477
|
+
* @spec openspec/changes/screens-links-module-cards/specs/link-cards-page/spec.md#requirement-a-links-page-can-show-modules
|
|
478
|
+
*/
|
|
479
|
+
/**
|
|
480
|
+
* The icon at the end of a list row: an external-link icon for a URL,
|
|
481
|
+
* a chevron for a page in the app. A resolved `trailing` wins.
|
|
482
|
+
*
|
|
483
|
+
* @param {object} card The resolved card.
|
|
484
|
+
* @return {string} `external` or `chevron`.
|
|
485
|
+
* @spec openspec/changes/screens-links-module-cards/specs/link-cards-page/spec.md#requirement-a-group-can-be-a-list-card
|
|
486
|
+
*/
|
|
487
|
+
trailingOf(card) {
|
|
488
|
+
if (card.trailing === 'external' || card.trailing === 'chevron') {
|
|
489
|
+
return card.trailing
|
|
490
|
+
}
|
|
491
|
+
return card.external ? 'external' : 'chevron'
|
|
492
|
+
},
|
|
493
|
+
|
|
494
|
+
moduleEnabled(module, runtime) {
|
|
495
|
+
if (typeof module.enabled === 'boolean') {
|
|
496
|
+
return module.enabled
|
|
497
|
+
}
|
|
498
|
+
if (module.enabledIf && typeof module.enabledIf === 'object') {
|
|
499
|
+
return this.passesVisibleIf({ visibleIf: module.enabledIf }, runtime)
|
|
500
|
+
}
|
|
501
|
+
return false
|
|
502
|
+
},
|
|
503
|
+
|
|
293
504
|
/**
|
|
294
505
|
* Translate a manifest string through the app's translate function.
|
|
295
506
|
*
|
|
@@ -514,4 +725,126 @@ export default {
|
|
|
514
725
|
font-size: 0.9em;
|
|
515
726
|
line-height: 1.4;
|
|
516
727
|
}
|
|
728
|
+
|
|
729
|
+
.cn-link-cards-page__group--list {
|
|
730
|
+
padding: 20px 22px;
|
|
731
|
+
border: 1px solid var(--color-border);
|
|
732
|
+
border-radius: var(--border-radius-container-large, 12px);
|
|
733
|
+
background: var(--color-main-background);
|
|
734
|
+
gap: 6px;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
.cn-link-cards-page__group--list .cn-link-cards-page__caption {
|
|
738
|
+
margin: 0 0 8px;
|
|
739
|
+
color: var(--color-main-text);
|
|
740
|
+
font-size: 17px;
|
|
741
|
+
font-weight: 700;
|
|
742
|
+
letter-spacing: normal;
|
|
743
|
+
text-transform: none;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
.cn-link-cards-page__list {
|
|
747
|
+
margin: 0;
|
|
748
|
+
padding: 0;
|
|
749
|
+
list-style: none;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
.cn-link-cards-page__row {
|
|
753
|
+
display: flex;
|
|
754
|
+
align-items: center;
|
|
755
|
+
gap: 12px;
|
|
756
|
+
padding: 10px 0;
|
|
757
|
+
border-top: 1px solid var(--color-background-dark);
|
|
758
|
+
color: var(--color-main-text);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
.cn-link-cards-page__row,
|
|
762
|
+
.cn-link-cards-page__row:hover,
|
|
763
|
+
.cn-link-cards-page__row:focus,
|
|
764
|
+
.cn-link-cards-page__row:active {
|
|
765
|
+
text-decoration: none !important;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
.cn-link-cards-page__row:focus-visible {
|
|
769
|
+
outline: 2px solid var(--color-primary-element);
|
|
770
|
+
outline-offset: 2px;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
.cn-link-cards-page__row-icon {
|
|
774
|
+
display: flex;
|
|
775
|
+
flex: none;
|
|
776
|
+
align-items: center;
|
|
777
|
+
justify-content: center;
|
|
778
|
+
width: 34px;
|
|
779
|
+
height: 34px;
|
|
780
|
+
border-radius: var(--border-radius-element, 8px);
|
|
781
|
+
background: var(--color-background-dark);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
.cn-link-cards-page__row-body {
|
|
785
|
+
display: flex;
|
|
786
|
+
flex: 1 1 auto;
|
|
787
|
+
flex-direction: column;
|
|
788
|
+
gap: 2px;
|
|
789
|
+
min-width: 0;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
.cn-link-cards-page__row-label {
|
|
793
|
+
font-size: 15px;
|
|
794
|
+
font-weight: 600;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
.cn-link-cards-page__row-description {
|
|
798
|
+
font-size: 13px;
|
|
799
|
+
color: var(--color-text-maxcontrast);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
.cn-link-cards-page__row-trailing {
|
|
803
|
+
flex: none;
|
|
804
|
+
color: var(--color-text-maxcontrast);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
.cn-link-cards-page__modules {
|
|
808
|
+
display: flex;
|
|
809
|
+
flex-direction: column;
|
|
810
|
+
gap: 14px;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
.cn-link-cards-page__modules-head {
|
|
814
|
+
display: flex;
|
|
815
|
+
flex-wrap: wrap;
|
|
816
|
+
justify-content: space-between;
|
|
817
|
+
align-items: baseline;
|
|
818
|
+
gap: 8px;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
.cn-link-cards-page__modules-title {
|
|
822
|
+
margin: 0;
|
|
823
|
+
font-size: 20px;
|
|
824
|
+
font-weight: 700;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
.cn-link-cards-page__modules-count {
|
|
828
|
+
font-size: 14px;
|
|
829
|
+
color: var(--color-text-maxcontrast);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
.cn-link-cards-page__modules-grid {
|
|
833
|
+
display: grid;
|
|
834
|
+
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
|
835
|
+
gap: 16px;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
.cn-link-cards-page__modules-note {
|
|
839
|
+
display: flex;
|
|
840
|
+
align-items: flex-start;
|
|
841
|
+
gap: 8px;
|
|
842
|
+
margin: 0;
|
|
843
|
+
font-size: 14px;
|
|
844
|
+
color: var(--color-text-maxcontrast);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
.cn-link-cards-page__modules-note-icon {
|
|
848
|
+
flex: none;
|
|
849
|
+
}
|
|
517
850
|
</style>
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
- SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
- SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
-->
|
|
5
|
+
<template>
|
|
6
|
+
<article
|
|
7
|
+
class="cn-module-card"
|
|
8
|
+
:aria-labelledby="headingId"
|
|
9
|
+
data-testid="cn-module-card"
|
|
10
|
+
:data-module-id="module.id">
|
|
11
|
+
<div class="cn-module-card__head">
|
|
12
|
+
<span
|
|
13
|
+
v-if="module.icon"
|
|
14
|
+
class="cn-module-card__icon"
|
|
15
|
+
:class="{ 'cn-module-card__icon--off': !module.enabled }"
|
|
16
|
+
aria-hidden="true">
|
|
17
|
+
<CnIcon :name="module.icon" :size="22" />
|
|
18
|
+
</span>
|
|
19
|
+
<h3 :id="headingId" class="cn-module-card__title">
|
|
20
|
+
{{ module.label }}
|
|
21
|
+
</h3>
|
|
22
|
+
<CnStatusBadge
|
|
23
|
+
class="cn-module-card__state"
|
|
24
|
+
:label="module.enabled ? t('nextcloud-vue', 'In the menu') : t('nextcloud-vue', 'Off')"
|
|
25
|
+
:variant="module.enabled ? 'success' : 'default'"
|
|
26
|
+
data-testid="cn-module-card-state" />
|
|
27
|
+
</div>
|
|
28
|
+
<p v-if="module.description" class="cn-module-card__description">
|
|
29
|
+
{{ module.description }}
|
|
30
|
+
</p>
|
|
31
|
+
<template v-if="module.pages.length > 0">
|
|
32
|
+
<span class="cn-module-card__pages-line" data-testid="cn-module-card-pages-line">{{ pagesLine }}</span>
|
|
33
|
+
<ul class="cn-module-card__pages" :aria-label="t('nextcloud-vue', 'Pages')">
|
|
34
|
+
<li v-for="page in module.pages" :key="page.key">
|
|
35
|
+
<a
|
|
36
|
+
class="cn-module-card__page"
|
|
37
|
+
:href="page.url"
|
|
38
|
+
@click="follow(page, $event)">{{ page.label }}</a>
|
|
39
|
+
</li>
|
|
40
|
+
</ul>
|
|
41
|
+
</template>
|
|
42
|
+
<div v-if="module.open || (!module.enabled && module.settings)" class="cn-module-card__footer">
|
|
43
|
+
<a
|
|
44
|
+
v-if="module.open"
|
|
45
|
+
class="cn-module-card__open"
|
|
46
|
+
:href="module.open.url"
|
|
47
|
+
:aria-label="t('nextcloud-vue', 'Open {module}', { module: module.label })"
|
|
48
|
+
data-testid="cn-module-card-open"
|
|
49
|
+
@click="follow(module.open, $event)">
|
|
50
|
+
{{ t('nextcloud-vue', 'Open') }}
|
|
51
|
+
<ChevronRight :size="18" />
|
|
52
|
+
</a>
|
|
53
|
+
<span v-if="!module.enabled && module.settings" class="cn-module-card__settings" data-testid="cn-module-card-settings">
|
|
54
|
+
{{ module.settings.prefix || t('nextcloud-vue', 'Put it in the menu:') }}
|
|
55
|
+
<a
|
|
56
|
+
:href="module.settings.url"
|
|
57
|
+
:target="module.settings.external ? '_blank' : null"
|
|
58
|
+
:rel="module.settings.external ? 'noopener noreferrer' : null"
|
|
59
|
+
@click="follow(module.settings, $event)">{{ module.settings.label || t('nextcloud-vue', 'Admin settings') }}</a>
|
|
60
|
+
</span>
|
|
61
|
+
</div>
|
|
62
|
+
</article>
|
|
63
|
+
</template>
|
|
64
|
+
|
|
65
|
+
<script>
|
|
66
|
+
import { translatePlural as n, translate as t } from '@nextcloud/l10n'
|
|
67
|
+
import ChevronRight from 'vue-material-design-icons/ChevronRight.vue'
|
|
68
|
+
import CnStatusBadge from '../CnStatusBadge/CnStatusBadge.vue'
|
|
69
|
+
import { followLinkClick } from '../../utils/linkNavigation.js'
|
|
70
|
+
import { CnIcon } from '../CnIcon/index.js'
|
|
71
|
+
|
|
72
|
+
/** A module with more pages than this names its highlights instead of every page. */
|
|
73
|
+
const LISTED_PAGES = 4
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* CnModuleCard - one module on a `type: "links"` page (board PqModules): an
|
|
77
|
+
* icon, the title and a state badge (in the menu, or off), a description,
|
|
78
|
+
* the module's pages as a line and as links, an Open button, and for a
|
|
79
|
+
* module that is off a link to where an admin puts it in the menu.
|
|
80
|
+
*
|
|
81
|
+
* Rendered by CnLinkCardsPage for each entry of `config.modules`; the page
|
|
82
|
+
* resolves the links and the text before handing the module over.
|
|
83
|
+
*
|
|
84
|
+
* @spec openspec/changes/screens-links-module-cards/specs/link-cards-page/spec.md#requirement-a-links-page-can-show-modules
|
|
85
|
+
*/
|
|
86
|
+
export default {
|
|
87
|
+
name: 'CnModuleCard',
|
|
88
|
+
|
|
89
|
+
components: { ChevronRight, CnIcon, CnStatusBadge },
|
|
90
|
+
|
|
91
|
+
props: {
|
|
92
|
+
/**
|
|
93
|
+
* The resolved module: `{ id, label, description, icon, enabled,
|
|
94
|
+
* pages: [{ key, label, url, location, highlight }], open: { url,
|
|
95
|
+
* location }|null, settings: { url, location, external, label, prefix }|null }`.
|
|
96
|
+
*/
|
|
97
|
+
module: {
|
|
98
|
+
type: Object,
|
|
99
|
+
required: true,
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
/** Id of the card's heading, unique on the page. */
|
|
103
|
+
headingId: {
|
|
104
|
+
type: String,
|
|
105
|
+
required: true,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
computed: {
|
|
110
|
+
/**
|
|
111
|
+
* "4 pages: Overview, Leads, Prospects, Pipeline", or for a larger
|
|
112
|
+
* module "15 pages, including Campaigns, Blasts and Mailing lists"
|
|
113
|
+
* from the pages marked `highlight` (else the first three).
|
|
114
|
+
*
|
|
115
|
+
* @return {string} The line.
|
|
116
|
+
*/
|
|
117
|
+
pagesLine() {
|
|
118
|
+
const pages = this.module.pages
|
|
119
|
+
if (pages.length <= LISTED_PAGES) {
|
|
120
|
+
return n('nextcloud-vue', '%n page: {pages}', '%n pages: {pages}', pages.length, { pages: pages.map((page) => page.label).join(', ') })
|
|
121
|
+
}
|
|
122
|
+
const marked = pages.filter((page) => page.highlight)
|
|
123
|
+
const named = (marked.length > 0 ? marked : pages).slice(0, 3).map((page) => page.label)
|
|
124
|
+
const last = named.pop()
|
|
125
|
+
const list = named.length > 0 ? t('nextcloud-vue', '{list} and {last}', { list: named.join(', '), last }) : last
|
|
126
|
+
return n('nextcloud-vue', '%n page, including {pages}', '%n pages, including {pages}', pages.length, { pages: list })
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
methods: {
|
|
131
|
+
t,
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Route a plain click on an in-app link, so the page does not reload.
|
|
135
|
+
*
|
|
136
|
+
* @param {{location: (object|null)}} target The resolved link.
|
|
137
|
+
* @param {MouseEvent} event The click.
|
|
138
|
+
*/
|
|
139
|
+
follow(target, event) {
|
|
140
|
+
if (target && target.location) {
|
|
141
|
+
followLinkClick(event, target.location, this.$router)
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
}
|
|
146
|
+
</script>
|
|
147
|
+
|
|
148
|
+
<style scoped>
|
|
149
|
+
.cn-module-card {
|
|
150
|
+
display: flex;
|
|
151
|
+
flex-direction: column;
|
|
152
|
+
gap: 12px;
|
|
153
|
+
padding: 20px;
|
|
154
|
+
border: 1px solid var(--color-border);
|
|
155
|
+
border-radius: var(--border-radius-container-large, 12px);
|
|
156
|
+
background: var(--color-main-background);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.cn-module-card__head {
|
|
160
|
+
display: flex;
|
|
161
|
+
align-items: center;
|
|
162
|
+
gap: 12px;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.cn-module-card__icon {
|
|
166
|
+
display: flex;
|
|
167
|
+
flex: none;
|
|
168
|
+
align-items: center;
|
|
169
|
+
justify-content: center;
|
|
170
|
+
width: 42px;
|
|
171
|
+
height: 42px;
|
|
172
|
+
border-radius: var(--border-radius-large, 10px);
|
|
173
|
+
background: var(--color-primary-element-light);
|
|
174
|
+
color: var(--color-primary-element-light-text, var(--color-primary-element));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.cn-module-card__title {
|
|
178
|
+
flex: 1 1 auto;
|
|
179
|
+
margin: 0;
|
|
180
|
+
font-size: 17px;
|
|
181
|
+
font-weight: 700;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.cn-module-card__state {
|
|
185
|
+
flex: none;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.cn-module-card__description {
|
|
189
|
+
margin: 0;
|
|
190
|
+
font-size: 15px;
|
|
191
|
+
line-height: 1.5;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.cn-module-card__pages-line {
|
|
195
|
+
font-size: 13px;
|
|
196
|
+
color: var(--color-text-maxcontrast);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.cn-module-card__pages {
|
|
200
|
+
display: flex;
|
|
201
|
+
flex-wrap: wrap;
|
|
202
|
+
gap: 6px 12px;
|
|
203
|
+
margin: 0;
|
|
204
|
+
padding: 0;
|
|
205
|
+
list-style: none;
|
|
206
|
+
font-size: 14px;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.cn-module-card__page {
|
|
210
|
+
font-weight: 600;
|
|
211
|
+
color: var(--color-primary-element);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.cn-module-card__footer {
|
|
215
|
+
display: flex;
|
|
216
|
+
flex-wrap: wrap;
|
|
217
|
+
align-items: center;
|
|
218
|
+
gap: 8px 14px;
|
|
219
|
+
margin-top: auto;
|
|
220
|
+
padding-top: 4px;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.cn-module-card__open {
|
|
224
|
+
display: inline-flex;
|
|
225
|
+
align-items: center;
|
|
226
|
+
gap: 6px;
|
|
227
|
+
box-sizing: border-box;
|
|
228
|
+
height: 36px;
|
|
229
|
+
padding: 0 14px;
|
|
230
|
+
font-size: 14px;
|
|
231
|
+
font-weight: 600;
|
|
232
|
+
text-decoration: none;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.cn-module-card__open {
|
|
236
|
+
border: 1px solid var(--color-border-dark);
|
|
237
|
+
border-radius: var(--border-radius-element, 8px);
|
|
238
|
+
background: var(--color-main-background);
|
|
239
|
+
color: var(--color-main-text);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.cn-module-card__icon--off {
|
|
243
|
+
background: var(--color-background-dark);
|
|
244
|
+
color: var(--color-main-text);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.cn-module-card__settings {
|
|
248
|
+
font-size: 13px;
|
|
249
|
+
color: var(--color-text-maxcontrast);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.cn-module-card__open:focus-visible,
|
|
253
|
+
.cn-module-card__settings a:focus-visible,
|
|
254
|
+
.cn-module-card__page:focus-visible {
|
|
255
|
+
outline: 2px solid var(--color-primary-element);
|
|
256
|
+
outline-offset: 2px;
|
|
257
|
+
}
|
|
258
|
+
</style>
|