@conduction/nextcloud-vue 2.61.0 → 2.63.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue.js +10 -0
  2. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue.js.map +1 -0
  3. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue2.js +189 -0
  4. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue2.js.map +1 -0
  5. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue3.js +72 -0
  6. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue3.js.map +1 -0
  7. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +394 -27
  8. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
  9. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
  10. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +99 -9
  11. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  12. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +39 -18
  13. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  14. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +79 -2
  15. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
  16. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js +38 -25
  17. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
  18. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue2.js +21 -1
  19. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue2.js.map +1 -1
  20. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue3.js +4 -1
  21. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue3.js.map +1 -1
  22. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
  23. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js +53 -37
  24. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
  25. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +30 -6
  26. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  27. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +2 -2
  28. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  29. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue.js +10 -0
  30. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue.js.map +1 -0
  31. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js +293 -0
  32. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js.map +1 -0
  33. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js +189 -0
  34. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js.map +1 -0
  35. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +256 -23
  36. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  37. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +119 -51
  38. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  39. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  40. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +2 -1
  41. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  42. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue2.js +3 -1
  43. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue2.js.map +1 -1
  44. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue3.js +1 -1
  45. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue3.js.map +1 -1
  46. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +234 -9
  47. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  48. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +96 -57
  49. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  50. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +51 -2
  51. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  52. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +5 -1
  53. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  54. package/dist/esm/components/CnIndexPage/selfModeActions.js +3 -3
  55. package/dist/esm/components/CnIndexPage/selfModeActions.js.map +1 -1
  56. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue2.js.map +1 -1
  57. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue3.js.map +1 -1
  58. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +36 -1
  59. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  60. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +4 -1
  61. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  62. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +53 -0
  63. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  64. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +22 -5
  65. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  66. package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js.map +1 -1
  67. package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js.map +1 -1
  68. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue2.js +121 -12
  69. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue2.js.map +1 -1
  70. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue3.js +3 -1
  71. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue3.js.map +1 -1
  72. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +326 -1
  73. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
  74. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +112 -26
  75. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
  76. package/dist/esm/components/CnStorePage/CnStorePage.vue2.js.map +1 -1
  77. package/dist/esm/components/CnStorePage/CnStorePage.vue3.js.map +1 -1
  78. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js +10 -0
  79. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js.map +1 -0
  80. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js +337 -0
  81. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js.map +1 -0
  82. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js +102 -0
  83. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js.map +1 -0
  84. package/dist/esm/components/CnTimelineWidget/dashboardRegistration.js +34 -0
  85. package/dist/esm/components/CnTimelineWidget/dashboardRegistration.js.map +1 -0
  86. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js +131 -8
  87. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js.map +1 -1
  88. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js +40 -38
  89. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js.map +1 -1
  90. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +8 -6
  91. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js.map +1 -1
  92. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  93. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  94. package/dist/esm/components/CnWikiPage/CnWikiPage.vue2.js.map +1 -1
  95. package/dist/esm/components/CnWikiPage/CnWikiPage.vue3.js.map +1 -1
  96. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js +19 -0
  97. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js.map +1 -1
  98. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js +1 -1
  99. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js.map +1 -1
  100. package/dist/esm/composables/useDependencyCheck.js +65 -0
  101. package/dist/esm/composables/useDependencyCheck.js.map +1 -0
  102. package/dist/esm/composables/useWalkthrough.js +218 -6
  103. package/dist/esm/composables/useWalkthrough.js.map +1 -1
  104. package/dist/esm/index.js +2 -0
  105. package/dist/esm/index.js.map +1 -1
  106. package/dist/esm/l10n/en.json.js +48 -0
  107. package/dist/esm/l10n/en.json.js.map +1 -1
  108. package/dist/esm/l10n/nl.json.js +48 -0
  109. package/dist/esm/l10n/nl.json.js.map +1 -1
  110. package/dist/esm/nextcloud-vue.css +557 -9
  111. package/dist/esm/store/useObjectStore.js +8 -0
  112. package/dist/esm/store/useObjectStore.js.map +1 -1
  113. package/dist/esm/utils/columnFilters.js +311 -0
  114. package/dist/esm/utils/columnFilters.js.map +1 -0
  115. package/dist/esm/utils/groupAutocomplete.js +123 -0
  116. package/dist/esm/utils/groupAutocomplete.js.map +1 -0
  117. package/dist/esm/utils/libraryWidgetKeys.js +2 -0
  118. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  119. package/dist/esm/utils/pickerOptions.js +198 -0
  120. package/dist/esm/utils/pickerOptions.js.map +1 -0
  121. package/dist/esm/utils/schema.js +83 -7
  122. package/dist/esm/utils/schema.js.map +1 -1
  123. package/dist/esm/utils/timelineEvents.js +174 -0
  124. package/dist/esm/utils/timelineEvents.js.map +1 -0
  125. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  126. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  127. package/dist/esm/utils/walkthroughSignals.js +88 -0
  128. package/dist/esm/utils/walkthroughSignals.js.map +1 -0
  129. package/dist/nextcloud-vue.cjs.js +9779 -5236
  130. package/dist/nextcloud-vue.cjs.js.map +2 -2
  131. package/dist/nextcloud-vue.css +557 -9
  132. package/l10n/en.json +48 -0
  133. package/l10n/nl.json +48 -0
  134. package/package.json +3 -2
  135. package/scripts/manifest-schema-hash.js +41 -0
  136. package/scripts/record-manifest-schema-hash.js +52 -0
  137. package/src/components/CnAdminActionCard/CnAdminActionCard.vue +234 -0
  138. package/src/components/CnAdminActionCard/index.js +4 -0
  139. package/src/components/CnAppNav/CnAppNav.vue +394 -27
  140. package/src/components/CnAppRoot/CnAppRoot.vue +129 -15
  141. package/src/components/CnBannerWidget/CnBannerWidget.vue +113 -2
  142. package/src/components/CnBrandStripe/CnBrandStripe.md +16 -0
  143. package/src/components/CnBrandStripe/CnBrandStripe.vue +58 -5
  144. package/src/components/CnChoiceCards/CnChoiceCards.vue +56 -31
  145. package/src/components/CnDashboardPage/CnDashboardPage.vue +32 -8
  146. package/src/components/CnDataTable/CnColumnFilterPopover.vue +524 -0
  147. package/src/components/CnDataTable/CnDataTable.md +4 -1
  148. package/src/components/CnDataTable/CnDataTable.vue +302 -21
  149. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +2 -2
  150. package/src/components/CnFieldHelper/CnFieldHelper.vue +4 -2
  151. package/src/components/CnFormDialog/CnFormDialog.vue +290 -31
  152. package/src/components/CnIndexPage/CnIndexPage.vue +55 -2
  153. package/src/components/CnIndexPage/selfModeActions.js +3 -3
  154. package/src/components/CnLinkCardsPage/CnLinkCardsPage.vue +35 -2
  155. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +39 -1
  156. package/src/components/CnObjectSidebar/CnFilesTab.vue +65 -0
  157. package/src/components/CnReportsPage/CnReportsPage.vue +7 -0
  158. package/src/components/CnResourceSelect/CnResourceSelect.vue +123 -12
  159. package/src/components/CnSetupWizard/CnSetupWizard.vue +392 -3
  160. package/src/components/CnStorePage/CnStorePage.vue +7 -0
  161. package/src/components/CnTimelineWidget/CnTimelineWidget.vue +477 -0
  162. package/src/components/CnTimelineWidget/dashboardRegistration.js +32 -0
  163. package/src/components/CnTimelineWidget/index.js +4 -0
  164. package/src/components/CnWalkthrough/CnWalkthrough.vue +143 -10
  165. package/src/components/CnWidgetGrid/builtInWidgets.js +2 -0
  166. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  167. package/src/components/CnWikiPage/CnWikiPage.vue +7 -0
  168. package/src/components/CnWizardDialog/CnWizardDialog.vue +20 -1
  169. package/src/components/index.js +2 -0
  170. package/src/composables/useDependencyCheck.js +61 -0
  171. package/src/composables/useWalkthrough.js +217 -5
  172. package/src/css/dashboard.css +49 -0
  173. package/src/css/detail-page.css +19 -0
  174. package/src/css/table.css +82 -2
  175. package/src/index.js +2 -0
  176. package/src/public/index.js +5 -0
  177. package/src/schemas/app-manifest-v2.schema.json +23 -1
  178. package/src/store/useObjectStore.js +8 -0
  179. package/src/utils/columnFilters.js +324 -0
  180. package/src/utils/groupAutocomplete.js +119 -0
  181. package/src/utils/libraryWidgetKeys.js +2 -0
  182. package/src/utils/pickerOptions.js +194 -0
  183. package/src/utils/schema.js +83 -7
  184. package/src/utils/timelineEvents.js +171 -0
  185. package/src/utils/validateManifestV2.compiled.js +2 -2
  186. package/src/utils/walkthroughSignals.js +94 -0
@@ -2,7 +2,10 @@ import { openBlock, createElementBlock, normalizeClass, createElementVNode } fro
2
2
 
3
3
  function render(_ctx, _cache, $props, $setup, $data, $options) {
4
4
  return (openBlock(), createElementBlock("div", {
5
- class: normalizeClass(["cn-brand-stripe", { 'cn-brand-stripe--vertical': $props.orientation === 'vertical' }]),
5
+ class: normalizeClass(["cn-brand-stripe", {
6
+ 'cn-brand-stripe--vertical': $props.orientation === 'vertical',
7
+ 'cn-brand-stripe--inverse': $props.variant === 'inverse',
8
+ }]),
6
9
  "aria-hidden": "true",
7
10
  "data-testid": "cn-brand-stripe"
8
11
  }, [...(_cache[0] || (_cache[0] = [
@@ -1 +1 @@
1
- {"version":3,"file":"CnBrandStripe.vue3.js","sources":["../../../../src/components/CnBrandStripe/CnBrandStripe.vue?vue&type=template&id=6fdfa763&lang.js"],"sourcesContent":["<!--\n SPDX-FileCopyrightText: 2026 Conduction B.V.\n SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<div\n\t\tclass=\"cn-brand-stripe\"\n\t\t:class=\"{ 'cn-brand-stripe--vertical': orientation === 'vertical' }\"\n\t\taria-hidden=\"true\"\n\t\tdata-testid=\"cn-brand-stripe\">\n\t\t<span class=\"cn-brand-stripe__band cn-brand-stripe__band--1\" />\n\t\t<span class=\"cn-brand-stripe__band cn-brand-stripe__band--2\" />\n\t\t<span class=\"cn-brand-stripe__band cn-brand-stripe__band--3\" />\n\t</div>\n</template>\n\n<script>\n/**\n * CnBrandStripe draws the organisation's brand stripe: up to three coloured\n * bands next to each other, for the top of a page, a card or a sidebar.\n *\n * The theme decides what it looks like, through the library's own custom\n * properties: `--cn-brand-stripe-color-1/2/3` (the colours, start to end),\n * `--cn-brand-stripe-ratio-1/2/3` (unitless shares, such as 6, 3 and 1)\n * and `--cn-brand-stripe-height` (such as 5px). A theming app maps its own\n * tokens onto these; the library never reads a theme's tokens. Without them the\n * stripe is one band in the Nextcloud primary colour, 4px high.\n *\n * The stripe is decoration: it carries no meaning and is hidden from\n * assistive technology.\n *\n * ```vue\n * <CnBrandStripe />\n * ```\n */\nexport default {\n\tname: 'CnBrandStripe',\n\n\tprops: {\n\t\t/**\n\t\t * The direction the stripe runs in: `horizontal` (the default, a bar\n\t\t * across the top) or `vertical` (a bar down the side; the height\n\t\t * token then sets its width).\n\t\t *\n\t\t * @type {'horizontal'|'vertical'}\n\t\t */\n\t\torientation: {\n\t\t\ttype: String,\n\t\t\tdefault: 'horizontal',\n\t\t\tvalidator: (v) => ['horizontal', 'vertical'].includes(v),\n\t\t},\n\t},\n}\n</script>\n\n<!--\n The stripe reads the library's own `--cn-brand-stripe-*` properties, each\n with a Nextcloud fallback. A theming app sets them from its own tokens.\n-->\n<style scoped>\n.cn-brand-stripe {\n\tdisplay: flex;\n\tflex: none;\n\twidth: 100%;\n\theight: var(--cn-brand-stripe-height, 4px);\n}\n\n.cn-brand-stripe--vertical {\n\tflex-direction: column;\n\twidth: var(--cn-brand-stripe-height, 4px);\n\theight: 100%;\n}\n\n.cn-brand-stripe__band {\n\tflex-basis: 0;\n\tflex-shrink: 1;\n}\n\n.cn-brand-stripe__band--1 {\n\tflex-grow: var(--cn-brand-stripe-ratio-1, 1);\n\tbackground-color: var(--cn-brand-stripe-color-1, var(--color-primary-element));\n}\n\n.cn-brand-stripe__band--2 {\n\tflex-grow: var(--cn-brand-stripe-ratio-2, 1);\n\tbackground-color: var(--cn-brand-stripe-color-2, var(--color-primary-element));\n}\n\n.cn-brand-stripe__band--3 {\n\tflex-grow: var(--cn-brand-stripe-ratio-3, 1);\n\tbackground-color: var(--cn-brand-stripe-color-3, var(--color-primary-element));\n}\n\n@media print {\n\t.cn-brand-stripe {\n\t\tprint-color-adjust: exact;\n\t\t-webkit-print-color-adjust: exact;\n\t}\n}\n</style>\n"],"names":["_normalizeClass"],"mappings":";;;;IAUE,KAAM,EAAAA,cAAA,CAAA,CAAA,iBAAiB,EAAA,EAAA,2BAAA,EACgB,MAAW,CAAA,WAAA,KAAA,UAAA,EAAA,CAAA,CAAA;AAClD,IAAA,aAAW,EAAC,MAAM;AAClB,IAAA,aAAI,EAAA,iBAAA;;;;;;;;;;"}
1
+ {"version":3,"file":"CnBrandStripe.vue3.js","sources":["../../../../src/components/CnBrandStripe/CnBrandStripe.vue?vue&type=template&id=6fdfa763&lang.js"],"sourcesContent":["<!--\n SPDX-FileCopyrightText: 2026 Conduction B.V.\n SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<div\n\t\tclass=\"cn-brand-stripe\"\n\t\t:class=\"{\n\t\t\t'cn-brand-stripe--vertical': orientation === 'vertical',\n\t\t\t'cn-brand-stripe--inverse': variant === 'inverse',\n\t\t}\"\n\t\taria-hidden=\"true\"\n\t\tdata-testid=\"cn-brand-stripe\">\n\t\t<span class=\"cn-brand-stripe__band cn-brand-stripe__band--1\" />\n\t\t<span class=\"cn-brand-stripe__band cn-brand-stripe__band--2\" />\n\t\t<span class=\"cn-brand-stripe__band cn-brand-stripe__band--3\" />\n\t</div>\n</template>\n\n<script>\n/**\n * CnBrandStripe draws the organisation's brand stripe: up to three coloured\n * bands next to each other, or the organisation's own motif, for the top of a\n * page, a card or a sidebar.\n *\n * The theme decides what it looks like, through the library's own custom\n * properties: `--cn-brand-stripe-color-1/2/3` (the colours, start to end),\n * `--cn-brand-stripe-ratio-1/2/3` (unitless shares, such as 6, 3 and 1)\n * and `--cn-brand-stripe-height` (such as 5px). A theming app maps its own\n * tokens onto these; the library never reads a theme's tokens. Without them the\n * stripe is one band in the Nextcloud primary colour, 4px high.\n *\n * A theme whose motif is not three bands (hanging twigs, a canal and its bank,\n * a slanted cut) names it in `--cn-brand-stripe-image`, any background-image\n * value; the stripe then draws that image in the same box instead of the\n * bands. `--cn-brand-stripe-image-inverse` is the same motif for a dark band;\n * the `inverse` variant draws it, else the one image, else the bands.\n *\n * The stripe is decoration: it carries no meaning and is hidden from\n * assistive technology.\n *\n * ```vue\n * <CnBrandStripe />\n * ```\n */\nexport default {\n\tname: 'CnBrandStripe',\n\n\tprops: {\n\t\t/**\n\t\t * The direction the stripe runs in: `horizontal` (the default, a bar\n\t\t * across the top) or `vertical` (a bar down the side; the height\n\t\t * token then sets its width).\n\t\t *\n\t\t * @type {'horizontal'|'vertical'}\n\t\t */\n\t\torientation: {\n\t\t\ttype: String,\n\t\t\tdefault: 'horizontal',\n\t\t\tvalidator: (v) => ['horizontal', 'vertical'].includes(v),\n\t\t},\n\n\t\t/**\n\t\t * The ground the stripe sits on: `default` (a light surface) or\n\t\t * `inverse` (a dark band such as a footer), which draws the theme's\n\t\t * `--cn-brand-stripe-image-inverse` when it names one.\n\t\t *\n\t\t * @type {'default'|'inverse'}\n\t\t */\n\t\tvariant: {\n\t\t\ttype: String,\n\t\t\tdefault: 'default',\n\t\t\tvalidator: (v) => ['default', 'inverse'].includes(v),\n\t\t},\n\t},\n}\n</script>\n\n<!--\n The stripe reads the library's own `--cn-brand-stripe-*` properties, each\n with a Nextcloud fallback. A theming app sets them from its own tokens.\n\n THE ROOT PAINTS, NOT THE BANDS. The three bands are one background on the\n root, a gradient with hard stops computed from the ratios, so a theme's\n motif image can take its place with one `var()` fallback. A band painted on\n its own element could not be switched off by the presence of an image: CSS\n cannot ask whether a custom property is set. The band elements stay, empty,\n so markup and tests that count them keep working.\n-->\n<style scoped>\n.cn-brand-stripe {\n\t--cn-brand-stripe-first: var(--cn-brand-stripe-ratio-1, 1);\n\t--cn-brand-stripe-second: var(--cn-brand-stripe-ratio-2, 1);\n\t--cn-brand-stripe-total: calc(\n\t\tvar(--cn-brand-stripe-first) + var(--cn-brand-stripe-second) + var(--cn-brand-stripe-ratio-3, 1)\n\t);\n\t--cn-brand-stripe-stop-1: calc(var(--cn-brand-stripe-first) / var(--cn-brand-stripe-total) * 100%);\n\t--cn-brand-stripe-stop-2: calc(\n\t\t(var(--cn-brand-stripe-first) + var(--cn-brand-stripe-second)) / var(--cn-brand-stripe-total) * 100%\n\t);\n\t--cn-brand-stripe-direction: to right;\n\t--cn-brand-stripe-bands: linear-gradient(\n\t\tvar(--cn-brand-stripe-direction),\n\t\tvar(--cn-brand-stripe-color-1, var(--color-primary-element)) 0 var(--cn-brand-stripe-stop-1),\n\t\tvar(--cn-brand-stripe-color-2, var(--color-primary-element)) var(--cn-brand-stripe-stop-1) var(--cn-brand-stripe-stop-2),\n\t\tvar(--cn-brand-stripe-color-3, var(--color-primary-element)) var(--cn-brand-stripe-stop-2) 100%\n\t);\n\tdisplay: flex;\n\tflex: none;\n\twidth: 100%;\n\theight: var(--cn-brand-stripe-height, 4px);\n\tbackground-image: var(--cn-brand-stripe-image, var(--cn-brand-stripe-bands));\n\tbackground-repeat: repeat-x;\n\tbackground-size: auto 100%;\n}\n\n.cn-brand-stripe--inverse {\n\tbackground-image: var(--cn-brand-stripe-image-inverse, var(--cn-brand-stripe-image, var(--cn-brand-stripe-bands)));\n}\n\n.cn-brand-stripe--vertical {\n\t--cn-brand-stripe-direction: to bottom;\n\tflex-direction: column;\n\twidth: var(--cn-brand-stripe-height, 4px);\n\theight: 100%;\n\tbackground-repeat: repeat-y;\n\tbackground-size: 100% auto;\n}\n\n.cn-brand-stripe__band {\n\tflex-basis: 0;\n\tflex-shrink: 1;\n}\n\n.cn-brand-stripe__band--1 {\n\tflex-grow: var(--cn-brand-stripe-ratio-1, 1);\n}\n\n.cn-brand-stripe__band--2 {\n\tflex-grow: var(--cn-brand-stripe-ratio-2, 1);\n}\n\n.cn-brand-stripe__band--3 {\n\tflex-grow: var(--cn-brand-stripe-ratio-3, 1);\n}\n\n@media print {\n\t.cn-brand-stripe {\n\t\tprint-color-adjust: exact;\n\t\t-webkit-print-color-adjust: exact;\n\t}\n}\n</style>\n"],"names":[],"mappings":";;;;AAUE,IAAA,KAAC,kBAAA,iBAAA,EAAA;gCACgB,MAAA,CAAA,WAAA,KAAA,UAAA;+BAAA,MAAA,CAAA,OAAA,KAAA,SAAA;;AAIjB,IAAA,aAAW,EAAC,MAAM;AAClB,IAAA,aAAI,EAAA,iBAAA;;;;;;;;;;"}
@@ -1 +1 @@
1
- {"version":3,"file":"CnChoiceCards.vue2.js","sources":["../../../../src/components/CnChoiceCards/CnChoiceCards.vue"],"sourcesContent":["<!-- SPDX-License-Identifier: EUPL-1.2 -->\n<!-- SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl> -->\n<template>\n\t<fieldset class=\"cn-choice-cards\" :aria-busy=\"loading ? 'true' : 'false'\">\n\t\t<legend v-if=\"label\" class=\"cn-choice-cards__legend\">\n\t\t\t{{ label }}\n\t\t</legend>\n\n\t\t<div v-if=\"loading\" class=\"cn-choice-cards__loading\">\n\t\t\t<NcLoadingIcon :size=\"32\" />\n\t\t</div>\n\n\t\t<NcNoteCard v-else-if=\"normalizedOptions.length === 0\" type=\"warning\">\n\t\t\t{{ emptyText }}\n\t\t</NcNoteCard>\n\n\t\t<div v-else class=\"cn-choice-cards__grid\" :style=\"gridStyle\">\n\t\t\t<label\n\t\t\t\tv-for=\"option in normalizedOptions\"\n\t\t\t\t:key=\"String(option.value)\"\n\t\t\t\tclass=\"cn-choice-cards__option\"\n\t\t\t\t:class=\"{ 'cn-choice-cards__option--selected': isSelected(option) }\">\n\t\t\t\t<!-- A REAL, VISIBLE input, not a styled div with a click handler.\n\t\t\t\t It carries the checked state to assistive technology and to\n\t\t\t\t Windows high-contrast mode for free, and it keeps the\n\t\t\t\t selection perceivable without colour (WCAG 1.4.1) — the\n\t\t\t\t card's active border alone would not be. -->\n\t\t\t\t<input\n\t\t\t\t\tclass=\"cn-choice-cards__input\"\n\t\t\t\t\t:type=\"multiple ? 'checkbox' : 'radio'\"\n\t\t\t\t\t:name=\"groupName\"\n\t\t\t\t\t:value=\"String(option.value)\"\n\t\t\t\t\t:checked=\"isSelected(option)\"\n\t\t\t\t\t:disabled=\"disabled\"\n\t\t\t\t\t@change=\"onToggle(option)\">\n\t\t\t\t<CnCard\n\t\t\t\t\tclass=\"cn-choice-cards__card\"\n\t\t\t\t\t:title=\"option.label\"\n\t\t\t\t\ttitleTag=\"span\"\n\t\t\t\t\t:description=\"option.description\"\n\t\t\t\t\t:stats=\"option.stats || []\"\n\t\t\t\t\t:tags=\"option.tags || []\"\n\t\t\t\t\t:active=\"isSelected(option)\"\n\t\t\t\t\tactiveVariant=\"primary\"\n\t\t\t\t\t:descriptionLines=\"descriptionLines\">\n\t\t\t\t\t<template v-if=\"option.icon\" #icon>\n\t\t\t\t\t\t<CnIcon :name=\"option.icon\" :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t</CnCard>\n\t\t\t</label>\n\t\t</div>\n\t</fieldset>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon, NcNoteCard } from '@nextcloud/vue'\nimport CnCard from '../CnCard/CnCard.vue'\nimport CnIcon from '../CnIcon/CnIcon.vue'\n\nlet groupSeq = 0\n\n/**\n * CnChoiceCards — pick one option, or several, from a grid of cards.\n *\n * A card carries what a dropdown entry cannot: a description of what the\n * option actually is, and a few numbers about it. Use it where the choice\n * deserves explaining — the setup wizard's example-data step, an onboarding\n * template picker — and a plain `NcSelect` where a label already says enough.\n *\n * Each card is a `<label>` around a real radio (or checkbox when `multiple`),\n * so keyboard navigation, the checked state and high-contrast rendering are\n * the browser's, not a reimplementation.\n *\n * ```vue\n * <CnChoiceCards\n * v-model=\"picked\"\n * label=\"Which kind of organisation is this for?\"\n * :options=\"[\n * { value: 'municipality', label: 'Municipality',\n * description: 'A city council with committees and a decision list.',\n * stats: [{ label: 'Objects', value: 170 }] },\n * { value: 'none', label: 'None, I will set this up myself' },\n * ]\" />\n * ```\n */\nexport default {\n\tname: 'CnChoiceCards',\n\n\tcomponents: {\n\t\tCnCard,\n\t\tCnIcon,\n\t\tNcLoadingIcon,\n\t\tNcNoteCard,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The options to offer. Each: `{ value, label, description?, stats?,\n\t\t * tags?, icon? }` — `icon` is a PascalCase MDI name resolved by CnIcon.\n\t\t */\n\t\toptions: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * The selected value: a scalar, or an array when `multiple`.\n\t\t *\n\t\t * @type {string|number|boolean|Array|null}\n\t\t */\n\t\tmodelValue: {\n\t\t\ttype: [Boolean, String, Number, Array, Object],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Allow selecting several cards; the model becomes an array. */\n\t\tmultiple: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Group label, rendered as the fieldset's legend. */\n\t\tlabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Disable every card (e.g. a dependent choice with no parent value yet). */\n\t\tdisabled: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Show a spinner instead of the grid while options are being fetched. */\n\t\tloading: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Message shown when there is nothing to choose from. */\n\t\temptyText: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Nothing to choose from here.'),\n\t\t},\n\n\t\t/** Lines of description shown before clamping. */\n\t\tdescriptionLines: {\n\t\t\ttype: Number,\n\t\t\tdefault: 4,\n\t\t},\n\t},\n\n\temits: ['update:modelValue'],\n\n\tdata() {\n\t\tgroupSeq += 1\n\t\treturn {\n\t\t\t// Radios only group by `name`, so two card groups on one page would\n\t\t\t// otherwise share a selection.\n\t\t\tgroupName: 'cn-choice-cards-' + groupSeq,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Options with a usable `value` and `label`, tolerating the shapes a\n\t\t * server list arrives in: `id` instead of `value`, `name` instead of\n\t\t * `label`, and a bare string for a value that is its own label.\n\t\t *\n\t\t * @return {Array<object>} The normalised options.\n\t\t */\n\t\tnormalizedOptions() {\n\t\t\treturn (this.options || [])\n\t\t\t\t.filter((option) => option !== null && option !== undefined)\n\t\t\t\t.map((option) => {\n\t\t\t\t\tif (typeof option !== 'object') {\n\t\t\t\t\t\treturn { value: option, label: String(option) }\n\t\t\t\t\t}\n\t\t\t\t\tconst value = option.value !== undefined ? option.value : option.id\n\t\t\t\t\treturn {\n\t\t\t\t\t\t...option,\n\t\t\t\t\t\tvalue,\n\t\t\t\t\t\tlabel: option.label || option.name || String(value),\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.filter((option) => option.value !== undefined)\n\t\t},\n\n\t\t/**\n\t\t * The current selection as an array of values, whatever the model shape.\n\t\t *\n\t\t * @return {Array} The selected values.\n\t\t */\n\t\tselectedValues() {\n\t\t\tif (this.multiple) {\n\t\t\t\treturn Array.isArray(this.modelValue) ? this.modelValue : []\n\t\t\t}\n\t\t\treturn (this.modelValue === null || this.modelValue === undefined || this.modelValue === '')\n\t\t\t\t? []\n\t\t\t\t: [this.modelValue]\n\t\t},\n\n\t\t/**\n\t\t * Inline override for the grid's column count. `auto-fit` with a\n\t\t * 260px minimum wraps a small option count onto its own row well\n\t\t * before the container is actually too narrow for them side by\n\t\t * side — two cards would stack at ~550px wide even though halving\n\t\t * the available width still leaves each one legible. A binary (or\n\t\t * near-binary) choice should always sit in one row and fill the\n\t\t * available width; a larger option count keeps the responsive\n\t\t * `auto-fit` wrapping from the stylesheet (`{}` leaves it alone).\n\t\t *\n\t\t * @return {object} A `gridTemplateColumns` override, or `{}`.\n\t\t */\n\t\tgridStyle() {\n\t\t\tconst count = this.normalizedOptions.length\n\t\t\tif (count > 0 && count <= 2) {\n\t\t\t\treturn { gridTemplateColumns: `repeat(${count}, minmax(0, 1fr))` }\n\t\t\t}\n\t\t\treturn {}\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Whether an option is currently selected.\n\t\t *\n\t\t * Compares as strings: a value that made the round trip through the\n\t\t * server as `\"1\"` must still match the `1` it was sent as.\n\t\t *\n\t\t * @param {object} option The normalised option.\n\t\t * @return {boolean} True when selected.\n\t\t */\n\t\tisSelected(option) {\n\t\t\treturn this.selectedValues.some((v) => String(v) === String(option.value))\n\t\t},\n\n\t\t/**\n\t\t * Select (single) or toggle (multiple) an option and emit the new model.\n\t\t *\n\t\t * @param {object} option The normalised option.\n\t\t * @return {void}\n\t\t */\n\t\tonToggle(option) {\n\t\t\tif (this.disabled) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!this.multiple) {\n\t\t\t\t/**\n\t\t\t\t * @event update:modelValue The selected value, or the array of values when `multiple`.\n\t\t\t\t * @type {string|number|boolean|Array}\n\t\t\t\t */\n\t\t\t\t// Never the option OBJECT: a parent has to be able to hand back\n\t\t\t\t// what it receives as `modelValue`.\n\t\t\t\tthis.$emit('update:modelValue', option.value)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst next = this.selectedValues.filter((v) => String(v) !== String(option.value))\n\t\t\tif (next.length === this.selectedValues.length) {\n\t\t\t\tnext.push(option.value)\n\t\t\t}\n\t\t\tthis.$emit('update:modelValue', next)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped lang=\"scss\">\n.cn-choice-cards {\n\tborder: none;\n\tpadding: 0;\n\tmargin: 0;\n\tmin-inline-size: 0;\n}\n\n.cn-choice-cards__legend {\n\tpadding: 0;\n\tmargin-block-end: 8px;\n\tfont-weight: bold;\n}\n\n.cn-choice-cards__loading {\n\tdisplay: flex;\n\tjustify-content: center;\n\tpadding: 24px 0;\n}\n\n.cn-choice-cards__grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fit, minmax(260px, 1fr));\n\tgap: 12px;\n\t/* Room for the card shadow. `src/css/card.css` is loaded globally by every\n\t consumer and gives EVERY `.cn-card` a `box-shadow` — including these — so\n\t a grid flush against its container gets the shadow shaved off wherever\n\t that container clips, which the setup wizard's dialog body does on the\n\t sides and the bottom. 12px clears the widest of those shadows. */\n\tpadding: 12px;\n}\n\n.cn-choice-cards__option {\n\tposition: relative;\n\tdisplay: block;\n\tcursor: pointer;\n}\n\n.cn-choice-cards__input {\n\tposition: absolute;\n\tinset-block-start: 16px;\n\tinset-inline-end: 14px;\n\tz-index: 1;\n\tmargin: 0;\n\tcursor: pointer;\n}\n\n/* Room for the input, so a long title never runs underneath it. */\n.cn-choice-cards__option :deep(.cn-card) {\n\tpadding-inline-end: 44px;\n\ttransition: box-shadow 0.2s ease, border-color 0.2s ease;\n}\n\n/* The same global `card.css` also scales `.cn-card` up on hover. A transform\n counts towards the scrollable overflow of the nearest scroll container, so in\n a dialog body sized to its content a scrollbar appears the moment the pointer\n touches a card, and the grid twitches. The border colour below already\n carries hover, so the scale is dropped: at (0,3,0) this beats the global\n `.cn-card:hover` at (0,2,0). */\n.cn-choice-cards__option:hover :deep(.cn-card) {\n\tborder-color: var(--color-primary-element);\n\ttransform: none;\n}\n\n/* The focus ring belongs on the card, not on the small input inside it. */\n.cn-choice-cards__input:focus-visible + :deep(.cn-card) {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 2px;\n}\n\n.cn-choice-cards__input:disabled {\n\tcursor: default;\n}\n\n.cn-choice-cards__input:disabled + :deep(.cn-card) {\n\topacity: 0.6;\n}\n</style>\n"],"names":["CnCard","CnIcon","t"],"mappings":"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"}
1
+ {"version":3,"file":"CnChoiceCards.vue2.js","sources":["../../../../src/components/CnChoiceCards/CnChoiceCards.vue"],"sourcesContent":["<!-- SPDX-License-Identifier: EUPL-1.2 -->\n<!-- SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl> -->\n<template>\n\t<fieldset class=\"cn-choice-cards\" :aria-busy=\"loading ? 'true' : 'false'\">\n\t\t<legend v-if=\"label\" class=\"cn-choice-cards__legend\">\n\t\t\t{{ label }}\n\t\t</legend>\n\n\t\t<div v-if=\"loading\" class=\"cn-choice-cards__loading\">\n\t\t\t<NcLoadingIcon :size=\"32\" />\n\t\t</div>\n\n\t\t<NcNoteCard v-else-if=\"normalizedOptions.length === 0\" type=\"warning\">\n\t\t\t{{ emptyText }}\n\t\t</NcNoteCard>\n\n\t\t<div v-else class=\"cn-choice-cards__grid\" :style=\"gridStyle\">\n\t\t\t<!-- One cell per option: the card's label, and below it whatever the\n\t\t\t `#option-actions` slot adds. The slot sits OUTSIDE the label on\n\t\t\t purpose: a button inside a label is interactive content inside\n\t\t\t interactive content, and a click on it would toggle the radio. -->\n\t\t\t<div\n\t\t\t\tv-for=\"option in normalizedOptions\"\n\t\t\t\t:key=\"String(option.value)\"\n\t\t\t\tclass=\"cn-choice-cards__cell\">\n\t\t\t\t<label\n\t\t\t\t\tclass=\"cn-choice-cards__option\"\n\t\t\t\t\t:class=\"{ 'cn-choice-cards__option--selected': isSelected(option) }\">\n\t\t\t\t\t<!-- A REAL, VISIBLE input, not a styled div with a click handler.\n\t\t\t\t\t It carries the checked state to assistive technology and to\n\t\t\t\t\t Windows high-contrast mode for free, and it keeps the\n\t\t\t\t\t selection perceivable without colour (WCAG 1.4.1) — the\n\t\t\t\t\t card's active border alone would not be. -->\n\t\t\t\t\t<input\n\t\t\t\t\t\tclass=\"cn-choice-cards__input\"\n\t\t\t\t\t\t:type=\"multiple ? 'checkbox' : 'radio'\"\n\t\t\t\t\t\t:name=\"groupName\"\n\t\t\t\t\t\t:value=\"String(option.value)\"\n\t\t\t\t\t\t:checked=\"isSelected(option)\"\n\t\t\t\t\t\t:disabled=\"disabled\"\n\t\t\t\t\t\t@change=\"onToggle(option)\">\n\t\t\t\t\t<CnCard\n\t\t\t\t\t\tclass=\"cn-choice-cards__card\"\n\t\t\t\t\t\t:title=\"option.label\"\n\t\t\t\t\t\ttitleTag=\"span\"\n\t\t\t\t\t\t:description=\"option.description\"\n\t\t\t\t\t\t:stats=\"option.stats || []\"\n\t\t\t\t\t\t:tags=\"option.tags || []\"\n\t\t\t\t\t\t:active=\"isSelected(option)\"\n\t\t\t\t\t\tactiveVariant=\"primary\"\n\t\t\t\t\t\t:descriptionLines=\"descriptionLines\">\n\t\t\t\t\t\t<template v-if=\"option.icon\" #icon>\n\t\t\t\t\t\t\t<CnIcon :name=\"option.icon\" :size=\"20\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t</CnCard>\n\t\t\t\t</label>\n\t\t\t\t<div v-if=\"$slots['option-actions']\" class=\"cn-choice-cards__actions\">\n\t\t\t\t\t<!-- @slot option-actions Content below one card, outside its label\n\t\t\t\t\t (e.g. a per-option Load button). Scope: `{ option, selected }`. -->\n\t\t\t\t\t<slot name=\"option-actions\" :option=\"option\" :selected=\"isSelected(option)\" />\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</fieldset>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon, NcNoteCard } from '@nextcloud/vue'\nimport CnCard from '../CnCard/CnCard.vue'\nimport CnIcon from '../CnIcon/CnIcon.vue'\n\nlet groupSeq = 0\n\n/**\n * CnChoiceCards — pick one option, or several, from a grid of cards.\n *\n * A card carries what a dropdown entry cannot: a description of what the\n * option actually is, and a few numbers about it. Use it where the choice\n * deserves explaining — the setup wizard's example-data step, an onboarding\n * template picker — and a plain `NcSelect` where a label already says enough.\n *\n * Each card is a `<label>` around a real radio (or checkbox when `multiple`),\n * so keyboard navigation, the checked state and high-contrast rendering are\n * the browser's, not a reimplementation.\n *\n * ```vue\n * <CnChoiceCards\n * v-model=\"picked\"\n * label=\"Which kind of organisation is this for?\"\n * :options=\"[\n * { value: 'municipality', label: 'Municipality',\n * description: 'A city council with committees and a decision list.',\n * stats: [{ label: 'Objects', value: 170 }] },\n * { value: 'none', label: 'None, I will set this up myself' },\n * ]\" />\n * ```\n */\nexport default {\n\tname: 'CnChoiceCards',\n\n\tcomponents: {\n\t\tCnCard,\n\t\tCnIcon,\n\t\tNcLoadingIcon,\n\t\tNcNoteCard,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The options to offer. Each: `{ value, label, description?, stats?,\n\t\t * tags?, icon? }` — `icon` is a PascalCase MDI name resolved by CnIcon.\n\t\t */\n\t\toptions: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * The selected value: a scalar, or an array when `multiple`.\n\t\t *\n\t\t * @type {string|number|boolean|Array|null}\n\t\t */\n\t\tmodelValue: {\n\t\t\ttype: [Boolean, String, Number, Array, Object],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Allow selecting several cards; the model becomes an array. */\n\t\tmultiple: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Group label, rendered as the fieldset's legend. */\n\t\tlabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Disable every card (e.g. a dependent choice with no parent value yet). */\n\t\tdisabled: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Show a spinner instead of the grid while options are being fetched. */\n\t\tloading: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Message shown when there is nothing to choose from. */\n\t\temptyText: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Nothing to choose from here.'),\n\t\t},\n\n\t\t/** Lines of description shown before clamping. */\n\t\tdescriptionLines: {\n\t\t\ttype: Number,\n\t\t\tdefault: 4,\n\t\t},\n\t},\n\n\temits: ['update:modelValue'],\n\n\tdata() {\n\t\tgroupSeq += 1\n\t\treturn {\n\t\t\t// Radios only group by `name`, so two card groups on one page would\n\t\t\t// otherwise share a selection.\n\t\t\tgroupName: 'cn-choice-cards-' + groupSeq,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Options with a usable `value` and `label`, tolerating the shapes a\n\t\t * server list arrives in: `id` instead of `value`, `name` instead of\n\t\t * `label`, and a bare string for a value that is its own label.\n\t\t *\n\t\t * @return {Array<object>} The normalised options.\n\t\t */\n\t\tnormalizedOptions() {\n\t\t\treturn (this.options || [])\n\t\t\t\t.filter((option) => option !== null && option !== undefined)\n\t\t\t\t.map((option) => {\n\t\t\t\t\tif (typeof option !== 'object') {\n\t\t\t\t\t\treturn { value: option, label: String(option) }\n\t\t\t\t\t}\n\t\t\t\t\tconst value = option.value !== undefined ? option.value : option.id\n\t\t\t\t\treturn {\n\t\t\t\t\t\t...option,\n\t\t\t\t\t\tvalue,\n\t\t\t\t\t\tlabel: option.label || option.name || String(value),\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.filter((option) => option.value !== undefined)\n\t\t},\n\n\t\t/**\n\t\t * The current selection as an array of values, whatever the model shape.\n\t\t *\n\t\t * @return {Array} The selected values.\n\t\t */\n\t\tselectedValues() {\n\t\t\tif (this.multiple) {\n\t\t\t\treturn Array.isArray(this.modelValue) ? this.modelValue : []\n\t\t\t}\n\t\t\treturn (this.modelValue === null || this.modelValue === undefined || this.modelValue === '')\n\t\t\t\t? []\n\t\t\t\t: [this.modelValue]\n\t\t},\n\n\t\t/**\n\t\t * Inline override for the grid's column count. `auto-fit` with a\n\t\t * 260px minimum wraps a small option count onto its own row well\n\t\t * before the container is actually too narrow for them side by\n\t\t * side — two cards would stack at ~550px wide even though halving\n\t\t * the available width still leaves each one legible. A binary (or\n\t\t * near-binary) choice should always sit in one row and fill the\n\t\t * available width; a larger option count keeps the responsive\n\t\t * `auto-fit` wrapping from the stylesheet (`{}` leaves it alone).\n\t\t *\n\t\t * @return {object} A `gridTemplateColumns` override, or `{}`.\n\t\t */\n\t\tgridStyle() {\n\t\t\tconst count = this.normalizedOptions.length\n\t\t\tif (count > 0 && count <= 2) {\n\t\t\t\treturn { gridTemplateColumns: `repeat(${count}, minmax(0, 1fr))` }\n\t\t\t}\n\t\t\treturn {}\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Whether an option is currently selected.\n\t\t *\n\t\t * Compares as strings: a value that made the round trip through the\n\t\t * server as `\"1\"` must still match the `1` it was sent as.\n\t\t *\n\t\t * @param {object} option The normalised option.\n\t\t * @return {boolean} True when selected.\n\t\t */\n\t\tisSelected(option) {\n\t\t\treturn this.selectedValues.some((v) => String(v) === String(option.value))\n\t\t},\n\n\t\t/**\n\t\t * Select (single) or toggle (multiple) an option and emit the new model.\n\t\t *\n\t\t * @param {object} option The normalised option.\n\t\t * @return {void}\n\t\t */\n\t\tonToggle(option) {\n\t\t\tif (this.disabled) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!this.multiple) {\n\t\t\t\t/**\n\t\t\t\t * @event update:modelValue The selected value, or the array of values when `multiple`.\n\t\t\t\t * @type {string|number|boolean|Array}\n\t\t\t\t */\n\t\t\t\t// Never the option OBJECT: a parent has to be able to hand back\n\t\t\t\t// what it receives as `modelValue`.\n\t\t\t\tthis.$emit('update:modelValue', option.value)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst next = this.selectedValues.filter((v) => String(v) !== String(option.value))\n\t\t\tif (next.length === this.selectedValues.length) {\n\t\t\t\tnext.push(option.value)\n\t\t\t}\n\t\t\tthis.$emit('update:modelValue', next)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped lang=\"scss\">\n.cn-choice-cards {\n\tborder: none;\n\tpadding: 0;\n\tmargin: 0;\n\tmin-inline-size: 0;\n}\n\n.cn-choice-cards__legend {\n\tpadding: 0;\n\tmargin-block-end: 8px;\n\tfont-weight: bold;\n}\n\n.cn-choice-cards__loading {\n\tdisplay: flex;\n\tjustify-content: center;\n\tpadding: 24px 0;\n}\n\n.cn-choice-cards__grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fit, minmax(260px, 1fr));\n\tgap: 12px;\n\t/* Room for the card shadow. `src/css/card.css` is loaded globally by every\n\t consumer and gives EVERY `.cn-card` a `box-shadow` — including these — so\n\t a grid flush against its container gets the shadow shaved off wherever\n\t that container clips, which the setup wizard's dialog body does on the\n\t sides and the bottom. 12px clears the widest of those shadows. */\n\tpadding: 12px;\n}\n\n.cn-choice-cards__cell {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n\tmin-inline-size: 0;\n}\n\n.cn-choice-cards__actions {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n}\n\n.cn-choice-cards__option {\n\tposition: relative;\n\tdisplay: block;\n\tcursor: pointer;\n}\n\n.cn-choice-cards__input {\n\tposition: absolute;\n\tinset-block-start: 16px;\n\tinset-inline-end: 14px;\n\tz-index: 1;\n\tmargin: 0;\n\tcursor: pointer;\n}\n\n/* Room for the input, so a long title never runs underneath it. */\n.cn-choice-cards__option :deep(.cn-card) {\n\tpadding-inline-end: 44px;\n\ttransition: box-shadow 0.2s ease, border-color 0.2s ease;\n}\n\n/* The same global `card.css` also scales `.cn-card` up on hover. A transform\n counts towards the scrollable overflow of the nearest scroll container, so in\n a dialog body sized to its content a scrollbar appears the moment the pointer\n touches a card, and the grid twitches. The border colour below already\n carries hover, so the scale is dropped: at (0,3,0) this beats the global\n `.cn-card:hover` at (0,2,0). */\n.cn-choice-cards__option:hover :deep(.cn-card) {\n\tborder-color: var(--color-primary-element);\n\ttransform: none;\n}\n\n/* The focus ring belongs on the card, not on the small input inside it. */\n.cn-choice-cards__input:focus-visible + :deep(.cn-card) {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 2px;\n}\n\n.cn-choice-cards__input:disabled {\n\tcursor: default;\n}\n\n.cn-choice-cards__input:disabled + :deep(.cn-card) {\n\topacity: 0.6;\n}\n</style>\n"],"names":["CnCard","CnIcon","t"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { resolveComponent, openBlock, createElementBlock, toDisplayString, createCommentVNode, createVNode, createBlock, withCtx, createTextVNode, normalizeStyle, Fragment, renderList, normalizeClass, createElementVNode, createSlots } from 'vue';
1
+ import { resolveComponent, openBlock, createElementBlock, toDisplayString, createCommentVNode, createVNode, createBlock, withCtx, createTextVNode, normalizeStyle, Fragment, renderList, createElementVNode, normalizeClass, createSlots, renderSlot } from 'vue';
2
2
 
3
3
  const _hoisted_1 = ["aria-busy"];
4
4
  const _hoisted_2 = {
@@ -10,6 +10,10 @@ const _hoisted_3 = {
10
10
  class: "cn-choice-cards__loading"
11
11
  };
12
12
  const _hoisted_4 = ["type", "name", "value", "checked", "disabled", "onChange"];
13
+ const _hoisted_5 = {
14
+ key: 0,
15
+ class: "cn-choice-cards__actions"
16
+ };
13
17
 
14
18
  function render(_ctx, _cache, $props, $setup, $data, $options) {
15
19
  const _component_NcLoadingIcon = resolveComponent("NcLoadingIcon");
@@ -44,44 +48,56 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
44
48
  style: normalizeStyle($options.gridStyle)
45
49
  }, [
46
50
  (openBlock(true), createElementBlock(Fragment, null, renderList($options.normalizedOptions, (option) => {
47
- return (openBlock(), createElementBlock("label", {
51
+ return (openBlock(), createElementBlock("div", {
48
52
  key: String(option.value),
49
- class: normalizeClass(["cn-choice-cards__option", { 'cn-choice-cards__option--selected': $options.isSelected(option) }])
53
+ class: "cn-choice-cards__cell"
50
54
  }, [
51
- createElementVNode("input", {
52
- class: "cn-choice-cards__input",
53
- type: $props.multiple ? 'checkbox' : 'radio',
54
- name: $data.groupName,
55
- value: String(option.value),
56
- checked: $options.isSelected(option),
57
- disabled: $props.disabled,
58
- onChange: $event => ($options.onToggle(option))
59
- }, null, 40 /* PROPS, NEED_HYDRATION */, _hoisted_4),
60
- createVNode(_component_CnCard, {
61
- class: "cn-choice-cards__card",
62
- title: option.label,
63
- titleTag: "span",
64
- description: option.description,
65
- stats: option.stats || [],
66
- tags: option.tags || [],
67
- active: $options.isSelected(option),
68
- activeVariant: "primary",
69
- descriptionLines: $props.descriptionLines
70
- }, createSlots({ _: 2 /* DYNAMIC */ }, [
71
- (option.icon)
72
- ? {
73
- name: "icon",
74
- fn: withCtx(() => [
75
- createVNode(_component_CnIcon, {
76
- name: option.icon,
77
- size: 20
78
- }, null, 8 /* PROPS */, ["name"])
79
- ]),
80
- key: "0"
81
- }
82
- : undefined
83
- ]), 1032 /* PROPS, DYNAMIC_SLOTS */, ["title", "description", "stats", "tags", "active", "descriptionLines"])
84
- ], 2 /* CLASS */))
55
+ createElementVNode("label", {
56
+ class: normalizeClass(["cn-choice-cards__option", { 'cn-choice-cards__option--selected': $options.isSelected(option) }])
57
+ }, [
58
+ createElementVNode("input", {
59
+ class: "cn-choice-cards__input",
60
+ type: $props.multiple ? 'checkbox' : 'radio',
61
+ name: $data.groupName,
62
+ value: String(option.value),
63
+ checked: $options.isSelected(option),
64
+ disabled: $props.disabled,
65
+ onChange: $event => ($options.onToggle(option))
66
+ }, null, 40 /* PROPS, NEED_HYDRATION */, _hoisted_4),
67
+ createVNode(_component_CnCard, {
68
+ class: "cn-choice-cards__card",
69
+ title: option.label,
70
+ titleTag: "span",
71
+ description: option.description,
72
+ stats: option.stats || [],
73
+ tags: option.tags || [],
74
+ active: $options.isSelected(option),
75
+ activeVariant: "primary",
76
+ descriptionLines: $props.descriptionLines
77
+ }, createSlots({ _: 2 /* DYNAMIC */ }, [
78
+ (option.icon)
79
+ ? {
80
+ name: "icon",
81
+ fn: withCtx(() => [
82
+ createVNode(_component_CnIcon, {
83
+ name: option.icon,
84
+ size: 20
85
+ }, null, 8 /* PROPS */, ["name"])
86
+ ]),
87
+ key: "0"
88
+ }
89
+ : undefined
90
+ ]), 1032 /* PROPS, DYNAMIC_SLOTS */, ["title", "description", "stats", "tags", "active", "descriptionLines"])
91
+ ], 2 /* CLASS */),
92
+ (_ctx.$slots['option-actions'])
93
+ ? (openBlock(), createElementBlock("div", _hoisted_5, [
94
+ renderSlot(_ctx.$slots, "option-actions", {
95
+ option: option,
96
+ selected: $options.isSelected(option)
97
+ })
98
+ ]))
99
+ : createCommentVNode("v-if", true)
100
+ ]))
85
101
  }), 128 /* KEYED_FRAGMENT */))
86
102
  ], 4 /* STYLE */))
87
103
  ], 8 /* PROPS */, _hoisted_1))
@@ -1 +1 @@
1
- {"version":3,"file":"CnChoiceCards.vue3.js","sources":["../../../../src/components/CnChoiceCards/CnChoiceCards.vue?vue&type=template&id=93473002&lang.js"],"sourcesContent":["<!-- SPDX-License-Identifier: EUPL-1.2 -->\n<!-- SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl> -->\n<template>\n\t<fieldset class=\"cn-choice-cards\" :aria-busy=\"loading ? 'true' : 'false'\">\n\t\t<legend v-if=\"label\" class=\"cn-choice-cards__legend\">\n\t\t\t{{ label }}\n\t\t</legend>\n\n\t\t<div v-if=\"loading\" class=\"cn-choice-cards__loading\">\n\t\t\t<NcLoadingIcon :size=\"32\" />\n\t\t</div>\n\n\t\t<NcNoteCard v-else-if=\"normalizedOptions.length === 0\" type=\"warning\">\n\t\t\t{{ emptyText }}\n\t\t</NcNoteCard>\n\n\t\t<div v-else class=\"cn-choice-cards__grid\" :style=\"gridStyle\">\n\t\t\t<label\n\t\t\t\tv-for=\"option in normalizedOptions\"\n\t\t\t\t:key=\"String(option.value)\"\n\t\t\t\tclass=\"cn-choice-cards__option\"\n\t\t\t\t:class=\"{ 'cn-choice-cards__option--selected': isSelected(option) }\">\n\t\t\t\t<!-- A REAL, VISIBLE input, not a styled div with a click handler.\n\t\t\t\t It carries the checked state to assistive technology and to\n\t\t\t\t Windows high-contrast mode for free, and it keeps the\n\t\t\t\t selection perceivable without colour (WCAG 1.4.1) — the\n\t\t\t\t card's active border alone would not be. -->\n\t\t\t\t<input\n\t\t\t\t\tclass=\"cn-choice-cards__input\"\n\t\t\t\t\t:type=\"multiple ? 'checkbox' : 'radio'\"\n\t\t\t\t\t:name=\"groupName\"\n\t\t\t\t\t:value=\"String(option.value)\"\n\t\t\t\t\t:checked=\"isSelected(option)\"\n\t\t\t\t\t:disabled=\"disabled\"\n\t\t\t\t\t@change=\"onToggle(option)\">\n\t\t\t\t<CnCard\n\t\t\t\t\tclass=\"cn-choice-cards__card\"\n\t\t\t\t\t:title=\"option.label\"\n\t\t\t\t\ttitleTag=\"span\"\n\t\t\t\t\t:description=\"option.description\"\n\t\t\t\t\t:stats=\"option.stats || []\"\n\t\t\t\t\t:tags=\"option.tags || []\"\n\t\t\t\t\t:active=\"isSelected(option)\"\n\t\t\t\t\tactiveVariant=\"primary\"\n\t\t\t\t\t:descriptionLines=\"descriptionLines\">\n\t\t\t\t\t<template v-if=\"option.icon\" #icon>\n\t\t\t\t\t\t<CnIcon :name=\"option.icon\" :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t</CnCard>\n\t\t\t</label>\n\t\t</div>\n\t</fieldset>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon, NcNoteCard } from '@nextcloud/vue'\nimport CnCard from '../CnCard/CnCard.vue'\nimport CnIcon from '../CnIcon/CnIcon.vue'\n\nlet groupSeq = 0\n\n/**\n * CnChoiceCards — pick one option, or several, from a grid of cards.\n *\n * A card carries what a dropdown entry cannot: a description of what the\n * option actually is, and a few numbers about it. Use it where the choice\n * deserves explaining — the setup wizard's example-data step, an onboarding\n * template picker — and a plain `NcSelect` where a label already says enough.\n *\n * Each card is a `<label>` around a real radio (or checkbox when `multiple`),\n * so keyboard navigation, the checked state and high-contrast rendering are\n * the browser's, not a reimplementation.\n *\n * ```vue\n * <CnChoiceCards\n * v-model=\"picked\"\n * label=\"Which kind of organisation is this for?\"\n * :options=\"[\n * { value: 'municipality', label: 'Municipality',\n * description: 'A city council with committees and a decision list.',\n * stats: [{ label: 'Objects', value: 170 }] },\n * { value: 'none', label: 'None, I will set this up myself' },\n * ]\" />\n * ```\n */\nexport default {\n\tname: 'CnChoiceCards',\n\n\tcomponents: {\n\t\tCnCard,\n\t\tCnIcon,\n\t\tNcLoadingIcon,\n\t\tNcNoteCard,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The options to offer. Each: `{ value, label, description?, stats?,\n\t\t * tags?, icon? }` — `icon` is a PascalCase MDI name resolved by CnIcon.\n\t\t */\n\t\toptions: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * The selected value: a scalar, or an array when `multiple`.\n\t\t *\n\t\t * @type {string|number|boolean|Array|null}\n\t\t */\n\t\tmodelValue: {\n\t\t\ttype: [Boolean, String, Number, Array, Object],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Allow selecting several cards; the model becomes an array. */\n\t\tmultiple: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Group label, rendered as the fieldset's legend. */\n\t\tlabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Disable every card (e.g. a dependent choice with no parent value yet). */\n\t\tdisabled: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Show a spinner instead of the grid while options are being fetched. */\n\t\tloading: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Message shown when there is nothing to choose from. */\n\t\temptyText: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Nothing to choose from here.'),\n\t\t},\n\n\t\t/** Lines of description shown before clamping. */\n\t\tdescriptionLines: {\n\t\t\ttype: Number,\n\t\t\tdefault: 4,\n\t\t},\n\t},\n\n\temits: ['update:modelValue'],\n\n\tdata() {\n\t\tgroupSeq += 1\n\t\treturn {\n\t\t\t// Radios only group by `name`, so two card groups on one page would\n\t\t\t// otherwise share a selection.\n\t\t\tgroupName: 'cn-choice-cards-' + groupSeq,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Options with a usable `value` and `label`, tolerating the shapes a\n\t\t * server list arrives in: `id` instead of `value`, `name` instead of\n\t\t * `label`, and a bare string for a value that is its own label.\n\t\t *\n\t\t * @return {Array<object>} The normalised options.\n\t\t */\n\t\tnormalizedOptions() {\n\t\t\treturn (this.options || [])\n\t\t\t\t.filter((option) => option !== null && option !== undefined)\n\t\t\t\t.map((option) => {\n\t\t\t\t\tif (typeof option !== 'object') {\n\t\t\t\t\t\treturn { value: option, label: String(option) }\n\t\t\t\t\t}\n\t\t\t\t\tconst value = option.value !== undefined ? option.value : option.id\n\t\t\t\t\treturn {\n\t\t\t\t\t\t...option,\n\t\t\t\t\t\tvalue,\n\t\t\t\t\t\tlabel: option.label || option.name || String(value),\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.filter((option) => option.value !== undefined)\n\t\t},\n\n\t\t/**\n\t\t * The current selection as an array of values, whatever the model shape.\n\t\t *\n\t\t * @return {Array} The selected values.\n\t\t */\n\t\tselectedValues() {\n\t\t\tif (this.multiple) {\n\t\t\t\treturn Array.isArray(this.modelValue) ? this.modelValue : []\n\t\t\t}\n\t\t\treturn (this.modelValue === null || this.modelValue === undefined || this.modelValue === '')\n\t\t\t\t? []\n\t\t\t\t: [this.modelValue]\n\t\t},\n\n\t\t/**\n\t\t * Inline override for the grid's column count. `auto-fit` with a\n\t\t * 260px minimum wraps a small option count onto its own row well\n\t\t * before the container is actually too narrow for them side by\n\t\t * side — two cards would stack at ~550px wide even though halving\n\t\t * the available width still leaves each one legible. A binary (or\n\t\t * near-binary) choice should always sit in one row and fill the\n\t\t * available width; a larger option count keeps the responsive\n\t\t * `auto-fit` wrapping from the stylesheet (`{}` leaves it alone).\n\t\t *\n\t\t * @return {object} A `gridTemplateColumns` override, or `{}`.\n\t\t */\n\t\tgridStyle() {\n\t\t\tconst count = this.normalizedOptions.length\n\t\t\tif (count > 0 && count <= 2) {\n\t\t\t\treturn { gridTemplateColumns: `repeat(${count}, minmax(0, 1fr))` }\n\t\t\t}\n\t\t\treturn {}\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Whether an option is currently selected.\n\t\t *\n\t\t * Compares as strings: a value that made the round trip through the\n\t\t * server as `\"1\"` must still match the `1` it was sent as.\n\t\t *\n\t\t * @param {object} option The normalised option.\n\t\t * @return {boolean} True when selected.\n\t\t */\n\t\tisSelected(option) {\n\t\t\treturn this.selectedValues.some((v) => String(v) === String(option.value))\n\t\t},\n\n\t\t/**\n\t\t * Select (single) or toggle (multiple) an option and emit the new model.\n\t\t *\n\t\t * @param {object} option The normalised option.\n\t\t * @return {void}\n\t\t */\n\t\tonToggle(option) {\n\t\t\tif (this.disabled) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!this.multiple) {\n\t\t\t\t/**\n\t\t\t\t * @event update:modelValue The selected value, or the array of values when `multiple`.\n\t\t\t\t * @type {string|number|boolean|Array}\n\t\t\t\t */\n\t\t\t\t// Never the option OBJECT: a parent has to be able to hand back\n\t\t\t\t// what it receives as `modelValue`.\n\t\t\t\tthis.$emit('update:modelValue', option.value)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst next = this.selectedValues.filter((v) => String(v) !== String(option.value))\n\t\t\tif (next.length === this.selectedValues.length) {\n\t\t\t\tnext.push(option.value)\n\t\t\t}\n\t\t\tthis.$emit('update:modelValue', next)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped lang=\"scss\">\n.cn-choice-cards {\n\tborder: none;\n\tpadding: 0;\n\tmargin: 0;\n\tmin-inline-size: 0;\n}\n\n.cn-choice-cards__legend {\n\tpadding: 0;\n\tmargin-block-end: 8px;\n\tfont-weight: bold;\n}\n\n.cn-choice-cards__loading {\n\tdisplay: flex;\n\tjustify-content: center;\n\tpadding: 24px 0;\n}\n\n.cn-choice-cards__grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fit, minmax(260px, 1fr));\n\tgap: 12px;\n\t/* Room for the card shadow. `src/css/card.css` is loaded globally by every\n\t consumer and gives EVERY `.cn-card` a `box-shadow` — including these — so\n\t a grid flush against its container gets the shadow shaved off wherever\n\t that container clips, which the setup wizard's dialog body does on the\n\t sides and the bottom. 12px clears the widest of those shadows. */\n\tpadding: 12px;\n}\n\n.cn-choice-cards__option {\n\tposition: relative;\n\tdisplay: block;\n\tcursor: pointer;\n}\n\n.cn-choice-cards__input {\n\tposition: absolute;\n\tinset-block-start: 16px;\n\tinset-inline-end: 14px;\n\tz-index: 1;\n\tmargin: 0;\n\tcursor: pointer;\n}\n\n/* Room for the input, so a long title never runs underneath it. */\n.cn-choice-cards__option :deep(.cn-card) {\n\tpadding-inline-end: 44px;\n\ttransition: box-shadow 0.2s ease, border-color 0.2s ease;\n}\n\n/* The same global `card.css` also scales `.cn-card` up on hover. A transform\n counts towards the scrollable overflow of the nearest scroll container, so in\n a dialog body sized to its content a scrollbar appears the moment the pointer\n touches a card, and the grid twitches. The border colour below already\n carries hover, so the scale is dropped: at (0,3,0) this beats the global\n `.cn-card:hover` at (0,2,0). */\n.cn-choice-cards__option:hover :deep(.cn-card) {\n\tborder-color: var(--color-primary-element);\n\ttransform: none;\n}\n\n/* The focus ring belongs on the card, not on the small input inside it. */\n.cn-choice-cards__input:focus-visible + :deep(.cn-card) {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 2px;\n}\n\n.cn-choice-cards__input:disabled {\n\tcursor: default;\n}\n\n.cn-choice-cards__input:disabled + :deep(.cn-card) {\n\topacity: 0.6;\n}\n</style>\n"],"names":["_createElementBlock","_toDisplayString","_openBlock","_createBlock","_Fragment","_renderList","_normalizeClass","_createElementVNode","_createVNode"],"mappings":";;;;;AAMU,EAAA,KAAA,EAAA,yBAAA;;;;AAIH,EAAA,KAAA,EAAA,0BAAA;;;;;;;;;;uBALJA;SAAU,EAAA,iBAAA;AAAA,IAAA,WAAA,EAAA,MAAA,CAAA,OAAA,GAAA,MAAA,GAAA,OAAA;;KACH,MAAA,CAAA,KAAA;sBAARA,kBAES,CAAA,QAAA,EAFT,UAES,EAAAC,eAAA;;KAEJ,MAAA,CAAA,OAAA;AAAL,SAAAC,SAAA,EAAA,EAAAF,kBAAA,CAEM,OAFN,UAEM,EAAA;;;AAEM,QAAA,CAAA,QAAA,CAAA,iBAAA,CAAA,MAAA,KAAA,CAAA;wBAAZG,WAEa,CAAA,qBAAA,EAAA;;AAFD,YAAA,IAAA,EAAA,SAAA;;;;;;;wBAIVH;;AAAU,YAAA,KAAK,EAAE,uBAAiB;AAAA,YAAA,KAAA,iBAAA,QAAA,CAAA,SAAA,CAAA;;gDAiC3B,CAAAI,QAAA,EAAA,IAAA,EAAAC,UAAA,CA/BU,QAAa,CAAA,iBAAA,EAAA,CAAvB,MAAM,KAAA;qDA+BN,CAAA,OAAA,EAAA;AA9BN,gBAAA,GAAG,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,CAAA;AACzB,gBAAA,KAAK,EAAAC,cAAA,CAAA,CAAA,yBAAyB,EACiB,EAAA,mCAAA,EAAA,QAAA,CAAA,UAAW,CAAA,MAAK,CAAA,EAAA,CAAA,CAAA;;gBAM9DC,kBAO2B,CAAA,OAAA,EAAA;AAN3B,kBAAA,KAAK,EAAC,wBAAU;AACf,kBAAA,IAAI,EAAE,MAAQ,CAAA,QAAA,GAAA,UAAA,GAAA,OAAA;AACd,kBAAA,IAAI,EAAE,KAAS,CAAA,SAAA;AACf,kBAAA,KAAK,EAAE,MAAM,CAAC,MAAI,CAAA,KAAA,CAAA;kBAClB,OAAO,EAAE,QAAU,CAAA,UAAA,CAAC,MAAK,CAAA;AACzB,kBAAA,QAAI,EAAA,MAAA,CAAA,QAAA;kBACJ,QAAM,EAAA,MAAA,KAAE,QAAQ,CAAA,QAAA,CAAC,MAAM,CAAA,CAAA;;gBACxBC,WAaE,CAAA,iBAAA,EAAA;AAZF,kBAAA,KAAK,EAAC,uBAAQ;kBACb,KAAK,EAAE,MAAM,CAAC,KAAK;AACpB,kBAAA,QAAQ,EAAC,MAAM;kBACd,WAAW,EAAE,MAAM,CAAA,WAAI;kBACvB,KAAK,EAAE,MAAM,CAAC,KAAK,IAAA,EAAA;kBACnB,IAAI,EAAE,MAAM,CAAC,IAAI,IAAA,EAAA;kBACjB,MAAM,EAAE,QAAU,CAAA,UAAA,CAAC,MAAM,CAAA;AAC1B,kBAAA,aAAa,EAAC,SAAS;AACtB,kBAAA,gBAAgB,EAAE,MAAgB,CAAA,gBAAA;;AACzB,kBAAA,CAAA,MAAA,CAAA,IAAA;;8BAAA,MAAA;oCACT,MAAM;0BAANA,WAAM,CAAA,iBAAA,EAAA;4BAAA,IAAA,EAAA,MAAA,CAAA,IAAA;AAAA,4BAAA,IAAA,EAAA,EAAA;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"CnChoiceCards.vue3.js","sources":["../../../../src/components/CnChoiceCards/CnChoiceCards.vue?vue&type=template&id=93473002&lang.js"],"sourcesContent":["<!-- SPDX-License-Identifier: EUPL-1.2 -->\n<!-- SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl> -->\n<template>\n\t<fieldset class=\"cn-choice-cards\" :aria-busy=\"loading ? 'true' : 'false'\">\n\t\t<legend v-if=\"label\" class=\"cn-choice-cards__legend\">\n\t\t\t{{ label }}\n\t\t</legend>\n\n\t\t<div v-if=\"loading\" class=\"cn-choice-cards__loading\">\n\t\t\t<NcLoadingIcon :size=\"32\" />\n\t\t</div>\n\n\t\t<NcNoteCard v-else-if=\"normalizedOptions.length === 0\" type=\"warning\">\n\t\t\t{{ emptyText }}\n\t\t</NcNoteCard>\n\n\t\t<div v-else class=\"cn-choice-cards__grid\" :style=\"gridStyle\">\n\t\t\t<!-- One cell per option: the card's label, and below it whatever the\n\t\t\t `#option-actions` slot adds. The slot sits OUTSIDE the label on\n\t\t\t purpose: a button inside a label is interactive content inside\n\t\t\t interactive content, and a click on it would toggle the radio. -->\n\t\t\t<div\n\t\t\t\tv-for=\"option in normalizedOptions\"\n\t\t\t\t:key=\"String(option.value)\"\n\t\t\t\tclass=\"cn-choice-cards__cell\">\n\t\t\t\t<label\n\t\t\t\t\tclass=\"cn-choice-cards__option\"\n\t\t\t\t\t:class=\"{ 'cn-choice-cards__option--selected': isSelected(option) }\">\n\t\t\t\t\t<!-- A REAL, VISIBLE input, not a styled div with a click handler.\n\t\t\t\t\t It carries the checked state to assistive technology and to\n\t\t\t\t\t Windows high-contrast mode for free, and it keeps the\n\t\t\t\t\t selection perceivable without colour (WCAG 1.4.1) — the\n\t\t\t\t\t card's active border alone would not be. -->\n\t\t\t\t\t<input\n\t\t\t\t\t\tclass=\"cn-choice-cards__input\"\n\t\t\t\t\t\t:type=\"multiple ? 'checkbox' : 'radio'\"\n\t\t\t\t\t\t:name=\"groupName\"\n\t\t\t\t\t\t:value=\"String(option.value)\"\n\t\t\t\t\t\t:checked=\"isSelected(option)\"\n\t\t\t\t\t\t:disabled=\"disabled\"\n\t\t\t\t\t\t@change=\"onToggle(option)\">\n\t\t\t\t\t<CnCard\n\t\t\t\t\t\tclass=\"cn-choice-cards__card\"\n\t\t\t\t\t\t:title=\"option.label\"\n\t\t\t\t\t\ttitleTag=\"span\"\n\t\t\t\t\t\t:description=\"option.description\"\n\t\t\t\t\t\t:stats=\"option.stats || []\"\n\t\t\t\t\t\t:tags=\"option.tags || []\"\n\t\t\t\t\t\t:active=\"isSelected(option)\"\n\t\t\t\t\t\tactiveVariant=\"primary\"\n\t\t\t\t\t\t:descriptionLines=\"descriptionLines\">\n\t\t\t\t\t\t<template v-if=\"option.icon\" #icon>\n\t\t\t\t\t\t\t<CnIcon :name=\"option.icon\" :size=\"20\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t</CnCard>\n\t\t\t\t</label>\n\t\t\t\t<div v-if=\"$slots['option-actions']\" class=\"cn-choice-cards__actions\">\n\t\t\t\t\t<!-- @slot option-actions Content below one card, outside its label\n\t\t\t\t\t (e.g. a per-option Load button). Scope: `{ option, selected }`. -->\n\t\t\t\t\t<slot name=\"option-actions\" :option=\"option\" :selected=\"isSelected(option)\" />\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</fieldset>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcLoadingIcon, NcNoteCard } from '@nextcloud/vue'\nimport CnCard from '../CnCard/CnCard.vue'\nimport CnIcon from '../CnIcon/CnIcon.vue'\n\nlet groupSeq = 0\n\n/**\n * CnChoiceCards — pick one option, or several, from a grid of cards.\n *\n * A card carries what a dropdown entry cannot: a description of what the\n * option actually is, and a few numbers about it. Use it where the choice\n * deserves explaining — the setup wizard's example-data step, an onboarding\n * template picker — and a plain `NcSelect` where a label already says enough.\n *\n * Each card is a `<label>` around a real radio (or checkbox when `multiple`),\n * so keyboard navigation, the checked state and high-contrast rendering are\n * the browser's, not a reimplementation.\n *\n * ```vue\n * <CnChoiceCards\n * v-model=\"picked\"\n * label=\"Which kind of organisation is this for?\"\n * :options=\"[\n * { value: 'municipality', label: 'Municipality',\n * description: 'A city council with committees and a decision list.',\n * stats: [{ label: 'Objects', value: 170 }] },\n * { value: 'none', label: 'None, I will set this up myself' },\n * ]\" />\n * ```\n */\nexport default {\n\tname: 'CnChoiceCards',\n\n\tcomponents: {\n\t\tCnCard,\n\t\tCnIcon,\n\t\tNcLoadingIcon,\n\t\tNcNoteCard,\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The options to offer. Each: `{ value, label, description?, stats?,\n\t\t * tags?, icon? }` — `icon` is a PascalCase MDI name resolved by CnIcon.\n\t\t */\n\t\toptions: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * The selected value: a scalar, or an array when `multiple`.\n\t\t *\n\t\t * @type {string|number|boolean|Array|null}\n\t\t */\n\t\tmodelValue: {\n\t\t\ttype: [Boolean, String, Number, Array, Object],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Allow selecting several cards; the model becomes an array. */\n\t\tmultiple: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Group label, rendered as the fieldset's legend. */\n\t\tlabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Disable every card (e.g. a dependent choice with no parent value yet). */\n\t\tdisabled: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Show a spinner instead of the grid while options are being fetched. */\n\t\tloading: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Message shown when there is nothing to choose from. */\n\t\temptyText: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Nothing to choose from here.'),\n\t\t},\n\n\t\t/** Lines of description shown before clamping. */\n\t\tdescriptionLines: {\n\t\t\ttype: Number,\n\t\t\tdefault: 4,\n\t\t},\n\t},\n\n\temits: ['update:modelValue'],\n\n\tdata() {\n\t\tgroupSeq += 1\n\t\treturn {\n\t\t\t// Radios only group by `name`, so two card groups on one page would\n\t\t\t// otherwise share a selection.\n\t\t\tgroupName: 'cn-choice-cards-' + groupSeq,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Options with a usable `value` and `label`, tolerating the shapes a\n\t\t * server list arrives in: `id` instead of `value`, `name` instead of\n\t\t * `label`, and a bare string for a value that is its own label.\n\t\t *\n\t\t * @return {Array<object>} The normalised options.\n\t\t */\n\t\tnormalizedOptions() {\n\t\t\treturn (this.options || [])\n\t\t\t\t.filter((option) => option !== null && option !== undefined)\n\t\t\t\t.map((option) => {\n\t\t\t\t\tif (typeof option !== 'object') {\n\t\t\t\t\t\treturn { value: option, label: String(option) }\n\t\t\t\t\t}\n\t\t\t\t\tconst value = option.value !== undefined ? option.value : option.id\n\t\t\t\t\treturn {\n\t\t\t\t\t\t...option,\n\t\t\t\t\t\tvalue,\n\t\t\t\t\t\tlabel: option.label || option.name || String(value),\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.filter((option) => option.value !== undefined)\n\t\t},\n\n\t\t/**\n\t\t * The current selection as an array of values, whatever the model shape.\n\t\t *\n\t\t * @return {Array} The selected values.\n\t\t */\n\t\tselectedValues() {\n\t\t\tif (this.multiple) {\n\t\t\t\treturn Array.isArray(this.modelValue) ? this.modelValue : []\n\t\t\t}\n\t\t\treturn (this.modelValue === null || this.modelValue === undefined || this.modelValue === '')\n\t\t\t\t? []\n\t\t\t\t: [this.modelValue]\n\t\t},\n\n\t\t/**\n\t\t * Inline override for the grid's column count. `auto-fit` with a\n\t\t * 260px minimum wraps a small option count onto its own row well\n\t\t * before the container is actually too narrow for them side by\n\t\t * side — two cards would stack at ~550px wide even though halving\n\t\t * the available width still leaves each one legible. A binary (or\n\t\t * near-binary) choice should always sit in one row and fill the\n\t\t * available width; a larger option count keeps the responsive\n\t\t * `auto-fit` wrapping from the stylesheet (`{}` leaves it alone).\n\t\t *\n\t\t * @return {object} A `gridTemplateColumns` override, or `{}`.\n\t\t */\n\t\tgridStyle() {\n\t\t\tconst count = this.normalizedOptions.length\n\t\t\tif (count > 0 && count <= 2) {\n\t\t\t\treturn { gridTemplateColumns: `repeat(${count}, minmax(0, 1fr))` }\n\t\t\t}\n\t\t\treturn {}\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Whether an option is currently selected.\n\t\t *\n\t\t * Compares as strings: a value that made the round trip through the\n\t\t * server as `\"1\"` must still match the `1` it was sent as.\n\t\t *\n\t\t * @param {object} option The normalised option.\n\t\t * @return {boolean} True when selected.\n\t\t */\n\t\tisSelected(option) {\n\t\t\treturn this.selectedValues.some((v) => String(v) === String(option.value))\n\t\t},\n\n\t\t/**\n\t\t * Select (single) or toggle (multiple) an option and emit the new model.\n\t\t *\n\t\t * @param {object} option The normalised option.\n\t\t * @return {void}\n\t\t */\n\t\tonToggle(option) {\n\t\t\tif (this.disabled) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!this.multiple) {\n\t\t\t\t/**\n\t\t\t\t * @event update:modelValue The selected value, or the array of values when `multiple`.\n\t\t\t\t * @type {string|number|boolean|Array}\n\t\t\t\t */\n\t\t\t\t// Never the option OBJECT: a parent has to be able to hand back\n\t\t\t\t// what it receives as `modelValue`.\n\t\t\t\tthis.$emit('update:modelValue', option.value)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst next = this.selectedValues.filter((v) => String(v) !== String(option.value))\n\t\t\tif (next.length === this.selectedValues.length) {\n\t\t\t\tnext.push(option.value)\n\t\t\t}\n\t\t\tthis.$emit('update:modelValue', next)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped lang=\"scss\">\n.cn-choice-cards {\n\tborder: none;\n\tpadding: 0;\n\tmargin: 0;\n\tmin-inline-size: 0;\n}\n\n.cn-choice-cards__legend {\n\tpadding: 0;\n\tmargin-block-end: 8px;\n\tfont-weight: bold;\n}\n\n.cn-choice-cards__loading {\n\tdisplay: flex;\n\tjustify-content: center;\n\tpadding: 24px 0;\n}\n\n.cn-choice-cards__grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fit, minmax(260px, 1fr));\n\tgap: 12px;\n\t/* Room for the card shadow. `src/css/card.css` is loaded globally by every\n\t consumer and gives EVERY `.cn-card` a `box-shadow` — including these — so\n\t a grid flush against its container gets the shadow shaved off wherever\n\t that container clips, which the setup wizard's dialog body does on the\n\t sides and the bottom. 12px clears the widest of those shadows. */\n\tpadding: 12px;\n}\n\n.cn-choice-cards__cell {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n\tmin-inline-size: 0;\n}\n\n.cn-choice-cards__actions {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 4px;\n}\n\n.cn-choice-cards__option {\n\tposition: relative;\n\tdisplay: block;\n\tcursor: pointer;\n}\n\n.cn-choice-cards__input {\n\tposition: absolute;\n\tinset-block-start: 16px;\n\tinset-inline-end: 14px;\n\tz-index: 1;\n\tmargin: 0;\n\tcursor: pointer;\n}\n\n/* Room for the input, so a long title never runs underneath it. */\n.cn-choice-cards__option :deep(.cn-card) {\n\tpadding-inline-end: 44px;\n\ttransition: box-shadow 0.2s ease, border-color 0.2s ease;\n}\n\n/* The same global `card.css` also scales `.cn-card` up on hover. A transform\n counts towards the scrollable overflow of the nearest scroll container, so in\n a dialog body sized to its content a scrollbar appears the moment the pointer\n touches a card, and the grid twitches. The border colour below already\n carries hover, so the scale is dropped: at (0,3,0) this beats the global\n `.cn-card:hover` at (0,2,0). */\n.cn-choice-cards__option:hover :deep(.cn-card) {\n\tborder-color: var(--color-primary-element);\n\ttransform: none;\n}\n\n/* The focus ring belongs on the card, not on the small input inside it. */\n.cn-choice-cards__input:focus-visible + :deep(.cn-card) {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 2px;\n}\n\n.cn-choice-cards__input:disabled {\n\tcursor: default;\n}\n\n.cn-choice-cards__input:disabled + :deep(.cn-card) {\n\topacity: 0.6;\n}\n</style>\n"],"names":["_createElementBlock","_toDisplayString","_openBlock","_createBlock","_Fragment","_renderList","_createElementVNode","_normalizeClass","_createVNode","_renderSlot"],"mappings":";;;;;AAMU,EAAA,KAAA,EAAA,yBAAA;;;;AAIH,EAAA,KAAA,EAAA,0BAAA;;;;;AAgDkC,EAAA,KAAK,EAAC,0BAA2B;;;;;;;;;uBArDvEA;SAAU,EAAA,iBAAA;AAAA,IAAA,WAAA,EAAA,MAAA,CAAA,OAAA,GAAA,MAAA,GAAA,OAAA;;KACH,MAAA,CAAA,KAAA;sBAARA,kBAES,CAAA,QAAA,EAFT,UAES,EAAAC,eAAA;;KAEJ,MAAA,CAAA,OAAA;AAAL,SAAAC,SAAA,EAAA,EAAAF,kBAAA,CAEM,OAFN,UAEM,EAAA;;;AAEM,QAAA,CAAA,QAAA,CAAA,iBAAA,CAAA,MAAA,KAAA,CAAA;wBAAZG,WAEa,CAAA,qBAAA,EAAA;;AAFD,YAAA,IAAA,EAAA,SAAA;;;;;;;;;AAIA,YAAA,KAAK,EAAC,uBAAuB;AAAC,YAAA,KAAO,iBAAC,QAAS,CAAA,SAAA,CAAA;;gDA6CpD,CAAAC,QAAA,EAAA,IAAA,EAAAC,UAAA,CAvCY,QAAY,CAAA,iBAAA,EAAA,CAAtB,MAAM,KAAA;qDAuCR,CAAA,KAAA,EAAA;AAtCJ,gBAAA,GAAG,EAAC,MAAA,CAAA,MAAA,CAAA,KAAA,CAAA;AACJ,gBAAA,KAAI,EAAC,uBAAuB;;gBAC5BC,kBA8BO,CAAA,OAAA,EAAA;AA7BP,kBAAA,KAAK,EAAAC,cAAA,CAAA,CAAA,yBAAyB,EACiB,EAAA,mCAAA,EAAA,QAAA,CAAA,UAAW,CAAA,MAAK,CAAA,EAAA,CAAA,CAAA;;kBAM9DD,kBAO2B,CAAA,OAAA,EAAA;AAN3B,oBAAA,KAAK,EAAC,wBAAU;AACf,oBAAA,IAAI,EAAE,MAAQ,CAAA,QAAA,GAAA,UAAA,GAAA,OAAA;AACd,oBAAA,IAAI,EAAE,KAAS,CAAA,SAAA;AACf,oBAAA,KAAK,EAAE,MAAM,CAAC,MAAI,CAAA,KAAA,CAAA;oBAClB,OAAO,EAAE,QAAU,CAAA,UAAA,CAAC,MAAK,CAAA;AACzB,oBAAA,QAAI,EAAA,MAAA,CAAA,QAAA;oBACJ,QAAM,EAAA,MAAA,KAAE,QAAQ,CAAA,QAAA,CAAC,MAAM,CAAA,CAAA;;kBACxBE,WAaQ,CAAA,iBAAA,EAAA;AAZR,oBAAA,KAAK,EAAC,uBAAQ;oBACb,KAAK,EAAE,MAAM,CAAC,KAAK;AACpB,oBAAA,QAAQ,EAAC,MAAM;oBACd,WAAW,EAAE,MAAM,CAAA,WAAI;oBACvB,KAAK,EAAE,MAAM,CAAC,KAAK,IAAA,EAAA;oBACnB,IAAI,EAAE,MAAM,CAAC,IAAI,IAAA,EAAA;oBACjB,MAAM,EAAE,QAAU,CAAA,UAAA,CAAC,MAAM,CAAA;AAC1B,oBAAA,aAAa,EAAC,SAAS;AACtB,oBAAA,gBAAgB,EAAE,MAAgB,CAAA,gBAAA;;AACzB,oBAAA,CAAA,MAAA,CAAA,IAAA;;gCAAA,MAAA;sCACT,MAAM;4BAANA,WAAM,CAAA,iBAAA,EAAA;8BAAA,IAAA,EAAA,MAAA,CAAA,IAAA;AAAA,8BAAA,IAAA,EAAA,EAAA;;;;;;;;iBAIG,IAAK,CAAA,MAAA,CAAA,gBAAA,CAAA;qDAId,iBAAA,EAAA;sBADFC,UAAG,CAAA,IAAA,CAAA,MAAA,EAAA,gBAAA,EAAA;AAAA,wBAAA,MAAA,EAAA,MAAA;wBAAA,QAAA,EAAA,QAAA,CAAA,UAAA,CAAA,MAAA,CAAA;;;;;;;;;;;;"}
@@ -2067,8 +2067,12 @@ var script = {
2067
2067
  * which for an attention card is its `title` when it has no `text`.
2068
2068
  *
2069
2069
  * @param {object} def Widget definition.
2070
+ * `layout` is returned so the page can tell an attention card from a
2071
+ * plain banner: only the former keeps its cell when its check fails.
2072
+ *
2070
2073
  * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-an-attention-card-with-a-title-keeps-its-cell
2071
- * @return {{ text: string, visibleWhen: (object|null) }}
2074
+ * @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-an-attention-card-says-when-it-could-not-check
2075
+ * @return {{ text: string, visibleWhen: (object|null), layout: string }}
2072
2076
  */
2073
2077
  widgetDisplayConfig(def) {
2074
2078
  const content = (def && typeof def.content === 'object' && def.content) || {};
@@ -2082,6 +2086,7 @@ var script = {
2082
2086
  return {
2083
2087
  text: content.text || props.text || title || '',
2084
2088
  visibleWhen: (def && def.visibleWhen) || content.visibleWhen || props.visibleWhen || null,
2089
+ layout,
2085
2090
  }
2086
2091
  },
2087
2092
 
@@ -2091,15 +2096,22 @@ var script = {
2091
2096
  * text at all — and must therefore surrender its grid cell in live
2092
2097
  * mode instead of leaving an empty card behind.
2093
2098
  *
2099
+ * One exception: an attention card whose `visibleWhen` request FAILED
2100
+ * keeps its cell, so the card can say it could not check. A collapsed
2101
+ * cell reads as "nothing needs attention", which a failed count does
2102
+ * not know. A count that was read and does not meet the condition
2103
+ * still collapses.
2104
+ *
2094
2105
  * @param {object} item Layout item.
2095
2106
  * @return {boolean} true when the cell must collapse.
2107
+ * @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-an-attention-card-says-when-it-could-not-check
2096
2108
  */
2097
2109
  isCollapsedWidget(item) {
2098
2110
  const def = this.getWidgetDef(item.widgetId);
2099
2111
  if (!def || !def.type) {
2100
2112
  return false
2101
2113
  }
2102
- const { text, visibleWhen } = this.widgetDisplayConfig(def);
2114
+ const { text, visibleWhen, layout } = this.widgetDisplayConfig(def);
2103
2115
  if (this.isBannerDef(def) && text === '') {
2104
2116
  return true
2105
2117
  }
@@ -2107,6 +2119,9 @@ var script = {
2107
2119
  return false
2108
2120
  }
2109
2121
  const outcome = this.widgetConditionOutcome[item.widgetId];
2122
+ if (outcome && outcome.failed === true && layout === 'attention' && this.isBannerDef(def)) {
2123
+ return false
2124
+ }
2110
2125
  return !outcome || outcome.met !== true
2111
2126
  },
2112
2127
 
@@ -2117,13 +2132,17 @@ var script = {
2117
2132
  * Runs on created() and again whenever `widgets` changes (the
2118
2133
  * in-app editor mutates defs in place). Fail-safe like the banner
2119
2134
  * itself: any fetch/shape error counts as "not met", so a broken
2120
- * predicate collapses the cell rather than breaking the page.
2135
+ * predicate collapses the cell rather than breaking the page. The
2136
+ * outcome of a failed read carries `failed: true` and the `reason`,
2137
+ * which `isCollapsedWidget` and CnBannerWidget use to show an
2138
+ * attention card's "Could not check" line instead of nothing.
2121
2139
  *
2122
2140
  * Flips `widgetConditionsSettled` when the initial round is done —
2123
2141
  * synchronously when there is nothing to evaluate, so a dashboard
2124
2142
  * without conditional widgets never waits.
2125
2143
  *
2126
2144
  * @return {Promise<void>}
2145
+ * @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-an-attention-card-says-when-it-could-not-check
2127
2146
  */
2128
2147
  async evaluateWidgetConditions() {
2129
2148
  const conditional = (this.widgets || []).filter((def) => def && def.id && def.type
@@ -2145,12 +2164,17 @@ var script = {
2145
2164
  }
2146
2165
  await Promise.all(conditional.map(async (def) => {
2147
2166
  const cond = this.widgetDisplayConfig(def).visibleWhen;
2148
- let outcome = { met: false, value: null };
2167
+ let outcome;
2149
2168
  try {
2150
2169
  const value = await readVisibleWhenValue(cond);
2151
2170
  outcome = { met: compareVisibleWhen(value, cond.op || 'eq', cond.value), value };
2152
- } catch {
2153
- // fail-safe: hidden
2171
+ } catch (error) {
2172
+ // Fail-safe: not met. But the failure is kept, and said
2173
+ // out loud: a failed count and "nothing to report" look
2174
+ // the same on screen.
2175
+ outcome = { met: false, value: null, failed: true, reason: typeof error?.message === 'string' ? error.message : String(error ?? '') };
2176
+ // eslint-disable-next-line no-console
2177
+ console.warn(`[CnDashboardPage] visibleWhen of widget "${def.id}" could not be evaluated:`, error?.message || error);
2154
2178
  }
2155
2179
  // A newer run owns the map now — a stale verdict (possibly for
2156
2180
  // a def that no longer exists) must not overwrite its writes.