@conduction/nextcloud-vue 2.91.0 → 2.93.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +57 -4
  2. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  3. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +44 -17
  4. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  5. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue.js +1 -1
  6. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js +44 -2
  7. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
  8. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js +140 -59
  9. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
  10. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  11. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +20 -6
  12. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  13. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +3 -2
  14. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  15. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +10 -39
  16. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  17. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  18. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +5 -3
  19. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  20. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  21. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  22. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +6 -4
  23. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  24. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  25. package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue2.js +46 -0
  26. package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue2.js.map +1 -1
  27. package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue3.js +6 -2
  28. package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue3.js.map +1 -1
  29. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue.js +1 -1
  30. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js +82 -9
  31. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js.map +1 -1
  32. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js +104 -90
  33. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js.map +1 -1
  34. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue.js +9 -0
  35. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue.js.map +1 -0
  36. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js +355 -0
  37. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js.map +1 -0
  38. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js +462 -0
  39. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js.map +1 -0
  40. package/dist/esm/components/CnFormDialog/CnFormDialog.vue.js +1 -1
  41. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +39 -10
  42. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  43. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +418 -399
  44. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  45. package/dist/esm/components/CnIcon/semanticIcons.js +2 -2
  46. package/dist/esm/components/CnIcon/semanticIcons.js.map +1 -1
  47. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue.js +1 -1
  48. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue2.js +156 -3
  49. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue2.js.map +1 -1
  50. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue3.js +168 -52
  51. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue3.js.map +1 -1
  52. package/dist/esm/components/CnModuleCard/CnModuleCard.vue.js +9 -0
  53. package/dist/esm/components/CnModuleCard/CnModuleCard.vue.js.map +1 -0
  54. package/dist/esm/components/CnModuleCard/CnModuleCard.vue2.js +83 -0
  55. package/dist/esm/components/CnModuleCard/CnModuleCard.vue2.js.map +1 -0
  56. package/dist/esm/components/CnModuleCard/CnModuleCard.vue3.js +119 -0
  57. package/dist/esm/components/CnModuleCard/CnModuleCard.vue3.js.map +1 -0
  58. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue.js +1 -1
  59. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js +22 -1
  60. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js.map +1 -1
  61. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js +20 -14
  62. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js.map +1 -1
  63. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue.js +1 -1
  64. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue2.js +2 -0
  65. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue2.js.map +1 -1
  66. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue3.js +15 -5
  67. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue3.js.map +1 -1
  68. package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue.js +1 -1
  69. package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue2.js +8 -30
  70. package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue2.js.map +1 -1
  71. package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue3.js.map +1 -1
  72. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  73. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +33 -2
  74. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  75. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  76. package/dist/esm/components/CnTextWidget/CnTextWidget.vue.js +1 -1
  77. package/dist/esm/components/CnTextWidget/CnTextWidget.vue2.js +63 -4
  78. package/dist/esm/components/CnTextWidget/CnTextWidget.vue2.js.map +1 -1
  79. package/dist/esm/components/CnTextWidget/CnTextWidget.vue3.js +19 -2
  80. package/dist/esm/components/CnTextWidget/CnTextWidget.vue3.js.map +1 -1
  81. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue.js +9 -0
  82. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue.js.map +1 -0
  83. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js +164 -0
  84. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js.map +1 -0
  85. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js +93 -0
  86. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js.map +1 -0
  87. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue.js +1 -1
  88. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js +12 -0
  89. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js.map +1 -1
  90. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js.map +1 -1
  91. package/dist/esm/l10n/en.json.js +45 -1
  92. package/dist/esm/l10n/en.json.js.map +1 -1
  93. package/dist/esm/l10n/nl.json.js +44 -1
  94. package/dist/esm/l10n/nl.json.js.map +1 -1
  95. package/dist/esm/library-style.js +1 -1
  96. package/dist/esm/nextcloud-vue.css +1182 -328
  97. package/dist/esm/utils/boardDate.js +35 -1
  98. package/dist/esm/utils/boardDate.js.map +1 -1
  99. package/dist/esm/utils/featureCatalog.js +135 -0
  100. package/dist/esm/utils/featureCatalog.js.map +1 -0
  101. package/dist/esm/utils/fillTemplate.js +84 -4
  102. package/dist/esm/utils/fillTemplate.js.map +1 -1
  103. package/dist/esm/utils/headerMeta.js +62 -12
  104. package/dist/esm/utils/headerMeta.js.map +1 -1
  105. package/dist/esm/utils/roadmapIssues.js +86 -0
  106. package/dist/esm/utils/roadmapIssues.js.map +1 -0
  107. package/dist/esm/utils/userDisplayName.js +47 -0
  108. package/dist/esm/utils/userDisplayName.js.map +1 -0
  109. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  110. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  111. package/dist/nextcloud-vue.cjs.js +15618 -13097
  112. package/dist/nextcloud-vue.cjs.js.map +2 -2
  113. package/dist/nextcloud-vue.css +1182 -328
  114. package/l10n/en.json +45 -1
  115. package/l10n/nl.json +44 -1
  116. package/package.json +1 -1
  117. package/src/components/CnAppRoot/CnAppRoot.md +2 -0
  118. package/src/components/CnAppRoot/CnAppRoot.vue +66 -5
  119. package/src/components/CnChoiceCards/CnChoiceCards.md +27 -0
  120. package/src/components/CnChoiceCards/CnChoiceCards.vue +168 -3
  121. package/src/components/CnDashboardPage/CnDashboardPage.vue +45 -6
  122. package/src/components/CnDataTable/CnDataTable.vue +10 -39
  123. package/src/components/CnDetailPage/CnDetailPage.vue +5 -3
  124. package/src/components/CnFactsWidget/CnFactsWidget.vue +6 -4
  125. package/src/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue +51 -1
  126. package/src/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue +98 -13
  127. package/src/components/CnFeaturesBoard/CnFeaturesBoard.vue +1053 -0
  128. package/src/components/CnFormDialog/CnFormDialog.md +1 -0
  129. package/src/components/CnFormDialog/CnFormDialog.vue +619 -562
  130. package/src/components/CnLinkCardsPage/CnLinkCardsPage.vue +338 -5
  131. package/src/components/CnModuleCard/CnModuleCard.vue +258 -0
  132. package/src/components/CnNcWidgetWidget/CnNcWidgetWidget.vue +23 -2
  133. package/src/components/CnNotificationPreferences/CnNotificationPreferences.vue +5 -0
  134. package/src/components/CnRoadmapTab/CnRoadmapTab.vue +6 -28
  135. package/src/components/CnTabsWidget/CnTabsWidget.vue +33 -2
  136. package/src/components/CnTextWidget/CnTextWidget.vue +89 -4
  137. package/src/components/CnUserSettingsDialog/CnUserSettingsDialog.vue +305 -0
  138. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +12 -0
  139. package/src/css/dialog.css +47 -0
  140. package/src/css/look-board-detail.css +8 -0
  141. package/src/css/look-board.css +33 -0
  142. package/src/schemas/app-manifest-v2.schema.json +67 -4
  143. package/src/utils/boardDate.js +34 -0
  144. package/src/utils/featureCatalog.js +132 -0
  145. package/src/utils/fillTemplate.js +83 -3
  146. package/src/utils/headerMeta.js +62 -12
  147. package/src/utils/roadmapIssues.js +82 -0
  148. package/src/utils/userDisplayName.js +54 -0
  149. package/src/utils/validateManifestV2.compiled.js +3 -3
@@ -2,7 +2,7 @@ import script from './CnChoiceCards.vue2.js';
2
2
  import { render } from './CnChoiceCards.vue3.js';
3
3
 
4
4
  script.render = render;
5
- script.__scopeId = "data-v-c551354c";
5
+ script.__scopeId = "data-v-8d2b4ff4";
6
6
  const __sfc_main = script;
7
7
 
8
8
  export { __sfc_main as default };
@@ -42,7 +42,9 @@ var script = {
42
42
  props: {
43
43
  /**
44
44
  * The options to offer. Each: `{ value, label, description?, stats?,
45
- * tags?, icon? }` — `icon` is a PascalCase MDI name resolved by CnIcon.
45
+ * tags?, icon?, disabled?, disabledReason? }` — `icon` is a PascalCase
46
+ * MDI name resolved by CnIcon; a `disabled` option is greyed out and
47
+ * cannot be picked, with `disabledReason` shown under it.
46
48
  */
47
49
  options: {
48
50
  type: Array,
@@ -89,6 +91,23 @@ var script = {
89
91
  default: () => translate('nextcloud-vue', 'Nothing to choose from here.'),
90
92
  },
91
93
 
94
+ /**
95
+ * `cards`: a grid of cards with stats and tags. `rows`: one option per
96
+ * row, full width, the input on the left and the title over a muted
97
+ * description; with `multiple` and one option it is a checkbox card.
98
+ */
99
+ layout: {
100
+ type: String,
101
+ default: 'cards',
102
+ validator: (value) => ['cards', 'rows'].includes(value),
103
+ },
104
+
105
+ /** Columns of the card grid; 0 lets the grid fit as many as the width allows. */
106
+ columns: {
107
+ type: Number,
108
+ default: 0,
109
+ },
110
+
92
111
  /** Lines of description shown before clamping. */
93
112
  descriptionLines: {
94
113
  type: Number,
@@ -159,6 +178,9 @@ var script = {
159
178
  * @return {object} A `gridTemplateColumns` override, or `{}`.
160
179
  */
161
180
  gridStyle() {
181
+ if (this.columns > 0) {
182
+ return { gridTemplateColumns: `repeat(${this.columns}, minmax(0, 1fr))` }
183
+ }
162
184
  const count = this.normalizedOptions.length;
163
185
  if (count > 0 && count <= 2) {
164
186
  return { gridTemplateColumns: `repeat(${count}, minmax(0, 1fr))` }
@@ -177,6 +199,26 @@ var script = {
177
199
  * @param {object} option The normalised option.
178
200
  * @return {boolean} True when selected.
179
201
  */
202
+ /**
203
+ * Whether an option cannot be picked: the whole group is disabled, or the option is.
204
+ *
205
+ * @param {object} option The normalised option.
206
+ * @return {boolean} True when disabled.
207
+ */
208
+ isDisabled(option) {
209
+ return this.disabled || option.disabled === true
210
+ },
211
+
212
+ /**
213
+ * Id of an option's disabled reason, for `aria-describedby`.
214
+ *
215
+ * @param {object} option The normalised option.
216
+ * @return {string} The id.
217
+ */
218
+ reasonId(option) {
219
+ return `${this.groupName}-reason-${String(option.value).replace(/[^A-Za-z0-9_-]/g, '-')}`
220
+ },
221
+
180
222
  isSelected(option) {
181
223
  return this.selectedValues.some((v) => String(v) === String(option.value))
182
224
  },
@@ -188,7 +230,7 @@ var script = {
188
230
  * @return {void}
189
231
  */
190
232
  onToggle(option) {
191
- if (this.disabled) {
233
+ if (this.isDisabled(option)) {
192
234
  return
193
235
  }
194
236
  if (!this.multiple) {
@@ -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<!-- 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
+ {"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<!-- Rows: one full-width option per row, the input on the left and the\n\t\t title over a muted description, no card chrome. -->\n\t\t<div v-else-if=\"layout === 'rows'\" class=\"cn-choice-cards__rows\" data-testid=\"cn-choice-cards-rows\">\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__row\"\n\t\t\t\t\t:class=\"{ 'cn-choice-cards__row--selected': isSelected(option), 'cn-choice-cards__row--disabled': isDisabled(option) }\">\n\t\t\t\t\t<input\n\t\t\t\t\t\tclass=\"cn-choice-cards__row-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=\"isDisabled(option)\"\n\t\t\t\t\t\t:aria-describedby=\"option.disabled && option.disabledReason ? reasonId(option) : null\"\n\t\t\t\t\t\t@change=\"onToggle(option)\">\n\t\t\t\t\t<CnIcon v-if=\"option.icon\"\n\t\t\t\t\t\t:name=\"option.icon\"\n\t\t\t\t\t\t:size=\"20\"\n\t\t\t\t\t\tclass=\"cn-choice-cards__row-icon\" />\n\t\t\t\t\t<span class=\"cn-choice-cards__row-text\">\n\t\t\t\t\t\t<span class=\"cn-choice-cards__row-title\">{{ option.label }}</span>\n\t\t\t\t\t\t<span v-if=\"option.description\" class=\"cn-choice-cards__row-description\">{{ option.description }}</span>\n\t\t\t\t\t\t<span\n\t\t\t\t\t\t\tv-if=\"option.disabled && option.disabledReason\"\n\t\t\t\t\t\t\t:id=\"reasonId(option)\"\n\t\t\t\t\t\t\tclass=\"cn-choice-cards__reason\"\n\t\t\t\t\t\t\tdata-testid=\"cn-choice-cards-reason\">{{ option.disabledReason }}</span>\n\t\t\t\t\t</span>\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 name=\"option-actions\" :option=\"option\" :selected=\"isSelected(option)\" />\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\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=\"isDisabled(option)\"\n\t\t\t\t\t\t:aria-describedby=\"option.disabled && option.disabledReason ? reasonId(option) : null\"\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\t<span\n\t\t\t\t\t\tv-if=\"option.disabled && option.disabledReason\"\n\t\t\t\t\t\t:id=\"reasonId(option)\"\n\t\t\t\t\t\tclass=\"cn-choice-cards__reason cn-choice-cards__reason--card\"\n\t\t\t\t\t\tdata-testid=\"cn-choice-cards-reason\">{{ option.disabledReason }}</span>\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?, disabled?, disabledReason? }` — `icon` is a PascalCase\n\t\t * MDI name resolved by CnIcon; a `disabled` option is greyed out and\n\t\t * cannot be picked, with `disabledReason` shown under it.\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/**\n\t\t * `cards`: a grid of cards with stats and tags. `rows`: one option per\n\t\t * row, full width, the input on the left and the title over a muted\n\t\t * description; with `multiple` and one option it is a checkbox card.\n\t\t */\n\t\tlayout: {\n\t\t\ttype: String,\n\t\t\tdefault: 'cards',\n\t\t\tvalidator: (value) => ['cards', 'rows'].includes(value),\n\t\t},\n\n\t\t/** Columns of the card grid; 0 lets the grid fit as many as the width allows. */\n\t\tcolumns: {\n\t\t\ttype: Number,\n\t\t\tdefault: 0,\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\tif (this.columns > 0) {\n\t\t\t\treturn { gridTemplateColumns: `repeat(${this.columns}, minmax(0, 1fr))` }\n\t\t\t}\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\t/**\n\t\t * Whether an option cannot be picked: the whole group is disabled, or the option is.\n\t\t *\n\t\t * @param {object} option The normalised option.\n\t\t * @return {boolean} True when disabled.\n\t\t */\n\t\tisDisabled(option) {\n\t\t\treturn this.disabled || option.disabled === true\n\t\t},\n\n\t\t/**\n\t\t * Id of an option's disabled reason, for `aria-describedby`.\n\t\t *\n\t\t * @param {object} option The normalised option.\n\t\t * @return {string} The id.\n\t\t */\n\t\treasonId(option) {\n\t\t\treturn `${this.groupName}-reason-${String(option.value).replace(/[^A-Za-z0-9_-]/g, '-')}`\n\t\t},\n\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.isDisabled(option)) {\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\n.cn-choice-cards__rows {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n}\n\n.cn-choice-cards__row {\n\tdisplay: flex;\n\talign-items: flex-start;\n\tgap: 12px;\n\tpadding: 14px 16px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 8px);\n\tbackground: var(--color-main-background);\n\tcursor: pointer;\n}\n\n.cn-choice-cards__row:hover {\n\tborder-color: var(--color-border-dark);\n}\n\n.cn-choice-cards__row--selected,\n.cn-choice-cards__row--selected:hover {\n\tborder-color: var(--color-primary-element);\n\t/* The second pixel of the 2px selected border, without moving the content. */\n\tbox-shadow: inset 0 0 0 1px var(--color-primary-element);\n\tbackground: var(--color-primary-element-light);\n}\n\n.cn-choice-cards__row--disabled {\n\tcursor: default;\n\topacity: 0.6;\n}\n\n.cn-choice-cards__row:has(.cn-choice-cards__row-input:focus-visible) {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 2px;\n}\n\n.cn-choice-cards__row-input {\n\tflex: none;\n\tmargin: 2px 0 0;\n\taccent-color: var(--color-primary-element);\n\tcursor: inherit;\n}\n\n.cn-choice-cards__row-icon {\n\tflex: none;\n}\n\n.cn-choice-cards__row-text {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n\tmin-width: 0;\n}\n\n.cn-choice-cards__row-title {\n\tfont-size: 15px;\n\tfont-weight: 600;\n}\n\n.cn-choice-cards__row-description {\n\tfont-size: 13px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-choice-cards__reason {\n\tfont-size: 13px;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-style: italic;\n}\n\n.cn-choice-cards__reason--card {\n\tdisplay: block;\n\tmargin-block-start: 4px;\n}\n</style>\n"],"names":["CnCard","CnIcon","t"],"mappings":";;;;;AAqHA,IAAI,QAAO,GAAI,EAAA;;AAEf;;AAEC;;;AAGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;;AAEA;;;AAGA;;AAED,aAAe;AACd,CAAA,IAAI,EAAE,eAAe;;CAErB,UAAU,EAAE;AACX,UAAAA,UAAM;AACN,UAAAC,YAAM;EACN,aAAa;EACb,UAAU;EACV;;AAED,CAAA,KAAK,EAAE;AACN;;;;;;AAMA,EAAA,OAAO,EAAE;GACR,IAAI,EAAE,KAAK;AACX,GAAA,OAAO,EAAE,MAAM,EAAE;GACjB;;AAED;;AAEC;AACA;;EAED,UAAU,EAAE;AACX,GAAA,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC;GAC9C,OAAO,EAAE,IAAI;GACb;;;AAGD,EAAA,QAAQ,EAAE;GACT,IAAI,EAAE,OAAO;GACb,OAAO,EAAE,KAAK;GACd;;AAED;AACA,EAAA,KAAK,EAAE;GACN,IAAI,EAAE,MAAM;GACZ,OAAO,EAAE,EAAE;GACX;;AAED;AACA,EAAA,QAAQ,EAAE;GACT,IAAI,EAAE,OAAO;GACb,OAAO,EAAE,KAAK;GACd;;;AAGD,EAAA,OAAO,EAAE;GACR,IAAI,EAAE,OAAO;GACb,OAAO,EAAE,KAAK;GACd;;;EAGD,SAAS,EAAE;GACV,IAAI,EAAE,MAAM;GACZ,OAAO,EAAE,MAAMC,SAAC,CAAC,eAAe,EAAE,8BAA8B,CAAC;GACjE;;AAED;;;;;AAKA,EAAA,MAAM,EAAE;GACP,IAAI,EAAE,MAAM;AACZ,GAAA,OAAO,EAAE,OAAO;GAChB,SAAS,EAAE,CAAC,KAAK,KAAK,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC;GACvD;;AAED;AACA,EAAA,OAAO,EAAE;GACR,IAAI,EAAE,MAAM;AACZ,GAAA,OAAO,EAAE,CAAC;GACV;;AAED;AACA,EAAA,gBAAgB,EAAE;GACjB,IAAI,EAAE,MAAM;AACZ,GAAA,OAAO,EAAE,CAAC;GACV;EACD;;CAED,KAAK,EAAE,CAAC,mBAAmB,CAAC;;AAE5B,CAAA,IAAI,GAAG;EACN,QAAO,IAAK,EAAA;AACZ,EAAA,OAAO;;;AAGN,GAAA,SAAS,EAAE,kBAAiB,GAAI,QAAQ;AACzC,GAAA;EACA;;AAED,CAAA,QAAQ,EAAE;AACT;;;;AAIC;AACA;;AAED,EAAA,iBAAiB,GAAG;GACnB,OAAO,CAAC,IAAI,CAAC,OAAQ,IAAG,EAAE;KACxB,MAAM,CAAC,CAAC,MAAM,KAAK,MAAO,KAAI,IAAG,IAAK,WAAW,SAAS,CAAA;AAC3D,KAAC,GAAG,CAAC,CAAC,MAAM,KAAK;KAChB,IAAI,OAAO,MAAO,KAAI,QAAQ,EAAE;AAC/B,MAAA,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,CAAE,EAAA;AAC/C,MAAA;KACA,MAAM,KAAM,GAAE,MAAM,CAAC,KAAM,KAAI,SAAQ,GAAI,MAAM,CAAC,KAAI,GAAI,MAAM,CAAC,GAAC;AAClE,KAAA,OAAO;AACN,MAAA,GAAG,MAAM;MACT,KAAK;MACL,KAAK,EAAE,MAAM,CAAC,SAAS,MAAM,CAAC,IAAK,IAAG,MAAM,CAAC,KAAK,CAAC;AACpD,MAAA;KACA,CAAA;AACD,KAAC,MAAM,CAAC,CAAC,MAAM,KAAK,MAAM,CAAC,UAAU,SAAS,CAAA;GAC/C;;AAED;;AAEC;AACA;;AAED,EAAA,cAAc,GAAG;AAChB,GAAA,IAAI,IAAI,CAAC,QAAQ,EAAE;IAClB,OAAO,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,CAAE,GAAE,IAAI,CAAC,UAAW,GAAE,EAAC;AAC5D,IAAA;AACA,GAAA,OAAO,CAAC,IAAI,CAAC,eAAe,IAAG,IAAK,IAAI,CAAC,UAAW,KAAI,aAAa,IAAI,CAAC,UAAS,KAAM,EAAE;AAC1F,MAAE,EAAC;AACH,MAAE,CAAC,IAAI,CAAC,UAAU,CAAA;GACnB;;AAED;;;;;;;;;AASC;;;EAGD,SAAS,GAAG;AACX,GAAA,IAAI,IAAI,CAAC,OAAQ,GAAE,CAAC,EAAE;AACrB,IAAA,OAAO,EAAE,mBAAmB,EAAE,CAAC,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,iBAAiB,CAAE,EAAA;AACzE,IAAA;AACA,GAAA,MAAM,KAAM,GAAE,IAAI,CAAC,iBAAiB,CAAC,OAAK;AAC1C,GAAA,IAAI,QAAQ,CAAA,IAAK,SAAS,CAAC,EAAE;IAC5B,OAAO,EAAE,mBAAmB,EAAE,CAAC,OAAO,EAAE,KAAK,CAAC,iBAAiB,CAAE,EAAA;AAClE,IAAA;AACA,GAAA,OAAO,EAAC;GACR;EACD;;AAED,CAAA,OAAO,EAAE;AACR;AACC;AACA;;AAEA;AACA;AACA;AACA;;AAED;AACC;AACA;AACA;AACA;;AAED,EAAA,UAAU,CAAC,MAAM,EAAE;AAClB,GAAA,OAAO,IAAI,CAAC,YAAY,MAAM,CAAC,QAAS,KAAI,IAAG;GAC/C;;AAED;;AAEC;AACA;AACA;;AAED,EAAA,QAAQ,CAAC,MAAM,EAAE;AAChB,GAAA,OAAO,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,iBAAiB,EAAE,GAAG,CAAC,CAAC,CAAA;GACxF;;AAED,EAAA,UAAU,CAAC,MAAM,EAAE;AAClB,GAAA,OAAO,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,CAAC,CAAE,KAAI,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;GACzE;;AAED;;AAEC;AACA;;;AAGD,EAAA,QAAQ,CAAC,MAAM,EAAE;GAChB,IAAI,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE;IAC5B,MAAK;AACN,IAAA;AACA,GAAA,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE;AACnB;AACC;AACA;;;AAGD;AACA,IAAA,IAAI,CAAC,KAAK,CAAC,mBAAmB,EAAE,MAAM,CAAC,KAAK,EAAA;IAC5C,MAAK;AACN,IAAA;AACA,GAAA,MAAM,IAAK,GAAE,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,CAAC,CAAA,KAAM,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,EAAA;AACjF,GAAA,IAAI,IAAI,CAAC,WAAW,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE;IAC/C,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,EAAA;AACvB,IAAA;AACA,GAAA,IAAI,CAAC,KAAK,CAAC,mBAAmB,EAAE,IAAI,EAAA;GACpC;EACD;AACF;;;;"}
@@ -1,4 +1,4 @@
1
- import { resolveComponent, openBlock, createElementBlock, toDisplayString, createCommentVNode, createVNode, createBlock, withCtx, createTextVNode, normalizeStyle, Fragment, renderList, createElementVNode, normalizeClass, createSlots, renderSlot } from 'vue';
1
+ import { resolveComponent, openBlock, createElementBlock, toDisplayString, createCommentVNode, createVNode, createBlock, withCtx, createTextVNode, Fragment, renderList, createElementVNode, normalizeClass, renderSlot, normalizeStyle, createSlots } from 'vue';
2
2
 
3
3
  const _hoisted_1 = ["aria-busy"];
4
4
  const _hoisted_2 = {
@@ -9,8 +9,26 @@ const _hoisted_3 = {
9
9
  key: 1,
10
10
  class: "cn-choice-cards__loading"
11
11
  };
12
- const _hoisted_4 = ["type", "name", "value", "checked", "disabled", "onChange"];
13
- const _hoisted_5 = {
12
+ const _hoisted_4 = {
13
+ key: 3,
14
+ class: "cn-choice-cards__rows",
15
+ "data-testid": "cn-choice-cards-rows"
16
+ };
17
+ const _hoisted_5 = ["type", "name", "value", "checked", "disabled", "aria-describedby", "onChange"];
18
+ const _hoisted_6 = { class: "cn-choice-cards__row-text" };
19
+ const _hoisted_7 = { class: "cn-choice-cards__row-title" };
20
+ const _hoisted_8 = {
21
+ key: 0,
22
+ class: "cn-choice-cards__row-description"
23
+ };
24
+ const _hoisted_9 = ["id"];
25
+ const _hoisted_10 = {
26
+ key: 0,
27
+ class: "cn-choice-cards__actions"
28
+ };
29
+ const _hoisted_11 = ["type", "name", "value", "checked", "disabled", "aria-describedby", "onChange"];
30
+ const _hoisted_12 = ["id"];
31
+ const _hoisted_13 = {
14
32
  key: 0,
15
33
  class: "cn-choice-cards__actions"
16
34
  };
@@ -42,64 +60,127 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
42
60
  ]),
43
61
  _: 1 /* STABLE */
44
62
  }))
45
- : (openBlock(), createElementBlock("div", {
46
- key: 3,
47
- class: "cn-choice-cards__grid",
48
- style: normalizeStyle($options.gridStyle)
49
- }, [
50
- (openBlock(true), createElementBlock(Fragment, null, renderList($options.normalizedOptions, (option) => {
51
- return (openBlock(), createElementBlock("div", {
52
- key: String(option.value),
53
- class: "cn-choice-cards__cell"
54
- }, [
55
- createElementVNode("label", {
56
- class: normalizeClass(["cn-choice-cards__option", { 'cn-choice-cards__option--selected': $options.isSelected(option) }])
63
+ : ($props.layout === 'rows')
64
+ ? (openBlock(), createElementBlock("div", _hoisted_4, [
65
+ (openBlock(true), createElementBlock(Fragment, null, renderList($options.normalizedOptions, (option) => {
66
+ return (openBlock(), createElementBlock("div", {
67
+ key: String(option.value),
68
+ class: "cn-choice-cards__cell"
57
69
  }, [
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 */ }, [
70
+ createElementVNode("label", {
71
+ class: normalizeClass(["cn-choice-cards__row", { 'cn-choice-cards__row--selected': $options.isSelected(option), 'cn-choice-cards__row--disabled': $options.isDisabled(option) }])
72
+ }, [
73
+ createElementVNode("input", {
74
+ class: "cn-choice-cards__row-input",
75
+ type: $props.multiple ? 'checkbox' : 'radio',
76
+ name: $data.groupName,
77
+ value: String(option.value),
78
+ checked: $options.isSelected(option),
79
+ disabled: $options.isDisabled(option),
80
+ "aria-describedby": option.disabled && option.disabledReason ? $options.reasonId(option) : null,
81
+ onChange: $event => ($options.onToggle(option))
82
+ }, null, 40 /* PROPS, NEED_HYDRATION */, _hoisted_5),
78
83
  (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
- ]))
101
- }), 128 /* KEYED_FRAGMENT */))
102
- ], 4 /* STYLE */))
84
+ ? (openBlock(), createBlock(_component_CnIcon, {
85
+ key: 0,
86
+ name: option.icon,
87
+ size: 20,
88
+ class: "cn-choice-cards__row-icon"
89
+ }, null, 8 /* PROPS */, ["name"]))
90
+ : createCommentVNode("v-if", true),
91
+ createElementVNode("span", _hoisted_6, [
92
+ createElementVNode("span", _hoisted_7, toDisplayString(option.label), 1 /* TEXT */),
93
+ (option.description)
94
+ ? (openBlock(), createElementBlock("span", _hoisted_8, toDisplayString(option.description), 1 /* TEXT */))
95
+ : createCommentVNode("v-if", true),
96
+ (option.disabled && option.disabledReason)
97
+ ? (openBlock(), createElementBlock("span", {
98
+ key: 1,
99
+ id: $options.reasonId(option),
100
+ class: "cn-choice-cards__reason",
101
+ "data-testid": "cn-choice-cards-reason"
102
+ }, toDisplayString(option.disabledReason), 9 /* TEXT, PROPS */, _hoisted_9))
103
+ : createCommentVNode("v-if", true)
104
+ ])
105
+ ], 2 /* CLASS */),
106
+ (_ctx.$slots['option-actions'])
107
+ ? (openBlock(), createElementBlock("div", _hoisted_10, [
108
+ renderSlot(_ctx.$slots, "option-actions", {
109
+ option: option,
110
+ selected: $options.isSelected(option)
111
+ })
112
+ ]))
113
+ : createCommentVNode("v-if", true)
114
+ ]))
115
+ }), 128 /* KEYED_FRAGMENT */))
116
+ ]))
117
+ : (openBlock(), createElementBlock("div", {
118
+ key: 4,
119
+ class: "cn-choice-cards__grid",
120
+ style: normalizeStyle($options.gridStyle)
121
+ }, [
122
+ (openBlock(true), createElementBlock(Fragment, null, renderList($options.normalizedOptions, (option) => {
123
+ return (openBlock(), createElementBlock("div", {
124
+ key: String(option.value),
125
+ class: "cn-choice-cards__cell"
126
+ }, [
127
+ createElementVNode("label", {
128
+ class: normalizeClass(["cn-choice-cards__option", { 'cn-choice-cards__option--selected': $options.isSelected(option) }])
129
+ }, [
130
+ createElementVNode("input", {
131
+ class: "cn-choice-cards__input",
132
+ type: $props.multiple ? 'checkbox' : 'radio',
133
+ name: $data.groupName,
134
+ value: String(option.value),
135
+ checked: $options.isSelected(option),
136
+ disabled: $options.isDisabled(option),
137
+ "aria-describedby": option.disabled && option.disabledReason ? $options.reasonId(option) : null,
138
+ onChange: $event => ($options.onToggle(option))
139
+ }, null, 40 /* PROPS, NEED_HYDRATION */, _hoisted_11),
140
+ createVNode(_component_CnCard, {
141
+ class: "cn-choice-cards__card",
142
+ title: option.label,
143
+ titleTag: "span",
144
+ description: option.description,
145
+ stats: option.stats || [],
146
+ tags: option.tags || [],
147
+ active: $options.isSelected(option),
148
+ activeVariant: "primary",
149
+ descriptionLines: $props.descriptionLines
150
+ }, createSlots({ _: 2 /* DYNAMIC */ }, [
151
+ (option.icon)
152
+ ? {
153
+ name: "icon",
154
+ fn: withCtx(() => [
155
+ createVNode(_component_CnIcon, {
156
+ name: option.icon,
157
+ size: 20
158
+ }, null, 8 /* PROPS */, ["name"])
159
+ ]),
160
+ key: "0"
161
+ }
162
+ : undefined
163
+ ]), 1032 /* PROPS, DYNAMIC_SLOTS */, ["title", "description", "stats", "tags", "active", "descriptionLines"]),
164
+ (option.disabled && option.disabledReason)
165
+ ? (openBlock(), createElementBlock("span", {
166
+ key: 0,
167
+ id: $options.reasonId(option),
168
+ class: "cn-choice-cards__reason cn-choice-cards__reason--card",
169
+ "data-testid": "cn-choice-cards-reason"
170
+ }, toDisplayString(option.disabledReason), 9 /* TEXT, PROPS */, _hoisted_12))
171
+ : createCommentVNode("v-if", true)
172
+ ], 2 /* CLASS */),
173
+ (_ctx.$slots['option-actions'])
174
+ ? (openBlock(), createElementBlock("div", _hoisted_13, [
175
+ renderSlot(_ctx.$slots, "option-actions", {
176
+ option: option,
177
+ selected: $options.isSelected(option)
178
+ })
179
+ ]))
180
+ : createCommentVNode("v-if", true)
181
+ ]))
182
+ }), 128 /* KEYED_FRAGMENT */))
183
+ ], 4 /* STYLE */))
103
184
  ], 8 /* PROPS */, _hoisted_1))
104
185
  }
105
186
 
@@ -1 +1 @@
1
- {"version":3,"file":"CnChoiceCards.vue3.js","sources":["../../../../src/components/CnChoiceCards/CnChoiceCards.vue?vue&type=template&id=c551354c&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;;;;;;;;;;;;"}
1
+ {"version":3,"file":"CnChoiceCards.vue3.js","sources":["../../../../src/components/CnChoiceCards/CnChoiceCards.vue?vue&type=template&id=8d2b4ff4&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<!-- Rows: one full-width option per row, the input on the left and the\n\t\t title over a muted description, no card chrome. -->\n\t\t<div v-else-if=\"layout === 'rows'\" class=\"cn-choice-cards__rows\" data-testid=\"cn-choice-cards-rows\">\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__row\"\n\t\t\t\t\t:class=\"{ 'cn-choice-cards__row--selected': isSelected(option), 'cn-choice-cards__row--disabled': isDisabled(option) }\">\n\t\t\t\t\t<input\n\t\t\t\t\t\tclass=\"cn-choice-cards__row-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=\"isDisabled(option)\"\n\t\t\t\t\t\t:aria-describedby=\"option.disabled && option.disabledReason ? reasonId(option) : null\"\n\t\t\t\t\t\t@change=\"onToggle(option)\">\n\t\t\t\t\t<CnIcon v-if=\"option.icon\"\n\t\t\t\t\t\t:name=\"option.icon\"\n\t\t\t\t\t\t:size=\"20\"\n\t\t\t\t\t\tclass=\"cn-choice-cards__row-icon\" />\n\t\t\t\t\t<span class=\"cn-choice-cards__row-text\">\n\t\t\t\t\t\t<span class=\"cn-choice-cards__row-title\">{{ option.label }}</span>\n\t\t\t\t\t\t<span v-if=\"option.description\" class=\"cn-choice-cards__row-description\">{{ option.description }}</span>\n\t\t\t\t\t\t<span\n\t\t\t\t\t\t\tv-if=\"option.disabled && option.disabledReason\"\n\t\t\t\t\t\t\t:id=\"reasonId(option)\"\n\t\t\t\t\t\t\tclass=\"cn-choice-cards__reason\"\n\t\t\t\t\t\t\tdata-testid=\"cn-choice-cards-reason\">{{ option.disabledReason }}</span>\n\t\t\t\t\t</span>\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 name=\"option-actions\" :option=\"option\" :selected=\"isSelected(option)\" />\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\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=\"isDisabled(option)\"\n\t\t\t\t\t\t:aria-describedby=\"option.disabled && option.disabledReason ? reasonId(option) : null\"\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\t<span\n\t\t\t\t\t\tv-if=\"option.disabled && option.disabledReason\"\n\t\t\t\t\t\t:id=\"reasonId(option)\"\n\t\t\t\t\t\tclass=\"cn-choice-cards__reason cn-choice-cards__reason--card\"\n\t\t\t\t\t\tdata-testid=\"cn-choice-cards-reason\">{{ option.disabledReason }}</span>\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?, disabled?, disabledReason? }` — `icon` is a PascalCase\n\t\t * MDI name resolved by CnIcon; a `disabled` option is greyed out and\n\t\t * cannot be picked, with `disabledReason` shown under it.\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/**\n\t\t * `cards`: a grid of cards with stats and tags. `rows`: one option per\n\t\t * row, full width, the input on the left and the title over a muted\n\t\t * description; with `multiple` and one option it is a checkbox card.\n\t\t */\n\t\tlayout: {\n\t\t\ttype: String,\n\t\t\tdefault: 'cards',\n\t\t\tvalidator: (value) => ['cards', 'rows'].includes(value),\n\t\t},\n\n\t\t/** Columns of the card grid; 0 lets the grid fit as many as the width allows. */\n\t\tcolumns: {\n\t\t\ttype: Number,\n\t\t\tdefault: 0,\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\tif (this.columns > 0) {\n\t\t\t\treturn { gridTemplateColumns: `repeat(${this.columns}, minmax(0, 1fr))` }\n\t\t\t}\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\t/**\n\t\t * Whether an option cannot be picked: the whole group is disabled, or the option is.\n\t\t *\n\t\t * @param {object} option The normalised option.\n\t\t * @return {boolean} True when disabled.\n\t\t */\n\t\tisDisabled(option) {\n\t\t\treturn this.disabled || option.disabled === true\n\t\t},\n\n\t\t/**\n\t\t * Id of an option's disabled reason, for `aria-describedby`.\n\t\t *\n\t\t * @param {object} option The normalised option.\n\t\t * @return {string} The id.\n\t\t */\n\t\treasonId(option) {\n\t\t\treturn `${this.groupName}-reason-${String(option.value).replace(/[^A-Za-z0-9_-]/g, '-')}`\n\t\t},\n\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.isDisabled(option)) {\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\n.cn-choice-cards__rows {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n}\n\n.cn-choice-cards__row {\n\tdisplay: flex;\n\talign-items: flex-start;\n\tgap: 12px;\n\tpadding: 14px 16px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 8px);\n\tbackground: var(--color-main-background);\n\tcursor: pointer;\n}\n\n.cn-choice-cards__row:hover {\n\tborder-color: var(--color-border-dark);\n}\n\n.cn-choice-cards__row--selected,\n.cn-choice-cards__row--selected:hover {\n\tborder-color: var(--color-primary-element);\n\t/* The second pixel of the 2px selected border, without moving the content. */\n\tbox-shadow: inset 0 0 0 1px var(--color-primary-element);\n\tbackground: var(--color-primary-element-light);\n}\n\n.cn-choice-cards__row--disabled {\n\tcursor: default;\n\topacity: 0.6;\n}\n\n.cn-choice-cards__row:has(.cn-choice-cards__row-input:focus-visible) {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 2px;\n}\n\n.cn-choice-cards__row-input {\n\tflex: none;\n\tmargin: 2px 0 0;\n\taccent-color: var(--color-primary-element);\n\tcursor: inherit;\n}\n\n.cn-choice-cards__row-icon {\n\tflex: none;\n}\n\n.cn-choice-cards__row-text {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n\tmin-width: 0;\n}\n\n.cn-choice-cards__row-title {\n\tfont-size: 15px;\n\tfont-weight: 600;\n}\n\n.cn-choice-cards__row-description {\n\tfont-size: 13px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-choice-cards__reason {\n\tfont-size: 13px;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-style: italic;\n}\n\n.cn-choice-cards__reason--card {\n\tdisplay: block;\n\tmargin-block-start: 4px;\n}\n</style>\n"],"names":["_createElementBlock","_toDisplayString","_openBlock","_createBlock","_normalizeClass","_createElementVNode","_renderSlot","_Fragment","_renderList","_createVNode"],"mappings":"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"}
@@ -2,7 +2,7 @@ import script from './CnDashboardPage.vue2.js';
2
2
  import { render } from './CnDashboardPage.vue3.js';
3
3
 
4
4
  script.render = render;
5
- script.__scopeId = "data-v-42bc14a4";
5
+ script.__scopeId = "data-v-582eaf2c";
6
6
  const __sfc_main = script;
7
7
 
8
8
  export { __sfc_main as default };
@@ -296,12 +296,16 @@ var script = {
296
296
  * October 2026", `"full"` uses the whole display name. The page
297
297
  * `description`, when set, follows after another middle dot. Off by
298
298
  * default: the subtitle is the description alone, as before.
299
+ * `"date"` leads with the date alone, and `{ label }` puts the
300
+ * translated label where the greeting would be ("Customer contact ·
301
+ * Wednesday 7 October 2026 · ...").
299
302
  *
300
303
  * @spec openspec/changes/screens-dashboard-greeting-header/specs/dashboard-page/spec.md#requirement-the-header-subtitle-can-greet-the-reader
301
- * @type {boolean|string}
304
+ * @spec openspec/changes/dashboard-greeting-label/specs/dashboard-page/spec.md#requirement-the-header-subtitle-can-lead-with-a-label-or-the-date
305
+ * @type {boolean|string|object}
302
306
  */
303
307
  greeting: {
304
- type: [Boolean, String],
308
+ type: [Boolean, String, Object],
305
309
  default: false,
306
310
  },
307
311
 
@@ -1036,6 +1040,10 @@ var script = {
1036
1040
  * @return {string}
1037
1041
  */
1038
1042
  greetingLine() {
1043
+ if (this.greeting && typeof this.greeting === 'object') {
1044
+ const label = typeof this.greeting.label === 'string' ? this.greeting.label.trim() : '';
1045
+ return label ? this.effectiveTranslate(label) : ''
1046
+ }
1039
1047
  if (this.greeting !== true && this.greeting !== 'first' && this.greeting !== 'full') {
1040
1048
  return ''
1041
1049
  }
@@ -1058,17 +1066,20 @@ var script = {
1058
1066
  },
1059
1067
 
1060
1068
  /**
1061
- * The header subtitle: with `greeting` on, the greeting, today's date
1062
- * written out and the description, joined by a middle dot; else the
1063
- * description alone, as before.
1069
+ * The header subtitle: with `greeting` on, the greeting (or the
1070
+ * label), today's date written out and the description, joined by a
1071
+ * middle dot; with `greeting: "date"` the date leads, capitalised;
1072
+ * else the description alone, as before.
1064
1073
  *
1065
1074
  * @spec openspec/changes/screens-dashboard-greeting-header/specs/dashboard-page/spec.md#requirement-the-header-subtitle-can-greet-the-reader
1075
+ * @spec openspec/changes/dashboard-greeting-label/specs/dashboard-page/spec.md#requirement-the-header-subtitle-can-lead-with-a-label-or-the-date
1066
1076
  * @return {string}
1067
1077
  */
1068
1078
  subtitleText() {
1069
1079
  const description = this.description ? this.resolvedDescription : '';
1080
+ const dateOnly = this.greeting === 'date';
1070
1081
  const greeting = this.greetingLine;
1071
- if (greeting === '') {
1082
+ if (greeting === '' && !dateOnly) {
1072
1083
  return description
1073
1084
  }
1074
1085
  const options = { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' };
@@ -1078,6 +1089,9 @@ var script = {
1078
1089
  } catch {
1079
1090
  date = new Intl.DateTimeFormat(undefined, options).format(new Date());
1080
1091
  }
1092
+ if (dateOnly) {
1093
+ date = date.charAt(0).toLocaleUpperCase() + date.slice(1);
1094
+ }
1081
1095
  return [greeting, date, description].filter((part) => part !== '').join(' · ')
1082
1096
  },
1083
1097