@conduction/nextcloud-vue 2.69.0 → 2.71.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 (153) hide show
  1. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js +25 -0
  2. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
  3. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +155 -152
  4. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
  5. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +77 -0
  6. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
  7. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +8 -8
  8. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
  9. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +151 -13
  10. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  11. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +3 -1
  12. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  13. package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue.js +1 -0
  14. package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue.js.map +1 -1
  15. package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue2.js +28 -0
  16. package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue2.js.map +1 -1
  17. package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue3.js +4 -2
  18. package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue3.js.map +1 -1
  19. package/dist/esm/components/CnCalendarWidget/CnCalendarWidget.vue2.js +25 -1
  20. package/dist/esm/components/CnCalendarWidget/CnCalendarWidget.vue2.js.map +1 -1
  21. package/dist/esm/components/CnCalendarWidget/CnCalendarWidget.vue3.js +32 -17
  22. package/dist/esm/components/CnCalendarWidget/CnCalendarWidget.vue3.js.map +1 -1
  23. package/dist/esm/components/CnCapabilityTable/CnCapabilityTable.vue2.js +14 -1
  24. package/dist/esm/components/CnCapabilityTable/CnCapabilityTable.vue2.js.map +1 -1
  25. package/dist/esm/components/CnCapabilityTable/CnCapabilityTable.vue3.js.map +1 -1
  26. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js +43 -2
  27. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js.map +1 -1
  28. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js +3 -3
  29. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js.map +1 -1
  30. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +25 -1
  31. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  32. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +40 -43
  33. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  34. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +35 -1
  35. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  36. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  37. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +54 -3
  38. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  39. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +6 -3
  40. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  41. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +28 -0
  42. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  43. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +3 -2
  44. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  45. package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue2.js +12 -0
  46. package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue2.js.map +1 -1
  47. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js +9 -0
  48. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js.map +1 -1
  49. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js.map +1 -1
  50. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +29 -2
  51. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  52. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +71 -34
  53. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  54. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js +54 -7
  55. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js.map +1 -1
  56. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js +26 -8
  57. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js.map +1 -1
  58. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +165 -0
  59. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  60. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +69 -26
  61. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  62. package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue2.js.map +1 -1
  63. package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue3.js.map +1 -1
  64. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +4 -1
  65. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  66. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  67. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue2.js +1 -1
  68. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue2.js.map +1 -1
  69. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue3.js.map +1 -1
  70. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
  71. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +46 -32
  72. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
  73. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue2.js +17 -3
  74. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue2.js.map +1 -1
  75. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue3.js +25 -24
  76. package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue3.js.map +1 -1
  77. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
  78. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
  79. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +27 -0
  80. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  81. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +7 -0
  82. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  83. package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue2.js +24 -1
  84. package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue2.js.map +1 -1
  85. package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue3.js +1 -1
  86. package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue3.js.map +1 -1
  87. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js +11 -0
  88. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js.map +1 -1
  89. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js +4 -2
  90. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js.map +1 -1
  91. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js +30 -9
  92. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js.map +1 -1
  93. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js.map +1 -1
  94. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js +15 -2
  95. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js.map +1 -1
  96. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue3.js +31 -32
  97. package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue3.js.map +1 -1
  98. package/dist/esm/composables/useWalkthrough.js +11 -3
  99. package/dist/esm/composables/useWalkthrough.js.map +1 -1
  100. package/dist/esm/nextcloud-vue.css +198 -0
  101. package/dist/esm/utils/capabilityComparison.js +29 -1
  102. package/dist/esm/utils/capabilityComparison.js.map +1 -1
  103. package/dist/esm/utils/columnFilters.js +54 -4
  104. package/dist/esm/utils/columnFilters.js.map +1 -1
  105. package/dist/esm/utils/userAutocomplete.js +69 -3
  106. package/dist/esm/utils/userAutocomplete.js.map +1 -1
  107. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  108. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  109. package/dist/nextcloud-vue.cjs.js +2778 -1630
  110. package/dist/nextcloud-vue.cjs.js.map +2 -2
  111. package/dist/nextcloud-vue.css +198 -0
  112. package/package.json +1 -1
  113. package/src/components/CnActionsBar/CnActionsBar.md +2 -0
  114. package/src/components/CnActionsBar/CnActionsBar.vue +28 -2
  115. package/src/components/CnAppNav/CnAppNav.vue +85 -8
  116. package/src/components/CnAppRoot/CnAppRoot.vue +153 -13
  117. package/src/components/CnBreadcrumbs/CnBreadcrumbs.vue +45 -0
  118. package/src/components/CnCalendarWidget/CnCalendarWidget.vue +32 -4
  119. package/src/components/CnCapabilityTable/CnCapabilityTable.vue +14 -0
  120. package/src/components/CnDashboardGrid/CnDashboardGrid.md +2 -0
  121. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +54 -2
  122. package/src/components/CnDashboardPage/CnDashboardPage.vue +46 -19
  123. package/src/components/CnDataTable/CnDataTable.vue +35 -1
  124. package/src/components/CnDetailPage/CnDetailPage.md +2 -0
  125. package/src/components/CnDetailPage/CnDetailPage.vue +57 -3
  126. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +30 -1
  127. package/src/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue +12 -0
  128. package/src/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue +9 -0
  129. package/src/components/CnFormDialog/CnFormDialog.vue +62 -2
  130. package/src/components/CnHeaderWidget/CnHeaderWidget.vue +95 -8
  131. package/src/components/CnIndexPage/CnIndexPage.vue +194 -7
  132. package/src/components/CnNextStepCard/CnNextStepCard.vue +11 -0
  133. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +4 -1
  134. package/src/components/CnObjectSidebar/CnNotesTab.vue +1 -1
  135. package/src/components/CnSetupWizard/CnSetupWizard.vue +7 -0
  136. package/src/components/CnStackedBarWidget/CnStackedBarWidget.vue +34 -7
  137. package/src/components/CnStagesWidget/CnStagesWidget.vue +19 -0
  138. package/src/components/CnStatWidget/CnStatWidget.vue +36 -0
  139. package/src/components/CnStatsBlock/CnStatsBlock.md +2 -0
  140. package/src/components/CnStatsBlock/CnStatsBlock.vue +25 -2
  141. package/src/components/CnStatsBlockWidget/CnStatsBlockWidget.vue +13 -0
  142. package/src/components/CnWalkthrough/CnWalkthrough.vue +30 -9
  143. package/src/components/CnWeekStripWidget/CnWeekStripWidget.vue +24 -3
  144. package/src/composables/useWalkthrough.js +11 -3
  145. package/src/css/dashboard.css +44 -0
  146. package/src/css/detail-page.css +10 -0
  147. package/src/css/index-page.css +12 -0
  148. package/src/css/kpi-card.css +15 -0
  149. package/src/schemas/app-manifest-v2.schema.json +47 -5
  150. package/src/utils/capabilityComparison.js +28 -0
  151. package/src/utils/columnFilters.js +54 -4
  152. package/src/utils/userAutocomplete.js +68 -2
  153. package/src/utils/validateManifestV2.compiled.js +2 -2
@@ -1,7 +1,7 @@
1
1
  import { getLanguage, translatePlural, translate } from '@nextcloud/l10n';
2
2
  import { NcButton, NcEmptyContent, NcTextField } from '@nextcloud/vue';
3
3
  import Magnify from 'vue-material-design-icons/Magnify.vue';
4
- import { defaultGroupMode, hasProviders, groupCapabilities, resolveProvider, RATINGS } from '../../utils/capabilityComparison.js';
4
+ import { defaultGroupMode, hasProviders, groupCapabilities, warnCapabilityComparisonDeprecated, resolveProvider, RATINGS } from '../../utils/capabilityComparison.js';
5
5
 
6
6
  /**
7
7
  * How long the table waits after the last keystroke before it filters.
@@ -26,6 +26,13 @@ var script = {
26
26
  * document without them renders the table this component's consumers
27
27
  * already had.
28
28
  *
29
+ * Deprecated, with the whole component: publish the comparison on the
30
+ * app's docs site and link to it (as dossiq #3312 and pipelinq #2195
31
+ * did). Still rendered; removal waits for a major version. A
32
+ * development build warns once when a comparison is given.
33
+ *
34
+ * @deprecated since 2.66.0, publish the comparison on the app's docs site and link to it.
35
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-the-in-app-capability-comparison-is-deprecated
29
36
  * @type {object}
30
37
  */
31
38
  comparison: {
@@ -251,6 +258,12 @@ var script = {
251
258
  }
252
259
  },
253
260
 
261
+ mounted() {
262
+ if (this.comparison) {
263
+ warnCapabilityComparisonDeprecated('CnCapabilityTable');
264
+ }
265
+ },
266
+
254
267
  methods: {
255
268
  /**
256
269
  * Debounce the reader's typing into `query`.
@@ -1 +1 @@
1
- {"version":3,"file":"CnCapabilityTable.vue2.js","sources":["../../../../src/components/CnCapabilityTable/CnCapabilityTable.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n -\n - CnCapabilityTable — the capability comparison, searchable and grouped.\n -\n - Two readers arrive at this table. One is shopping for a FEATURE, the\n - sellable bundle they can name in a tender. The other is checking one\n - CAPABILITY, a behaviour they know they need. The grouping toggle serves the\n - first and the search box serves the second, over the same rows.\n -\n - The provided-by column answers the question a Nextcloud buyer asks next:\n - does this app do the work itself, does a sibling app do it, or does the\n - platform already do it? A capability that Nextcloud provides is not a gap,\n - and a table that cannot say so understates the product.\n -\n - EVERYTHING HERE IS DRIVEN BY THE DOCUMENT, and no row is ever dropped. An\n - unknown provider key renders as the raw key. An area or feature the\n - document never declared gets a group of its own. A row that names no\n - feature lands under a heading that says so. See utils/capabilityComparison.js.\n-->\n\n<template>\n\t<div class=\"cn-capability-table\">\n\t\t<div class=\"cn-capability-table__controls\">\n\t\t\t<div v-if=\"searchable\" class=\"cn-capability-table__search\">\n\t\t\t\t<NcTextField\n\t\t\t\t\t:modelValue=\"rawQuery\"\n\t\t\t\t\t:label=\"searchLabel\"\n\t\t\t\t\t:placeholder=\"searchPlaceholder\"\n\t\t\t\t\ttrailingButtonIcon=\"close\"\n\t\t\t\t\t:showTrailingButton=\"rawQuery !== ''\"\n\t\t\t\t\t:trailingButtonLabel=\"clearLabel\"\n\t\t\t\t\t@update:modelValue=\"onQueryInput\"\n\t\t\t\t\t@trailingButtonClick=\"clearQuery\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<Magnify :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t</NcTextField>\n\t\t\t</div>\n\n\t\t\t<!-- A toggle group, deliberately not an ARIA tab widget: a tab role\n\t\t\t promises the arrow-key roving focus the ARIA practices describe,\n\t\t\t and two plain buttons carrying aria-pressed keep the native\n\t\t\t button semantics that already work for everyone. -->\n\t\t\t<div\n\t\t\t\tv-if=\"groupingAvailable\"\n\t\t\t\tclass=\"cn-capability-table__grouping\"\n\t\t\t\trole=\"group\"\n\t\t\t\t:aria-label=\"groupingGroupLabel\">\n\t\t\t\t<NcButton\n\t\t\t\t\tclass=\"cn-capability-table__group-button\"\n\t\t\t\t\t:aria-pressed=\"String(groupMode === 'feature')\"\n\t\t\t\t\t:variant=\"groupMode === 'feature' ? 'primary' : 'tertiary'\"\n\t\t\t\t\t@click=\"setGroupMode('feature')\">\n\t\t\t\t\t{{ byFeatureLabel }}\n\t\t\t\t</NcButton>\n\t\t\t\t<NcButton\n\t\t\t\t\tclass=\"cn-capability-table__group-button\"\n\t\t\t\t\t:aria-pressed=\"String(groupMode === 'area')\"\n\t\t\t\t\t:variant=\"groupMode === 'area' ? 'primary' : 'tertiary'\"\n\t\t\t\t\t@click=\"setGroupMode('area')\">\n\t\t\t\t\t{{ byAreaLabel }}\n\t\t\t\t</NcButton>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- role=\"status\" is an implicit polite live region, so a reader on a\n\t\t screen reader hears the new count after each keystroke settles. -->\n\t\t<p class=\"cn-capability-table__count\" role=\"status\">\n\t\t\t{{ resultCountText }}\n\t\t</p>\n\n\t\t<div v-if=\"groups.length === 0\" class=\"cn-capability-table__empty\">\n\t\t\t<NcEmptyContent :name=\"emptyName\" :description=\"emptyDescription\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<Magnify :size=\"48\" />\n\t\t\t\t</template>\n\t\t\t</NcEmptyContent>\n\t\t</div>\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-capability-table__group\">\n\t\t\t<h3 class=\"cn-capability-table__group-title\">\n\t\t\t\t{{ group.label }}\n\t\t\t\t<span class=\"cn-capability-table__group-count\">{{ group.capabilities.length }}</span>\n\t\t\t</h3>\n\t\t\t<div class=\"cn-capability-table__scroller\">\n\t\t\t\t<table class=\"cn-capability-table__table\">\n\t\t\t\t\t<caption class=\"cn-capability-table__caption\">\n\t\t\t\t\t\t{{ captionFor(group) }}\n\t\t\t\t\t</caption>\n\t\t\t\t\t<thead>\n\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t<th scope=\"col\" class=\"cn-capability-table__num\">\n\t\t\t\t\t\t\t\t{{ numberHeader }}\n\t\t\t\t\t\t\t</th>\n\t\t\t\t\t\t\t<th scope=\"col\">\n\t\t\t\t\t\t\t\t{{ capabilityHeader }}\n\t\t\t\t\t\t\t</th>\n\t\t\t\t\t\t\t<th v-if=\"showProviderColumn\" scope=\"col\">\n\t\t\t\t\t\t\t\t{{ providerHeader }}\n\t\t\t\t\t\t\t</th>\n\t\t\t\t\t\t\t<th\n\t\t\t\t\t\t\t\tv-for=\"system in systems\"\n\t\t\t\t\t\t\t\t:key=\"system.key\"\n\t\t\t\t\t\t\t\tscope=\"col\"\n\t\t\t\t\t\t\t\t:class=\"{ 'cn-capability-table__col--self': system.isSelf }\">\n\t\t\t\t\t\t\t\t{{ system.name }}\n\t\t\t\t\t\t\t</th>\n\t\t\t\t\t\t</tr>\n\t\t\t\t\t</thead>\n\t\t\t\t\t<tbody>\n\t\t\t\t\t\t<!-- Keyed by position and not by `row.id`: a document\n\t\t\t\t\t\t that repeats an id, or omits one, is exactly the\n\t\t\t\t\t\t shape this table promises to still render, and a\n\t\t\t\t\t\t duplicate key is where that promise would break. -->\n\t\t\t\t\t\t<tr\n\t\t\t\t\t\t\tv-for=\"(row, index) in group.capabilities\"\n\t\t\t\t\t\t\t:key=\"`${group.key}-${index}`\">\n\t\t\t\t\t\t\t<td class=\"cn-capability-table__num\">\n\t\t\t\t\t\t\t\t{{ row.id }}\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t<th scope=\"row\" class=\"cn-capability-table__cap\">\n\t\t\t\t\t\t\t\t{{ row.label }}\n\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\tv-if=\"isLowConfidence(row)\"\n\t\t\t\t\t\t\t\t\tclass=\"cn-capability-table__confidence\"\n\t\t\t\t\t\t\t\t\t:title=\"lowConfidenceHint\">\n\t\t\t\t\t\t\t\t\t<span aria-hidden=\"true\">?</span>\n\t\t\t\t\t\t\t\t\t<span class=\"cn-capability-table__sr-only\">{{ lowConfidenceHint }}</span>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</th>\n\t\t\t\t\t\t\t<td v-if=\"showProviderColumn\" class=\"cn-capability-table__provider-cell\">\n\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\tv-if=\"providerOf(row)\"\n\t\t\t\t\t\t\t\t\tclass=\"cn-capability-table__provider\"\n\t\t\t\t\t\t\t\t\t:class=\"'cn-capability-table__provider--' + providerOf(row).kind\">\n\t\t\t\t\t\t\t\t\t<span class=\"cn-capability-table__provider-name\">{{ providerOf(row).name }}</span>\n\t\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\t\tv-if=\"providerHowOf(row)\"\n\t\t\t\t\t\t\t\t\t\tclass=\"cn-capability-table__provider-how\">\n\t\t\t\t\t\t\t\t\t\t{{ providerHowOf(row) }}\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\t\tv-if=\"providerKindLabel(providerOf(row).kind)\"\n\t\t\t\t\t\t\t\t\t\tclass=\"cn-capability-table__provider-kind\">\n\t\t\t\t\t\t\t\t\t\t{{ providerKindLabel(providerOf(row).kind) }}\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t<td\n\t\t\t\t\t\t\t\tv-for=\"system in systems\"\n\t\t\t\t\t\t\t\t:key=\"system.key\"\n\t\t\t\t\t\t\t\t:class=\"{ 'cn-capability-table__col--self': system.isSelf }\">\n\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\tclass=\"cn-capability-table__chip\"\n\t\t\t\t\t\t\t\t\t:class=\"'cn-capability-table__chip--' + ratingOf(row, system.key)\">\n\t\t\t\t\t\t\t\t\t{{ ratingLabel(ratingOf(row, system.key)) }}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t</tr>\n\t\t\t\t\t</tbody>\n\t\t\t\t</table>\n\t\t\t</div>\n\t\t</section>\n\t</div>\n</template>\n\n<script>\n/**\n * SPDX-License-Identifier: EUPL-1.2\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n *\n * CnCapabilityTable — a searchable, groupable capability comparison.\n *\n * Spec: features-roadmap-component — Requirement \"CnCapabilityTable\".\n */\nimport { getLanguage, translatePlural as n, translate as t } from '@nextcloud/l10n'\nimport { NcButton, NcEmptyContent, NcTextField } from '@nextcloud/vue'\nimport Magnify from 'vue-material-design-icons/Magnify.vue'\nimport {\n\tdefaultGroupMode,\n\tgroupCapabilities,\n\thasProviders,\n\tRATINGS,\n\tresolveProvider,\n} from '../../utils/capabilityComparison.js'\n\n/**\n * How long the table waits after the last keystroke before it filters.\n *\n * 200 ms is the shortest pause that stops a 371-row regroup running on every\n * character while still feeling immediate to a touch typist.\n *\n * @type {number}\n */\nconst SEARCH_DEBOUNCE_MS = 200\n\nexport default {\n\tname: 'CnCapabilityTable',\n\n\tcomponents: { NcButton, NcEmptyContent, NcTextField, Magnify },\n\n\tprops: {\n\t\t/**\n\t\t * The comparison document the host app supplies. Shape:\n\t\t * `{systems, areas, features?, providers?, capabilities}`. Every key\n\t\t * beyond `systems`, `areas` and `capabilities` is optional, and a\n\t\t * document without them renders the table this component's consumers\n\t\t * already had.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tcomparison: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * BCP 47 locale used to pick `name` or `name_nl` on every labelled\n\t\t * entry. Defaults to the reader's Nextcloud language.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tlocale: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Render the search box. Leave it on unless the host already filters\n\t\t * the rows it passes.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tsearchable: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Force a grouping: `feature` or `area`. When empty the table opens on\n\t\t * features if any row carries one, and on areas otherwise, and the\n\t\t * reader can switch.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tgroupBy: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\t/**\n\t\t\t * What the reader has typed, bound straight to the field. Kept\n\t\t\t * separate from `query`: binding the field to the DEBOUNCED value\n\t\t\t * makes it a controlled input that snaps back to the old text\n\t\t\t * between keystrokes, which reads as a broken search box.\n\t\t\t */\n\t\t\trawQuery: '',\n\t\t\t/** The debounced query the table actually filters on. */\n\t\t\tquery: '',\n\t\t\t/** The reader's grouping choice; empty until they make one. */\n\t\t\tchosenGroupMode: '',\n\t\t\t/** Handle of the pending debounce, cleared on unmount. */\n\t\t\tdebounceHandle: null,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * @return {string} The reader's locale.\n\t\t */\n\t\teffectiveLocale() {\n\t\t\tif (this.locale) {\n\t\t\t\treturn this.locale\n\t\t\t}\n\t\t\t// `getLanguage()` and not a Dutch-defaulting resolver: mixing a\n\t\t\t// Dutch-defaulting label helper with `t()`, which falls back to its\n\t\t\t// English source, ships Dutch capability names inside English\n\t\t\t// chrome. One source, one language.\n\t\t\treturn getLanguage() || 'en'\n\t\t},\n\n\t\t/**\n\t\t * @return {Array<object>} The system columns, in document order.\n\t\t */\n\t\tsystems() {\n\t\t\treturn this.comparison?.systems ?? []\n\t\t},\n\n\t\t/**\n\t\t * @return {number} How many rows the document holds.\n\t\t */\n\t\ttotalCount() {\n\t\t\treturn this.comparison?.capabilities?.length ?? 0\n\t\t},\n\n\t\t/**\n\t\t * The grouping `groupBy` locks, or empty when it names neither.\n\t\t *\n\t\t * Read by both `groupMode` and `groupingAvailable`, because a value\n\t\t * neither of them recognises must be ignored by both: testing the raw\n\t\t * prop for emptiness let `groupBy=\"areas\"` hide the toggle while the\n\t\t * mode fell back to auto, so a typo silently removed the control.\n\t\t *\n\t\t * @return {string} `feature`, `area`, or empty.\n\t\t */\n\t\tforcedGroupMode() {\n\t\t\treturn this.groupBy === 'feature' || this.groupBy === 'area' ? this.groupBy : ''\n\t\t},\n\n\t\t/**\n\t\t * The grouping in force: the `groupBy` prop, else the reader's choice,\n\t\t * else what the document suggests.\n\t\t *\n\t\t * @return {'feature'|'area'} The mode.\n\t\t */\n\t\tgroupMode() {\n\t\t\treturn this.forcedGroupMode || this.chosenGroupMode || defaultGroupMode(this.comparison)\n\t\t},\n\n\t\t/**\n\t\t * The toggle is pointless on a document that maps no features, so it\n\t\t * only renders once there is a second grouping to switch to.\n\t\t *\n\t\t * @return {boolean} Whether to render the grouping toggle.\n\t\t */\n\t\tgroupingAvailable() {\n\t\t\treturn !this.forcedGroupMode && defaultGroupMode(this.comparison) === 'feature'\n\t\t},\n\n\t\t/**\n\t\t * @return {boolean} Whether any row names a provider.\n\t\t */\n\t\tshowProviderColumn() {\n\t\t\treturn hasProviders(this.comparison)\n\t\t},\n\n\t\t/**\n\t\t * @return {Array<object>} The groups to render, already filtered.\n\t\t */\n\t\tgroups() {\n\t\t\treturn groupCapabilities(this.comparison, {\n\t\t\t\tmode: this.groupMode,\n\t\t\t\tlocale: this.effectiveLocale,\n\t\t\t\tquery: this.query,\n\t\t\t\tunmappedLabel: this.unmappedLabel,\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * @return {number} How many rows survived the search.\n\t\t */\n\t\tvisibleCount() {\n\t\t\treturn this.groups.reduce((sum, group) => sum + group.capabilities.length, 0)\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The line that tells a reader how much they are seeing.\n\t\t */\n\t\tresultCountText() {\n\t\t\treturn n(\n\t\t\t\t'nextcloud-vue',\n\t\t\t\t'You see {count} capability of {total}.',\n\t\t\t\t'You see {count} capabilities of {total}.',\n\t\t\t\tthis.visibleCount,\n\t\t\t\t{ count: this.visibleCount, total: this.totalCount },\n\t\t\t)\n\t\t},\n\n\t\tsearchLabel() {\n\t\t\treturn t('nextcloud-vue', 'Search capabilities')\n\t\t},\n\n\t\tsearchPlaceholder() {\n\t\t\treturn t('nextcloud-vue', 'Name, number, area or feature')\n\t\t},\n\n\t\tclearLabel() {\n\t\t\treturn t('nextcloud-vue', 'Clear the search')\n\t\t},\n\n\t\tgroupingGroupLabel() {\n\t\t\treturn t('nextcloud-vue', 'Group the capabilities')\n\t\t},\n\n\t\tbyFeatureLabel() {\n\t\t\treturn t('nextcloud-vue', 'Group by feature')\n\t\t},\n\n\t\tbyAreaLabel() {\n\t\t\treturn t('nextcloud-vue', 'Group by area')\n\t\t},\n\n\t\tunmappedLabel() {\n\t\t\treturn t('nextcloud-vue', 'Not yet mapped to a feature')\n\t\t},\n\n\t\tnumberHeader() {\n\t\t\treturn t('nextcloud-vue', 'No.')\n\t\t},\n\n\t\tcapabilityHeader() {\n\t\t\treturn t('nextcloud-vue', 'Capability')\n\t\t},\n\n\t\tproviderHeader() {\n\t\t\treturn t('nextcloud-vue', 'Provided by')\n\t\t},\n\n\t\tlowConfidenceHint() {\n\t\t\treturn t('nextcloud-vue', 'We matched this row to its feature by judgement.')\n\t\t},\n\n\t\temptyName() {\n\t\t\t// `escape: false` because the result is rendered through `{{ }}`,\n\t\t\t// which escapes it already. Left on, t() turns a search for\n\t\t\t// `a & b` into the literal \"No capability matches a &amp; b\".\n\t\t\treturn t('nextcloud-vue', 'No capability matches {query}', { query: this.query }, undefined, { escape: false })\n\t\t},\n\n\t\temptyDescription() {\n\t\t\treturn t('nextcloud-vue', 'Clear the search box or try another word.')\n\t\t},\n\t},\n\n\tbeforeUnmount() {\n\t\tif (this.debounceHandle !== null) {\n\t\t\tclearTimeout(this.debounceHandle)\n\t\t\tthis.debounceHandle = null\n\t\t}\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Debounce the reader's typing into `query`.\n\t\t *\n\t\t * @param {string} value The raw input value.\n\t\t */\n\t\tonQueryInput(value) {\n\t\t\tthis.rawQuery = value ?? ''\n\t\t\tif (this.debounceHandle !== null) {\n\t\t\t\tclearTimeout(this.debounceHandle)\n\t\t\t}\n\t\t\tthis.debounceHandle = setTimeout(() => {\n\t\t\t\tthis.debounceHandle = null\n\t\t\t\tthis.query = this.rawQuery\n\t\t\t}, SEARCH_DEBOUNCE_MS)\n\t\t},\n\n\t\t/**\n\t\t * Empty the search box at once, without waiting out the debounce.\n\t\t */\n\t\tclearQuery() {\n\t\t\tif (this.debounceHandle !== null) {\n\t\t\t\tclearTimeout(this.debounceHandle)\n\t\t\t\tthis.debounceHandle = null\n\t\t\t}\n\t\t\tthis.rawQuery = ''\n\t\t\tthis.query = ''\n\t\t},\n\n\t\t/**\n\t\t * @param {'feature'|'area'} mode The grouping the reader picked.\n\t\t */\n\t\tsetGroupMode(mode) {\n\t\t\tthis.chosenGroupMode = mode\n\t\t},\n\n\t\t/**\n\t\t * @param {object} row One capability row.\n\t\t * @return {object|null} Its provider, or null when it names none.\n\t\t */\n\t\tproviderOf(row) {\n\t\t\treturn resolveProvider(row, this.comparison, this.effectiveLocale)\n\t\t},\n\n\t\t/**\n\t\t * The kind word rendered beside the provider name. It is TEXT, not a\n\t\t * colour: a reader who cannot tell the chips apart still reads which\n\t\t * of the three kinds this is.\n\t\t *\n\t\t * @param {string} kind One of `self`, `app`, `platform`, `unknown`.\n\t\t * @return {string} The label, empty for an unrecognised kind.\n\t\t */\n\t\tproviderKindLabel(kind) {\n\t\t\tif (kind === 'self') {\n\t\t\t\treturn t('nextcloud-vue', 'this app')\n\t\t\t}\n\t\t\tif (kind === 'app') {\n\t\t\t\treturn t('nextcloud-vue', 'another app')\n\t\t\t}\n\t\t\tif (kind === 'platform') {\n\t\t\t\treturn t('nextcloud-vue', 'the platform')\n\t\t\t}\n\t\t\treturn ''\n\t\t},\n\n\t\t/**\n\t\t * The mechanism the document names for this row, rendered under the\n\t\t * provider name.\n\t\t *\n\t\t * It is a free-text key the audit writes, so it is shown as given\n\t\t * rather than translated. Rendering it is a deliberate choice: a field\n\t\t * the contract carries and the table quietly ignores is a field nobody\n\t\t * finds out is wrong.\n\t\t *\n\t\t * @param {object} row One capability row.\n\t\t * @return {string} The mechanism, empty when the row names none.\n\t\t */\n\t\tproviderHowOf(row) {\n\t\t\tconst how = row?.providerHow\n\t\t\treturn typeof how === 'string' ? how.trim() : ''\n\t\t},\n\n\t\t/**\n\t\t * @param {object} row One capability row.\n\t\t * @return {boolean} Whether to mark the row's feature as a judgement.\n\t\t */\n\t\tisLowConfidence(row) {\n\t\t\treturn row?.featureConfidence === 'low'\n\t\t\t\t&& typeof row?.feature === 'string'\n\t\t\t\t&& row.feature.trim() !== ''\n\t\t},\n\n\t\t/**\n\t\t * A rating outside the three the audit emits is shown as `unknown`\n\t\t * rather than blanked, so a typo in the document is visible.\n\t\t *\n\t\t * @param {object} row One capability row.\n\t\t * @param {string} systemKey The system column.\n\t\t * @return {string} The rating to render.\n\t\t */\n\t\tratingOf(row, systemKey) {\n\t\t\tconst rating = row?.[systemKey]\n\t\t\treturn RATINGS.includes(rating) ? rating : 'unknown'\n\t\t},\n\n\t\t/**\n\t\t * @param {string} rating One of `yes`, `partial`, `no`, `unknown`.\n\t\t * @return {string} The reader's word for it.\n\t\t */\n\t\tratingLabel(rating) {\n\t\t\tif (rating === 'yes') {\n\t\t\t\treturn t('nextcloud-vue', 'Yes')\n\t\t\t}\n\t\t\tif (rating === 'partial') {\n\t\t\t\treturn t('nextcloud-vue', 'Partial')\n\t\t\t}\n\t\t\tif (rating === 'no') {\n\t\t\t\treturn t('nextcloud-vue', 'No')\n\t\t\t}\n\t\t\treturn t('nextcloud-vue', 'Unknown')\n\t\t},\n\n\t\t/**\n\t\t * @param {object} group One rendered group.\n\t\t * @return {string} Its table caption.\n\t\t */\n\t\tcaptionFor(group) {\n\t\t\t// See `emptyName`: an area called \"Zaken & Documenten\" would reach\n\t\t\t// the caption, and a screen reader, as \"Zaken &amp; Documenten\".\n\t\t\treturn t('nextcloud-vue', 'Capabilities in {group}, rated per system.', {\n\t\t\t\tgroup: group.label,\n\t\t\t}, undefined, { escape: false })\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-capability-table__controls {\n\tdisplay: flex;\n\tgap: 12px;\n\talign-items: flex-end;\n\tflex-wrap: wrap;\n\tmargin-bottom: 8px;\n}\n\n.cn-capability-table__search {\n\tflex: 1 1 280px;\n\tmin-width: 220px;\n}\n\n.cn-capability-table__grouping {\n\tdisplay: flex;\n\tgap: 8px;\n\tflex-wrap: wrap;\n}\n\n.cn-capability-table__count {\n\tmargin: 0 0 16px;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n}\n\n.cn-capability-table__group {\n\tmargin-bottom: 24px;\n}\n\n.cn-capability-table__group-title {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\tmargin: 0 0 8px;\n\tfont-size: 1.05em;\n\tcolor: var(--color-main-text);\n}\n\n.cn-capability-table__group-count {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n\tfont-weight: normal;\n}\n\n.cn-capability-table__scroller {\n\toverflow-x: auto;\n}\n\n.cn-capability-table__table {\n\twidth: 100%;\n\tborder-collapse: collapse;\n}\n\n.cn-capability-table__caption {\n\ttext-align: start;\n\tcaption-side: top;\n\tpadding-bottom: 8px;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n}\n\n.cn-capability-table__table th,\n.cn-capability-table__table td {\n\tborder-bottom: 1px solid var(--color-border);\n\tpadding: 6px 8px;\n\ttext-align: start;\n\tvertical-align: top;\n}\n\n.cn-capability-table__table thead th {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-weight: bold;\n\twhite-space: nowrap;\n}\n\n.cn-capability-table__num {\n\twhite-space: nowrap;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-capability-table__cap {\n\tfont-weight: normal;\n\tcolor: var(--color-main-text);\n\tmin-width: 240px;\n}\n\n.cn-capability-table__col--self {\n\tbackground-color: var(--color-background-hover);\n}\n\n.cn-capability-table__confidence {\n\tdisplay: inline-block;\n\tmargin-inline-start: 4px;\n\tcolor: var(--color-text-maxcontrast);\n\tcursor: help;\n}\n\n.cn-capability-table__sr-only {\n\tposition: absolute;\n\twidth: 1px;\n\theight: 1px;\n\toverflow: hidden;\n\tclip-path: inset(50%);\n\twhite-space: nowrap;\n\t/* Padding and border are part of the recipe, not decoration: either one\n\t inherited onto the 1px box grows it back into the layout. */\n\tpadding: 0;\n\tborder: 0;\n\tmargin: -1px;\n}\n\n.cn-capability-table__provider {\n\tdisplay: inline-flex;\n\tflex-direction: column;\n\tgap: 2px;\n}\n\n.cn-capability-table__provider-name {\n\tborder-inline-start: 3px solid var(--color-border-dark);\n\tpadding-inline-start: 6px;\n\tcolor: var(--color-main-text);\n}\n\n.cn-capability-table__provider--self .cn-capability-table__provider-name {\n\tborder-inline-start-color: var(--color-primary-element);\n}\n\n.cn-capability-table__provider--app .cn-capability-table__provider-name {\n\tborder-inline-start-color: var(--color-success);\n}\n\n.cn-capability-table__provider--platform .cn-capability-table__provider-name {\n\tborder-inline-start-color: var(--color-warning);\n}\n\n.cn-capability-table__provider-kind,\n.cn-capability-table__provider-how {\n\tpadding-inline-start: 9px;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.8em;\n}\n\n.cn-capability-table__chip {\n\tdisplay: inline-block;\n\tpadding: 1px 8px;\n\tborder-radius: var(--border-radius-pill, 100px);\n\tborder: 1px solid var(--color-border-dark);\n\tcolor: var(--color-main-text);\n\tfont-size: 0.85em;\n\twhite-space: nowrap;\n}\n\n.cn-capability-table__chip--yes {\n\tborder-color: var(--color-success);\n}\n\n.cn-capability-table__chip--partial {\n\tborder-color: var(--color-warning);\n}\n\n.cn-capability-table__chip--no {\n\tborder-color: var(--color-error);\n}\n\n.cn-capability-table__chip--unknown {\n\tborder-style: dashed;\n\tcolor: var(--color-text-maxcontrast);\n}\n</style>\n"],"names":["n","t"],"mappings":"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"}
1
+ {"version":3,"file":"CnCapabilityTable.vue2.js","sources":["../../../../src/components/CnCapabilityTable/CnCapabilityTable.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n -\n - CnCapabilityTable — the capability comparison, searchable and grouped.\n -\n - Two readers arrive at this table. One is shopping for a FEATURE, the\n - sellable bundle they can name in a tender. The other is checking one\n - CAPABILITY, a behaviour they know they need. The grouping toggle serves the\n - first and the search box serves the second, over the same rows.\n -\n - The provided-by column answers the question a Nextcloud buyer asks next:\n - does this app do the work itself, does a sibling app do it, or does the\n - platform already do it? A capability that Nextcloud provides is not a gap,\n - and a table that cannot say so understates the product.\n -\n - EVERYTHING HERE IS DRIVEN BY THE DOCUMENT, and no row is ever dropped. An\n - unknown provider key renders as the raw key. An area or feature the\n - document never declared gets a group of its own. A row that names no\n - feature lands under a heading that says so. See utils/capabilityComparison.js.\n-->\n\n<template>\n\t<div class=\"cn-capability-table\">\n\t\t<div class=\"cn-capability-table__controls\">\n\t\t\t<div v-if=\"searchable\" class=\"cn-capability-table__search\">\n\t\t\t\t<NcTextField\n\t\t\t\t\t:modelValue=\"rawQuery\"\n\t\t\t\t\t:label=\"searchLabel\"\n\t\t\t\t\t:placeholder=\"searchPlaceholder\"\n\t\t\t\t\ttrailingButtonIcon=\"close\"\n\t\t\t\t\t:showTrailingButton=\"rawQuery !== ''\"\n\t\t\t\t\t:trailingButtonLabel=\"clearLabel\"\n\t\t\t\t\t@update:modelValue=\"onQueryInput\"\n\t\t\t\t\t@trailingButtonClick=\"clearQuery\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<Magnify :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t</NcTextField>\n\t\t\t</div>\n\n\t\t\t<!-- A toggle group, deliberately not an ARIA tab widget: a tab role\n\t\t\t promises the arrow-key roving focus the ARIA practices describe,\n\t\t\t and two plain buttons carrying aria-pressed keep the native\n\t\t\t button semantics that already work for everyone. -->\n\t\t\t<div\n\t\t\t\tv-if=\"groupingAvailable\"\n\t\t\t\tclass=\"cn-capability-table__grouping\"\n\t\t\t\trole=\"group\"\n\t\t\t\t:aria-label=\"groupingGroupLabel\">\n\t\t\t\t<NcButton\n\t\t\t\t\tclass=\"cn-capability-table__group-button\"\n\t\t\t\t\t:aria-pressed=\"String(groupMode === 'feature')\"\n\t\t\t\t\t:variant=\"groupMode === 'feature' ? 'primary' : 'tertiary'\"\n\t\t\t\t\t@click=\"setGroupMode('feature')\">\n\t\t\t\t\t{{ byFeatureLabel }}\n\t\t\t\t</NcButton>\n\t\t\t\t<NcButton\n\t\t\t\t\tclass=\"cn-capability-table__group-button\"\n\t\t\t\t\t:aria-pressed=\"String(groupMode === 'area')\"\n\t\t\t\t\t:variant=\"groupMode === 'area' ? 'primary' : 'tertiary'\"\n\t\t\t\t\t@click=\"setGroupMode('area')\">\n\t\t\t\t\t{{ byAreaLabel }}\n\t\t\t\t</NcButton>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- role=\"status\" is an implicit polite live region, so a reader on a\n\t\t screen reader hears the new count after each keystroke settles. -->\n\t\t<p class=\"cn-capability-table__count\" role=\"status\">\n\t\t\t{{ resultCountText }}\n\t\t</p>\n\n\t\t<div v-if=\"groups.length === 0\" class=\"cn-capability-table__empty\">\n\t\t\t<NcEmptyContent :name=\"emptyName\" :description=\"emptyDescription\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<Magnify :size=\"48\" />\n\t\t\t\t</template>\n\t\t\t</NcEmptyContent>\n\t\t</div>\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-capability-table__group\">\n\t\t\t<h3 class=\"cn-capability-table__group-title\">\n\t\t\t\t{{ group.label }}\n\t\t\t\t<span class=\"cn-capability-table__group-count\">{{ group.capabilities.length }}</span>\n\t\t\t</h3>\n\t\t\t<div class=\"cn-capability-table__scroller\">\n\t\t\t\t<table class=\"cn-capability-table__table\">\n\t\t\t\t\t<caption class=\"cn-capability-table__caption\">\n\t\t\t\t\t\t{{ captionFor(group) }}\n\t\t\t\t\t</caption>\n\t\t\t\t\t<thead>\n\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t<th scope=\"col\" class=\"cn-capability-table__num\">\n\t\t\t\t\t\t\t\t{{ numberHeader }}\n\t\t\t\t\t\t\t</th>\n\t\t\t\t\t\t\t<th scope=\"col\">\n\t\t\t\t\t\t\t\t{{ capabilityHeader }}\n\t\t\t\t\t\t\t</th>\n\t\t\t\t\t\t\t<th v-if=\"showProviderColumn\" scope=\"col\">\n\t\t\t\t\t\t\t\t{{ providerHeader }}\n\t\t\t\t\t\t\t</th>\n\t\t\t\t\t\t\t<th\n\t\t\t\t\t\t\t\tv-for=\"system in systems\"\n\t\t\t\t\t\t\t\t:key=\"system.key\"\n\t\t\t\t\t\t\t\tscope=\"col\"\n\t\t\t\t\t\t\t\t:class=\"{ 'cn-capability-table__col--self': system.isSelf }\">\n\t\t\t\t\t\t\t\t{{ system.name }}\n\t\t\t\t\t\t\t</th>\n\t\t\t\t\t\t</tr>\n\t\t\t\t\t</thead>\n\t\t\t\t\t<tbody>\n\t\t\t\t\t\t<!-- Keyed by position and not by `row.id`: a document\n\t\t\t\t\t\t that repeats an id, or omits one, is exactly the\n\t\t\t\t\t\t shape this table promises to still render, and a\n\t\t\t\t\t\t duplicate key is where that promise would break. -->\n\t\t\t\t\t\t<tr\n\t\t\t\t\t\t\tv-for=\"(row, index) in group.capabilities\"\n\t\t\t\t\t\t\t:key=\"`${group.key}-${index}`\">\n\t\t\t\t\t\t\t<td class=\"cn-capability-table__num\">\n\t\t\t\t\t\t\t\t{{ row.id }}\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t<th scope=\"row\" class=\"cn-capability-table__cap\">\n\t\t\t\t\t\t\t\t{{ row.label }}\n\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\tv-if=\"isLowConfidence(row)\"\n\t\t\t\t\t\t\t\t\tclass=\"cn-capability-table__confidence\"\n\t\t\t\t\t\t\t\t\t:title=\"lowConfidenceHint\">\n\t\t\t\t\t\t\t\t\t<span aria-hidden=\"true\">?</span>\n\t\t\t\t\t\t\t\t\t<span class=\"cn-capability-table__sr-only\">{{ lowConfidenceHint }}</span>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</th>\n\t\t\t\t\t\t\t<td v-if=\"showProviderColumn\" class=\"cn-capability-table__provider-cell\">\n\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\tv-if=\"providerOf(row)\"\n\t\t\t\t\t\t\t\t\tclass=\"cn-capability-table__provider\"\n\t\t\t\t\t\t\t\t\t:class=\"'cn-capability-table__provider--' + providerOf(row).kind\">\n\t\t\t\t\t\t\t\t\t<span class=\"cn-capability-table__provider-name\">{{ providerOf(row).name }}</span>\n\t\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\t\tv-if=\"providerHowOf(row)\"\n\t\t\t\t\t\t\t\t\t\tclass=\"cn-capability-table__provider-how\">\n\t\t\t\t\t\t\t\t\t\t{{ providerHowOf(row) }}\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\t\tv-if=\"providerKindLabel(providerOf(row).kind)\"\n\t\t\t\t\t\t\t\t\t\tclass=\"cn-capability-table__provider-kind\">\n\t\t\t\t\t\t\t\t\t\t{{ providerKindLabel(providerOf(row).kind) }}\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t<td\n\t\t\t\t\t\t\t\tv-for=\"system in systems\"\n\t\t\t\t\t\t\t\t:key=\"system.key\"\n\t\t\t\t\t\t\t\t:class=\"{ 'cn-capability-table__col--self': system.isSelf }\">\n\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\tclass=\"cn-capability-table__chip\"\n\t\t\t\t\t\t\t\t\t:class=\"'cn-capability-table__chip--' + ratingOf(row, system.key)\">\n\t\t\t\t\t\t\t\t\t{{ ratingLabel(ratingOf(row, system.key)) }}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t</tr>\n\t\t\t\t\t</tbody>\n\t\t\t\t</table>\n\t\t\t</div>\n\t\t</section>\n\t</div>\n</template>\n\n<script>\n/**\n * SPDX-License-Identifier: EUPL-1.2\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n *\n * CnCapabilityTable — a searchable, groupable capability comparison.\n *\n * Spec: features-roadmap-component — Requirement \"CnCapabilityTable\".\n */\nimport { getLanguage, translatePlural as n, translate as t } from '@nextcloud/l10n'\nimport { NcButton, NcEmptyContent, NcTextField } from '@nextcloud/vue'\nimport Magnify from 'vue-material-design-icons/Magnify.vue'\nimport {\n\tdefaultGroupMode,\n\tgroupCapabilities,\n\thasProviders,\n\tRATINGS,\n\tresolveProvider,\n\twarnCapabilityComparisonDeprecated,\n} from '../../utils/capabilityComparison.js'\n\n/**\n * How long the table waits after the last keystroke before it filters.\n *\n * 200 ms is the shortest pause that stops a 371-row regroup running on every\n * character while still feeling immediate to a touch typist.\n *\n * @type {number}\n */\nconst SEARCH_DEBOUNCE_MS = 200\n\nexport default {\n\tname: 'CnCapabilityTable',\n\n\tcomponents: { NcButton, NcEmptyContent, NcTextField, Magnify },\n\n\tprops: {\n\t\t/**\n\t\t * The comparison document the host app supplies. Shape:\n\t\t * `{systems, areas, features?, providers?, capabilities}`. Every key\n\t\t * beyond `systems`, `areas` and `capabilities` is optional, and a\n\t\t * document without them renders the table this component's consumers\n\t\t * already had.\n\t\t *\n\t\t * Deprecated, with the whole component: publish the comparison on the\n\t\t * app's docs site and link to it (as dossiq #3312 and pipelinq #2195\n\t\t * did). Still rendered; removal waits for a major version. A\n\t\t * development build warns once when a comparison is given.\n\t\t *\n\t\t * @deprecated since 2.66.0, publish the comparison on the app's docs site and link to it.\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-the-in-app-capability-comparison-is-deprecated\n\t\t * @type {object}\n\t\t */\n\t\tcomparison: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * BCP 47 locale used to pick `name` or `name_nl` on every labelled\n\t\t * entry. Defaults to the reader's Nextcloud language.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tlocale: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Render the search box. Leave it on unless the host already filters\n\t\t * the rows it passes.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tsearchable: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Force a grouping: `feature` or `area`. When empty the table opens on\n\t\t * features if any row carries one, and on areas otherwise, and the\n\t\t * reader can switch.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tgroupBy: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\t/**\n\t\t\t * What the reader has typed, bound straight to the field. Kept\n\t\t\t * separate from `query`: binding the field to the DEBOUNCED value\n\t\t\t * makes it a controlled input that snaps back to the old text\n\t\t\t * between keystrokes, which reads as a broken search box.\n\t\t\t */\n\t\t\trawQuery: '',\n\t\t\t/** The debounced query the table actually filters on. */\n\t\t\tquery: '',\n\t\t\t/** The reader's grouping choice; empty until they make one. */\n\t\t\tchosenGroupMode: '',\n\t\t\t/** Handle of the pending debounce, cleared on unmount. */\n\t\t\tdebounceHandle: null,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * @return {string} The reader's locale.\n\t\t */\n\t\teffectiveLocale() {\n\t\t\tif (this.locale) {\n\t\t\t\treturn this.locale\n\t\t\t}\n\t\t\t// `getLanguage()` and not a Dutch-defaulting resolver: mixing a\n\t\t\t// Dutch-defaulting label helper with `t()`, which falls back to its\n\t\t\t// English source, ships Dutch capability names inside English\n\t\t\t// chrome. One source, one language.\n\t\t\treturn getLanguage() || 'en'\n\t\t},\n\n\t\t/**\n\t\t * @return {Array<object>} The system columns, in document order.\n\t\t */\n\t\tsystems() {\n\t\t\treturn this.comparison?.systems ?? []\n\t\t},\n\n\t\t/**\n\t\t * @return {number} How many rows the document holds.\n\t\t */\n\t\ttotalCount() {\n\t\t\treturn this.comparison?.capabilities?.length ?? 0\n\t\t},\n\n\t\t/**\n\t\t * The grouping `groupBy` locks, or empty when it names neither.\n\t\t *\n\t\t * Read by both `groupMode` and `groupingAvailable`, because a value\n\t\t * neither of them recognises must be ignored by both: testing the raw\n\t\t * prop for emptiness let `groupBy=\"areas\"` hide the toggle while the\n\t\t * mode fell back to auto, so a typo silently removed the control.\n\t\t *\n\t\t * @return {string} `feature`, `area`, or empty.\n\t\t */\n\t\tforcedGroupMode() {\n\t\t\treturn this.groupBy === 'feature' || this.groupBy === 'area' ? this.groupBy : ''\n\t\t},\n\n\t\t/**\n\t\t * The grouping in force: the `groupBy` prop, else the reader's choice,\n\t\t * else what the document suggests.\n\t\t *\n\t\t * @return {'feature'|'area'} The mode.\n\t\t */\n\t\tgroupMode() {\n\t\t\treturn this.forcedGroupMode || this.chosenGroupMode || defaultGroupMode(this.comparison)\n\t\t},\n\n\t\t/**\n\t\t * The toggle is pointless on a document that maps no features, so it\n\t\t * only renders once there is a second grouping to switch to.\n\t\t *\n\t\t * @return {boolean} Whether to render the grouping toggle.\n\t\t */\n\t\tgroupingAvailable() {\n\t\t\treturn !this.forcedGroupMode && defaultGroupMode(this.comparison) === 'feature'\n\t\t},\n\n\t\t/**\n\t\t * @return {boolean} Whether any row names a provider.\n\t\t */\n\t\tshowProviderColumn() {\n\t\t\treturn hasProviders(this.comparison)\n\t\t},\n\n\t\t/**\n\t\t * @return {Array<object>} The groups to render, already filtered.\n\t\t */\n\t\tgroups() {\n\t\t\treturn groupCapabilities(this.comparison, {\n\t\t\t\tmode: this.groupMode,\n\t\t\t\tlocale: this.effectiveLocale,\n\t\t\t\tquery: this.query,\n\t\t\t\tunmappedLabel: this.unmappedLabel,\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * @return {number} How many rows survived the search.\n\t\t */\n\t\tvisibleCount() {\n\t\t\treturn this.groups.reduce((sum, group) => sum + group.capabilities.length, 0)\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The line that tells a reader how much they are seeing.\n\t\t */\n\t\tresultCountText() {\n\t\t\treturn n(\n\t\t\t\t'nextcloud-vue',\n\t\t\t\t'You see {count} capability of {total}.',\n\t\t\t\t'You see {count} capabilities of {total}.',\n\t\t\t\tthis.visibleCount,\n\t\t\t\t{ count: this.visibleCount, total: this.totalCount },\n\t\t\t)\n\t\t},\n\n\t\tsearchLabel() {\n\t\t\treturn t('nextcloud-vue', 'Search capabilities')\n\t\t},\n\n\t\tsearchPlaceholder() {\n\t\t\treturn t('nextcloud-vue', 'Name, number, area or feature')\n\t\t},\n\n\t\tclearLabel() {\n\t\t\treturn t('nextcloud-vue', 'Clear the search')\n\t\t},\n\n\t\tgroupingGroupLabel() {\n\t\t\treturn t('nextcloud-vue', 'Group the capabilities')\n\t\t},\n\n\t\tbyFeatureLabel() {\n\t\t\treturn t('nextcloud-vue', 'Group by feature')\n\t\t},\n\n\t\tbyAreaLabel() {\n\t\t\treturn t('nextcloud-vue', 'Group by area')\n\t\t},\n\n\t\tunmappedLabel() {\n\t\t\treturn t('nextcloud-vue', 'Not yet mapped to a feature')\n\t\t},\n\n\t\tnumberHeader() {\n\t\t\treturn t('nextcloud-vue', 'No.')\n\t\t},\n\n\t\tcapabilityHeader() {\n\t\t\treturn t('nextcloud-vue', 'Capability')\n\t\t},\n\n\t\tproviderHeader() {\n\t\t\treturn t('nextcloud-vue', 'Provided by')\n\t\t},\n\n\t\tlowConfidenceHint() {\n\t\t\treturn t('nextcloud-vue', 'We matched this row to its feature by judgement.')\n\t\t},\n\n\t\temptyName() {\n\t\t\t// `escape: false` because the result is rendered through `{{ }}`,\n\t\t\t// which escapes it already. Left on, t() turns a search for\n\t\t\t// `a & b` into the literal \"No capability matches a &amp; b\".\n\t\t\treturn t('nextcloud-vue', 'No capability matches {query}', { query: this.query }, undefined, { escape: false })\n\t\t},\n\n\t\temptyDescription() {\n\t\t\treturn t('nextcloud-vue', 'Clear the search box or try another word.')\n\t\t},\n\t},\n\n\tbeforeUnmount() {\n\t\tif (this.debounceHandle !== null) {\n\t\t\tclearTimeout(this.debounceHandle)\n\t\t\tthis.debounceHandle = null\n\t\t}\n\t},\n\n\tmounted() {\n\t\tif (this.comparison) {\n\t\t\twarnCapabilityComparisonDeprecated('CnCapabilityTable')\n\t\t}\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Debounce the reader's typing into `query`.\n\t\t *\n\t\t * @param {string} value The raw input value.\n\t\t */\n\t\tonQueryInput(value) {\n\t\t\tthis.rawQuery = value ?? ''\n\t\t\tif (this.debounceHandle !== null) {\n\t\t\t\tclearTimeout(this.debounceHandle)\n\t\t\t}\n\t\t\tthis.debounceHandle = setTimeout(() => {\n\t\t\t\tthis.debounceHandle = null\n\t\t\t\tthis.query = this.rawQuery\n\t\t\t}, SEARCH_DEBOUNCE_MS)\n\t\t},\n\n\t\t/**\n\t\t * Empty the search box at once, without waiting out the debounce.\n\t\t */\n\t\tclearQuery() {\n\t\t\tif (this.debounceHandle !== null) {\n\t\t\t\tclearTimeout(this.debounceHandle)\n\t\t\t\tthis.debounceHandle = null\n\t\t\t}\n\t\t\tthis.rawQuery = ''\n\t\t\tthis.query = ''\n\t\t},\n\n\t\t/**\n\t\t * @param {'feature'|'area'} mode The grouping the reader picked.\n\t\t */\n\t\tsetGroupMode(mode) {\n\t\t\tthis.chosenGroupMode = mode\n\t\t},\n\n\t\t/**\n\t\t * @param {object} row One capability row.\n\t\t * @return {object|null} Its provider, or null when it names none.\n\t\t */\n\t\tproviderOf(row) {\n\t\t\treturn resolveProvider(row, this.comparison, this.effectiveLocale)\n\t\t},\n\n\t\t/**\n\t\t * The kind word rendered beside the provider name. It is TEXT, not a\n\t\t * colour: a reader who cannot tell the chips apart still reads which\n\t\t * of the three kinds this is.\n\t\t *\n\t\t * @param {string} kind One of `self`, `app`, `platform`, `unknown`.\n\t\t * @return {string} The label, empty for an unrecognised kind.\n\t\t */\n\t\tproviderKindLabel(kind) {\n\t\t\tif (kind === 'self') {\n\t\t\t\treturn t('nextcloud-vue', 'this app')\n\t\t\t}\n\t\t\tif (kind === 'app') {\n\t\t\t\treturn t('nextcloud-vue', 'another app')\n\t\t\t}\n\t\t\tif (kind === 'platform') {\n\t\t\t\treturn t('nextcloud-vue', 'the platform')\n\t\t\t}\n\t\t\treturn ''\n\t\t},\n\n\t\t/**\n\t\t * The mechanism the document names for this row, rendered under the\n\t\t * provider name.\n\t\t *\n\t\t * It is a free-text key the audit writes, so it is shown as given\n\t\t * rather than translated. Rendering it is a deliberate choice: a field\n\t\t * the contract carries and the table quietly ignores is a field nobody\n\t\t * finds out is wrong.\n\t\t *\n\t\t * @param {object} row One capability row.\n\t\t * @return {string} The mechanism, empty when the row names none.\n\t\t */\n\t\tproviderHowOf(row) {\n\t\t\tconst how = row?.providerHow\n\t\t\treturn typeof how === 'string' ? how.trim() : ''\n\t\t},\n\n\t\t/**\n\t\t * @param {object} row One capability row.\n\t\t * @return {boolean} Whether to mark the row's feature as a judgement.\n\t\t */\n\t\tisLowConfidence(row) {\n\t\t\treturn row?.featureConfidence === 'low'\n\t\t\t\t&& typeof row?.feature === 'string'\n\t\t\t\t&& row.feature.trim() !== ''\n\t\t},\n\n\t\t/**\n\t\t * A rating outside the three the audit emits is shown as `unknown`\n\t\t * rather than blanked, so a typo in the document is visible.\n\t\t *\n\t\t * @param {object} row One capability row.\n\t\t * @param {string} systemKey The system column.\n\t\t * @return {string} The rating to render.\n\t\t */\n\t\tratingOf(row, systemKey) {\n\t\t\tconst rating = row?.[systemKey]\n\t\t\treturn RATINGS.includes(rating) ? rating : 'unknown'\n\t\t},\n\n\t\t/**\n\t\t * @param {string} rating One of `yes`, `partial`, `no`, `unknown`.\n\t\t * @return {string} The reader's word for it.\n\t\t */\n\t\tratingLabel(rating) {\n\t\t\tif (rating === 'yes') {\n\t\t\t\treturn t('nextcloud-vue', 'Yes')\n\t\t\t}\n\t\t\tif (rating === 'partial') {\n\t\t\t\treturn t('nextcloud-vue', 'Partial')\n\t\t\t}\n\t\t\tif (rating === 'no') {\n\t\t\t\treturn t('nextcloud-vue', 'No')\n\t\t\t}\n\t\t\treturn t('nextcloud-vue', 'Unknown')\n\t\t},\n\n\t\t/**\n\t\t * @param {object} group One rendered group.\n\t\t * @return {string} Its table caption.\n\t\t */\n\t\tcaptionFor(group) {\n\t\t\t// See `emptyName`: an area called \"Zaken & Documenten\" would reach\n\t\t\t// the caption, and a screen reader, as \"Zaken &amp; Documenten\".\n\t\t\treturn t('nextcloud-vue', 'Capabilities in {group}, rated per system.', {\n\t\t\t\tgroup: group.label,\n\t\t\t}, undefined, { escape: false })\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-capability-table__controls {\n\tdisplay: flex;\n\tgap: 12px;\n\talign-items: flex-end;\n\tflex-wrap: wrap;\n\tmargin-bottom: 8px;\n}\n\n.cn-capability-table__search {\n\tflex: 1 1 280px;\n\tmin-width: 220px;\n}\n\n.cn-capability-table__grouping {\n\tdisplay: flex;\n\tgap: 8px;\n\tflex-wrap: wrap;\n}\n\n.cn-capability-table__count {\n\tmargin: 0 0 16px;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n}\n\n.cn-capability-table__group {\n\tmargin-bottom: 24px;\n}\n\n.cn-capability-table__group-title {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\tmargin: 0 0 8px;\n\tfont-size: 1.05em;\n\tcolor: var(--color-main-text);\n}\n\n.cn-capability-table__group-count {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n\tfont-weight: normal;\n}\n\n.cn-capability-table__scroller {\n\toverflow-x: auto;\n}\n\n.cn-capability-table__table {\n\twidth: 100%;\n\tborder-collapse: collapse;\n}\n\n.cn-capability-table__caption {\n\ttext-align: start;\n\tcaption-side: top;\n\tpadding-bottom: 8px;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n}\n\n.cn-capability-table__table th,\n.cn-capability-table__table td {\n\tborder-bottom: 1px solid var(--color-border);\n\tpadding: 6px 8px;\n\ttext-align: start;\n\tvertical-align: top;\n}\n\n.cn-capability-table__table thead th {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-weight: bold;\n\twhite-space: nowrap;\n}\n\n.cn-capability-table__num {\n\twhite-space: nowrap;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-capability-table__cap {\n\tfont-weight: normal;\n\tcolor: var(--color-main-text);\n\tmin-width: 240px;\n}\n\n.cn-capability-table__col--self {\n\tbackground-color: var(--color-background-hover);\n}\n\n.cn-capability-table__confidence {\n\tdisplay: inline-block;\n\tmargin-inline-start: 4px;\n\tcolor: var(--color-text-maxcontrast);\n\tcursor: help;\n}\n\n.cn-capability-table__sr-only {\n\tposition: absolute;\n\twidth: 1px;\n\theight: 1px;\n\toverflow: hidden;\n\tclip-path: inset(50%);\n\twhite-space: nowrap;\n\t/* Padding and border are part of the recipe, not decoration: either one\n\t inherited onto the 1px box grows it back into the layout. */\n\tpadding: 0;\n\tborder: 0;\n\tmargin: -1px;\n}\n\n.cn-capability-table__provider {\n\tdisplay: inline-flex;\n\tflex-direction: column;\n\tgap: 2px;\n}\n\n.cn-capability-table__provider-name {\n\tborder-inline-start: 3px solid var(--color-border-dark);\n\tpadding-inline-start: 6px;\n\tcolor: var(--color-main-text);\n}\n\n.cn-capability-table__provider--self .cn-capability-table__provider-name {\n\tborder-inline-start-color: var(--color-primary-element);\n}\n\n.cn-capability-table__provider--app .cn-capability-table__provider-name {\n\tborder-inline-start-color: var(--color-success);\n}\n\n.cn-capability-table__provider--platform .cn-capability-table__provider-name {\n\tborder-inline-start-color: var(--color-warning);\n}\n\n.cn-capability-table__provider-kind,\n.cn-capability-table__provider-how {\n\tpadding-inline-start: 9px;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.8em;\n}\n\n.cn-capability-table__chip {\n\tdisplay: inline-block;\n\tpadding: 1px 8px;\n\tborder-radius: var(--border-radius-pill, 100px);\n\tborder: 1px solid var(--color-border-dark);\n\tcolor: var(--color-main-text);\n\tfont-size: 0.85em;\n\twhite-space: nowrap;\n}\n\n.cn-capability-table__chip--yes {\n\tborder-color: var(--color-success);\n}\n\n.cn-capability-table__chip--partial {\n\tborder-color: var(--color-warning);\n}\n\n.cn-capability-table__chip--no {\n\tborder-color: var(--color-error);\n}\n\n.cn-capability-table__chip--unknown {\n\tborder-style: dashed;\n\tcolor: var(--color-text-maxcontrast);\n}\n</style>\n"],"names":["n","t"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"CnCapabilityTable.vue3.js","sources":["../../../../src/components/CnCapabilityTable/CnCapabilityTable.vue?vue&type=template&id=e76836b2&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n -\n - CnCapabilityTable — the capability comparison, searchable and grouped.\n -\n - Two readers arrive at this table. One is shopping for a FEATURE, the\n - sellable bundle they can name in a tender. The other is checking one\n - CAPABILITY, a behaviour they know they need. The grouping toggle serves the\n - first and the search box serves the second, over the same rows.\n -\n - The provided-by column answers the question a Nextcloud buyer asks next:\n - does this app do the work itself, does a sibling app do it, or does the\n - platform already do it? A capability that Nextcloud provides is not a gap,\n - and a table that cannot say so understates the product.\n -\n - EVERYTHING HERE IS DRIVEN BY THE DOCUMENT, and no row is ever dropped. An\n - unknown provider key renders as the raw key. An area or feature the\n - document never declared gets a group of its own. A row that names no\n - feature lands under a heading that says so. See utils/capabilityComparison.js.\n-->\n\n<template>\n\t<div class=\"cn-capability-table\">\n\t\t<div class=\"cn-capability-table__controls\">\n\t\t\t<div v-if=\"searchable\" class=\"cn-capability-table__search\">\n\t\t\t\t<NcTextField\n\t\t\t\t\t:modelValue=\"rawQuery\"\n\t\t\t\t\t:label=\"searchLabel\"\n\t\t\t\t\t:placeholder=\"searchPlaceholder\"\n\t\t\t\t\ttrailingButtonIcon=\"close\"\n\t\t\t\t\t:showTrailingButton=\"rawQuery !== ''\"\n\t\t\t\t\t:trailingButtonLabel=\"clearLabel\"\n\t\t\t\t\t@update:modelValue=\"onQueryInput\"\n\t\t\t\t\t@trailingButtonClick=\"clearQuery\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<Magnify :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t</NcTextField>\n\t\t\t</div>\n\n\t\t\t<!-- A toggle group, deliberately not an ARIA tab widget: a tab role\n\t\t\t promises the arrow-key roving focus the ARIA practices describe,\n\t\t\t and two plain buttons carrying aria-pressed keep the native\n\t\t\t button semantics that already work for everyone. -->\n\t\t\t<div\n\t\t\t\tv-if=\"groupingAvailable\"\n\t\t\t\tclass=\"cn-capability-table__grouping\"\n\t\t\t\trole=\"group\"\n\t\t\t\t:aria-label=\"groupingGroupLabel\">\n\t\t\t\t<NcButton\n\t\t\t\t\tclass=\"cn-capability-table__group-button\"\n\t\t\t\t\t:aria-pressed=\"String(groupMode === 'feature')\"\n\t\t\t\t\t:variant=\"groupMode === 'feature' ? 'primary' : 'tertiary'\"\n\t\t\t\t\t@click=\"setGroupMode('feature')\">\n\t\t\t\t\t{{ byFeatureLabel }}\n\t\t\t\t</NcButton>\n\t\t\t\t<NcButton\n\t\t\t\t\tclass=\"cn-capability-table__group-button\"\n\t\t\t\t\t:aria-pressed=\"String(groupMode === 'area')\"\n\t\t\t\t\t:variant=\"groupMode === 'area' ? 'primary' : 'tertiary'\"\n\t\t\t\t\t@click=\"setGroupMode('area')\">\n\t\t\t\t\t{{ byAreaLabel }}\n\t\t\t\t</NcButton>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- role=\"status\" is an implicit polite live region, so a reader on a\n\t\t screen reader hears the new count after each keystroke settles. -->\n\t\t<p class=\"cn-capability-table__count\" role=\"status\">\n\t\t\t{{ resultCountText }}\n\t\t</p>\n\n\t\t<div v-if=\"groups.length === 0\" class=\"cn-capability-table__empty\">\n\t\t\t<NcEmptyContent :name=\"emptyName\" :description=\"emptyDescription\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<Magnify :size=\"48\" />\n\t\t\t\t</template>\n\t\t\t</NcEmptyContent>\n\t\t</div>\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-capability-table__group\">\n\t\t\t<h3 class=\"cn-capability-table__group-title\">\n\t\t\t\t{{ group.label }}\n\t\t\t\t<span class=\"cn-capability-table__group-count\">{{ group.capabilities.length }}</span>\n\t\t\t</h3>\n\t\t\t<div class=\"cn-capability-table__scroller\">\n\t\t\t\t<table class=\"cn-capability-table__table\">\n\t\t\t\t\t<caption class=\"cn-capability-table__caption\">\n\t\t\t\t\t\t{{ captionFor(group) }}\n\t\t\t\t\t</caption>\n\t\t\t\t\t<thead>\n\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t<th scope=\"col\" class=\"cn-capability-table__num\">\n\t\t\t\t\t\t\t\t{{ numberHeader }}\n\t\t\t\t\t\t\t</th>\n\t\t\t\t\t\t\t<th scope=\"col\">\n\t\t\t\t\t\t\t\t{{ capabilityHeader }}\n\t\t\t\t\t\t\t</th>\n\t\t\t\t\t\t\t<th v-if=\"showProviderColumn\" scope=\"col\">\n\t\t\t\t\t\t\t\t{{ providerHeader }}\n\t\t\t\t\t\t\t</th>\n\t\t\t\t\t\t\t<th\n\t\t\t\t\t\t\t\tv-for=\"system in systems\"\n\t\t\t\t\t\t\t\t:key=\"system.key\"\n\t\t\t\t\t\t\t\tscope=\"col\"\n\t\t\t\t\t\t\t\t:class=\"{ 'cn-capability-table__col--self': system.isSelf }\">\n\t\t\t\t\t\t\t\t{{ system.name }}\n\t\t\t\t\t\t\t</th>\n\t\t\t\t\t\t</tr>\n\t\t\t\t\t</thead>\n\t\t\t\t\t<tbody>\n\t\t\t\t\t\t<!-- Keyed by position and not by `row.id`: a document\n\t\t\t\t\t\t that repeats an id, or omits one, is exactly the\n\t\t\t\t\t\t shape this table promises to still render, and a\n\t\t\t\t\t\t duplicate key is where that promise would break. -->\n\t\t\t\t\t\t<tr\n\t\t\t\t\t\t\tv-for=\"(row, index) in group.capabilities\"\n\t\t\t\t\t\t\t:key=\"`${group.key}-${index}`\">\n\t\t\t\t\t\t\t<td class=\"cn-capability-table__num\">\n\t\t\t\t\t\t\t\t{{ row.id }}\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t<th scope=\"row\" class=\"cn-capability-table__cap\">\n\t\t\t\t\t\t\t\t{{ row.label }}\n\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\tv-if=\"isLowConfidence(row)\"\n\t\t\t\t\t\t\t\t\tclass=\"cn-capability-table__confidence\"\n\t\t\t\t\t\t\t\t\t:title=\"lowConfidenceHint\">\n\t\t\t\t\t\t\t\t\t<span aria-hidden=\"true\">?</span>\n\t\t\t\t\t\t\t\t\t<span class=\"cn-capability-table__sr-only\">{{ lowConfidenceHint }}</span>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</th>\n\t\t\t\t\t\t\t<td v-if=\"showProviderColumn\" class=\"cn-capability-table__provider-cell\">\n\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\tv-if=\"providerOf(row)\"\n\t\t\t\t\t\t\t\t\tclass=\"cn-capability-table__provider\"\n\t\t\t\t\t\t\t\t\t:class=\"'cn-capability-table__provider--' + providerOf(row).kind\">\n\t\t\t\t\t\t\t\t\t<span class=\"cn-capability-table__provider-name\">{{ providerOf(row).name }}</span>\n\t\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\t\tv-if=\"providerHowOf(row)\"\n\t\t\t\t\t\t\t\t\t\tclass=\"cn-capability-table__provider-how\">\n\t\t\t\t\t\t\t\t\t\t{{ providerHowOf(row) }}\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\t\tv-if=\"providerKindLabel(providerOf(row).kind)\"\n\t\t\t\t\t\t\t\t\t\tclass=\"cn-capability-table__provider-kind\">\n\t\t\t\t\t\t\t\t\t\t{{ providerKindLabel(providerOf(row).kind) }}\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t<td\n\t\t\t\t\t\t\t\tv-for=\"system in systems\"\n\t\t\t\t\t\t\t\t:key=\"system.key\"\n\t\t\t\t\t\t\t\t:class=\"{ 'cn-capability-table__col--self': system.isSelf }\">\n\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\tclass=\"cn-capability-table__chip\"\n\t\t\t\t\t\t\t\t\t:class=\"'cn-capability-table__chip--' + ratingOf(row, system.key)\">\n\t\t\t\t\t\t\t\t\t{{ ratingLabel(ratingOf(row, system.key)) }}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t</tr>\n\t\t\t\t\t</tbody>\n\t\t\t\t</table>\n\t\t\t</div>\n\t\t</section>\n\t</div>\n</template>\n\n<script>\n/**\n * SPDX-License-Identifier: EUPL-1.2\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n *\n * CnCapabilityTable — a searchable, groupable capability comparison.\n *\n * Spec: features-roadmap-component — Requirement \"CnCapabilityTable\".\n */\nimport { getLanguage, translatePlural as n, translate as t } from '@nextcloud/l10n'\nimport { NcButton, NcEmptyContent, NcTextField } from '@nextcloud/vue'\nimport Magnify from 'vue-material-design-icons/Magnify.vue'\nimport {\n\tdefaultGroupMode,\n\tgroupCapabilities,\n\thasProviders,\n\tRATINGS,\n\tresolveProvider,\n} from '../../utils/capabilityComparison.js'\n\n/**\n * How long the table waits after the last keystroke before it filters.\n *\n * 200 ms is the shortest pause that stops a 371-row regroup running on every\n * character while still feeling immediate to a touch typist.\n *\n * @type {number}\n */\nconst SEARCH_DEBOUNCE_MS = 200\n\nexport default {\n\tname: 'CnCapabilityTable',\n\n\tcomponents: { NcButton, NcEmptyContent, NcTextField, Magnify },\n\n\tprops: {\n\t\t/**\n\t\t * The comparison document the host app supplies. Shape:\n\t\t * `{systems, areas, features?, providers?, capabilities}`. Every key\n\t\t * beyond `systems`, `areas` and `capabilities` is optional, and a\n\t\t * document without them renders the table this component's consumers\n\t\t * already had.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tcomparison: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * BCP 47 locale used to pick `name` or `name_nl` on every labelled\n\t\t * entry. Defaults to the reader's Nextcloud language.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tlocale: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Render the search box. Leave it on unless the host already filters\n\t\t * the rows it passes.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tsearchable: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Force a grouping: `feature` or `area`. When empty the table opens on\n\t\t * features if any row carries one, and on areas otherwise, and the\n\t\t * reader can switch.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tgroupBy: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\t/**\n\t\t\t * What the reader has typed, bound straight to the field. Kept\n\t\t\t * separate from `query`: binding the field to the DEBOUNCED value\n\t\t\t * makes it a controlled input that snaps back to the old text\n\t\t\t * between keystrokes, which reads as a broken search box.\n\t\t\t */\n\t\t\trawQuery: '',\n\t\t\t/** The debounced query the table actually filters on. */\n\t\t\tquery: '',\n\t\t\t/** The reader's grouping choice; empty until they make one. */\n\t\t\tchosenGroupMode: '',\n\t\t\t/** Handle of the pending debounce, cleared on unmount. */\n\t\t\tdebounceHandle: null,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * @return {string} The reader's locale.\n\t\t */\n\t\teffectiveLocale() {\n\t\t\tif (this.locale) {\n\t\t\t\treturn this.locale\n\t\t\t}\n\t\t\t// `getLanguage()` and not a Dutch-defaulting resolver: mixing a\n\t\t\t// Dutch-defaulting label helper with `t()`, which falls back to its\n\t\t\t// English source, ships Dutch capability names inside English\n\t\t\t// chrome. One source, one language.\n\t\t\treturn getLanguage() || 'en'\n\t\t},\n\n\t\t/**\n\t\t * @return {Array<object>} The system columns, in document order.\n\t\t */\n\t\tsystems() {\n\t\t\treturn this.comparison?.systems ?? []\n\t\t},\n\n\t\t/**\n\t\t * @return {number} How many rows the document holds.\n\t\t */\n\t\ttotalCount() {\n\t\t\treturn this.comparison?.capabilities?.length ?? 0\n\t\t},\n\n\t\t/**\n\t\t * The grouping `groupBy` locks, or empty when it names neither.\n\t\t *\n\t\t * Read by both `groupMode` and `groupingAvailable`, because a value\n\t\t * neither of them recognises must be ignored by both: testing the raw\n\t\t * prop for emptiness let `groupBy=\"areas\"` hide the toggle while the\n\t\t * mode fell back to auto, so a typo silently removed the control.\n\t\t *\n\t\t * @return {string} `feature`, `area`, or empty.\n\t\t */\n\t\tforcedGroupMode() {\n\t\t\treturn this.groupBy === 'feature' || this.groupBy === 'area' ? this.groupBy : ''\n\t\t},\n\n\t\t/**\n\t\t * The grouping in force: the `groupBy` prop, else the reader's choice,\n\t\t * else what the document suggests.\n\t\t *\n\t\t * @return {'feature'|'area'} The mode.\n\t\t */\n\t\tgroupMode() {\n\t\t\treturn this.forcedGroupMode || this.chosenGroupMode || defaultGroupMode(this.comparison)\n\t\t},\n\n\t\t/**\n\t\t * The toggle is pointless on a document that maps no features, so it\n\t\t * only renders once there is a second grouping to switch to.\n\t\t *\n\t\t * @return {boolean} Whether to render the grouping toggle.\n\t\t */\n\t\tgroupingAvailable() {\n\t\t\treturn !this.forcedGroupMode && defaultGroupMode(this.comparison) === 'feature'\n\t\t},\n\n\t\t/**\n\t\t * @return {boolean} Whether any row names a provider.\n\t\t */\n\t\tshowProviderColumn() {\n\t\t\treturn hasProviders(this.comparison)\n\t\t},\n\n\t\t/**\n\t\t * @return {Array<object>} The groups to render, already filtered.\n\t\t */\n\t\tgroups() {\n\t\t\treturn groupCapabilities(this.comparison, {\n\t\t\t\tmode: this.groupMode,\n\t\t\t\tlocale: this.effectiveLocale,\n\t\t\t\tquery: this.query,\n\t\t\t\tunmappedLabel: this.unmappedLabel,\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * @return {number} How many rows survived the search.\n\t\t */\n\t\tvisibleCount() {\n\t\t\treturn this.groups.reduce((sum, group) => sum + group.capabilities.length, 0)\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The line that tells a reader how much they are seeing.\n\t\t */\n\t\tresultCountText() {\n\t\t\treturn n(\n\t\t\t\t'nextcloud-vue',\n\t\t\t\t'You see {count} capability of {total}.',\n\t\t\t\t'You see {count} capabilities of {total}.',\n\t\t\t\tthis.visibleCount,\n\t\t\t\t{ count: this.visibleCount, total: this.totalCount },\n\t\t\t)\n\t\t},\n\n\t\tsearchLabel() {\n\t\t\treturn t('nextcloud-vue', 'Search capabilities')\n\t\t},\n\n\t\tsearchPlaceholder() {\n\t\t\treturn t('nextcloud-vue', 'Name, number, area or feature')\n\t\t},\n\n\t\tclearLabel() {\n\t\t\treturn t('nextcloud-vue', 'Clear the search')\n\t\t},\n\n\t\tgroupingGroupLabel() {\n\t\t\treturn t('nextcloud-vue', 'Group the capabilities')\n\t\t},\n\n\t\tbyFeatureLabel() {\n\t\t\treturn t('nextcloud-vue', 'Group by feature')\n\t\t},\n\n\t\tbyAreaLabel() {\n\t\t\treturn t('nextcloud-vue', 'Group by area')\n\t\t},\n\n\t\tunmappedLabel() {\n\t\t\treturn t('nextcloud-vue', 'Not yet mapped to a feature')\n\t\t},\n\n\t\tnumberHeader() {\n\t\t\treturn t('nextcloud-vue', 'No.')\n\t\t},\n\n\t\tcapabilityHeader() {\n\t\t\treturn t('nextcloud-vue', 'Capability')\n\t\t},\n\n\t\tproviderHeader() {\n\t\t\treturn t('nextcloud-vue', 'Provided by')\n\t\t},\n\n\t\tlowConfidenceHint() {\n\t\t\treturn t('nextcloud-vue', 'We matched this row to its feature by judgement.')\n\t\t},\n\n\t\temptyName() {\n\t\t\t// `escape: false` because the result is rendered through `{{ }}`,\n\t\t\t// which escapes it already. Left on, t() turns a search for\n\t\t\t// `a & b` into the literal \"No capability matches a &amp; b\".\n\t\t\treturn t('nextcloud-vue', 'No capability matches {query}', { query: this.query }, undefined, { escape: false })\n\t\t},\n\n\t\temptyDescription() {\n\t\t\treturn t('nextcloud-vue', 'Clear the search box or try another word.')\n\t\t},\n\t},\n\n\tbeforeUnmount() {\n\t\tif (this.debounceHandle !== null) {\n\t\t\tclearTimeout(this.debounceHandle)\n\t\t\tthis.debounceHandle = null\n\t\t}\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Debounce the reader's typing into `query`.\n\t\t *\n\t\t * @param {string} value The raw input value.\n\t\t */\n\t\tonQueryInput(value) {\n\t\t\tthis.rawQuery = value ?? ''\n\t\t\tif (this.debounceHandle !== null) {\n\t\t\t\tclearTimeout(this.debounceHandle)\n\t\t\t}\n\t\t\tthis.debounceHandle = setTimeout(() => {\n\t\t\t\tthis.debounceHandle = null\n\t\t\t\tthis.query = this.rawQuery\n\t\t\t}, SEARCH_DEBOUNCE_MS)\n\t\t},\n\n\t\t/**\n\t\t * Empty the search box at once, without waiting out the debounce.\n\t\t */\n\t\tclearQuery() {\n\t\t\tif (this.debounceHandle !== null) {\n\t\t\t\tclearTimeout(this.debounceHandle)\n\t\t\t\tthis.debounceHandle = null\n\t\t\t}\n\t\t\tthis.rawQuery = ''\n\t\t\tthis.query = ''\n\t\t},\n\n\t\t/**\n\t\t * @param {'feature'|'area'} mode The grouping the reader picked.\n\t\t */\n\t\tsetGroupMode(mode) {\n\t\t\tthis.chosenGroupMode = mode\n\t\t},\n\n\t\t/**\n\t\t * @param {object} row One capability row.\n\t\t * @return {object|null} Its provider, or null when it names none.\n\t\t */\n\t\tproviderOf(row) {\n\t\t\treturn resolveProvider(row, this.comparison, this.effectiveLocale)\n\t\t},\n\n\t\t/**\n\t\t * The kind word rendered beside the provider name. It is TEXT, not a\n\t\t * colour: a reader who cannot tell the chips apart still reads which\n\t\t * of the three kinds this is.\n\t\t *\n\t\t * @param {string} kind One of `self`, `app`, `platform`, `unknown`.\n\t\t * @return {string} The label, empty for an unrecognised kind.\n\t\t */\n\t\tproviderKindLabel(kind) {\n\t\t\tif (kind === 'self') {\n\t\t\t\treturn t('nextcloud-vue', 'this app')\n\t\t\t}\n\t\t\tif (kind === 'app') {\n\t\t\t\treturn t('nextcloud-vue', 'another app')\n\t\t\t}\n\t\t\tif (kind === 'platform') {\n\t\t\t\treturn t('nextcloud-vue', 'the platform')\n\t\t\t}\n\t\t\treturn ''\n\t\t},\n\n\t\t/**\n\t\t * The mechanism the document names for this row, rendered under the\n\t\t * provider name.\n\t\t *\n\t\t * It is a free-text key the audit writes, so it is shown as given\n\t\t * rather than translated. Rendering it is a deliberate choice: a field\n\t\t * the contract carries and the table quietly ignores is a field nobody\n\t\t * finds out is wrong.\n\t\t *\n\t\t * @param {object} row One capability row.\n\t\t * @return {string} The mechanism, empty when the row names none.\n\t\t */\n\t\tproviderHowOf(row) {\n\t\t\tconst how = row?.providerHow\n\t\t\treturn typeof how === 'string' ? how.trim() : ''\n\t\t},\n\n\t\t/**\n\t\t * @param {object} row One capability row.\n\t\t * @return {boolean} Whether to mark the row's feature as a judgement.\n\t\t */\n\t\tisLowConfidence(row) {\n\t\t\treturn row?.featureConfidence === 'low'\n\t\t\t\t&& typeof row?.feature === 'string'\n\t\t\t\t&& row.feature.trim() !== ''\n\t\t},\n\n\t\t/**\n\t\t * A rating outside the three the audit emits is shown as `unknown`\n\t\t * rather than blanked, so a typo in the document is visible.\n\t\t *\n\t\t * @param {object} row One capability row.\n\t\t * @param {string} systemKey The system column.\n\t\t * @return {string} The rating to render.\n\t\t */\n\t\tratingOf(row, systemKey) {\n\t\t\tconst rating = row?.[systemKey]\n\t\t\treturn RATINGS.includes(rating) ? rating : 'unknown'\n\t\t},\n\n\t\t/**\n\t\t * @param {string} rating One of `yes`, `partial`, `no`, `unknown`.\n\t\t * @return {string} The reader's word for it.\n\t\t */\n\t\tratingLabel(rating) {\n\t\t\tif (rating === 'yes') {\n\t\t\t\treturn t('nextcloud-vue', 'Yes')\n\t\t\t}\n\t\t\tif (rating === 'partial') {\n\t\t\t\treturn t('nextcloud-vue', 'Partial')\n\t\t\t}\n\t\t\tif (rating === 'no') {\n\t\t\t\treturn t('nextcloud-vue', 'No')\n\t\t\t}\n\t\t\treturn t('nextcloud-vue', 'Unknown')\n\t\t},\n\n\t\t/**\n\t\t * @param {object} group One rendered group.\n\t\t * @return {string} Its table caption.\n\t\t */\n\t\tcaptionFor(group) {\n\t\t\t// See `emptyName`: an area called \"Zaken & Documenten\" would reach\n\t\t\t// the caption, and a screen reader, as \"Zaken &amp; Documenten\".\n\t\t\treturn t('nextcloud-vue', 'Capabilities in {group}, rated per system.', {\n\t\t\t\tgroup: group.label,\n\t\t\t}, undefined, { escape: false })\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-capability-table__controls {\n\tdisplay: flex;\n\tgap: 12px;\n\talign-items: flex-end;\n\tflex-wrap: wrap;\n\tmargin-bottom: 8px;\n}\n\n.cn-capability-table__search {\n\tflex: 1 1 280px;\n\tmin-width: 220px;\n}\n\n.cn-capability-table__grouping {\n\tdisplay: flex;\n\tgap: 8px;\n\tflex-wrap: wrap;\n}\n\n.cn-capability-table__count {\n\tmargin: 0 0 16px;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n}\n\n.cn-capability-table__group {\n\tmargin-bottom: 24px;\n}\n\n.cn-capability-table__group-title {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\tmargin: 0 0 8px;\n\tfont-size: 1.05em;\n\tcolor: var(--color-main-text);\n}\n\n.cn-capability-table__group-count {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n\tfont-weight: normal;\n}\n\n.cn-capability-table__scroller {\n\toverflow-x: auto;\n}\n\n.cn-capability-table__table {\n\twidth: 100%;\n\tborder-collapse: collapse;\n}\n\n.cn-capability-table__caption {\n\ttext-align: start;\n\tcaption-side: top;\n\tpadding-bottom: 8px;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n}\n\n.cn-capability-table__table th,\n.cn-capability-table__table td {\n\tborder-bottom: 1px solid var(--color-border);\n\tpadding: 6px 8px;\n\ttext-align: start;\n\tvertical-align: top;\n}\n\n.cn-capability-table__table thead th {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-weight: bold;\n\twhite-space: nowrap;\n}\n\n.cn-capability-table__num {\n\twhite-space: nowrap;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-capability-table__cap {\n\tfont-weight: normal;\n\tcolor: var(--color-main-text);\n\tmin-width: 240px;\n}\n\n.cn-capability-table__col--self {\n\tbackground-color: var(--color-background-hover);\n}\n\n.cn-capability-table__confidence {\n\tdisplay: inline-block;\n\tmargin-inline-start: 4px;\n\tcolor: var(--color-text-maxcontrast);\n\tcursor: help;\n}\n\n.cn-capability-table__sr-only {\n\tposition: absolute;\n\twidth: 1px;\n\theight: 1px;\n\toverflow: hidden;\n\tclip-path: inset(50%);\n\twhite-space: nowrap;\n\t/* Padding and border are part of the recipe, not decoration: either one\n\t inherited onto the 1px box grows it back into the layout. */\n\tpadding: 0;\n\tborder: 0;\n\tmargin: -1px;\n}\n\n.cn-capability-table__provider {\n\tdisplay: inline-flex;\n\tflex-direction: column;\n\tgap: 2px;\n}\n\n.cn-capability-table__provider-name {\n\tborder-inline-start: 3px solid var(--color-border-dark);\n\tpadding-inline-start: 6px;\n\tcolor: var(--color-main-text);\n}\n\n.cn-capability-table__provider--self .cn-capability-table__provider-name {\n\tborder-inline-start-color: var(--color-primary-element);\n}\n\n.cn-capability-table__provider--app .cn-capability-table__provider-name {\n\tborder-inline-start-color: var(--color-success);\n}\n\n.cn-capability-table__provider--platform .cn-capability-table__provider-name {\n\tborder-inline-start-color: var(--color-warning);\n}\n\n.cn-capability-table__provider-kind,\n.cn-capability-table__provider-how {\n\tpadding-inline-start: 9px;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.8em;\n}\n\n.cn-capability-table__chip {\n\tdisplay: inline-block;\n\tpadding: 1px 8px;\n\tborder-radius: var(--border-radius-pill, 100px);\n\tborder: 1px solid var(--color-border-dark);\n\tcolor: var(--color-main-text);\n\tfont-size: 0.85em;\n\twhite-space: nowrap;\n}\n\n.cn-capability-table__chip--yes {\n\tborder-color: var(--color-success);\n}\n\n.cn-capability-table__chip--partial {\n\tborder-color: var(--color-warning);\n}\n\n.cn-capability-table__chip--no {\n\tborder-color: var(--color-error);\n}\n\n.cn-capability-table__chip--unknown {\n\tborder-style: dashed;\n\tcolor: var(--color-text-maxcontrast);\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_createVNode","_createElementVNode","_toDisplayString","_Fragment","_renderList","_normalizeClass"],"mappings":"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"}
1
+ {"version":3,"file":"CnCapabilityTable.vue3.js","sources":["../../../../src/components/CnCapabilityTable/CnCapabilityTable.vue?vue&type=template&id=e76836b2&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n -\n - CnCapabilityTable — the capability comparison, searchable and grouped.\n -\n - Two readers arrive at this table. One is shopping for a FEATURE, the\n - sellable bundle they can name in a tender. The other is checking one\n - CAPABILITY, a behaviour they know they need. The grouping toggle serves the\n - first and the search box serves the second, over the same rows.\n -\n - The provided-by column answers the question a Nextcloud buyer asks next:\n - does this app do the work itself, does a sibling app do it, or does the\n - platform already do it? A capability that Nextcloud provides is not a gap,\n - and a table that cannot say so understates the product.\n -\n - EVERYTHING HERE IS DRIVEN BY THE DOCUMENT, and no row is ever dropped. An\n - unknown provider key renders as the raw key. An area or feature the\n - document never declared gets a group of its own. A row that names no\n - feature lands under a heading that says so. See utils/capabilityComparison.js.\n-->\n\n<template>\n\t<div class=\"cn-capability-table\">\n\t\t<div class=\"cn-capability-table__controls\">\n\t\t\t<div v-if=\"searchable\" class=\"cn-capability-table__search\">\n\t\t\t\t<NcTextField\n\t\t\t\t\t:modelValue=\"rawQuery\"\n\t\t\t\t\t:label=\"searchLabel\"\n\t\t\t\t\t:placeholder=\"searchPlaceholder\"\n\t\t\t\t\ttrailingButtonIcon=\"close\"\n\t\t\t\t\t:showTrailingButton=\"rawQuery !== ''\"\n\t\t\t\t\t:trailingButtonLabel=\"clearLabel\"\n\t\t\t\t\t@update:modelValue=\"onQueryInput\"\n\t\t\t\t\t@trailingButtonClick=\"clearQuery\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<Magnify :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t</NcTextField>\n\t\t\t</div>\n\n\t\t\t<!-- A toggle group, deliberately not an ARIA tab widget: a tab role\n\t\t\t promises the arrow-key roving focus the ARIA practices describe,\n\t\t\t and two plain buttons carrying aria-pressed keep the native\n\t\t\t button semantics that already work for everyone. -->\n\t\t\t<div\n\t\t\t\tv-if=\"groupingAvailable\"\n\t\t\t\tclass=\"cn-capability-table__grouping\"\n\t\t\t\trole=\"group\"\n\t\t\t\t:aria-label=\"groupingGroupLabel\">\n\t\t\t\t<NcButton\n\t\t\t\t\tclass=\"cn-capability-table__group-button\"\n\t\t\t\t\t:aria-pressed=\"String(groupMode === 'feature')\"\n\t\t\t\t\t:variant=\"groupMode === 'feature' ? 'primary' : 'tertiary'\"\n\t\t\t\t\t@click=\"setGroupMode('feature')\">\n\t\t\t\t\t{{ byFeatureLabel }}\n\t\t\t\t</NcButton>\n\t\t\t\t<NcButton\n\t\t\t\t\tclass=\"cn-capability-table__group-button\"\n\t\t\t\t\t:aria-pressed=\"String(groupMode === 'area')\"\n\t\t\t\t\t:variant=\"groupMode === 'area' ? 'primary' : 'tertiary'\"\n\t\t\t\t\t@click=\"setGroupMode('area')\">\n\t\t\t\t\t{{ byAreaLabel }}\n\t\t\t\t</NcButton>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- role=\"status\" is an implicit polite live region, so a reader on a\n\t\t screen reader hears the new count after each keystroke settles. -->\n\t\t<p class=\"cn-capability-table__count\" role=\"status\">\n\t\t\t{{ resultCountText }}\n\t\t</p>\n\n\t\t<div v-if=\"groups.length === 0\" class=\"cn-capability-table__empty\">\n\t\t\t<NcEmptyContent :name=\"emptyName\" :description=\"emptyDescription\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<Magnify :size=\"48\" />\n\t\t\t\t</template>\n\t\t\t</NcEmptyContent>\n\t\t</div>\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-capability-table__group\">\n\t\t\t<h3 class=\"cn-capability-table__group-title\">\n\t\t\t\t{{ group.label }}\n\t\t\t\t<span class=\"cn-capability-table__group-count\">{{ group.capabilities.length }}</span>\n\t\t\t</h3>\n\t\t\t<div class=\"cn-capability-table__scroller\">\n\t\t\t\t<table class=\"cn-capability-table__table\">\n\t\t\t\t\t<caption class=\"cn-capability-table__caption\">\n\t\t\t\t\t\t{{ captionFor(group) }}\n\t\t\t\t\t</caption>\n\t\t\t\t\t<thead>\n\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t<th scope=\"col\" class=\"cn-capability-table__num\">\n\t\t\t\t\t\t\t\t{{ numberHeader }}\n\t\t\t\t\t\t\t</th>\n\t\t\t\t\t\t\t<th scope=\"col\">\n\t\t\t\t\t\t\t\t{{ capabilityHeader }}\n\t\t\t\t\t\t\t</th>\n\t\t\t\t\t\t\t<th v-if=\"showProviderColumn\" scope=\"col\">\n\t\t\t\t\t\t\t\t{{ providerHeader }}\n\t\t\t\t\t\t\t</th>\n\t\t\t\t\t\t\t<th\n\t\t\t\t\t\t\t\tv-for=\"system in systems\"\n\t\t\t\t\t\t\t\t:key=\"system.key\"\n\t\t\t\t\t\t\t\tscope=\"col\"\n\t\t\t\t\t\t\t\t:class=\"{ 'cn-capability-table__col--self': system.isSelf }\">\n\t\t\t\t\t\t\t\t{{ system.name }}\n\t\t\t\t\t\t\t</th>\n\t\t\t\t\t\t</tr>\n\t\t\t\t\t</thead>\n\t\t\t\t\t<tbody>\n\t\t\t\t\t\t<!-- Keyed by position and not by `row.id`: a document\n\t\t\t\t\t\t that repeats an id, or omits one, is exactly the\n\t\t\t\t\t\t shape this table promises to still render, and a\n\t\t\t\t\t\t duplicate key is where that promise would break. -->\n\t\t\t\t\t\t<tr\n\t\t\t\t\t\t\tv-for=\"(row, index) in group.capabilities\"\n\t\t\t\t\t\t\t:key=\"`${group.key}-${index}`\">\n\t\t\t\t\t\t\t<td class=\"cn-capability-table__num\">\n\t\t\t\t\t\t\t\t{{ row.id }}\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t<th scope=\"row\" class=\"cn-capability-table__cap\">\n\t\t\t\t\t\t\t\t{{ row.label }}\n\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\tv-if=\"isLowConfidence(row)\"\n\t\t\t\t\t\t\t\t\tclass=\"cn-capability-table__confidence\"\n\t\t\t\t\t\t\t\t\t:title=\"lowConfidenceHint\">\n\t\t\t\t\t\t\t\t\t<span aria-hidden=\"true\">?</span>\n\t\t\t\t\t\t\t\t\t<span class=\"cn-capability-table__sr-only\">{{ lowConfidenceHint }}</span>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</th>\n\t\t\t\t\t\t\t<td v-if=\"showProviderColumn\" class=\"cn-capability-table__provider-cell\">\n\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\tv-if=\"providerOf(row)\"\n\t\t\t\t\t\t\t\t\tclass=\"cn-capability-table__provider\"\n\t\t\t\t\t\t\t\t\t:class=\"'cn-capability-table__provider--' + providerOf(row).kind\">\n\t\t\t\t\t\t\t\t\t<span class=\"cn-capability-table__provider-name\">{{ providerOf(row).name }}</span>\n\t\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\t\tv-if=\"providerHowOf(row)\"\n\t\t\t\t\t\t\t\t\t\tclass=\"cn-capability-table__provider-how\">\n\t\t\t\t\t\t\t\t\t\t{{ providerHowOf(row) }}\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\t\tv-if=\"providerKindLabel(providerOf(row).kind)\"\n\t\t\t\t\t\t\t\t\t\tclass=\"cn-capability-table__provider-kind\">\n\t\t\t\t\t\t\t\t\t\t{{ providerKindLabel(providerOf(row).kind) }}\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t<td\n\t\t\t\t\t\t\t\tv-for=\"system in systems\"\n\t\t\t\t\t\t\t\t:key=\"system.key\"\n\t\t\t\t\t\t\t\t:class=\"{ 'cn-capability-table__col--self': system.isSelf }\">\n\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\tclass=\"cn-capability-table__chip\"\n\t\t\t\t\t\t\t\t\t:class=\"'cn-capability-table__chip--' + ratingOf(row, system.key)\">\n\t\t\t\t\t\t\t\t\t{{ ratingLabel(ratingOf(row, system.key)) }}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t</tr>\n\t\t\t\t\t</tbody>\n\t\t\t\t</table>\n\t\t\t</div>\n\t\t</section>\n\t</div>\n</template>\n\n<script>\n/**\n * SPDX-License-Identifier: EUPL-1.2\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n *\n * CnCapabilityTable — a searchable, groupable capability comparison.\n *\n * Spec: features-roadmap-component — Requirement \"CnCapabilityTable\".\n */\nimport { getLanguage, translatePlural as n, translate as t } from '@nextcloud/l10n'\nimport { NcButton, NcEmptyContent, NcTextField } from '@nextcloud/vue'\nimport Magnify from 'vue-material-design-icons/Magnify.vue'\nimport {\n\tdefaultGroupMode,\n\tgroupCapabilities,\n\thasProviders,\n\tRATINGS,\n\tresolveProvider,\n\twarnCapabilityComparisonDeprecated,\n} from '../../utils/capabilityComparison.js'\n\n/**\n * How long the table waits after the last keystroke before it filters.\n *\n * 200 ms is the shortest pause that stops a 371-row regroup running on every\n * character while still feeling immediate to a touch typist.\n *\n * @type {number}\n */\nconst SEARCH_DEBOUNCE_MS = 200\n\nexport default {\n\tname: 'CnCapabilityTable',\n\n\tcomponents: { NcButton, NcEmptyContent, NcTextField, Magnify },\n\n\tprops: {\n\t\t/**\n\t\t * The comparison document the host app supplies. Shape:\n\t\t * `{systems, areas, features?, providers?, capabilities}`. Every key\n\t\t * beyond `systems`, `areas` and `capabilities` is optional, and a\n\t\t * document without them renders the table this component's consumers\n\t\t * already had.\n\t\t *\n\t\t * Deprecated, with the whole component: publish the comparison on the\n\t\t * app's docs site and link to it (as dossiq #3312 and pipelinq #2195\n\t\t * did). Still rendered; removal waits for a major version. A\n\t\t * development build warns once when a comparison is given.\n\t\t *\n\t\t * @deprecated since 2.66.0, publish the comparison on the app's docs site and link to it.\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-the-in-app-capability-comparison-is-deprecated\n\t\t * @type {object}\n\t\t */\n\t\tcomparison: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * BCP 47 locale used to pick `name` or `name_nl` on every labelled\n\t\t * entry. Defaults to the reader's Nextcloud language.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tlocale: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Render the search box. Leave it on unless the host already filters\n\t\t * the rows it passes.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tsearchable: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Force a grouping: `feature` or `area`. When empty the table opens on\n\t\t * features if any row carries one, and on areas otherwise, and the\n\t\t * reader can switch.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tgroupBy: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\t/**\n\t\t\t * What the reader has typed, bound straight to the field. Kept\n\t\t\t * separate from `query`: binding the field to the DEBOUNCED value\n\t\t\t * makes it a controlled input that snaps back to the old text\n\t\t\t * between keystrokes, which reads as a broken search box.\n\t\t\t */\n\t\t\trawQuery: '',\n\t\t\t/** The debounced query the table actually filters on. */\n\t\t\tquery: '',\n\t\t\t/** The reader's grouping choice; empty until they make one. */\n\t\t\tchosenGroupMode: '',\n\t\t\t/** Handle of the pending debounce, cleared on unmount. */\n\t\t\tdebounceHandle: null,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * @return {string} The reader's locale.\n\t\t */\n\t\teffectiveLocale() {\n\t\t\tif (this.locale) {\n\t\t\t\treturn this.locale\n\t\t\t}\n\t\t\t// `getLanguage()` and not a Dutch-defaulting resolver: mixing a\n\t\t\t// Dutch-defaulting label helper with `t()`, which falls back to its\n\t\t\t// English source, ships Dutch capability names inside English\n\t\t\t// chrome. One source, one language.\n\t\t\treturn getLanguage() || 'en'\n\t\t},\n\n\t\t/**\n\t\t * @return {Array<object>} The system columns, in document order.\n\t\t */\n\t\tsystems() {\n\t\t\treturn this.comparison?.systems ?? []\n\t\t},\n\n\t\t/**\n\t\t * @return {number} How many rows the document holds.\n\t\t */\n\t\ttotalCount() {\n\t\t\treturn this.comparison?.capabilities?.length ?? 0\n\t\t},\n\n\t\t/**\n\t\t * The grouping `groupBy` locks, or empty when it names neither.\n\t\t *\n\t\t * Read by both `groupMode` and `groupingAvailable`, because a value\n\t\t * neither of them recognises must be ignored by both: testing the raw\n\t\t * prop for emptiness let `groupBy=\"areas\"` hide the toggle while the\n\t\t * mode fell back to auto, so a typo silently removed the control.\n\t\t *\n\t\t * @return {string} `feature`, `area`, or empty.\n\t\t */\n\t\tforcedGroupMode() {\n\t\t\treturn this.groupBy === 'feature' || this.groupBy === 'area' ? this.groupBy : ''\n\t\t},\n\n\t\t/**\n\t\t * The grouping in force: the `groupBy` prop, else the reader's choice,\n\t\t * else what the document suggests.\n\t\t *\n\t\t * @return {'feature'|'area'} The mode.\n\t\t */\n\t\tgroupMode() {\n\t\t\treturn this.forcedGroupMode || this.chosenGroupMode || defaultGroupMode(this.comparison)\n\t\t},\n\n\t\t/**\n\t\t * The toggle is pointless on a document that maps no features, so it\n\t\t * only renders once there is a second grouping to switch to.\n\t\t *\n\t\t * @return {boolean} Whether to render the grouping toggle.\n\t\t */\n\t\tgroupingAvailable() {\n\t\t\treturn !this.forcedGroupMode && defaultGroupMode(this.comparison) === 'feature'\n\t\t},\n\n\t\t/**\n\t\t * @return {boolean} Whether any row names a provider.\n\t\t */\n\t\tshowProviderColumn() {\n\t\t\treturn hasProviders(this.comparison)\n\t\t},\n\n\t\t/**\n\t\t * @return {Array<object>} The groups to render, already filtered.\n\t\t */\n\t\tgroups() {\n\t\t\treturn groupCapabilities(this.comparison, {\n\t\t\t\tmode: this.groupMode,\n\t\t\t\tlocale: this.effectiveLocale,\n\t\t\t\tquery: this.query,\n\t\t\t\tunmappedLabel: this.unmappedLabel,\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * @return {number} How many rows survived the search.\n\t\t */\n\t\tvisibleCount() {\n\t\t\treturn this.groups.reduce((sum, group) => sum + group.capabilities.length, 0)\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The line that tells a reader how much they are seeing.\n\t\t */\n\t\tresultCountText() {\n\t\t\treturn n(\n\t\t\t\t'nextcloud-vue',\n\t\t\t\t'You see {count} capability of {total}.',\n\t\t\t\t'You see {count} capabilities of {total}.',\n\t\t\t\tthis.visibleCount,\n\t\t\t\t{ count: this.visibleCount, total: this.totalCount },\n\t\t\t)\n\t\t},\n\n\t\tsearchLabel() {\n\t\t\treturn t('nextcloud-vue', 'Search capabilities')\n\t\t},\n\n\t\tsearchPlaceholder() {\n\t\t\treturn t('nextcloud-vue', 'Name, number, area or feature')\n\t\t},\n\n\t\tclearLabel() {\n\t\t\treturn t('nextcloud-vue', 'Clear the search')\n\t\t},\n\n\t\tgroupingGroupLabel() {\n\t\t\treturn t('nextcloud-vue', 'Group the capabilities')\n\t\t},\n\n\t\tbyFeatureLabel() {\n\t\t\treturn t('nextcloud-vue', 'Group by feature')\n\t\t},\n\n\t\tbyAreaLabel() {\n\t\t\treturn t('nextcloud-vue', 'Group by area')\n\t\t},\n\n\t\tunmappedLabel() {\n\t\t\treturn t('nextcloud-vue', 'Not yet mapped to a feature')\n\t\t},\n\n\t\tnumberHeader() {\n\t\t\treturn t('nextcloud-vue', 'No.')\n\t\t},\n\n\t\tcapabilityHeader() {\n\t\t\treturn t('nextcloud-vue', 'Capability')\n\t\t},\n\n\t\tproviderHeader() {\n\t\t\treturn t('nextcloud-vue', 'Provided by')\n\t\t},\n\n\t\tlowConfidenceHint() {\n\t\t\treturn t('nextcloud-vue', 'We matched this row to its feature by judgement.')\n\t\t},\n\n\t\temptyName() {\n\t\t\t// `escape: false` because the result is rendered through `{{ }}`,\n\t\t\t// which escapes it already. Left on, t() turns a search for\n\t\t\t// `a & b` into the literal \"No capability matches a &amp; b\".\n\t\t\treturn t('nextcloud-vue', 'No capability matches {query}', { query: this.query }, undefined, { escape: false })\n\t\t},\n\n\t\temptyDescription() {\n\t\t\treturn t('nextcloud-vue', 'Clear the search box or try another word.')\n\t\t},\n\t},\n\n\tbeforeUnmount() {\n\t\tif (this.debounceHandle !== null) {\n\t\t\tclearTimeout(this.debounceHandle)\n\t\t\tthis.debounceHandle = null\n\t\t}\n\t},\n\n\tmounted() {\n\t\tif (this.comparison) {\n\t\t\twarnCapabilityComparisonDeprecated('CnCapabilityTable')\n\t\t}\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Debounce the reader's typing into `query`.\n\t\t *\n\t\t * @param {string} value The raw input value.\n\t\t */\n\t\tonQueryInput(value) {\n\t\t\tthis.rawQuery = value ?? ''\n\t\t\tif (this.debounceHandle !== null) {\n\t\t\t\tclearTimeout(this.debounceHandle)\n\t\t\t}\n\t\t\tthis.debounceHandle = setTimeout(() => {\n\t\t\t\tthis.debounceHandle = null\n\t\t\t\tthis.query = this.rawQuery\n\t\t\t}, SEARCH_DEBOUNCE_MS)\n\t\t},\n\n\t\t/**\n\t\t * Empty the search box at once, without waiting out the debounce.\n\t\t */\n\t\tclearQuery() {\n\t\t\tif (this.debounceHandle !== null) {\n\t\t\t\tclearTimeout(this.debounceHandle)\n\t\t\t\tthis.debounceHandle = null\n\t\t\t}\n\t\t\tthis.rawQuery = ''\n\t\t\tthis.query = ''\n\t\t},\n\n\t\t/**\n\t\t * @param {'feature'|'area'} mode The grouping the reader picked.\n\t\t */\n\t\tsetGroupMode(mode) {\n\t\t\tthis.chosenGroupMode = mode\n\t\t},\n\n\t\t/**\n\t\t * @param {object} row One capability row.\n\t\t * @return {object|null} Its provider, or null when it names none.\n\t\t */\n\t\tproviderOf(row) {\n\t\t\treturn resolveProvider(row, this.comparison, this.effectiveLocale)\n\t\t},\n\n\t\t/**\n\t\t * The kind word rendered beside the provider name. It is TEXT, not a\n\t\t * colour: a reader who cannot tell the chips apart still reads which\n\t\t * of the three kinds this is.\n\t\t *\n\t\t * @param {string} kind One of `self`, `app`, `platform`, `unknown`.\n\t\t * @return {string} The label, empty for an unrecognised kind.\n\t\t */\n\t\tproviderKindLabel(kind) {\n\t\t\tif (kind === 'self') {\n\t\t\t\treturn t('nextcloud-vue', 'this app')\n\t\t\t}\n\t\t\tif (kind === 'app') {\n\t\t\t\treturn t('nextcloud-vue', 'another app')\n\t\t\t}\n\t\t\tif (kind === 'platform') {\n\t\t\t\treturn t('nextcloud-vue', 'the platform')\n\t\t\t}\n\t\t\treturn ''\n\t\t},\n\n\t\t/**\n\t\t * The mechanism the document names for this row, rendered under the\n\t\t * provider name.\n\t\t *\n\t\t * It is a free-text key the audit writes, so it is shown as given\n\t\t * rather than translated. Rendering it is a deliberate choice: a field\n\t\t * the contract carries and the table quietly ignores is a field nobody\n\t\t * finds out is wrong.\n\t\t *\n\t\t * @param {object} row One capability row.\n\t\t * @return {string} The mechanism, empty when the row names none.\n\t\t */\n\t\tproviderHowOf(row) {\n\t\t\tconst how = row?.providerHow\n\t\t\treturn typeof how === 'string' ? how.trim() : ''\n\t\t},\n\n\t\t/**\n\t\t * @param {object} row One capability row.\n\t\t * @return {boolean} Whether to mark the row's feature as a judgement.\n\t\t */\n\t\tisLowConfidence(row) {\n\t\t\treturn row?.featureConfidence === 'low'\n\t\t\t\t&& typeof row?.feature === 'string'\n\t\t\t\t&& row.feature.trim() !== ''\n\t\t},\n\n\t\t/**\n\t\t * A rating outside the three the audit emits is shown as `unknown`\n\t\t * rather than blanked, so a typo in the document is visible.\n\t\t *\n\t\t * @param {object} row One capability row.\n\t\t * @param {string} systemKey The system column.\n\t\t * @return {string} The rating to render.\n\t\t */\n\t\tratingOf(row, systemKey) {\n\t\t\tconst rating = row?.[systemKey]\n\t\t\treturn RATINGS.includes(rating) ? rating : 'unknown'\n\t\t},\n\n\t\t/**\n\t\t * @param {string} rating One of `yes`, `partial`, `no`, `unknown`.\n\t\t * @return {string} The reader's word for it.\n\t\t */\n\t\tratingLabel(rating) {\n\t\t\tif (rating === 'yes') {\n\t\t\t\treturn t('nextcloud-vue', 'Yes')\n\t\t\t}\n\t\t\tif (rating === 'partial') {\n\t\t\t\treturn t('nextcloud-vue', 'Partial')\n\t\t\t}\n\t\t\tif (rating === 'no') {\n\t\t\t\treturn t('nextcloud-vue', 'No')\n\t\t\t}\n\t\t\treturn t('nextcloud-vue', 'Unknown')\n\t\t},\n\n\t\t/**\n\t\t * @param {object} group One rendered group.\n\t\t * @return {string} Its table caption.\n\t\t */\n\t\tcaptionFor(group) {\n\t\t\t// See `emptyName`: an area called \"Zaken & Documenten\" would reach\n\t\t\t// the caption, and a screen reader, as \"Zaken &amp; Documenten\".\n\t\t\treturn t('nextcloud-vue', 'Capabilities in {group}, rated per system.', {\n\t\t\t\tgroup: group.label,\n\t\t\t}, undefined, { escape: false })\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-capability-table__controls {\n\tdisplay: flex;\n\tgap: 12px;\n\talign-items: flex-end;\n\tflex-wrap: wrap;\n\tmargin-bottom: 8px;\n}\n\n.cn-capability-table__search {\n\tflex: 1 1 280px;\n\tmin-width: 220px;\n}\n\n.cn-capability-table__grouping {\n\tdisplay: flex;\n\tgap: 8px;\n\tflex-wrap: wrap;\n}\n\n.cn-capability-table__count {\n\tmargin: 0 0 16px;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n}\n\n.cn-capability-table__group {\n\tmargin-bottom: 24px;\n}\n\n.cn-capability-table__group-title {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\tmargin: 0 0 8px;\n\tfont-size: 1.05em;\n\tcolor: var(--color-main-text);\n}\n\n.cn-capability-table__group-count {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n\tfont-weight: normal;\n}\n\n.cn-capability-table__scroller {\n\toverflow-x: auto;\n}\n\n.cn-capability-table__table {\n\twidth: 100%;\n\tborder-collapse: collapse;\n}\n\n.cn-capability-table__caption {\n\ttext-align: start;\n\tcaption-side: top;\n\tpadding-bottom: 8px;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n}\n\n.cn-capability-table__table th,\n.cn-capability-table__table td {\n\tborder-bottom: 1px solid var(--color-border);\n\tpadding: 6px 8px;\n\ttext-align: start;\n\tvertical-align: top;\n}\n\n.cn-capability-table__table thead th {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-weight: bold;\n\twhite-space: nowrap;\n}\n\n.cn-capability-table__num {\n\twhite-space: nowrap;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-capability-table__cap {\n\tfont-weight: normal;\n\tcolor: var(--color-main-text);\n\tmin-width: 240px;\n}\n\n.cn-capability-table__col--self {\n\tbackground-color: var(--color-background-hover);\n}\n\n.cn-capability-table__confidence {\n\tdisplay: inline-block;\n\tmargin-inline-start: 4px;\n\tcolor: var(--color-text-maxcontrast);\n\tcursor: help;\n}\n\n.cn-capability-table__sr-only {\n\tposition: absolute;\n\twidth: 1px;\n\theight: 1px;\n\toverflow: hidden;\n\tclip-path: inset(50%);\n\twhite-space: nowrap;\n\t/* Padding and border are part of the recipe, not decoration: either one\n\t inherited onto the 1px box grows it back into the layout. */\n\tpadding: 0;\n\tborder: 0;\n\tmargin: -1px;\n}\n\n.cn-capability-table__provider {\n\tdisplay: inline-flex;\n\tflex-direction: column;\n\tgap: 2px;\n}\n\n.cn-capability-table__provider-name {\n\tborder-inline-start: 3px solid var(--color-border-dark);\n\tpadding-inline-start: 6px;\n\tcolor: var(--color-main-text);\n}\n\n.cn-capability-table__provider--self .cn-capability-table__provider-name {\n\tborder-inline-start-color: var(--color-primary-element);\n}\n\n.cn-capability-table__provider--app .cn-capability-table__provider-name {\n\tborder-inline-start-color: var(--color-success);\n}\n\n.cn-capability-table__provider--platform .cn-capability-table__provider-name {\n\tborder-inline-start-color: var(--color-warning);\n}\n\n.cn-capability-table__provider-kind,\n.cn-capability-table__provider-how {\n\tpadding-inline-start: 9px;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.8em;\n}\n\n.cn-capability-table__chip {\n\tdisplay: inline-block;\n\tpadding: 1px 8px;\n\tborder-radius: var(--border-radius-pill, 100px);\n\tborder: 1px solid var(--color-border-dark);\n\tcolor: var(--color-main-text);\n\tfont-size: 0.85em;\n\twhite-space: nowrap;\n}\n\n.cn-capability-table__chip--yes {\n\tborder-color: var(--color-success);\n}\n\n.cn-capability-table__chip--partial {\n\tborder-color: var(--color-warning);\n}\n\n.cn-capability-table__chip--no {\n\tborder-color: var(--color-error);\n}\n\n.cn-capability-table__chip--unknown {\n\tborder-style: dashed;\n\tcolor: var(--color-text-maxcontrast);\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_createVNode","_createElementVNode","_toDisplayString","_Fragment","_renderList","_normalizeClass"],"mappings":"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"}
@@ -186,6 +186,19 @@ var script = {
186
186
  type: Boolean,
187
187
  default: true,
188
188
  },
189
+
190
+ /**
191
+ * GridStack's `float`. True (the default) keeps every item on the row
192
+ * it was placed on, with empty rows above it if need be. False packs
193
+ * items upward, so a size-to-content cell that shrinks, or a widget
194
+ * that is not shown, closes its gap instead of leaving a band.
195
+ *
196
+ * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-dashboard-closes-the-gap-a-shrinking-or-hidden-widget-leaves
197
+ */
198
+ float: {
199
+ type: Boolean,
200
+ default: true,
201
+ },
189
202
  },
190
203
 
191
204
  emits: [
@@ -209,6 +222,8 @@ var script = {
209
222
  return {
210
223
  grid: null,
211
224
  contentObserver: null,
225
+ /** True while the grid moves an item to where the layout put it. */
226
+ applyingLayout: false,
212
227
  domId: `cn-dashboard-grid-${++domIdCounter}`,
213
228
  /** Text currently held in the polite live region. */
214
229
  announcement: '',
@@ -416,7 +431,7 @@ var script = {
416
431
  column: this.columns,
417
432
  cellHeight: this.cellHeight,
418
433
  margin: this.margin,
419
- float: true,
434
+ float: this.float,
420
435
  animate: true,
421
436
  disableDrag: !this.editable,
422
437
  disableResize: !this.editable,
@@ -489,6 +504,10 @@ var script = {
489
504
  // switched off as the grid narrows, and back on as it widens.
490
505
  this.syncInteractivity();
491
506
 
507
+ if (this.applyingLayout) {
508
+ return
509
+ }
510
+
492
511
  // GridStack fires `change` for its own responsive rescale too, and at a
493
512
  // 12 → 1 breakpoint every item reads `gridX: 0, gridWidth: 1`. Emitting
494
513
  // that has the host persist the reflow AS the authored layout, which
@@ -888,7 +907,29 @@ var script = {
888
907
  const node = this.grid.engine.nodes.find((n) => String(n.id) === String(item.id));
889
908
  if (!node) {
890
909
  this.grid.makeWidget(el);
891
- } else if (node.el !== el) {
910
+ } else if (node.el === el) {
911
+ // The layout MOVED an item the grid already tracks: the
912
+ // page re-compacts its display layout when a widget's
913
+ // condition hides it, and the item below was given a new
914
+ // gridY that never reached GridStack, so a band stayed
915
+ // where the hidden widget had been. A drag reports the
916
+ // grid's own position back, which matches and does
917
+ // nothing here.
918
+ const x = item.gridX ?? node.x;
919
+ const y = item.gridY ?? node.y;
920
+ if (x !== node.x || y !== node.y) {
921
+ // The layout said where it goes; GridStack's `change`
922
+ // for it is not a user's arrangement and is not
923
+ // reported back (it would write the display-only
924
+ // compaction into the authored layout).
925
+ this.applyingLayout = true;
926
+ try {
927
+ this.grid.update(el, { x, y });
928
+ } finally {
929
+ this.applyingLayout = false;
930
+ }
931
+ }
932
+ } else {
892
933
  // Drop the stale node without touching its detached DOM,
893
934
  // then adopt the new element so GridStack sizes/places it.
894
935
  this.grid.removeWidget(node.el, false, false);