@conduction/nextcloud-vue 2.144.0 → 2.146.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 (133) hide show
  1. package/dist/esm/components/CnActionDialog/CnActionDialog.vue.js +1 -1
  2. package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js.map +1 -1
  3. package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js +1 -0
  4. package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js.map +1 -1
  5. package/dist/esm/components/CnAiCompanion/CnAiChatPanel.vue2.js.map +1 -1
  6. package/dist/esm/components/CnAiCompanion/CnAiChatPanel.vue3.js.map +1 -1
  7. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue.js +1 -1
  8. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +55 -2
  9. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
  10. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
  11. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue.js +1 -1
  12. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js +16 -0
  13. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js.map +1 -1
  14. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js +3 -1
  15. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js.map +1 -1
  16. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue.js +1 -1
  17. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue2.js.map +1 -1
  18. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue3.js.map +1 -1
  19. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +12 -2
  20. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  21. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +42 -30
  22. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  23. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +22 -0
  24. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  25. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +2 -1
  26. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  27. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue.js +1 -1
  28. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue2.js +1 -1
  29. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue2.js.map +1 -1
  30. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue3.js +3 -3
  31. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue3.js.map +1 -1
  32. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  33. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +46 -8
  34. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  35. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +3 -3
  36. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  37. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js +1 -1
  38. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +2 -2
  39. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
  40. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
  41. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +6 -3
  42. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  43. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  44. package/dist/esm/components/CnMailTab/CnMailTab.vue.js +1 -1
  45. package/dist/esm/components/CnMailTab/CnMailTab.vue2.js.map +1 -1
  46. package/dist/esm/components/CnMailTab/CnMailTab.vue3.js +2 -2
  47. package/dist/esm/components/CnMailTab/CnMailTab.vue3.js.map +1 -1
  48. package/dist/esm/components/CnNotesCard/CnNotesCard.vue.js +1 -1
  49. package/dist/esm/components/CnNotesCard/CnNotesCard.vue2.js.map +1 -1
  50. package/dist/esm/components/CnNotesCard/CnNotesCard.vue3.js +1 -0
  51. package/dist/esm/components/CnNotesCard/CnNotesCard.vue3.js.map +1 -1
  52. package/dist/esm/components/CnNotificationMatrix/CnNotificationMatrix.vue.js +1 -1
  53. package/dist/esm/components/CnNotificationMatrix/CnNotificationMatrix.vue2.js +21 -2
  54. package/dist/esm/components/CnNotificationMatrix/CnNotificationMatrix.vue2.js.map +1 -1
  55. package/dist/esm/components/CnNotificationMatrix/CnNotificationMatrix.vue3.js.map +1 -1
  56. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  57. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +73 -12
  58. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  59. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +165 -126
  60. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  61. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue.js +1 -1
  62. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue2.js.map +1 -1
  63. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue3.js.map +1 -1
  64. package/dist/esm/components/CnSettingsField/CnSettingsField.vue.js +1 -1
  65. package/dist/esm/components/CnSettingsField/CnSettingsField.vue2.js +14 -0
  66. package/dist/esm/components/CnSettingsField/CnSettingsField.vue2.js.map +1 -1
  67. package/dist/esm/components/CnSettingsField/CnSettingsField.vue3.js +2 -1
  68. package/dist/esm/components/CnSettingsField/CnSettingsField.vue3.js.map +1 -1
  69. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  70. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  71. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  72. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue.js +1 -1
  73. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +13 -8
  74. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
  75. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
  76. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue.js +1 -1
  77. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js +10 -2
  78. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js.map +1 -1
  79. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js.map +1 -1
  80. package/dist/esm/composables/indexSources.js +35 -9
  81. package/dist/esm/composables/indexSources.js.map +1 -1
  82. package/dist/esm/l10n/nl.json.js +2 -0
  83. package/dist/esm/l10n/nl.json.js.map +1 -1
  84. package/dist/esm/library-style.js +1 -1
  85. package/dist/esm/nextcloud-vue.css +1204 -467
  86. package/dist/esm/utils/builtInFormatters.js +18 -1
  87. package/dist/esm/utils/builtInFormatters.js.map +1 -1
  88. package/dist/esm/utils/detailObjectContext.js +7 -2
  89. package/dist/esm/utils/detailObjectContext.js.map +1 -1
  90. package/dist/esm/utils/localHeaderRows.js +95 -0
  91. package/dist/esm/utils/localHeaderRows.js.map +1 -0
  92. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  93. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  94. package/dist/esm/utils/visibleWhen.js +6 -0
  95. package/dist/esm/utils/visibleWhen.js.map +1 -1
  96. package/dist/nextcloud-vue.cjs.js +686 -239
  97. package/dist/nextcloud-vue.cjs.js.map +2 -2
  98. package/dist/nextcloud-vue.css +1204 -467
  99. package/l10n/nl.json +2 -0
  100. package/package.json +1 -1
  101. package/src/components/CnActionDialog/CnActionDialog.vue +8 -1
  102. package/src/components/CnAiCompanion/CnAiChatPanel.vue +3 -0
  103. package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +55 -2
  104. package/src/components/CnAuditTrailWidget/CnAuditTrailWidget.vue +18 -0
  105. package/src/components/CnChecklistRunWidget/CnChecklistRunWidget.vue +4 -2
  106. package/src/components/CnDataTable/CnDataTable.vue +19 -2
  107. package/src/components/CnDetailPage/CnDetailPage.md +1 -0
  108. package/src/components/CnDetailPage/CnDetailPage.vue +23 -0
  109. package/src/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue +3 -1
  110. package/src/components/CnFactsWidget/CnFactsWidget.vue +74 -9
  111. package/src/components/CnFieldListWidget/CnFieldListWidget.vue +2 -2
  112. package/src/components/CnIndexPage/CnIndexPage.vue +6 -3
  113. package/src/components/CnMailTab/CnMailTab.vue +6 -4
  114. package/src/components/CnNotesCard/CnNotesCard.vue +7 -2
  115. package/src/components/CnNotificationMatrix/CnNotificationMatrix.vue +25 -3
  116. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +132 -20
  117. package/src/components/CnRetentionWidget/CnRetentionWidget.vue +2 -0
  118. package/src/components/CnSettingsField/CnSettingsField.vue +15 -0
  119. package/src/components/CnTabsWidget/CnTabsWidget.vue +6 -1
  120. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +13 -8
  121. package/src/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue +10 -2
  122. package/src/composables/indexSources.js +35 -9
  123. package/src/css/dialog.css +25 -9
  124. package/src/css/look-board-detail.css +370 -30
  125. package/src/css/look-board-index.css +116 -4
  126. package/src/css/look-board.css +168 -8
  127. package/src/css/table.css +17 -1
  128. package/src/schemas/app-manifest-v2.schema.json +2 -2
  129. package/src/utils/builtInFormatters.js +17 -0
  130. package/src/utils/detailObjectContext.js +7 -2
  131. package/src/utils/localHeaderRows.js +91 -0
  132. package/src/utils/validateManifestV2.compiled.js +2 -2
  133. package/src/utils/visibleWhen.js +6 -0
@@ -1 +1 @@
1
- {"version":3,"file":"CnNotesCard.vue3.js","sources":["../../../../src/components/CnNotesCard/CnNotesCard.vue?vue&type=template&id=b206c0bc&lang.js"],"sourcesContent":["<!--\n CnNotesCard — Inline notes card for detail pages.\n\n Displays up to 5 recent notes with author, content, and timestamp.\n Includes an add-note form and integrates CnUserActionMenu on author names.\n Wraps CnDetailCard for consistent styling.\n-->\n<template>\n\t<CnDetailCard :title=\"titleLabel\"\n\t\t:icon=\"CommentTextOutline\"\n\t\t:collapsible=\"collapsible\"\n\t\t:chromeless=\"chromeless\">\n\t\t<template v-if=\"composerBehindButton\" #actions>\n\t\t\t<NcButton\n\t\t\t\tvariant=\"secondary\"\n\t\t\t\t:aria-expanded=\"composerShown ? 'true' : 'false'\"\n\t\t\t\tdata-testid=\"cn-notes-card-open-composer\"\n\t\t\t\t@click=\"composerOpen = !composerOpen\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<NotePlusOutline :size=\"20\" />\n\t\t\t\t</template>\n\t\t\t\t{{ addNoteLabel }}\n\t\t\t</NcButton>\n\t\t</template>\n\t\t<div class=\"cn-notes-card\">\n\t\t\t<!-- Add note input -->\n\t\t\t<div v-if=\"!unavailable && composerShown\" class=\"cn-notes-card__add-form\">\n\t\t\t\t<!-- Replying: the form says to whom; a reply to a reply goes to the top of its thread. -->\n\t\t\t\t<p\n\t\t\t\t\tv-if=\"replyTo\"\n\t\t\t\t\tclass=\"cn-notes-card__replying\"\n\t\t\t\t\trole=\"status\"\n\t\t\t\t\tdata-testid=\"cn-notes-card-replying\">\n\t\t\t\t\t{{ replyingLabel }}\n\t\t\t\t\t<NcButton variant=\"tertiary\" @click=\"replyTo = null\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Cancel') }}\n\t\t\t\t\t</NcButton>\n\t\t\t\t</p>\n\t\t\t\t<CnNoteComposer\n\t\t\t\t\tclass=\"cn-notes-card__textarea\"\n\t\t\t\t\t:modelValue=\"newNoteText\"\n\t\t\t\t\t:placeholder=\"addNotePlaceholder\"\n\t\t\t\t\t:register=\"isFileSource || isSourceMode ? '' : registerId\"\n\t\t\t\t\t:schema=\"isFileSource || isSourceMode ? '' : schemaId\"\n\t\t\t\t\t:objectId=\"isFileSource || isSourceMode ? '' : objectId\"\n\t\t\t\t\t:apiBase=\"apiBase\"\n\t\t\t\t\t@update:modelValue=\"newNoteText = $event\"\n\t\t\t\t\t@submit=\"submitNote\" />\n\t\t\t\t<!-- Internal or public, chosen before the note is written (only when the host says the caller may set it). -->\n\t\t\t\t<NcCheckboxRadioSwitch\n\t\t\t\t\tv-if=\"visibilityToggleShown\"\n\t\t\t\t\t:modelValue=\"newNoteVisibility === 'public'\"\n\t\t\t\t\ttype=\"switch\"\n\t\t\t\t\tdata-testid=\"cn-notes-card-visibility-switch\"\n\t\t\t\t\t@update:modelValue=\"(on) => newNoteVisibility = on ? 'public' : 'internal'\">\n\t\t\t\t\t{{ publicSwitchLabel }}\n\t\t\t\t</NcCheckboxRadioSwitch>\n\t\t\t\t<NcButton\n\t\t\t\t\tvariant=\"primary\"\n\t\t\t\t\t:disabled=\"!newNoteText.trim() || noteSaving\"\n\t\t\t\t\t@click=\"submitNote\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<Send :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t{{ addNoteLabel }}\n\t\t\t\t</NcButton>\n\t\t\t</div>\n\n\t\t\t<!-- Loading state -->\n\t\t\t<NcLoadingIcon v-if=\"loading\" />\n\n\t\t\t<!-- Empty state -->\n\t\t\t<div v-else-if=\"unavailable\" class=\"cn-notes-card__empty\" data-testid=\"cn-notes-card-unavailable\">\n\t\t\t\t{{ unavailableLabel }}\n\t\t\t</div>\n\t\t\t<p v-else-if=\"failed\"\n\t\t\t\tclass=\"cn-notes-card__empty\"\n\t\t\t\trole=\"alert\"\n\t\t\t\tdata-testid=\"cn-notes-card-failed\">\n\t\t\t\t{{ failedLabel }}\n\t\t\t</p>\n\t\t\t<div v-else-if=\"allNotes.length === 0\" class=\"cn-notes-card__empty\">\n\t\t\t\t{{ noNotesLabel }}\n\t\t\t</div>\n\n\t\t\t<!-- Notes list (last 5), threaded: a note, then its replies. -->\n\t\t\t<div v-else class=\"cn-notes-card__list\">\n\t\t\t\t<div\n\t\t\t\t\tv-for=\"thread in threads\"\n\t\t\t\t\t:key=\"thread.note.id\"\n\t\t\t\t\tclass=\"cn-notes-card__thread\"\n\t\t\t\t\trole=\"group\"\n\t\t\t\t\t:aria-label=\"threadLabel(thread.note)\">\n\t\t\t\t\t<div\n\t\t\t\t\t\tv-for=\"row in threadRows(thread)\"\n\t\t\t\t\t\t:key=\"row.note.id\"\n\t\t\t\t\t\tclass=\"cn-notes-card__note\"\n\t\t\t\t\t\t:class=\"{ 'cn-notes-card__note--reply': row.isReply }\">\n\t\t\t\t\t\t<span class=\"cn-notes-card__initials\" aria-hidden=\"true\">{{ initialsOf(getNoteAuthorName(row.note)) }}</span>\n\t\t\t\t\t\t<div class=\"cn-notes-card__note-header\">\n\t\t\t\t\t\t\t<CnUserActionMenu\n\t\t\t\t\t\t\t\tv-if=\"!isCurrentUser(row.note)\"\n\t\t\t\t\t\t\t\t:userId=\"getNoteAuthorId(row.note)\"\n\t\t\t\t\t\t\t\t:displayName=\"getNoteAuthorName(row.note)\">\n\t\t\t\t\t\t\t\t<strong class=\"cn-notes-card__author\">{{ getNoteAuthorName(row.note) }}</strong>\n\t\t\t\t\t\t\t</CnUserActionMenu>\n\t\t\t\t\t\t\t<strong v-else class=\"cn-notes-card__author cn-notes-card__author--self\">\n\t\t\t\t\t\t\t\t{{ getNoteAuthorName(row.note) }}\n\t\t\t\t\t\t\t</strong>\n\t\t\t\t\t\t\t<span class=\"cn-notes-card__time\">{{ formatDate(row.note.creationDateTime || row.note.created || row.note.createdAt) }}</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div v-if=\"visibilityShown\" class=\"cn-notes-card__visibility\">\n\t\t\t\t\t\t\t<CnVisibilityChip :visibility=\"row.note.visibility\" />\n\t\t\t\t\t\t\t<NcButton\n\t\t\t\t\t\t\t\tv-if=\"visibilityToggleShown\"\n\t\t\t\t\t\t\t\tvariant=\"tertiary\"\n\t\t\t\t\t\t\t\tsize=\"small\"\n\t\t\t\t\t\t\t\tdata-testid=\"cn-notes-card-visibility-toggle\"\n\t\t\t\t\t\t\t\t@click=\"toggleVisibility(row.note)\">\n\t\t\t\t\t\t\t\t{{ noteVisibility(row.note) === 'public' ? makeInternalLabel : makePublicLabel }}\n\t\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<p v-if=\"row.quote\" class=\"cn-notes-card__quote\" data-testid=\"cn-notes-card-quote\">\n\t\t\t\t\t\t\t{{ row.quote }}\n\t\t\t\t\t\t</p>\n\t\t\t\t\t\t<p class=\"cn-notes-card__body\">\n\t\t\t\t\t\t\t<CnNoteBody :message=\"row.note.message || row.note.content || ''\" :record=\"isFileSource || isSourceMode ? null : record\" />\n\t\t\t\t\t\t</p>\n\t\t\t\t\t\t<NcButton\n\t\t\t\t\t\t\tv-if=\"canReply\"\n\t\t\t\t\t\t\tvariant=\"tertiary\"\n\t\t\t\t\t\t\tsize=\"small\"\n\t\t\t\t\t\t\t:aria-label=\"replyAria(row.note)\"\n\t\t\t\t\t\t\tdata-testid=\"cn-notes-card-reply\"\n\t\t\t\t\t\t\t@click=\"replyTo = row.note\">\n\t\t\t\t\t\t\t{{ replyLabel }}\n\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t\t<NcButton\n\t\t\t\t\t\t\tv-if=\"canDeleteNote(row.note)\"\n\t\t\t\t\t\t\tvariant=\"tertiary-no-background\"\n\t\t\t\t\t\t\tclass=\"cn-notes-card__delete-btn\"\n\t\t\t\t\t\t\t:aria-label=\"deleteLabel\"\n\t\t\t\t\t\t\t@click=\"confirmDelete(row.note)\">\n\t\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t\t<Delete :size=\"16\" />\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- Footer: \"Show all\" link -->\n\t\t<template v-if=\"allNotes.length > maxDisplay\" #footer>\n\t\t\t<button\n\t\t\t\tclass=\"cn-notes-card__show-all\"\n\t\t\t\t@click=\"$emit('show-all')\">\n\t\t\t\t{{ showAllLabel }} ({{ allNotes.length }})\n\t\t\t</button>\n\t\t</template>\n\t</CnDetailCard>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcButton, NcCheckboxRadioSwitch, NcLoadingIcon } from '@nextcloud/vue'\nimport { markRaw } from 'vue'\nimport CommentTextOutline from 'vue-material-design-icons/CommentTextOutline.vue'\nimport Delete from 'vue-material-design-icons/Delete.vue'\nimport NotePlusOutline from 'vue-material-design-icons/NotePlusOutline.vue'\nimport Send from 'vue-material-design-icons/Send.vue'\nimport CnDetailCard from '../CnDetailCard/CnDetailCard.vue'\nimport CnNoteBody from '../CnNoteBody/CnNoteBody.vue'\nimport CnNoteComposer from '../CnNoteComposer/CnNoteComposer.vue'\nimport CnUserActionMenu from '../CnUserActionMenu/CnUserActionMenu.vue'\nimport CnVisibilityChip from '../CnVisibilityChip/CnVisibilityChip.vue'\nimport { useFileComments } from '../../composables/useFileComments.js'\nimport { buildHeaders, prefixUrl } from '../../utils/index.js'\nimport { initialsOf } from '../../utils/initials.js'\nimport { leafSourceUrl } from '../../utils/leafSourceUrl.js'\nimport { extractMentionedGroupIds, extractMentionedIds } from '../../utils/mentions.js'\nimport { quoteFor, supportsReplies, threadNotes, threadRootId } from '../../utils/noteThreads.js'\nimport { serverErrorMessage } from '../../utils/serverErrorMessage.js'\nimport { userLocale } from '../../utils/userLocale.js'\n\n/**\n * CnNotesCard — Inline notes widget for detail pages.\n *\n * Shows up to 5 recent notes with add/delete functionality.\n * Integrates CnUserActionMenu on author names for quick communication.\n *\n * Basic usage\n * ```vue\n * <CnNotesCard\n * register-id=\"uuid-register\"\n * schema-id=\"uuid-schema\"\n * object-id=\"uuid-object\" />\n * ```\n *\n * With sidebar sync\n * ```vue\n * <CnNotesCard\n * register-id=\"reg\"\n * schema-id=\"schema\"\n * object-id=\"obj\"\n * @note-added=\"refreshNotes\"\n * @note-deleted=\"refreshNotes\"\n * @show-all=\"openSidebarNotesTab\" />\n * ```\n *\n * @event note-added Emitted after a new note is successfully persisted. Payload: the created note object.\n * @event note-deleted Emitted after a note is successfully deleted. Payload: the deleted note ID.\n * @event visibility-changed Emitted after a note's visibility was changed. Payload: `{ id, visibility }`.\n * @event mention Emitted after a note that mentions someone was added. Payload: `{ objectId, register, schema, noteId, mentionedUserIds }`, plus `mentionedGroupIds` when a group (`@\"group/<gid>\"`) is mentioned. nc-vue notifies nobody; the listener does.\n * @event show-all Emitted when the user clicks the \"Show all\" button — parents typically open a full notes sidebar tab.\n */\n/**\n * The server's own sentence on a refused note write, '' when it gave none.\n *\n * @param {Response} response The refused response.\n * @return {Promise<string>} The message.\n */\nasync function refusalMessage(response) {\n\ttry {\n\t\tconst data = await response.json()\n\t\tconst message = serverErrorMessage({ message: '-', response: { data } })\n\t\treturn message === '-' ? '' : message\n\t} catch {\n\t\treturn ''\n\t}\n}\n\nexport default {\n\tname: 'CnNotesCard',\n\n\tcomponents: {\n\t\tCnDetailCard,\n\t\tCnNoteBody,\n\t\tCnNoteComposer,\n\t\tCnUserActionMenu,\n\t\tCnVisibilityChip,\n\t\tNcButton,\n\t\tNcCheckboxRadioSwitch,\n\t\tNcLoadingIcon,\n\t\tNotePlusOutline,\n\t\tSend,\n\t\tDelete,\n\t},\n\n\tprops: {\n\t\t/** OpenRegister register ID */\n\t\tregisterId: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** OpenRegister schema ID */\n\t\tschemaId: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Object UUID */\n\t\tobjectId: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Show the notes of a plain Nextcloud file instead of an OpenRegister\n\t\t * object: the file's id. Used when no `objectId` is given; the notes\n\t\t * are the file's comments (`/remote.php/dav/comments/files/{fileId}`),\n\t\t * the same ones the Files sidebar shows.\n\t\t */\n\t\tfileId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Base API URL for OpenRegister */\n\t\tapiBase: {\n\t\t\ttype: String,\n\t\t\tdefault: '/apps/openregister/api',\n\t\t},\n\n\t\t/**\n\t\t * Read the notes of something that is not an OpenRegister object:\n\t\t * `{ kind: 'flow-task', id }` reads `${apiBase}/flow-tasks/{id}/notes`.\n\t\t * `id` may be left out or written `@objectId` for `objectId`.\n\t\t *\n\t\t * @type {{kind: string, id?: (string|number)}|null}\n\t\t */\n\t\tsource: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * A notes URL that replaces the object's own: the list and the add go\n\t\t * to it, a delete to `{endpoint}/{noteId}`. `{id}` is filled with the\n\t\t * source's id or `objectId`.\n\t\t */\n\t\tendpoint: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Maximum number of notes to display */\n\t\tmaxDisplay: {\n\t\t\ttype: Number,\n\t\t\tdefault: 5,\n\t\t},\n\n\t\t/** Whether the card is collapsible */\n\t\tcollapsible: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Render the notes body without the surrounding CnDetailCard.\n\t\t *\n\t\t * Set when a surface already supplies the card and the title, such as a\n\t\t * tab panel whose open tab names the panel. Leaving the card on there\n\t\t * nests a card inside a card and shows the label twice.\n\t\t */\n\t\tchromeless: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Where the add form lives: `inline` (default) keeps it open above the\n\t\t * notes; `button` puts it behind an \"Add note\" button in the card's\n\t\t * header (DqTaak \"Notities\"), and closes it again once the note is\n\t\t * saved. A reply opens it either way. Without the card's header\n\t\t * (`chromeless`) the form stays inline.\n\t\t *\n\t\t * @spec openspec/changes/notes-card-composer-button/specs/notes-card-composer-button/spec.md#requirement-a-notes-card-can-keep-its-composer-behind-a-header-button\n\t\t */\n\t\tcomposer: {\n\t\t\ttype: String,\n\t\t\tdefault: 'inline',\n\t\t\tvalidator: (v) => ['inline', 'button'].includes(v),\n\t\t},\n\n\t\t/**\n\t\t * Show a chip on every note reading internal or public. A note without a\n\t\t * value reads as internal. Off by default, so a host that passes nothing\n\t\t * renders the card as before. Not used for a file source (file comments\n\t\t * carry no flag).\n\t\t */\n\t\tshowVisibility: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Whether the caller may set visibility on this object: the answer the\n\t\t * server gives for `update` on it. The host passes it; the card never\n\t\t * infers it from the current user. When true the card offers the\n\t\t * public switch in the add-note form and a toggle on each note.\n\t\t */\n\t\tcanSetVisibility: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t// --- Pre-translated labels ---\n\t\t/** Card header title. */\n\t\ttitleLabel: { type: String, default: () => t('nextcloud-vue', 'Notes') },\n\t\t/** Label for the submit button that creates a new note. */\n\t\taddNoteLabel: { type: String, default: () => t('nextcloud-vue', 'Add note') },\n\t\t/** Placeholder shown inside the new-note textarea before any input. */\n\t\taddNotePlaceholder: { type: String, default: () => t('nextcloud-vue', 'Write a note…') },\n\t\t/** Empty-state text shown when the object has zero notes. */\n\t\tnoNotesLabel: { type: String, default: () => t('nextcloud-vue', 'No notes yet') },\n\t\t/** Label for the \"Show all\" button rendered when the note list is truncated. */\n\t\tshowAllLabel: { type: String, default: () => t('nextcloud-vue', 'Show all') },\n\t\t/** Text shown instead of the notes when Nextcloud refuses a file's comments (no access). */\n\t\tunavailableLabel: { type: String, default: () => t('nextcloud-vue', 'Notes are not available for this file') },\n\t\t/** Label of the add-note switch that makes the note public. */\n\t\tpublicSwitchLabel: { type: String, default: () => t('nextcloud-vue', 'Public (visible to the customer)') },\n\t\t/** Label of the per-note action that makes an internal note public. */\n\t\tmakePublicLabel: { type: String, default: () => t('nextcloud-vue', 'Make public') },\n\t\t/** Label of the per-note action that makes a public note internal. */\n\t\tmakeInternalLabel: { type: String, default: () => t('nextcloud-vue', 'Make internal') },\n\t\t/** Label of the Reply button (shown only when the backend supports replies). */\n\t\treplyLabel: { type: String, default: () => t('nextcloud-vue', 'Reply') },\n\t\t/** Aria label for the per-note delete icon button. */\n\t\tdeleteLabel: { type: String, default: () => t('nextcloud-vue', 'Delete note') },\n\t\t/** Text shown when a `source` or `endpoint` read failed. */\n\t\tfailedLabel: { type: String, default: () => t('nextcloud-vue', 'The notes could not be loaded') },\n\t},\n\n\temits: ['mention', 'note-added', 'note-deleted', 'show-all', 'visibility-changed'],\n\n\tdata() {\n\t\treturn {\n\t\t\tCommentTextOutline: markRaw(CommentTextOutline),\n\t\t\tallNotes: [],\n\t\t\tloading: false,\n\t\t\tnewNoteText: '',\n\t\t\t/** Whether the add form is open while `composer` is `button`. */\n\t\t\tcomposerOpen: false,\n\t\t\t/** Visibility the next note is written with (only sent when the caller may set it). */\n\t\t\tnewNoteVisibility: 'internal',\n\t\t\tnoteSaving: false,\n\t\t\tdeleteConfirmId: null,\n\t\t\t/** The file source answered 403 or 404: no notes and no add field. */\n\t\t\tunavailable: false,\n\t\t\t/** A `source` or `endpoint` read failed: said, rather than shown as no notes. */\n\t\t\tfailed: false,\n\t\t\t/** The note a reply is being written to, or null. */\n\t\t\treplyTo: null,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** @return {boolean} Whether the add form sits behind the header's button. */\n\t\tcomposerBehindButton() {\n\t\t\treturn this.composer === 'button' && !this.chromeless && !this.unavailable\n\t\t},\n\n\t\t/** @return {boolean} Whether the add form shows. */\n\t\tcomposerShown() {\n\t\t\treturn !this.composerBehindButton || this.composerOpen || Boolean(this.replyTo)\n\t\t},\n\n\t\t/** Chips are shown: the host asked for them (or lets the caller set visibility) and the notes are object notes. */\n\t\tvisibilityShown() {\n\t\t\treturn (this.showVisibility || this.canSetVisibility) && !this.isFileSource\n\t\t},\n\n\t\t/** The switch and the per-note toggle are offered: only when the host says the caller may set it. */\n\t\tvisibilityToggleShown() {\n\t\t\treturn this.canSetVisibility && !this.isFileSource\n\t\t},\n\n\t\t/**\n\t\t * The notes URL a `source` or `endpoint` names, '' while its record id\n\t\t * is unknown, or null when the card reads an object.\n\t\t *\n\t\t * @return {string|null}\n\t\t * @spec openspec/changes/leaf-cards-read-a-source/specs/leaf-cards-read-a-source/spec.md#requirement-a-leaf-card-reads-a-source-or-an-endpoint\n\t\t */\n\t\tsourceUrl() {\n\t\t\treturn leafSourceUrl({ source: this.source, endpoint: this.endpoint, apiBase: this.apiBase, objectId: this.objectId }, 'notes')\n\t\t},\n\n\t\t/** The notes come from a `source` or `endpoint`, not from an object. */\n\t\tisSourceMode() {\n\t\t\treturn this.sourceUrl !== null\n\t\t},\n\n\t\t/** @return {string} The notes URL the list, add and delete use. */\n\t\tnotesUrl() {\n\t\t\tif (this.isSourceMode) {\n\t\t\t\treturn this.sourceUrl\n\t\t\t}\n\t\t\treturn `${this.apiBase}/objects/${this.registerId}/${this.schemaId}/${this.objectId}/notes`\n\t\t},\n\n\t\t/** The file source applies: a file id and no object. */\n\t\tisFileSource() {\n\t\t\treturn this.fileId !== null && this.fileId !== undefined && this.fileId !== '' && !this.objectId\n\t\t},\n\n\t\t/** @return {{apiBase: string, register: string, schema: string, objectId: string}} Where this record's files live, for pasted images. */\n\t\trecord() {\n\t\t\treturn { apiBase: this.apiBase, register: this.registerId, schema: this.schemaId, objectId: this.objectId }\n\t\t},\n\n\t\t/** @return {boolean} Whether the backend supports replies: its notes carry a `parentId` key. Not for a file's comments. */\n\t\tcanReply() {\n\t\t\treturn !this.isFileSource && supportsReplies(this.allNotes)\n\t\t},\n\n\t\t/** @return {Array<{note: object, replies: object[]}>} The shown notes as threads. */\n\t\tthreads() {\n\t\t\treturn threadNotes(this.displayedNotes)\n\t\t},\n\n\t\t/** @return {string} The line above the form while replying. */\n\t\treplyingLabel() {\n\t\t\treturn this.replyTo ? t('nextcloud-vue', 'Replying to {author}', { author: this.getNoteAuthorName(this.replyTo) }) : ''\n\t\t},\n\n\t\tdisplayedNotes() {\n\t\t\t// Reverse chronological, limited to maxDisplay\n\t\t\tconst sorted = [...this.allNotes].sort((a, b) => {\n\t\t\t\tconst dateA = new Date(a.creationDateTime || a.created || a.createdAt || 0)\n\t\t\t\tconst dateB = new Date(b.creationDateTime || b.created || b.createdAt || 0)\n\t\t\t\treturn dateB - dateA\n\t\t\t})\n\t\t\treturn sorted.slice(0, this.maxDisplay)\n\t\t},\n\t},\n\n\twatch: {\n\t\tfileId: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tif (this.isFileSource) {\n\t\t\t\t\tthis.fetchNotes()\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\n\t\tobjectId: {\n\t\t\timmediate: true,\n\t\t\thandler(newId) {\n\t\t\t\tif (newId && this.registerId && this.schemaId && !this.isSourceMode) {\n\t\t\t\t\tthis.fetchNotes()\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\n\t\tsourceUrl: {\n\t\t\timmediate: true,\n\t\t\thandler(url) {\n\t\t\t\tif (typeof url === 'string' && url !== '') {\n\t\t\t\t\tthis.fetchNotes()\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * A note's visibility; an entry with no value reads as internal.\n\t\t *\n\t\t * @param {object} note The note.\n\t\t * @return {'public'|'internal'} The visibility.\n\t\t */\n\t\tnoteVisibility(note) {\n\t\t\treturn note && note.visibility === 'public' ? 'public' : 'internal'\n\t\t},\n\n\t\t/**\n\t\t * The one place a note write goes out: creating a note and changing its\n\t\t * visibility both send through here, so the value is sent from one spot.\n\t\t *\n\t\t * @param {'POST'|'PATCH'} method The HTTP method.\n\t\t * @param {string} url The notes URL (with the note id for a change).\n\t\t * @param {object} body The JSON body.\n\t\t * @return {Promise<Response>} The response.\n\t\t */\n\t\twriteNote(method, url, body) {\n\t\t\treturn fetch(prefixUrl(url), { method, headers: buildHeaders(), body: JSON.stringify(body) })\n\t\t},\n\n\t\t/**\n\t\t * Flip a note between internal and public.\n\t\t *\n\t\t * @param {object} note The note.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync toggleVisibility(note) {\n\t\t\tif (!this.visibilityToggleShown) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst next = this.noteVisibility(note) === 'public' ? 'internal' : 'public'\n\t\t\ttry {\n\t\t\t\tconst url = `${this.notesUrl}/${note.id}`\n\t\t\t\tconst response = await this.writeNote('PATCH', url, { visibility: next })\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tthis.allNotes = this.allNotes.map((n) => (n.id === note.id ? { ...n, visibility: next } : n))\n\t\t\t\t\t/** @event visibility-changed Emitted after a note's visibility was changed. Payload: `{ id, visibility }`. */\n\t\t\t\t\tthis.$emit('visibility-changed', { id: note.id, visibility: next })\n\t\t\t\t} else {\n\t\t\t\t\tthis.showError('Failed to change visibility')\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console -- diagnostic for a failure this code already degrades from\n\t\t\t\tconsole.error('CnNotesCard: Failed to change visibility', err)\n\t\t\t\tthis.showError('Failed to change visibility')\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The rows of one thread: the note, then its replies. A reply whose parent\n\t\t * is far up the list carries a one-line quote of it.\n\t\t *\n\t\t * @param {{note: object, replies: object[]}} thread The thread.\n\t\t * @return {Array<{note: object, isReply: boolean, quote: string}>} The rows.\n\t\t */\n\t\tthreadRows(thread) {\n\t\t\tconst flat = this.threads.flatMap((t2) => [t2.note, ...t2.replies])\n\t\t\treturn [\n\t\t\t\t{ note: thread.note, isReply: false, quote: '' },\n\t\t\t\t...thread.replies.map((reply) => ({ note: reply, isReply: true, quote: quoteFor(reply, flat) })),\n\t\t\t]\n\t\t},\n\n\t\t/**\n\t\t * @param {object} note A top-level note.\n\t\t * @return {string} The name of its thread for assistive technology.\n\t\t */\n\t\tthreadLabel(note) {\n\t\t\treturn t('nextcloud-vue', 'Note by {author} and its replies', { author: this.getNoteAuthorName(note) })\n\t\t},\n\n\t\t/**\n\t\t * @param {object} note The note a Reply button answers.\n\t\t * @return {string} The button's accessible name, naming the author.\n\t\t */\n\t\treplyAria(note) {\n\t\t\treturn t('nextcloud-vue', 'Reply to {author}', { author: this.getNoteAuthorName(note) })\n\t\t},\n\n\t\t/**\n\t\t * Tell the host a saved note mentions someone. The payload keeps the\n\t\t * user ids and adds the group ids only when a group is mentioned.\n\t\t *\n\t\t * @param {string} text The saved text.\n\t\t * @param {string|null} noteId The note's id.\n\t\t * @return {void}\n\t\t */\n\t\temitMentionEvent(text, noteId) {\n\t\t\tconst mentionedUserIds = extractMentionedIds(text)\n\t\t\tconst mentionedGroupIds = extractMentionedGroupIds(text)\n\t\t\tif (mentionedUserIds.length === 0 && mentionedGroupIds.length === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.$emit('mention', {\n\t\t\t\tobjectId: this.objectId,\n\t\t\t\tregister: this.registerId,\n\t\t\t\tschema: this.schemaId,\n\t\t\t\tnoteId,\n\t\t\t\tmentionedUserIds,\n\t\t\t\t...(mentionedGroupIds.length > 0 ? { mentionedGroupIds } : {}),\n\t\t\t})\n\t\t},\n\n\t\tgetNoteAuthorId(note) {\n\t\t\treturn note.actorId || note.author || ''\n\t\t},\n\n\t\tinitialsOf,\n\n\t\tgetNoteAuthorName(note) {\n\t\t\treturn note.actorDisplayName || note.author || 'Unknown'\n\t\t},\n\n\t\tisCurrentUser(note) {\n\t\t\tif (note.isCurrentUser === true) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tconst authorId = this.getNoteAuthorId(note)\n\t\t\tconst currentUser = typeof OC !== 'undefined' ? OC?.currentUser : null\n\t\t\treturn authorId === currentUser\n\t\t},\n\n\t\tcanDeleteNote(note) {\n\t\t\treturn this.isCurrentUser(note)\n\t\t},\n\n\t\tfileComments() {\n\t\t\treturn useFileComments(this.fileId)\n\t\t},\n\n\t\tasync fetchNotes() {\n\t\t\tif (this.isFileSource) {\n\t\t\t\tthis.loading = true\n\t\t\t\tthis.unavailable = false\n\t\t\t\ttry {\n\t\t\t\t\tthis.allNotes = await this.fileComments().list()\n\t\t\t\t} catch (err) {\n\t\t\t\t\tthis.allNotes = []\n\t\t\t\t\tthis.unavailable = err && (err.status === 403 || err.status === 404)\n\t\t\t\t\tif (!this.unavailable) {\n\t\t\t\t\t\t// eslint-disable-next-line no-console -- diagnostic for a failure this code already degrades from\n\t\t\t\t\t\tconsole.error('CnNotesCard: Failed to fetch file comments', err)\n\t\t\t\t\t}\n\t\t\t\t} finally {\n\t\t\t\t\tthis.loading = false\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.isSourceMode ? this.notesUrl === '' : (!this.registerId || !this.schemaId || !this.objectId)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tthis.failed = false\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(this.notesUrl), { headers: buildHeaders() })\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tconst data = await response.json()\n\t\t\t\t\tthis.allNotes = data.results || data || []\n\t\t\t\t} else if (this.isSourceMode) {\n\t\t\t\t\tthis.allNotes = []\n\t\t\t\t\tthis.failed = true\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\tthis.failed = this.isSourceMode\n\t\t\t\t// eslint-disable-next-line no-console -- diagnostic for a failure this code already degrades from\n\t\t\t\tconsole.error('CnNotesCard: Failed to fetch notes', err)\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\tasync submitNote() {\n\t\t\tif (!this.newNoteText.trim() || this.noteSaving) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.noteSaving = true\n\t\t\tif (this.isFileSource) {\n\t\t\t\ttry {\n\t\t\t\t\tawait this.fileComments().add(this.newNoteText.trim())\n\t\t\t\t\tthis.composerOpen = false\n\t\t\t\t\tthis.newNoteText = ''\n\t\t\t\t\tawait this.fetchNotes()\n\t\t\t\t\tthis.$emit('note-added')\n\t\t\t\t} catch (err) {\n\t\t\t\t\t// eslint-disable-next-line no-console -- diagnostic for a failure this code already degrades from\n\t\t\t\t\tconsole.error('CnNotesCard: Failed to add file comment', err)\n\t\t\t\t\tthis.showError('Failed to add note')\n\t\t\t\t} finally {\n\t\t\t\t\tthis.noteSaving = false\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst url = this.notesUrl\n\t\t\t\tconst body = { message: this.newNoteText.trim() }\n\t\t\t\tif (this.visibilityToggleShown) {\n\t\t\t\t\tbody.visibility = this.newNoteVisibility\n\t\t\t\t}\n\t\t\t\tif (this.replyTo) {\n\t\t\t\t\tbody.parentId = threadRootId(this.replyTo, this.allNotes)\n\t\t\t\t}\n\t\t\t\tconst response = await this.writeNote('POST', url, body)\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tlet noteId = null\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst created = await response.json()\n\t\t\t\t\t\tnoteId = (created && (created.id || (created.results && created.results.id))) || null\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// Body not JSON: the event still fires with noteId null.\n\t\t\t\t\t}\n\t\t\t\t\tthis.emitMentionEvent(body.message, noteId)\n\t\t\t\t\tthis.replyTo = null\n\t\t\t\t\tthis.composerOpen = false\n\t\t\t\t\tthis.newNoteText = ''\n\t\t\t\t\tthis.newNoteVisibility = 'internal'\n\t\t\t\t\tawait this.fetchNotes()\n\t\t\t\t\tthis.$emit('note-added')\n\t\t\t\t} else {\n\t\t\t\t\tthis.showError(await refusalMessage(response) || 'Failed to add note')\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console -- diagnostic for a failure this code already degrades from\n\t\t\t\tconsole.error('CnNotesCard: Failed to add note', err)\n\t\t\t\tthis.showError('Failed to add note')\n\t\t\t} finally {\n\t\t\t\tthis.noteSaving = false\n\t\t\t}\n\t\t},\n\n\t\tasync confirmDelete(note) {\n\t\t\t// Simple inline confirmation — delete directly\n\t\t\tif (this.isFileSource) {\n\t\t\t\ttry {\n\t\t\t\t\tawait this.fileComments().remove(note.id)\n\t\t\t\t\tthis.allNotes = this.allNotes.filter((n) => n.id !== note.id)\n\t\t\t\t\tthis.$emit('note-deleted')\n\t\t\t\t} catch (err) {\n\t\t\t\t\t// eslint-disable-next-line no-console -- diagnostic for a failure this code already degrades from\n\t\t\t\t\tconsole.error('CnNotesCard: Failed to delete file comment', err)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst url = `${this.notesUrl}/${note.id}`\n\t\t\t\tconst response = await fetch(prefixUrl(url), {\n\t\t\t\t\tmethod: 'DELETE',\n\t\t\t\t\theaders: buildHeaders(),\n\t\t\t\t})\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tthis.allNotes = this.allNotes.filter((n) => n.id !== note.id)\n\t\t\t\t\tthis.$emit('note-deleted')\n\t\t\t\t} else {\n\t\t\t\t\tthis.showError(await refusalMessage(response) || 'Failed to delete note')\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console -- diagnostic for a failure this code already degrades from\n\t\t\t\tconsole.error('CnNotesCard: Failed to delete note', err)\n\t\t\t}\n\t\t},\n\n\t\tformatDate(dateStr) {\n\t\t\tif (!dateStr) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\ttry {\n\t\t\t\treturn new Date(dateStr).toLocaleString(userLocale(), {\n\t\t\t\t\tyear: 'numeric',\n\t\t\t\t\tmonth: 'short',\n\t\t\t\t\tday: 'numeric',\n\t\t\t\t\thour: '2-digit',\n\t\t\t\t\tminute: '2-digit',\n\t\t\t\t})\n\t\t\t} catch {\n\t\t\t\treturn dateStr\n\t\t\t}\n\t\t},\n\n\t\tshowError(message) {\n\t\t\ttry {\n\t\t\t\timport('@nextcloud/dialogs').then(({ showError }) => {\n\t\t\t\t\tshowError(message)\n\t\t\t\t})\n\t\t\t} catch {\n\t\t\t\t// eslint-disable-next-line no-console -- diagnostic for a failure this code already degrades from\n\t\t\t\tconsole.error(message)\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-notes-card__add-form {\n\tmargin-bottom: 12px;\n}\n\n.cn-notes-card__textarea {\n\twidth: 100%;\n\tmargin-bottom: 8px;\n\tbox-sizing: border-box;\n}\n\n.cn-notes-card__replying {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n\tmargin: 0 0 4px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-notes-card__note--reply {\n\tmargin-inline-start: 24px;\n}\n\n.cn-notes-card__quote {\n\tmargin: 0 0 2px;\n\tpadding-inline-start: 8px;\n\tborder-inline-start: 2px solid var(--color-border-dark);\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n}\n\n.cn-notes-card__empty {\n\ttext-align: center;\n\tpadding: 16px 12px;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 13px;\n}\n\n.cn-notes-card__list {\n\tdisplay: flex;\n\tflex-direction: column;\n}\n\n.cn-notes-card__note {\n\tpadding: 10px 0;\n\tborder-bottom: 1px solid var(--color-border);\n\tposition: relative;\n}\n\n.cn-notes-card__note:last-child {\n\tborder-bottom: none;\n}\n\n.cn-notes-card__initials {\n\tdisplay: none;\n}\n\n.cn-notes-card__note-header {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\talign-items: baseline;\n\tmargin-bottom: 4px;\n}\n\n.cn-notes-card__author {\n\tfont-size: 13px;\n}\n\n.cn-notes-card__author--self {\n\tcolor: var(--color-main-text);\n}\n\n.cn-notes-card__visibility {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\tmargin-bottom: 4px;\n}\n\n.cn-notes-card__time {\n\tfont-size: 11px;\n\tcolor: var(--color-text-maxcontrast);\n\tflex-shrink: 0;\n\tmargin-left: 8px;\n}\n\n.cn-notes-card__body {\n\tfont-size: 13px;\n\tmargin: 0;\n\twhite-space: pre-wrap;\n\toverflow-wrap: anywhere;\n\tpadding-right: 32px;\n}\n\n.cn-notes-card__delete-btn {\n\tposition: absolute;\n\ttop: 8px;\n\tright: -4px;\n\topacity: 0;\n\ttransition: opacity 0.15s ease;\n}\n\n.cn-notes-card__note:hover .cn-notes-card__delete-btn {\n\topacity: 1;\n}\n\n.cn-notes-card__show-all {\n\tbackground: none;\n\tborder: none;\n\tcolor: var(--color-primary-element);\n\tfont-size: 13px;\n\tfont-weight: 500;\n\tcursor: pointer;\n\tpadding: 0;\n\twidth: 100%;\n\ttext-align: center;\n}\n\n.cn-notes-card__show-all:hover {\n\ttext-decoration: underline;\n}\n</style>\n"],"names":["_createBlock","_openBlock","_createElementBlock","_createTextVNode","_toDisplayString","_createVNode","_Fragment","_renderList","_normalizeClass","_createElementVNode"],"mappings":"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"}
1
+ {"version":3,"file":"CnNotesCard.vue3.js","sources":["../../../../src/components/CnNotesCard/CnNotesCard.vue?vue&type=template&id=25437d6c&lang.js"],"sourcesContent":["<!--\n CnNotesCard — Inline notes card for detail pages.\n\n Displays up to 5 recent notes with author, content, and timestamp.\n Includes an add-note form and integrates CnUserActionMenu on author names.\n Wraps CnDetailCard for consistent styling.\n-->\n<template>\n\t<CnDetailCard :title=\"titleLabel\"\n\t\t:icon=\"CommentTextOutline\"\n\t\t:collapsible=\"collapsible\"\n\t\t:chromeless=\"chromeless\">\n\t\t<template v-if=\"composerBehindButton\" #actions>\n\t\t\t<NcButton\n\t\t\t\tvariant=\"secondary\"\n\t\t\t\t:aria-expanded=\"composerShown ? 'true' : 'false'\"\n\t\t\t\tdata-testid=\"cn-notes-card-open-composer\"\n\t\t\t\t@click=\"composerOpen = !composerOpen\">\n\t\t\t\t<template #icon>\n\t\t\t\t\t<NotePlusOutline :size=\"20\" />\n\t\t\t\t</template>\n\t\t\t\t{{ addNoteLabel }}\n\t\t\t</NcButton>\n\t\t</template>\n\t\t<div class=\"cn-notes-card\">\n\t\t\t<!-- Add note input -->\n\t\t\t<div v-if=\"!unavailable && composerShown\" class=\"cn-notes-card__add-form\">\n\t\t\t\t<!-- Replying: the form says to whom; a reply to a reply goes to the top of its thread. -->\n\t\t\t\t<p\n\t\t\t\t\tv-if=\"replyTo\"\n\t\t\t\t\tclass=\"cn-notes-card__replying\"\n\t\t\t\t\trole=\"status\"\n\t\t\t\t\tdata-testid=\"cn-notes-card-replying\">\n\t\t\t\t\t{{ replyingLabel }}\n\t\t\t\t\t<NcButton variant=\"tertiary\" @click=\"replyTo = null\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Cancel') }}\n\t\t\t\t\t</NcButton>\n\t\t\t\t</p>\n\t\t\t\t<CnNoteComposer\n\t\t\t\t\tclass=\"cn-notes-card__textarea\"\n\t\t\t\t\t:modelValue=\"newNoteText\"\n\t\t\t\t\t:placeholder=\"addNotePlaceholder\"\n\t\t\t\t\t:register=\"isFileSource || isSourceMode ? '' : registerId\"\n\t\t\t\t\t:schema=\"isFileSource || isSourceMode ? '' : schemaId\"\n\t\t\t\t\t:objectId=\"isFileSource || isSourceMode ? '' : objectId\"\n\t\t\t\t\t:apiBase=\"apiBase\"\n\t\t\t\t\t@update:modelValue=\"newNoteText = $event\"\n\t\t\t\t\t@submit=\"submitNote\" />\n\t\t\t\t<!-- Internal or public, chosen before the note is written (only when the host says the caller may set it). -->\n\t\t\t\t<NcCheckboxRadioSwitch\n\t\t\t\t\tv-if=\"visibilityToggleShown\"\n\t\t\t\t\t:modelValue=\"newNoteVisibility === 'public'\"\n\t\t\t\t\ttype=\"switch\"\n\t\t\t\t\tdata-testid=\"cn-notes-card-visibility-switch\"\n\t\t\t\t\t@update:modelValue=\"(on) => newNoteVisibility = on ? 'public' : 'internal'\">\n\t\t\t\t\t{{ publicSwitchLabel }}\n\t\t\t\t</NcCheckboxRadioSwitch>\n\t\t\t\t<NcButton\n\t\t\t\t\tvariant=\"primary\"\n\t\t\t\t\t:disabled=\"!newNoteText.trim() || noteSaving\"\n\t\t\t\t\t@click=\"submitNote\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<Send :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t{{ addNoteLabel }}\n\t\t\t\t</NcButton>\n\t\t\t</div>\n\n\t\t\t<!-- Loading state -->\n\t\t\t<NcLoadingIcon v-if=\"loading\" />\n\n\t\t\t<!-- Empty state -->\n\t\t\t<div v-else-if=\"unavailable\" class=\"cn-notes-card__empty\" data-testid=\"cn-notes-card-unavailable\">\n\t\t\t\t{{ unavailableLabel }}\n\t\t\t</div>\n\t\t\t<p v-else-if=\"failed\"\n\t\t\t\tclass=\"cn-notes-card__empty\"\n\t\t\t\trole=\"alert\"\n\t\t\t\tdata-testid=\"cn-notes-card-failed\">\n\t\t\t\t{{ failedLabel }}\n\t\t\t</p>\n\t\t\t<div v-else-if=\"allNotes.length === 0\" class=\"cn-notes-card__empty\">\n\t\t\t\t{{ noNotesLabel }}\n\t\t\t</div>\n\n\t\t\t<!-- Notes list (last 5), threaded: a note, then its replies. -->\n\t\t\t<div v-else class=\"cn-notes-card__list\">\n\t\t\t\t<div\n\t\t\t\t\tv-for=\"thread in threads\"\n\t\t\t\t\t:key=\"thread.note.id\"\n\t\t\t\t\tclass=\"cn-notes-card__thread\"\n\t\t\t\t\trole=\"group\"\n\t\t\t\t\t:aria-label=\"threadLabel(thread.note)\">\n\t\t\t\t\t<div\n\t\t\t\t\t\tv-for=\"row in threadRows(thread)\"\n\t\t\t\t\t\t:key=\"row.note.id\"\n\t\t\t\t\t\tclass=\"cn-notes-card__note\"\n\t\t\t\t\t\t:class=\"{ 'cn-notes-card__note--reply': row.isReply }\">\n\t\t\t\t\t\t<span class=\"cn-notes-card__initials\" aria-hidden=\"true\">{{ initialsOf(getNoteAuthorName(row.note)) }}</span>\n\t\t\t\t\t\t<div class=\"cn-notes-card__note-header\">\n\t\t\t\t\t\t\t<CnUserActionMenu\n\t\t\t\t\t\t\t\tv-if=\"!isCurrentUser(row.note)\"\n\t\t\t\t\t\t\t\t:userId=\"getNoteAuthorId(row.note)\"\n\t\t\t\t\t\t\t\t:displayName=\"getNoteAuthorName(row.note)\">\n\t\t\t\t\t\t\t\t<strong class=\"cn-notes-card__author\">{{ getNoteAuthorName(row.note) }}</strong>\n\t\t\t\t\t\t\t</CnUserActionMenu>\n\t\t\t\t\t\t\t<strong v-else class=\"cn-notes-card__author cn-notes-card__author--self\">\n\t\t\t\t\t\t\t\t{{ getNoteAuthorName(row.note) }}\n\t\t\t\t\t\t\t</strong>\n\t\t\t\t\t\t\t<span class=\"cn-notes-card__time\">{{ formatDate(row.note.creationDateTime || row.note.created || row.note.createdAt) }}</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div v-if=\"visibilityShown\" class=\"cn-notes-card__visibility\">\n\t\t\t\t\t\t\t<CnVisibilityChip :visibility=\"row.note.visibility\" />\n\t\t\t\t\t\t\t<NcButton\n\t\t\t\t\t\t\t\tv-if=\"visibilityToggleShown\"\n\t\t\t\t\t\t\t\tvariant=\"tertiary\"\n\t\t\t\t\t\t\t\tsize=\"small\"\n\t\t\t\t\t\t\t\tdata-testid=\"cn-notes-card-visibility-toggle\"\n\t\t\t\t\t\t\t\t@click=\"toggleVisibility(row.note)\">\n\t\t\t\t\t\t\t\t{{ noteVisibility(row.note) === 'public' ? makeInternalLabel : makePublicLabel }}\n\t\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<p v-if=\"row.quote\" class=\"cn-notes-card__quote\" data-testid=\"cn-notes-card-quote\">\n\t\t\t\t\t\t\t{{ row.quote }}\n\t\t\t\t\t\t</p>\n\t\t\t\t\t\t<p class=\"cn-notes-card__body\">\n\t\t\t\t\t\t\t<CnNoteBody :message=\"row.note.message || row.note.content || ''\" :record=\"isFileSource || isSourceMode ? null : record\" />\n\t\t\t\t\t\t</p>\n\t\t\t\t\t\t<NcButton\n\t\t\t\t\t\t\tv-if=\"canReply\"\n\t\t\t\t\t\t\tclass=\"cn-notes-card__reply-btn\"\n\t\t\t\t\t\t\tvariant=\"tertiary\"\n\t\t\t\t\t\t\tsize=\"small\"\n\t\t\t\t\t\t\t:aria-label=\"replyAria(row.note)\"\n\t\t\t\t\t\t\tdata-testid=\"cn-notes-card-reply\"\n\t\t\t\t\t\t\t@click=\"replyTo = row.note\">\n\t\t\t\t\t\t\t{{ replyLabel }}\n\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t\t<NcButton\n\t\t\t\t\t\t\tv-if=\"canDeleteNote(row.note)\"\n\t\t\t\t\t\t\tvariant=\"tertiary-no-background\"\n\t\t\t\t\t\t\tclass=\"cn-notes-card__delete-btn\"\n\t\t\t\t\t\t\t:aria-label=\"deleteLabel\"\n\t\t\t\t\t\t\t@click=\"confirmDelete(row.note)\">\n\t\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t\t<Delete :size=\"16\" />\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- Footer: \"Show all\" link -->\n\t\t<template v-if=\"allNotes.length > maxDisplay\" #footer>\n\t\t\t<button\n\t\t\t\tclass=\"cn-notes-card__show-all\"\n\t\t\t\t@click=\"$emit('show-all')\">\n\t\t\t\t{{ showAllLabel }} ({{ allNotes.length }})\n\t\t\t</button>\n\t\t</template>\n\t</CnDetailCard>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcButton, NcCheckboxRadioSwitch, NcLoadingIcon } from '@nextcloud/vue'\nimport { markRaw } from 'vue'\nimport CommentTextOutline from 'vue-material-design-icons/CommentTextOutline.vue'\nimport Delete from 'vue-material-design-icons/Delete.vue'\nimport NotePlusOutline from 'vue-material-design-icons/NotePlusOutline.vue'\nimport Send from 'vue-material-design-icons/Send.vue'\nimport CnDetailCard from '../CnDetailCard/CnDetailCard.vue'\nimport CnNoteBody from '../CnNoteBody/CnNoteBody.vue'\nimport CnNoteComposer from '../CnNoteComposer/CnNoteComposer.vue'\nimport CnUserActionMenu from '../CnUserActionMenu/CnUserActionMenu.vue'\nimport CnVisibilityChip from '../CnVisibilityChip/CnVisibilityChip.vue'\nimport { useFileComments } from '../../composables/useFileComments.js'\nimport { buildHeaders, prefixUrl } from '../../utils/index.js'\nimport { initialsOf } from '../../utils/initials.js'\nimport { leafSourceUrl } from '../../utils/leafSourceUrl.js'\nimport { extractMentionedGroupIds, extractMentionedIds } from '../../utils/mentions.js'\nimport { quoteFor, supportsReplies, threadNotes, threadRootId } from '../../utils/noteThreads.js'\nimport { serverErrorMessage } from '../../utils/serverErrorMessage.js'\nimport { userLocale } from '../../utils/userLocale.js'\n\n/**\n * CnNotesCard — Inline notes widget for detail pages.\n *\n * Shows up to 5 recent notes with add/delete functionality.\n * Integrates CnUserActionMenu on author names for quick communication.\n *\n * Basic usage\n * ```vue\n * <CnNotesCard\n * register-id=\"uuid-register\"\n * schema-id=\"uuid-schema\"\n * object-id=\"uuid-object\" />\n * ```\n *\n * With sidebar sync\n * ```vue\n * <CnNotesCard\n * register-id=\"reg\"\n * schema-id=\"schema\"\n * object-id=\"obj\"\n * @note-added=\"refreshNotes\"\n * @note-deleted=\"refreshNotes\"\n * @show-all=\"openSidebarNotesTab\" />\n * ```\n *\n * @event note-added Emitted after a new note is successfully persisted. Payload: the created note object.\n * @event note-deleted Emitted after a note is successfully deleted. Payload: the deleted note ID.\n * @event visibility-changed Emitted after a note's visibility was changed. Payload: `{ id, visibility }`.\n * @event mention Emitted after a note that mentions someone was added. Payload: `{ objectId, register, schema, noteId, mentionedUserIds }`, plus `mentionedGroupIds` when a group (`@\"group/<gid>\"`) is mentioned. nc-vue notifies nobody; the listener does.\n * @event show-all Emitted when the user clicks the \"Show all\" button — parents typically open a full notes sidebar tab.\n */\n/**\n * The server's own sentence on a refused note write, '' when it gave none.\n *\n * @param {Response} response The refused response.\n * @return {Promise<string>} The message.\n */\nasync function refusalMessage(response) {\n\ttry {\n\t\tconst data = await response.json()\n\t\tconst message = serverErrorMessage({ message: '-', response: { data } })\n\t\treturn message === '-' ? '' : message\n\t} catch {\n\t\treturn ''\n\t}\n}\n\nexport default {\n\tname: 'CnNotesCard',\n\n\tcomponents: {\n\t\tCnDetailCard,\n\t\tCnNoteBody,\n\t\tCnNoteComposer,\n\t\tCnUserActionMenu,\n\t\tCnVisibilityChip,\n\t\tNcButton,\n\t\tNcCheckboxRadioSwitch,\n\t\tNcLoadingIcon,\n\t\tNotePlusOutline,\n\t\tSend,\n\t\tDelete,\n\t},\n\n\tprops: {\n\t\t/** OpenRegister register ID */\n\t\tregisterId: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** OpenRegister schema ID */\n\t\tschemaId: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Object UUID */\n\t\tobjectId: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Show the notes of a plain Nextcloud file instead of an OpenRegister\n\t\t * object: the file's id. Used when no `objectId` is given; the notes\n\t\t * are the file's comments (`/remote.php/dav/comments/files/{fileId}`),\n\t\t * the same ones the Files sidebar shows.\n\t\t */\n\t\tfileId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Base API URL for OpenRegister */\n\t\tapiBase: {\n\t\t\ttype: String,\n\t\t\tdefault: '/apps/openregister/api',\n\t\t},\n\n\t\t/**\n\t\t * Read the notes of something that is not an OpenRegister object:\n\t\t * `{ kind: 'flow-task', id }` reads `${apiBase}/flow-tasks/{id}/notes`.\n\t\t * `id` may be left out or written `@objectId` for `objectId`.\n\t\t *\n\t\t * @type {{kind: string, id?: (string|number)}|null}\n\t\t */\n\t\tsource: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * A notes URL that replaces the object's own: the list and the add go\n\t\t * to it, a delete to `{endpoint}/{noteId}`. `{id}` is filled with the\n\t\t * source's id or `objectId`.\n\t\t */\n\t\tendpoint: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Maximum number of notes to display */\n\t\tmaxDisplay: {\n\t\t\ttype: Number,\n\t\t\tdefault: 5,\n\t\t},\n\n\t\t/** Whether the card is collapsible */\n\t\tcollapsible: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Render the notes body without the surrounding CnDetailCard.\n\t\t *\n\t\t * Set when a surface already supplies the card and the title, such as a\n\t\t * tab panel whose open tab names the panel. Leaving the card on there\n\t\t * nests a card inside a card and shows the label twice.\n\t\t */\n\t\tchromeless: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Where the add form lives: `inline` (default) keeps it open above the\n\t\t * notes; `button` puts it behind an \"Add note\" button in the card's\n\t\t * header (DqTaak \"Notities\"), and closes it again once the note is\n\t\t * saved. A reply opens it either way. Without the card's header\n\t\t * (`chromeless`) the form stays inline.\n\t\t *\n\t\t * @spec openspec/changes/notes-card-composer-button/specs/notes-card-composer-button/spec.md#requirement-a-notes-card-can-keep-its-composer-behind-a-header-button\n\t\t */\n\t\tcomposer: {\n\t\t\ttype: String,\n\t\t\tdefault: 'inline',\n\t\t\tvalidator: (v) => ['inline', 'button'].includes(v),\n\t\t},\n\n\t\t/**\n\t\t * Show a chip on every note reading internal or public. A note without a\n\t\t * value reads as internal. Off by default, so a host that passes nothing\n\t\t * renders the card as before. Not used for a file source (file comments\n\t\t * carry no flag).\n\t\t */\n\t\tshowVisibility: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Whether the caller may set visibility on this object: the answer the\n\t\t * server gives for `update` on it. The host passes it; the card never\n\t\t * infers it from the current user. When true the card offers the\n\t\t * public switch in the add-note form and a toggle on each note.\n\t\t */\n\t\tcanSetVisibility: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t// --- Pre-translated labels ---\n\t\t/** Card header title. */\n\t\ttitleLabel: { type: String, default: () => t('nextcloud-vue', 'Notes') },\n\t\t/** Label for the submit button that creates a new note. */\n\t\taddNoteLabel: { type: String, default: () => t('nextcloud-vue', 'Add note') },\n\t\t/** Placeholder shown inside the new-note textarea before any input. */\n\t\taddNotePlaceholder: { type: String, default: () => t('nextcloud-vue', 'Write a note…') },\n\t\t/** Empty-state text shown when the object has zero notes. */\n\t\tnoNotesLabel: { type: String, default: () => t('nextcloud-vue', 'No notes yet') },\n\t\t/** Label for the \"Show all\" button rendered when the note list is truncated. */\n\t\tshowAllLabel: { type: String, default: () => t('nextcloud-vue', 'Show all') },\n\t\t/** Text shown instead of the notes when Nextcloud refuses a file's comments (no access). */\n\t\tunavailableLabel: { type: String, default: () => t('nextcloud-vue', 'Notes are not available for this file') },\n\t\t/** Label of the add-note switch that makes the note public. */\n\t\tpublicSwitchLabel: { type: String, default: () => t('nextcloud-vue', 'Public (visible to the customer)') },\n\t\t/** Label of the per-note action that makes an internal note public. */\n\t\tmakePublicLabel: { type: String, default: () => t('nextcloud-vue', 'Make public') },\n\t\t/** Label of the per-note action that makes a public note internal. */\n\t\tmakeInternalLabel: { type: String, default: () => t('nextcloud-vue', 'Make internal') },\n\t\t/** Label of the Reply button (shown only when the backend supports replies). */\n\t\treplyLabel: { type: String, default: () => t('nextcloud-vue', 'Reply') },\n\t\t/** Aria label for the per-note delete icon button. */\n\t\tdeleteLabel: { type: String, default: () => t('nextcloud-vue', 'Delete note') },\n\t\t/** Text shown when a `source` or `endpoint` read failed. */\n\t\tfailedLabel: { type: String, default: () => t('nextcloud-vue', 'The notes could not be loaded') },\n\t},\n\n\temits: ['mention', 'note-added', 'note-deleted', 'show-all', 'visibility-changed'],\n\n\tdata() {\n\t\treturn {\n\t\t\tCommentTextOutline: markRaw(CommentTextOutline),\n\t\t\tallNotes: [],\n\t\t\tloading: false,\n\t\t\tnewNoteText: '',\n\t\t\t/** Whether the add form is open while `composer` is `button`. */\n\t\t\tcomposerOpen: false,\n\t\t\t/** Visibility the next note is written with (only sent when the caller may set it). */\n\t\t\tnewNoteVisibility: 'internal',\n\t\t\tnoteSaving: false,\n\t\t\tdeleteConfirmId: null,\n\t\t\t/** The file source answered 403 or 404: no notes and no add field. */\n\t\t\tunavailable: false,\n\t\t\t/** A `source` or `endpoint` read failed: said, rather than shown as no notes. */\n\t\t\tfailed: false,\n\t\t\t/** The note a reply is being written to, or null. */\n\t\t\treplyTo: null,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** @return {boolean} Whether the add form sits behind the header's button. */\n\t\tcomposerBehindButton() {\n\t\t\treturn this.composer === 'button' && !this.chromeless && !this.unavailable\n\t\t},\n\n\t\t/** @return {boolean} Whether the add form shows. */\n\t\tcomposerShown() {\n\t\t\treturn !this.composerBehindButton || this.composerOpen || Boolean(this.replyTo)\n\t\t},\n\n\t\t/** Chips are shown: the host asked for them (or lets the caller set visibility) and the notes are object notes. */\n\t\tvisibilityShown() {\n\t\t\treturn (this.showVisibility || this.canSetVisibility) && !this.isFileSource\n\t\t},\n\n\t\t/** The switch and the per-note toggle are offered: only when the host says the caller may set it. */\n\t\tvisibilityToggleShown() {\n\t\t\treturn this.canSetVisibility && !this.isFileSource\n\t\t},\n\n\t\t/**\n\t\t * The notes URL a `source` or `endpoint` names, '' while its record id\n\t\t * is unknown, or null when the card reads an object.\n\t\t *\n\t\t * @return {string|null}\n\t\t * @spec openspec/changes/leaf-cards-read-a-source/specs/leaf-cards-read-a-source/spec.md#requirement-a-leaf-card-reads-a-source-or-an-endpoint\n\t\t */\n\t\tsourceUrl() {\n\t\t\treturn leafSourceUrl({ source: this.source, endpoint: this.endpoint, apiBase: this.apiBase, objectId: this.objectId }, 'notes')\n\t\t},\n\n\t\t/** The notes come from a `source` or `endpoint`, not from an object. */\n\t\tisSourceMode() {\n\t\t\treturn this.sourceUrl !== null\n\t\t},\n\n\t\t/** @return {string} The notes URL the list, add and delete use. */\n\t\tnotesUrl() {\n\t\t\tif (this.isSourceMode) {\n\t\t\t\treturn this.sourceUrl\n\t\t\t}\n\t\t\treturn `${this.apiBase}/objects/${this.registerId}/${this.schemaId}/${this.objectId}/notes`\n\t\t},\n\n\t\t/** The file source applies: a file id and no object. */\n\t\tisFileSource() {\n\t\t\treturn this.fileId !== null && this.fileId !== undefined && this.fileId !== '' && !this.objectId\n\t\t},\n\n\t\t/** @return {{apiBase: string, register: string, schema: string, objectId: string}} Where this record's files live, for pasted images. */\n\t\trecord() {\n\t\t\treturn { apiBase: this.apiBase, register: this.registerId, schema: this.schemaId, objectId: this.objectId }\n\t\t},\n\n\t\t/** @return {boolean} Whether the backend supports replies: its notes carry a `parentId` key. Not for a file's comments. */\n\t\tcanReply() {\n\t\t\treturn !this.isFileSource && supportsReplies(this.allNotes)\n\t\t},\n\n\t\t/** @return {Array<{note: object, replies: object[]}>} The shown notes as threads. */\n\t\tthreads() {\n\t\t\treturn threadNotes(this.displayedNotes)\n\t\t},\n\n\t\t/** @return {string} The line above the form while replying. */\n\t\treplyingLabel() {\n\t\t\treturn this.replyTo ? t('nextcloud-vue', 'Replying to {author}', { author: this.getNoteAuthorName(this.replyTo) }) : ''\n\t\t},\n\n\t\tdisplayedNotes() {\n\t\t\t// Reverse chronological, limited to maxDisplay\n\t\t\tconst sorted = [...this.allNotes].sort((a, b) => {\n\t\t\t\tconst dateA = new Date(a.creationDateTime || a.created || a.createdAt || 0)\n\t\t\t\tconst dateB = new Date(b.creationDateTime || b.created || b.createdAt || 0)\n\t\t\t\treturn dateB - dateA\n\t\t\t})\n\t\t\treturn sorted.slice(0, this.maxDisplay)\n\t\t},\n\t},\n\n\twatch: {\n\t\tfileId: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tif (this.isFileSource) {\n\t\t\t\t\tthis.fetchNotes()\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\n\t\tobjectId: {\n\t\t\timmediate: true,\n\t\t\thandler(newId) {\n\t\t\t\tif (newId && this.registerId && this.schemaId && !this.isSourceMode) {\n\t\t\t\t\tthis.fetchNotes()\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\n\t\tsourceUrl: {\n\t\t\timmediate: true,\n\t\t\thandler(url) {\n\t\t\t\tif (typeof url === 'string' && url !== '') {\n\t\t\t\t\tthis.fetchNotes()\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * A note's visibility; an entry with no value reads as internal.\n\t\t *\n\t\t * @param {object} note The note.\n\t\t * @return {'public'|'internal'} The visibility.\n\t\t */\n\t\tnoteVisibility(note) {\n\t\t\treturn note && note.visibility === 'public' ? 'public' : 'internal'\n\t\t},\n\n\t\t/**\n\t\t * The one place a note write goes out: creating a note and changing its\n\t\t * visibility both send through here, so the value is sent from one spot.\n\t\t *\n\t\t * @param {'POST'|'PATCH'} method The HTTP method.\n\t\t * @param {string} url The notes URL (with the note id for a change).\n\t\t * @param {object} body The JSON body.\n\t\t * @return {Promise<Response>} The response.\n\t\t */\n\t\twriteNote(method, url, body) {\n\t\t\treturn fetch(prefixUrl(url), { method, headers: buildHeaders(), body: JSON.stringify(body) })\n\t\t},\n\n\t\t/**\n\t\t * Flip a note between internal and public.\n\t\t *\n\t\t * @param {object} note The note.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync toggleVisibility(note) {\n\t\t\tif (!this.visibilityToggleShown) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst next = this.noteVisibility(note) === 'public' ? 'internal' : 'public'\n\t\t\ttry {\n\t\t\t\tconst url = `${this.notesUrl}/${note.id}`\n\t\t\t\tconst response = await this.writeNote('PATCH', url, { visibility: next })\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tthis.allNotes = this.allNotes.map((n) => (n.id === note.id ? { ...n, visibility: next } : n))\n\t\t\t\t\t/** @event visibility-changed Emitted after a note's visibility was changed. Payload: `{ id, visibility }`. */\n\t\t\t\t\tthis.$emit('visibility-changed', { id: note.id, visibility: next })\n\t\t\t\t} else {\n\t\t\t\t\tthis.showError('Failed to change visibility')\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console -- diagnostic for a failure this code already degrades from\n\t\t\t\tconsole.error('CnNotesCard: Failed to change visibility', err)\n\t\t\t\tthis.showError('Failed to change visibility')\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The rows of one thread: the note, then its replies. A reply whose parent\n\t\t * is far up the list carries a one-line quote of it.\n\t\t *\n\t\t * @param {{note: object, replies: object[]}} thread The thread.\n\t\t * @return {Array<{note: object, isReply: boolean, quote: string}>} The rows.\n\t\t */\n\t\tthreadRows(thread) {\n\t\t\tconst flat = this.threads.flatMap((t2) => [t2.note, ...t2.replies])\n\t\t\treturn [\n\t\t\t\t{ note: thread.note, isReply: false, quote: '' },\n\t\t\t\t...thread.replies.map((reply) => ({ note: reply, isReply: true, quote: quoteFor(reply, flat) })),\n\t\t\t]\n\t\t},\n\n\t\t/**\n\t\t * @param {object} note A top-level note.\n\t\t * @return {string} The name of its thread for assistive technology.\n\t\t */\n\t\tthreadLabel(note) {\n\t\t\treturn t('nextcloud-vue', 'Note by {author} and its replies', { author: this.getNoteAuthorName(note) })\n\t\t},\n\n\t\t/**\n\t\t * @param {object} note The note a Reply button answers.\n\t\t * @return {string} The button's accessible name, naming the author.\n\t\t */\n\t\treplyAria(note) {\n\t\t\treturn t('nextcloud-vue', 'Reply to {author}', { author: this.getNoteAuthorName(note) })\n\t\t},\n\n\t\t/**\n\t\t * Tell the host a saved note mentions someone. The payload keeps the\n\t\t * user ids and adds the group ids only when a group is mentioned.\n\t\t *\n\t\t * @param {string} text The saved text.\n\t\t * @param {string|null} noteId The note's id.\n\t\t * @return {void}\n\t\t */\n\t\temitMentionEvent(text, noteId) {\n\t\t\tconst mentionedUserIds = extractMentionedIds(text)\n\t\t\tconst mentionedGroupIds = extractMentionedGroupIds(text)\n\t\t\tif (mentionedUserIds.length === 0 && mentionedGroupIds.length === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.$emit('mention', {\n\t\t\t\tobjectId: this.objectId,\n\t\t\t\tregister: this.registerId,\n\t\t\t\tschema: this.schemaId,\n\t\t\t\tnoteId,\n\t\t\t\tmentionedUserIds,\n\t\t\t\t...(mentionedGroupIds.length > 0 ? { mentionedGroupIds } : {}),\n\t\t\t})\n\t\t},\n\n\t\tgetNoteAuthorId(note) {\n\t\t\treturn note.actorId || note.author || ''\n\t\t},\n\n\t\tinitialsOf,\n\n\t\tgetNoteAuthorName(note) {\n\t\t\treturn note.actorDisplayName || note.author || 'Unknown'\n\t\t},\n\n\t\tisCurrentUser(note) {\n\t\t\tif (note.isCurrentUser === true) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tconst authorId = this.getNoteAuthorId(note)\n\t\t\tconst currentUser = typeof OC !== 'undefined' ? OC?.currentUser : null\n\t\t\treturn authorId === currentUser\n\t\t},\n\n\t\tcanDeleteNote(note) {\n\t\t\treturn this.isCurrentUser(note)\n\t\t},\n\n\t\tfileComments() {\n\t\t\treturn useFileComments(this.fileId)\n\t\t},\n\n\t\tasync fetchNotes() {\n\t\t\tif (this.isFileSource) {\n\t\t\t\tthis.loading = true\n\t\t\t\tthis.unavailable = false\n\t\t\t\ttry {\n\t\t\t\t\tthis.allNotes = await this.fileComments().list()\n\t\t\t\t} catch (err) {\n\t\t\t\t\tthis.allNotes = []\n\t\t\t\t\tthis.unavailable = err && (err.status === 403 || err.status === 404)\n\t\t\t\t\tif (!this.unavailable) {\n\t\t\t\t\t\t// eslint-disable-next-line no-console -- diagnostic for a failure this code already degrades from\n\t\t\t\t\t\tconsole.error('CnNotesCard: Failed to fetch file comments', err)\n\t\t\t\t\t}\n\t\t\t\t} finally {\n\t\t\t\t\tthis.loading = false\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.isSourceMode ? this.notesUrl === '' : (!this.registerId || !this.schemaId || !this.objectId)) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.loading = true\n\t\t\tthis.failed = false\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(prefixUrl(this.notesUrl), { headers: buildHeaders() })\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tconst data = await response.json()\n\t\t\t\t\tthis.allNotes = data.results || data || []\n\t\t\t\t} else if (this.isSourceMode) {\n\t\t\t\t\tthis.allNotes = []\n\t\t\t\t\tthis.failed = true\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\tthis.failed = this.isSourceMode\n\t\t\t\t// eslint-disable-next-line no-console -- diagnostic for a failure this code already degrades from\n\t\t\t\tconsole.error('CnNotesCard: Failed to fetch notes', err)\n\t\t\t} finally {\n\t\t\t\tthis.loading = false\n\t\t\t}\n\t\t},\n\n\t\tasync submitNote() {\n\t\t\tif (!this.newNoteText.trim() || this.noteSaving) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.noteSaving = true\n\t\t\tif (this.isFileSource) {\n\t\t\t\ttry {\n\t\t\t\t\tawait this.fileComments().add(this.newNoteText.trim())\n\t\t\t\t\tthis.composerOpen = false\n\t\t\t\t\tthis.newNoteText = ''\n\t\t\t\t\tawait this.fetchNotes()\n\t\t\t\t\tthis.$emit('note-added')\n\t\t\t\t} catch (err) {\n\t\t\t\t\t// eslint-disable-next-line no-console -- diagnostic for a failure this code already degrades from\n\t\t\t\t\tconsole.error('CnNotesCard: Failed to add file comment', err)\n\t\t\t\t\tthis.showError('Failed to add note')\n\t\t\t\t} finally {\n\t\t\t\t\tthis.noteSaving = false\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst url = this.notesUrl\n\t\t\t\tconst body = { message: this.newNoteText.trim() }\n\t\t\t\tif (this.visibilityToggleShown) {\n\t\t\t\t\tbody.visibility = this.newNoteVisibility\n\t\t\t\t}\n\t\t\t\tif (this.replyTo) {\n\t\t\t\t\tbody.parentId = threadRootId(this.replyTo, this.allNotes)\n\t\t\t\t}\n\t\t\t\tconst response = await this.writeNote('POST', url, body)\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tlet noteId = null\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst created = await response.json()\n\t\t\t\t\t\tnoteId = (created && (created.id || (created.results && created.results.id))) || null\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// Body not JSON: the event still fires with noteId null.\n\t\t\t\t\t}\n\t\t\t\t\tthis.emitMentionEvent(body.message, noteId)\n\t\t\t\t\tthis.replyTo = null\n\t\t\t\t\tthis.composerOpen = false\n\t\t\t\t\tthis.newNoteText = ''\n\t\t\t\t\tthis.newNoteVisibility = 'internal'\n\t\t\t\t\tawait this.fetchNotes()\n\t\t\t\t\tthis.$emit('note-added')\n\t\t\t\t} else {\n\t\t\t\t\tthis.showError(await refusalMessage(response) || 'Failed to add note')\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console -- diagnostic for a failure this code already degrades from\n\t\t\t\tconsole.error('CnNotesCard: Failed to add note', err)\n\t\t\t\tthis.showError('Failed to add note')\n\t\t\t} finally {\n\t\t\t\tthis.noteSaving = false\n\t\t\t}\n\t\t},\n\n\t\tasync confirmDelete(note) {\n\t\t\t// Simple inline confirmation — delete directly\n\t\t\tif (this.isFileSource) {\n\t\t\t\ttry {\n\t\t\t\t\tawait this.fileComments().remove(note.id)\n\t\t\t\t\tthis.allNotes = this.allNotes.filter((n) => n.id !== note.id)\n\t\t\t\t\tthis.$emit('note-deleted')\n\t\t\t\t} catch (err) {\n\t\t\t\t\t// eslint-disable-next-line no-console -- diagnostic for a failure this code already degrades from\n\t\t\t\t\tconsole.error('CnNotesCard: Failed to delete file comment', err)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst url = `${this.notesUrl}/${note.id}`\n\t\t\t\tconst response = await fetch(prefixUrl(url), {\n\t\t\t\t\tmethod: 'DELETE',\n\t\t\t\t\theaders: buildHeaders(),\n\t\t\t\t})\n\t\t\t\tif (response.ok) {\n\t\t\t\t\tthis.allNotes = this.allNotes.filter((n) => n.id !== note.id)\n\t\t\t\t\tthis.$emit('note-deleted')\n\t\t\t\t} else {\n\t\t\t\t\tthis.showError(await refusalMessage(response) || 'Failed to delete note')\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console -- diagnostic for a failure this code already degrades from\n\t\t\t\tconsole.error('CnNotesCard: Failed to delete note', err)\n\t\t\t}\n\t\t},\n\n\t\tformatDate(dateStr) {\n\t\t\tif (!dateStr) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\ttry {\n\t\t\t\treturn new Date(dateStr).toLocaleString(userLocale(), {\n\t\t\t\t\tyear: 'numeric',\n\t\t\t\t\tmonth: 'short',\n\t\t\t\t\tday: 'numeric',\n\t\t\t\t\thour: '2-digit',\n\t\t\t\t\tminute: '2-digit',\n\t\t\t\t})\n\t\t\t} catch {\n\t\t\t\treturn dateStr\n\t\t\t}\n\t\t},\n\n\t\tshowError(message) {\n\t\t\ttry {\n\t\t\t\timport('@nextcloud/dialogs').then(({ showError }) => {\n\t\t\t\t\tshowError(message)\n\t\t\t\t})\n\t\t\t} catch {\n\t\t\t\t// eslint-disable-next-line no-console -- diagnostic for a failure this code already degrades from\n\t\t\t\tconsole.error(message)\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-notes-card__add-form {\n\tmargin-bottom: 12px;\n}\n\n.cn-notes-card__textarea {\n\twidth: 100%;\n\tmargin-bottom: 8px;\n\tbox-sizing: border-box;\n}\n\n.cn-notes-card__replying {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n\tmargin: 0 0 4px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-notes-card__note--reply {\n\tmargin-inline-start: 24px;\n}\n\n.cn-notes-card__quote {\n\tmargin: 0 0 2px;\n\tpadding-inline-start: 8px;\n\tborder-inline-start: 2px solid var(--color-border-dark);\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n}\n\n.cn-notes-card__empty {\n\ttext-align: center;\n\tpadding: 16px 12px;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 13px;\n}\n\n.cn-notes-card__list {\n\tdisplay: flex;\n\tflex-direction: column;\n}\n\n.cn-notes-card__note {\n\tpadding: 10px 0;\n\tborder-bottom: 1px solid var(--color-border);\n\tposition: relative;\n}\n\n.cn-notes-card__note:last-child {\n\tborder-bottom: none;\n}\n\n.cn-notes-card__initials {\n\tdisplay: none;\n}\n\n.cn-notes-card__note-header {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\talign-items: baseline;\n\tmargin-bottom: 4px;\n}\n\n.cn-notes-card__author {\n\tfont-size: 13px;\n}\n\n.cn-notes-card__author--self {\n\tcolor: var(--color-main-text);\n}\n\n.cn-notes-card__visibility {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\tmargin-bottom: 4px;\n}\n\n.cn-notes-card__time {\n\tfont-size: 11px;\n\tcolor: var(--color-text-maxcontrast);\n\tflex-shrink: 0;\n\tmargin-left: 8px;\n}\n\n.cn-notes-card__body {\n\tfont-size: 13px;\n\tmargin: 0;\n\twhite-space: pre-wrap;\n\toverflow-wrap: anywhere;\n\tpadding-right: 32px;\n}\n\n/* The note's own class leads so the position outranks NcButton's\n `position: relative`, which loads later at the same weight and put the\n button in the flow under the note. */\n.cn-notes-card__note > .cn-notes-card__delete-btn {\n\tposition: absolute;\n\ttop: 8px;\n\tright: -4px;\n\topacity: 0;\n\ttransition: opacity 0.15s ease;\n}\n\n.cn-notes-card__note:hover > .cn-notes-card__delete-btn,\n.cn-notes-card__note:focus-within > .cn-notes-card__delete-btn {\n\topacity: 1;\n}\n\n.cn-notes-card__show-all {\n\tbackground: none;\n\tborder: none;\n\tcolor: var(--color-primary-element);\n\tfont-size: 13px;\n\tfont-weight: 500;\n\tcursor: pointer;\n\tpadding: 0;\n\twidth: 100%;\n\ttext-align: center;\n}\n\n.cn-notes-card__show-all:hover {\n\ttext-decoration: underline;\n}\n</style>\n"],"names":["_createBlock","_openBlock","_createElementBlock","_createTextVNode","_toDisplayString","_createVNode","_Fragment","_renderList","_normalizeClass","_createElementVNode"],"mappings":";;AA+BO,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,eAAI,GAAA;;;AAEM,EAAA,KAAA,EAAA,yBAAA;;;;AAIlB,EAAA,KAAE,EAAA,yBAAA;AACF,EAAA,IAAI,EAAC,QAAQ;AACb,EAAA,aAAW,EAAC,wBAAkB;;;;AAwCf,EAAA,KAAA,EAAA,sBAAA;AAAA,EAAA,aAAA,EAAA,2BAAA;;;;AAIhB,EAAA,KAAI,EAAA,sBAAA;;AAEJ,EAAA,aAAW,EAAC,sBAAuB;;;;AAGV,EAAA,KAAA,EAAA,sBAAA;;;;AAKrB,EAAA,KAAA,EAAA,qBAAA;;;;AAYI,EAAA,KAAK,EAAC,yBAAO;AAAA,EAAA,aAAA,EAAA,MAAA;;AACd,MAAA,WAAA,GAAA,EAAA,KAAK,EAAC,4BAA4B,GAAA;AAK7B,MAAA,WAAA,GAAA,EAAA,KAAK,EAAC,uBAAuB,GAAA;;;AAE9B,EAAA,KAAA,EAAA,mDAAA;;AAGF,MAAA,WAAA,GAAA,EAAA,KAAK,EAAC,qBAAA,GAAA;;;AAEe,EAAA,KAAK,EAAC,2BAAG;;;;AAWtB,EAAA,KAAA,EAAA,sBAAA;AAAA,EAAA,aAAA,EAAA,qBAAA;;AAGZ,MAAA,WAAA,GAAA,EAAA,KAAK,EAAC,qBAAG,GAAA;;;;;;;;;;;;;;;uBArHdA;AAAY,IAAA,KAAK,EAAE,MAAW,CAAA,UAAA;AAC9B,IAAA,IAAG,EAAE,KAAkB,CAAA,kBAAA;AACvB,IAAA,WAAU,EAAE,MAAW,CAAA,WAAA;AACvB,IAAA,UAAQ,EAAG,MAAI,CAAA,UAAA;;2BA4IX;yBAAA,iBAAA,EAAA;AA7HO,QAAA,CAAA,CAAA,KAAA,CAAA,WAAQ,IAAA,QAAA,CAAA,aAAA;AAAlB,aAAAC,SAAA,EAAA,EAAAC,kBAAA,CAwCI,OAxCJ,UAwCI,EAAA;eArCE,KAAI,CAAA,OAAA;AADT,mBAAAD,SAAA,EAAA,EAAAC,kBAAA,CASE,KATF,UASE,EAAA;AAJA,oBAAAC,eAAA,CAAAC,eAAA,CAAA,QAAA,CAAA,aAAa,IAAG,GACnB,EAAA,CAAA,YAAA;oBAAAC,WAEW,CAAA,mBAAA,EAAA;AAFD,sBAAA,OAAO,EAAC,UAAU;AAAE,sBAAA,OAAE,uCAAA,KAAA,CAAA,OAAA,GAAA,IAAA,CAAA,CAAA;;uCAC/B,MAAkC;wDAA/B,IAAC,CAAA,CAAA,CAAA,eAAA,EAAA,QAAA,CAAA,CAAA,EAAA,CAAA,YAAA;;;;;;cAGLA,WASuB,CAAA,yBAAA,EAAA;AARvB,gBAAA,KAAK,EAAC,yBAAwB;AAC7B,gBAAA,UAAU,EAAE,KAAS,CAAA,WAAA;AACrB,gBAAA,WAAY,EAAC,MAAkB,CAAA,kBAAA;AAC/B,gBAAA,QAAQ,EAAE,QAAA,CAAA,YAAS,IAAA,QAAA,CAAA,YAAA,QAAA,MAAA,CAAA,UAAA;AACnB,gBAAA,MAAM,EAAE,QAAA,CAAA,YAAY,IAAI,QAAA,CAAA,YAAE,QAAA,MAAA,CAAA,QAAA;AAC1B,gBAAA,QAAQ,EAAE,QAAA,CAAA,YAAY,IAAI,QAAA,CAAA,YAAY,QAAI,MAAA,CAAA,QAAA;AAC1C,gBAAA,OAAO,EAAE,MAAE,CAAA,OAAA;gBACX,qBAAiB,EAAA,MAAA,CAAA,CAAA,CAAA,KAAA,MAAA,CAAA,CAAA,CAAA,GAAA,MAAA,KAAE,KAAW,CAAA,WAAA,GAAG,MAAM,CAAA,CAAA;AACvC,gBAAA,QAAM,EAAE,QAAU,CAAA,UAAA;;eAGb,QAAC,CAAA,qBAAA;gCADRL,WAOW,CAAA,gCAAA,EAAA;;AALT,oBAAA,UAAU,EAAE,KAAG,CAAA,iBAAA,KAAA,QAAA;AAChB,oBAAA,IAAI,EAAC,QAAQ;AACb,oBAAA,aAAW,EAAC,iCAAO;AAClB,oBAAA,qBAAc,EAAA,MAAA,CAAA,CAAA,CAAA,KAAA,MAAA,CAAA,CAAA,CAAA,GAAA,CAAA,EAAA,KAAA,KAAA,CAAA,iBAAA,GAAA,EAAA,GAAA,QAAA,GAAA,UAAA,CAAA;;qCACd,MAAkB;sDAAhB,MAAgB,CAAA,iBAAA,CAAA,EAAA,CAAA,YAAA;;;;;cAEnBK,WAQU,CAAA,mBAAA,EAAA;AAPV,gBAAA,OAAO,EAAC,SAAC;AACR,gBAAA,QAAE,EAAA,CAAA,KAAA,CAAA,WAAA,CAAA,IAAA,MAAA,KAAA,CAAA,UAAA;;;AAEQ,gBAAA,IAAI,UACd,MAAmB;kBAAnBA,WAAmB,CAAA,eAAA,EAAA,EAAZ,IAAG,EAAG,EAAE,EAAA,CAAA;;uCAEhB;qCAAA,mBAAG,MAAY,CAAA,YAAA,CAAA,EAAA,CAAA,YAAA;;;;;;SAKI,KAAO,CAAA,OAAA;qCAAA,CAAA,wBAAA,EAAA,EAAA,GAAA,EAAA,CAAA,EAAA,CAAA;aAGZ,KAAC,CAAA,WAAA;4BAAhBH,kBAEK,CAAA,KAAA,EAFL,UAEK,EAAAE,eAAA,CADH,MAAA,CAAA,gBAAA,CAAA,EAAA,CAAA,YAAA;eAEW,KAAI,CAAA,MAAA;8BAAjBF,kBAKG,CAAA,GAAA,EALH,UAKG,EAAAE,eAAA,CADC,MAAU,CAAA,WAAA,CAAA,EAAA,CAAA,YAAA;AAEC,gBAAA,CAAA,KAAA,CAAA,QAAQ,CAAC,MAAC,KAAA,CAAA;kDAEpB,CAAA,KAAA,YAAA,EAAAA,eAAA,CADF,MAAY,CAAA,YAAA,CAAA,EAAA,CAAA,YAAA;AAId,mBAAAH,SAAA,EAAA,EAAAC,kBAAA,CAgEI,OAhEJ,UAgEI,EAAA;sCA/DHA,kBA8DI,CAAAI,QAAA,EAAA,IAAA,EAAAC,UAAA,CA7DY,QAAC,CAAA,OAAA,EAAA,CAAX,MAAM,KAAA;2CADZL,kBA8DI,CAAA,KAAA,EAAA;AA5DJ,wBAAA,GAAG,EAAE,MAAM,CAAC,IAAI,CAAC,EAAE;6BACf,EAAC,uBAAuB;4BACzB,EAAC,OAAO;AACX,wBAAA,YAAU,EAAE,QAAA,CAAA,WAAW,CAAC,MAAM,CAAC,IAAI,CAAA;;AAClC,yBAAAD,SAAA,CAAA,IAAA,CAAA,EAAAC,kBAAA,CAuDEI,QAtDW,EAAA,IAAA,EAAAC,UAAA,CAAA,QAAA,CAAA,UAAU,CAAC,MAAM,IAAxB,GAAG,KAAA;+CADTL,kBAuDE,CAAA,KAAA,EAAA;AArDD,4BAAA,GAAE,EAAE,GAAG,CAAC,IAAI,CAAC,EAAE;AACf,4BAAA,KAAG,EAACM,cAAA,CAAA,CAAA,qBAAqB,EACa,EAAA,4BAAA,EAAA,GAAG,CAAC,OAAO,EAAA,CAAA,CAAA;;4BAClDC,kBAAkB,CAAA,MAAA,EAAlB,UAAkB,EAAAL,eAAA,CAAA,QAAA,CAAA,UAAA,CAAA,QAAA,CAAA,iBAAA,CAAA,GAAA,CAAA,IAAA,CAAA,CAAA,CAAA,EAAA,CAAA,YAAA;AAClB,4BAAAK,kBAAA,CAWK,OAXL,WAWK,EAAA;gCATG,QAAA,CAAA,aAAA,CAAA,GAAA,CAAA,IAAA,CAAA;gDADPT,WAKkB,CAAA,2BAAA,EAAA;;AAHjB,oCAAA,MAAM,EAAE,QAAA,CAAA,eAAe,CAAC,GAAG,CAAC,IAAI,CAAA;AAChC,oCAAA,WAAE,EAAA,QAAA,CAAA,iBAAA,CAAA,GAAA,CAAA,IAAA,CAAA;;qDACH,MAA2D;AAA3D,sCAAAS,kBAAA,CAA2D,UAA3D,WAA2D,EAAAL,eAAA,CAAlB,0BAAiB,CAAC,GAAA,CAAA,IAAA,CAAA,CAAA,EAAA,CAAA,YAAA;;;;AAE5D,mCAAAH,SAAA,EAAA,EAAAC,kBAAA,CAES,UAFT,WAES,EAAAE,eAAA,CADL,0BAAiB,CAAC,GAAG,CAAC,IAAE,CAAA,CAAA,EAAA,CAAA,YAAA,CAAA;gDAEhB,CAAA,MAAA,aAAA,EAAAA,eAAA,CAAA,QAAA,CAAA,UAAA,CAAA,GAAA,CAAA,IAAA,CAAA,gBAAA,IAAA,GAAA,CAAA,IAAA,CAAA,OAAA,IAAA,GAAA,CAAA,IAAA,CAAA,SAAA,CAAA,CAAA,EAAA,CAAA,YAAA;;6BAEF,QAAe,CAAA,eAAA;AAAxB,iCAAAH,SAAA,EAAA,EAAAC,kBAAA,CAUI,OAVJ,WAUI,EAAA;kCATJG,WAAqD,CAAA,2BAAA,EAAA;AAAnC,oCAAA,UAAU,EAAE,GAAG,CAAC,IAAI,CAAC,UAAU;;mCAE9C,QAAA,CAAA,qBAAA;oDADJL,WAOE,CAAA,mBAAA,EAAA;;+CALM,EAAC,UAAU;AAClB,wCAAA,IAAI,EAAC,OAAO;AACZ,wCAAA,aAAC,EAAA,iCAAA;AACA,wCAAA,OAAK,EAAE,MAAA,KAAA,QAAA,CAAA,gBAAgB,CAAC,GAAG,CAAA,IAAA,CAAA,CAAA;;yDAC5B,MAAiF;AAA9E,0CAAAG,eAAA,CAAAC,eAAA,CAAA,QAAA,CAAA,cAAc,CAAC,GAAG,CAAC,IAAI,CAAiB,KAAA,QAAA,GAAA,MAAA,CAAA,iBAAgB,GAAI,MAAe,CAAA,eAAA,CAAA,EAAA,CAAA,YAAA;;;;;;;AAGvE,4BAAA,CAAA,GAAG,CAAC,KAAE;AAAd,iCAAAH,SAAA,EAAA,EAAAC,kBAAA,CAEG,GAFH,EAAA,WAEG,EADAE,eAAA,CAAA,GAAG,CAAC,KAAK,CAAA,EAAA,CAAA,YAAA;;+CAIT,gBAAA,EAAA;8BADHC,WAAgC,CAAA,qBAAA,EAAA;gCAAnB,OAAO,EAAE,GAAG,CAAC,IAAI,CAAC,OAAC,IAAA,GAAA,CAAA,IAAA,CAAA,OAAA,IAAA,EAAA;AAAA,gCAAA,MAAA,EAAA,QAAA,CAAA,YAAA,IAAA,QAAA,CAAA,YAAA,UAAA,QAAA,CAAA,MAAA;;;6BAG1B,QAAQ,CAAA,QAAA;yDAQJ,CAAA,mBAAA,EAAA;;AAPV,kCAAA,KAAK,EAAC,0BAAG;AACT,kCAAA,OAAO,EAAA,UAAA;AACP,kCAAA,IAAI,EAAC,OAAO;AACX,kCAAA,YAAU,EAAE,QAAA,CAAA,SAAS,CAAC,GAAG,CAAC,IAAI,CAAA;AAC/B,kCAAA,aAAW,EAAC,qBAAoB;AAC/B,kCAAA,OAAK,EAAE,MAAA,KAAA,KAAA,CAAA,OAAO,GAAG,GAAG,CAAC,IAAE,CAAA;;mDACxB,MAAgB;oEAAb,MAAU,CAAA,UAAA,CAAA,EAAA,CAAA,YAAA;;;;;6BAGP,QAAI,CAAA,aAAA,CAAA,GAAA,CAAA,IAAA,CAAA;8CADTL,WASS,CAAA,mBAAA,EAAA;;AAPV,kCAAA,OAAO,EAAC,wBAAC;AACT,kCAAA,KAAG,EAAA,2BAAA;AACF,kCAAA,YAAC,EAAA,MAAA,CAAA,WAAA;AACD,kCAAA,OAAA,EAAA,MAAA,KAAA,QAAA,CAAA,aAAA,CAAA,GAAA,CAAA,IAAA,CAAA,CAAA;;AACD,kCAAA,IAAA;;;;;;;;;;;;;;;KApIW,QAAgB,CAAA,oBAAA;;gBAAA,SAAA;4BAUpB;uBAAA,CAAA,mBAAA,EAAA;AARV,cAAA,OAAO,EAAC,WAAS;AAChB,cAAA,eAAQ,EAAA,QAAA,CAAA,aAAA,GAAA,MAAA,GAAA,OAAA;AACT,cAAA,aAAQ,EAAA,6BAAA;cACP,OAAK,EAAA,MAAA,CAAA,CAAA,CAAA,KAAA,MAAA,CAAA,CAAA,CAAA,GAAA,MAAA,KAAE,KAAY,CAAA,YAAA,GAAA,CAAI,KAAA,CAAA,YAAA,CAAA,CAAA;;AACb,cAAA,IAAI,UACd,MAA8B;gBAA9BK,WAA8B,CAAA,0BAAA,EAAA,EAAZ,IAAI,EAAG,EAAC,EAAA,CAAA;;+BAChB,MACX;AADW,gBAAAF,eAAA,CAAA,GACX,mBAAC,MAAA,CAAA,YAAA,CAAA,EAAA,CAAA,YAAA;;;;;;;;KAqIU,KAAA,CAAA,QAAA,CAAA,MAAA,GAAA,MAAA,CAAA,UAAA;;gBAAA,QAAA;;;;;;;;;;;;;;;"}
@@ -2,7 +2,7 @@ import script from './CnNotificationMatrix.vue2.js';
2
2
  import { render } from './CnNotificationMatrix.vue3.js';
3
3
 
4
4
  script.render = render;
5
- script.__scopeId = "data-v-ebb6783e";
5
+ script.__scopeId = "data-v-22022a49";
6
6
  const __sfc_main = script;
7
7
 
8
8
  export { __sfc_main as default };
@@ -57,6 +57,23 @@ import { GLOBAL_SCOPE, valueForScope, scopeRowsFor } from '../../utils/preferenc
57
57
  * @event {object} digest-change — A channel's digest was set. Payload: `{ channelId, mode, timeOfDay }`.
58
58
  * @event {object} test-send — A test was asked for. Payload: `{ channelId }`.
59
59
  */
60
+ /**
61
+ * An event's shipped default in one channel: one boolean for every channel,
62
+ * or the channel's own entry when the default differs per channel.
63
+ *
64
+ * @param {object} event The event.
65
+ * @param {object} channel The channel.
66
+ * @return {boolean} The default.
67
+ * @spec openspec/changes/notification-matrix-default-per-channel/specs/notification-matrix/spec.md
68
+ */
69
+ function appDefaultFor(event, channel) {
70
+ const value = event?.appDefault;
71
+ if (value !== null && typeof value === 'object') {
72
+ return value[channel?.id] === true
73
+ }
74
+ return value === true
75
+ }
76
+
60
77
  var script = {
61
78
  name: 'CnNotificationMatrix',
62
79
 
@@ -68,7 +85,9 @@ var script = {
68
85
  props: {
69
86
  /**
70
87
  * The events, each `{ id, label, group, groupLabel, immediate,
71
- * appDefault }`. From the host's catalogue.
88
+ * appDefault }`. From the host's catalogue. `appDefault` is one
89
+ * boolean for every channel, or `{ [channelId]: boolean }` when the
90
+ * default differs per channel (an opt-in e-mail is off by default).
72
91
  */
73
92
  events: {
74
93
  type: Array,
@@ -333,7 +352,7 @@ var script = {
333
352
  });
334
353
 
335
354
  return resolvePreference({
336
- appDefault: event?.appDefault === true,
355
+ appDefault: appDefaultFor(event, channel),
337
356
  groupValue: this.at(this.groupValues, event, channel),
338
357
  personalValue: narrow.value === null
339
358
  ? this.at(this.personalValues, event, channel)
@@ -1 +1 @@
1
- {"version":3,"file":"CnNotificationMatrix.vue2.js","sources":["../../../../src/components/CnNotificationMatrix/CnNotificationMatrix.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<div class=\"cn-notification-matrix\" :class=\"{ 'cn-notification-matrix--board': isBoardLook }\" data-testid=\"cn-notification-matrix\">\n\t\t<p v-if=\"adminMode\" class=\"cn-notification-matrix__note\" data-testid=\"cn-nm-admin-note\">\n\t\t\t{{ adminNote }}\n\t\t</p>\n\n\t\t<table class=\"cn-notification-matrix__table\" data-testid=\"cn-nm-table\">\n\t\t\t<caption class=\"cn-notification-matrix__caption\">\n\t\t\t\t{{ tableCaption }}\n\t\t\t</caption>\n\t\t\t<thead>\n\t\t\t\t<tr>\n\t\t\t\t\t<th scope=\"col\">\n\t\t\t\t\t\t{{ eventHeader }}\n\t\t\t\t\t</th>\n\t\t\t\t\t<th\n\t\t\t\t\t\tv-for=\"channel in channels\"\n\t\t\t\t\t\t:key=\"`ch-${channel.id}`\"\n\t\t\t\t\t\tscope=\"col\"\n\t\t\t\t\t\tdata-testid=\"cn-nm-channel-header\"\n\t\t\t\t\t\t:data-channel=\"channel.id\">\n\t\t\t\t\t\t{{ channel.label }}\n\t\t\t\t\t\t<template v-if=\"!isBoardLook\">\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tv-if=\"channelState(channel).usable === false\"\n\t\t\t\t\t\t\t\tclass=\"cn-notification-matrix__muted\"\n\t\t\t\t\t\t\t\tdata-testid=\"cn-nm-channel-unusable\">\n\t\t\t\t\t\t\t\t{{ channelState(channel).reason || unavailableText }}\n\t\t\t\t\t\t\t</span>\n\n\t\t\t\t\t\t\t<template v-if=\"channelState(channel).usable !== false\">\n\t\t\t\t\t\t\t\t<label class=\"cn-notification-matrix__muted\">\n\t\t\t\t\t\t\t\t\t{{ digestLabel }}\n\t\t\t\t\t\t\t\t\t<select\n\t\t\t\t\t\t\t\t\t\tdata-testid=\"cn-nm-digest-mode\"\n\t\t\t\t\t\t\t\t\t\t:data-channel=\"channel.id\"\n\t\t\t\t\t\t\t\t\t\t:value=\"digestFor(channel).mode\"\n\t\t\t\t\t\t\t\t\t\t@change=\"onDigestMode(channel, $event)\">\n\t\t\t\t\t\t\t\t\t\t<option\n\t\t\t\t\t\t\t\t\t\t\tv-for=\"mode in digestModes\"\n\t\t\t\t\t\t\t\t\t\t\t:key=\"`dm-${channel.id}-${mode.id}`\"\n\t\t\t\t\t\t\t\t\t\t\t:value=\"mode.id\">\n\t\t\t\t\t\t\t\t\t\t\t{{ mode.label }}\n\t\t\t\t\t\t\t\t\t\t</option>\n\t\t\t\t\t\t\t\t\t</select>\n\t\t\t\t\t\t\t\t</label>\n\n\t\t\t\t\t\t\t\t<label\n\t\t\t\t\t\t\t\t\tv-if=\"digestFor(channel).mode !== 'off'\"\n\t\t\t\t\t\t\t\t\tclass=\"cn-notification-matrix__muted\">\n\t\t\t\t\t\t\t\t\t{{ digestTimeLabel }}\n\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\ttype=\"time\"\n\t\t\t\t\t\t\t\t\t\tdata-testid=\"cn-nm-digest-time\"\n\t\t\t\t\t\t\t\t\t\t:data-channel=\"channel.id\"\n\t\t\t\t\t\t\t\t\t\t:value=\"digestFor(channel).timeOfDay\"\n\t\t\t\t\t\t\t\t\t\t@change=\"onDigestTime(channel, $event)\">\n\t\t\t\t\t\t\t\t</label>\n\n\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\tv-if=\"digestFor(channel).mode !== 'off'\"\n\t\t\t\t\t\t\t\t\tclass=\"cn-notification-matrix__muted\"\n\t\t\t\t\t\t\t\t\tdata-testid=\"cn-nm-digest-immediate-note\">\n\t\t\t\t\t\t\t\t\t{{ digestImmediateNote }}\n\t\t\t\t\t\t\t\t</span>\n\n\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\tclass=\"cn-notification-matrix__test\"\n\t\t\t\t\t\t\t\t\tdata-testid=\"cn-nm-test-send\"\n\t\t\t\t\t\t\t\t\t:data-channel=\"channel.id\"\n\t\t\t\t\t\t\t\t\t:aria-label=\"testSendLabel(channel)\"\n\t\t\t\t\t\t\t\t\t@click=\"onTestSend(channel)\">\n\t\t\t\t\t\t\t\t\t{{ testSendText }}\n\t\t\t\t\t\t\t\t</button>\n\n\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\tv-if=\"testResultFor(channel) !== null\"\n\t\t\t\t\t\t\t\t\tclass=\"cn-notification-matrix__muted\"\n\t\t\t\t\t\t\t\t\tdata-testid=\"cn-nm-test-result\"\n\t\t\t\t\t\t\t\t\t:data-channel=\"channel.id\"\n\t\t\t\t\t\t\t\t\t:data-ok=\"String(testResultFor(channel).ok === true)\"\n\t\t\t\t\t\t\t\t\trole=\"status\">\n\t\t\t\t\t\t\t\t\t{{ testResultText(channel) }}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t</template>\n\t\t\t\t\t</th>\n\t\t\t\t</tr>\n\t\t\t</thead>\n\n\t\t\t<tbody v-for=\"group in groups\" :key=\"`grp-${group.key}`\">\n\t\t\t\t<tr>\n\t\t\t\t\t<th :colspan=\"channels.length + 1\" scope=\"colgroup\">\n\t\t\t\t\t\t<button\n\t\t\t\t\t\t\tclass=\"cn-notification-matrix__group\"\n\t\t\t\t\t\t\tdata-testid=\"cn-nm-group\"\n\t\t\t\t\t\t\t:data-group=\"group.key\"\n\t\t\t\t\t\t\t:aria-expanded=\"String(!isCollapsed(group.key))\"\n\t\t\t\t\t\t\t@click=\"toggleGroup(group.key)\">\n\t\t\t\t\t\t\t{{ group.label }}\n\t\t\t\t\t\t</button>\n\t\t\t\t\t</th>\n\t\t\t\t</tr>\n\n\t\t\t\t<tr\n\t\t\t\t\tv-for=\"row in rowsFor(group)\"\n\t\t\t\t\tv-show=\"!isCollapsed(group.key)\"\n\t\t\t\t\t:key=\"`ev-${row.event.id}-${row.scope}`\"\n\t\t\t\t\tdata-testid=\"cn-nm-row\"\n\t\t\t\t\t:class=\"{ 'cn-notification-matrix__row--scoped': !row.isGlobal }\"\n\t\t\t\t\t:data-event=\"row.event.id\"\n\t\t\t\t\t:data-scope=\"row.scope\">\n\t\t\t\t\t<th scope=\"row\" :class=\"{ 'cn-notification-matrix__scope': !row.isGlobal }\">\n\t\t\t\t\t\t<template v-if=\"row.isGlobal\">\n\t\t\t\t\t\t\t{{ row.event.label }}\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tv-if=\"row.event.immediate\"\n\t\t\t\t\t\t\t\tclass=\"cn-notification-matrix__muted\"\n\t\t\t\t\t\t\t\tdata-testid=\"cn-nm-immediate\">\n\t\t\t\t\t\t\t\t{{ immediateText }}\n\t\t\t\t\t\t\t</span>\n\n\t\t\t\t\t\t\t<select\n\t\t\t\t\t\t\t\tv-if=\"addableScopes(row.event).length > 0\"\n\t\t\t\t\t\t\t\tclass=\"cn-notification-matrix__muted\"\n\t\t\t\t\t\t\t\tdata-testid=\"cn-nm-add-scope\"\n\t\t\t\t\t\t\t\t:data-event=\"row.event.id\"\n\t\t\t\t\t\t\t\t:aria-label=\"addScopeLabel(row.event)\"\n\t\t\t\t\t\t\t\tvalue=\"\"\n\t\t\t\t\t\t\t\t@change=\"onAddScope(row.event, $event)\">\n\t\t\t\t\t\t\t\t<option value=\"\">\n\t\t\t\t\t\t\t\t\t{{ addScopeText }}\n\t\t\t\t\t\t\t\t</option>\n\t\t\t\t\t\t\t\t<option\n\t\t\t\t\t\t\t\t\tv-for=\"choice in addableScopes(row.event)\"\n\t\t\t\t\t\t\t\t\t:key=\"`sc-${row.event.id}-${choice.id}`\"\n\t\t\t\t\t\t\t\t\t:value=\"choice.id\">\n\t\t\t\t\t\t\t\t\t{{ choice.label }}\n\t\t\t\t\t\t\t\t</option>\n\t\t\t\t\t\t\t</select>\n\t\t\t\t\t\t</template>\n\t\t\t\t\t\t<template v-else>\n\t\t\t\t\t\t\t<span data-testid=\"cn-nm-scope-label\">{{ scopeLabel(row.scope) }}</span>\n\t\t\t\t\t\t</template>\n\t\t\t\t\t</th>\n\n\t\t\t\t\t<td\n\t\t\t\t\t\tv-for=\"channel in channels\"\n\t\t\t\t\t\t:key=\"`c-${row.event.id}-${row.scope}-${channel.id}`\"\n\t\t\t\t\t\tdata-testid=\"cn-nm-cell\"\n\t\t\t\t\t\t:data-event=\"row.event.id\"\n\t\t\t\t\t\t:data-scope=\"row.scope\"\n\t\t\t\t\t\t:data-channel=\"channel.id\"\n\t\t\t\t\t\t:data-level=\"cellFor(row.event, channel, row.scope).level\">\n\t\t\t\t\t\t<input\n\t\t\t\t\t\t\ttype=\"checkbox\"\n\t\t\t\t\t\t\tdata-testid=\"cn-nm-toggle\"\n\t\t\t\t\t\t\t:data-event=\"row.event.id\"\n\t\t\t\t\t\t\t:data-scope=\"row.scope\"\n\t\t\t\t\t\t\t:data-channel=\"channel.id\"\n\t\t\t\t\t\t\t:checked=\"cellFor(row.event, channel, row.scope).value\"\n\t\t\t\t\t\t\t:disabled=\"isLockedFor(row.event, channel, row.scope)\"\n\t\t\t\t\t\t\t:aria-label=\"cellLabelFor(row.event, channel, row.scope)\"\n\t\t\t\t\t\t\t:aria-describedby=\"isBoardLook ? sourceIdFor(row.event, channel, row.scope) : null\"\n\t\t\t\t\t\t\t@change=\"onToggle(row.event, channel, $event, row.scope)\">\n\t\t\t\t\t\t<!-- The board look draws only the checkbox; why it is set stays said, and stays visible when the cell is locked. -->\n\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t:id=\"sourceIdFor(row.event, channel, row.scope)\"\n\t\t\t\t\t\t\tclass=\"cn-notification-matrix__muted\"\n\t\t\t\t\t\t\t:class=\"{ 'hidden-visually': isBoardLook && !isLockedFor(row.event, channel, row.scope) }\"\n\t\t\t\t\t\t\tdata-testid=\"cn-nm-source\">\n\t\t\t\t\t\t\t{{ sourceTextFor(row.event, channel, row.scope) }}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t</tbody>\n\t\t</table>\n\n\t\t<!-- The board look keeps the grid to its checkboxes; each channel's bundling, its time and the test send live here. -->\n\t\t<details\n\t\t\tv-if=\"isBoardLook\"\n\t\t\tclass=\"cn-notification-matrix__channels\"\n\t\t\tdata-testid=\"cn-nm-channel-settings\">\n\t\t\t<summary class=\"cn-notification-matrix__channels-summary\">\n\t\t\t\t{{ channelSettingsTitle }}\n\t\t\t</summary>\n\t\t\t<div\n\t\t\t\tv-for=\"channel in channels\"\n\t\t\t\t:key=\"`cs-${channel.id}`\"\n\t\t\t\tclass=\"cn-notification-matrix__channel\"\n\t\t\t\t:data-channel=\"channel.id\">\n\t\t\t\t<h4 class=\"cn-notification-matrix__channel-name\">\n\t\t\t\t\t{{ channel.label }}\n\t\t\t\t</h4>\n\t\t\t\t<span\n\t\t\t\t\tv-if=\"channelState(channel).usable === false\"\n\t\t\t\t\tclass=\"cn-notification-matrix__muted\"\n\t\t\t\t\tdata-testid=\"cn-nm-channel-unusable\">\n\t\t\t\t\t{{ channelState(channel).reason || unavailableText }}\n\t\t\t\t</span>\n\n\t\t\t\t<template v-if=\"channelState(channel).usable !== false\">\n\t\t\t\t\t<label class=\"cn-notification-matrix__muted\">\n\t\t\t\t\t\t{{ digestLabel }}\n\t\t\t\t\t\t<select\n\t\t\t\t\t\t\tdata-testid=\"cn-nm-digest-mode\"\n\t\t\t\t\t\t\t:data-channel=\"channel.id\"\n\t\t\t\t\t\t\t:value=\"digestFor(channel).mode\"\n\t\t\t\t\t\t\t@change=\"onDigestMode(channel, $event)\">\n\t\t\t\t\t\t\t<option\n\t\t\t\t\t\t\t\tv-for=\"mode in digestModes\"\n\t\t\t\t\t\t\t\t:key=\"`dm-${channel.id}-${mode.id}`\"\n\t\t\t\t\t\t\t\t:value=\"mode.id\">\n\t\t\t\t\t\t\t\t{{ mode.label }}\n\t\t\t\t\t\t\t</option>\n\t\t\t\t\t\t</select>\n\t\t\t\t\t</label>\n\n\t\t\t\t\t<label\n\t\t\t\t\t\tv-if=\"digestFor(channel).mode !== 'off'\"\n\t\t\t\t\t\tclass=\"cn-notification-matrix__muted\">\n\t\t\t\t\t\t{{ digestTimeLabel }}\n\t\t\t\t\t\t<input\n\t\t\t\t\t\t\ttype=\"time\"\n\t\t\t\t\t\t\tdata-testid=\"cn-nm-digest-time\"\n\t\t\t\t\t\t\t:data-channel=\"channel.id\"\n\t\t\t\t\t\t\t:value=\"digestFor(channel).timeOfDay\"\n\t\t\t\t\t\t\t@change=\"onDigestTime(channel, $event)\">\n\t\t\t\t\t</label>\n\n\t\t\t\t\t<span\n\t\t\t\t\t\tv-if=\"digestFor(channel).mode !== 'off'\"\n\t\t\t\t\t\tclass=\"cn-notification-matrix__muted\"\n\t\t\t\t\t\tdata-testid=\"cn-nm-digest-immediate-note\">\n\t\t\t\t\t\t{{ digestImmediateNote }}\n\t\t\t\t\t</span>\n\n\t\t\t\t\t<button\n\t\t\t\t\t\tclass=\"cn-notification-matrix__test\"\n\t\t\t\t\t\tdata-testid=\"cn-nm-test-send\"\n\t\t\t\t\t\t:data-channel=\"channel.id\"\n\t\t\t\t\t\t:aria-label=\"testSendLabel(channel)\"\n\t\t\t\t\t\t@click=\"onTestSend(channel)\">\n\t\t\t\t\t\t{{ testSendText }}\n\t\t\t\t\t</button>\n\n\t\t\t\t\t<span\n\t\t\t\t\t\tv-if=\"testResultFor(channel) !== null\"\n\t\t\t\t\t\tclass=\"cn-notification-matrix__muted\"\n\t\t\t\t\t\tdata-testid=\"cn-nm-test-result\"\n\t\t\t\t\t\t:data-channel=\"channel.id\"\n\t\t\t\t\t\t:data-ok=\"String(testResultFor(channel).ok === true)\"\n\t\t\t\t\t\trole=\"status\">\n\t\t\t\t\t\t{{ testResultText(channel) }}\n\t\t\t\t\t</span>\n\t\t\t\t</template>\n\t\t\t</div>\n\t\t</details>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { normalizeLook } from '../../composables/useLook.js'\nimport {\n\tchannelAvailability,\n\tPREFERENCE_LEVELS,\n\tresolvePreference,\n} from '../../utils/notificationPreference.js'\nimport {\n\tGLOBAL_SCOPE,\n\tscopeRowsFor,\n\tvalueForScope,\n} from '../../utils/preferenceScopes.js'\n\n/**\n * CnNotificationMatrix — one screen where a person chooses what notifies them,\n * and finds out why they are getting what they are getting.\n *\n * 🔴 IT DOES NOT REPLACE `CnNotificationPreferences`, IT SITS BESIDE IT. That\n * component is a self-contained settings pane: it takes no props, fetches for\n * itself and scopes to the app whose settings modal is open, and `CnAppRoot`\n * mounts it as the default of its `#user-settings` slot. This one takes a\n * catalogue from its host, knows four levels rather than two and has a channel\n * axis. Replacing the older one in place, which an earlier revision of this\n * change did, left every app that mounts it through `CnAppRoot` rendering an\n * empty table with no error: a silent regression, not a breakage anybody would\n * see in a test.\n *\n * Events down, channels across. The catalogue is the host's, so this component\n * never knows what a \"term expires\" event is.\n *\n * 🔴 IT NEVER SHOWS A SETTING THAT DOES NOT APPLY. That is the whole point of\n * the screen and it is the one way it can fail while looking perfect. Three\n * cases, each handled rather than hidden:\n *\n * - A FORCED cell is locked, says who forced it and why. An administrator\n * has taken the choice away; a toggle the person can move which then does\n * nothing is worse than a locked one, because they believe they acted.\n * - A channel the instance has not configured is disabled with the reason.\n * - A channel REFUSED for this kind and recipient says the rule. An internal\n * notice to an outside recipient is a rule working, not a gap to fix, and\n * \"no channels available\" would send somebody to the wrong place.\n *\n * 🔴 IT IS A REAL TABLE. A grid of checkboxes with no row and column headers\n * is unreadable to a screen reader: every cell is \"checkbox, checked\" with no\n * way to know which event or which channel. The header cells are `th` with\n * scopes, and each toggle carries a name that says both.\n *\n * 🔴 A PREFERENCE IS SET GLOBALLY OR FOR ONE CASE DOMAIN. \"Tell me when a term\n * expires\" is usually too much and \"never\" is too little, so the event row is\n * the global one and scoped rows sit indented under it. The narrower row wins\n * only where it applies: everywhere else the global row still stands, because\n * a scope that replaced it would silence every other domain the moment\n * somebody narrowed one.\n *\n * 🔴 ONLY EVENTS IN THE CATALOGUE ARE LISTED. A stored preference for an event\n * that no longer exists is not rendered, because a switch for something that\n * can no longer happen is a lie; the store drops it on the next write.\n *\n * 🔴 A TEST SEND REPORTS WHAT HAPPENED, INCLUDING A REFUSAL. Nobody should have\n * to wait for a real event to find out a channel is broken, and a button that\n * silently did nothing would read as proof the channel works.\n *\n * @event {object} change — A cell was set. Payload: `{ eventId, channelId, scope, value }`.\n * @event {object} add-scope — A scope was added to a row. Payload: `{ eventId, scope }`.\n * @event {object} digest-change — A channel's digest was set. Payload: `{ channelId, mode, timeOfDay }`.\n * @event {object} test-send — A test was asked for. Payload: `{ channelId }`.\n */\nexport default {\n\tname: 'CnNotificationMatrix',\n\n\tinject: {\n\t\t/** The look CnAppRoot (or the page) draws in. */\n\t\tcnLook: { default: 'nextcloud' },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The events, each `{ id, label, group, groupLabel, immediate,\n\t\t * appDefault }`. From the host's catalogue.\n\t\t */\n\t\tevents: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** The channels, each `{ id, label, configured, unconfiguredReason }`. */\n\t\tchannels: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** The group's values, as `{ [eventId]: { [channelId]: boolean } }`. */\n\t\tgroupValues: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** This person's values, same shape. */\n\t\tpersonalValues: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * What an administrator has forced, as\n\t\t * `{ [eventId]: { [channelId]: { value, by, reason } } }`.\n\t\t *\n\t\t * A level ABOVE the person's own rather than another default: it is\n\t\t * the administrator removing the choice, not setting it.\n\t\t */\n\t\tforcedValues: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * What the platform refuses for this recipient, same shape, each\n\t\t * `{ reason }`. A refusal is not an absence: it is a rule, and the\n\t\t * screen says which.\n\t\t */\n\t\trefusals: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Whether this is the admin screen writing the group's defaults. The\n\t\t * screen says a person's own value wins, so an administrator is not\n\t\t * surprised when somebody does not get what they set.\n\t\t */\n\t\tadminMode: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * This person's narrower values, as\n\t\t * `{ [eventId]: { [channelId]: { [scope]: boolean } } }`.\n\t\t *\n\t\t * Separate from `personalValues` rather than replacing it, so the\n\t\t * global row keeps the flat shape every existing caller passes. A row\n\t\t * left unset here falls through to the global value, which is what\n\t\t * makes an absent scoped row an unanswered question rather than a no.\n\t\t */\n\t\tscopedValues: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * The scopes a row can be narrowed to, each `{ id, label }`. Case\n\t\t * domains or record types, named by whoever configured the app.\n\t\t */\n\t\tscopeChoices: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * The digest per channel, as `{ [channelId]: { mode, timeOfDay } }`\n\t\t * where mode is `off`, `daily` or `weekly`.\n\t\t */\n\t\tdigest: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * The last test send per channel, as\n\t\t * `{ [channelId]: { ok, message } }`. A refusal belongs here as much\n\t\t * as a success: both are results.\n\t\t */\n\t\ttestResults: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\t},\n\n\temits: ['change', 'add-scope', 'digest-change', 'test-send'],\n\n\tdata() {\n\t\treturn {\n\t\t\t/** Groups the reader has collapsed, for as long as they are here. */\n\t\t\tcollapsed: [],\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Whether the matrix draws the board's grid.\n\t\t *\n\t\t * @return {boolean} True in the board look.\n\t\t * @spec openspec/changes/notification-matrix-board-grid/specs/notification-matrix/spec.md\n\t\t */\n\t\tisBoardLook() {\n\t\t\treturn normalizeLook(this.cnLook) === 'board'\n\t\t},\n\n\t\t/** @return {string} The disclosure under the board grid. */\n\t\tchannelSettingsTitle() {\n\t\t\treturn t('nextcloud-vue', 'Bundling and test message')\n\t\t},\n\n\t\t/** @return {Array<object>} The events, in their groups. */\n\t\tgroups() {\n\t\t\tconst byKey = new Map()\n\t\t\tfor (const event of this.events) {\n\t\t\t\tconst key = String(event?.group ?? '')\n\t\t\t\tif (byKey.has(key) === false) {\n\t\t\t\t\tbyKey.set(key, {\n\t\t\t\t\t\tkey,\n\t\t\t\t\t\tlabel: String(event?.groupLabel ?? key) || this.ungroupedText,\n\t\t\t\t\t\tevents: [],\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t\tbyKey.get(key).events.push(event)\n\t\t\t}\n\t\t\treturn [...byKey.values()]\n\t\t},\n\n\t\t/** @return {string} The table's caption. */\n\t\ttableCaption() {\n\t\t\treturn t('nextcloud-vue', 'What notifies you, and over which channel')\n\t\t},\n\n\t\t/** @return {string} The events column header. */\n\t\teventHeader() {\n\t\t\treturn t('nextcloud-vue', 'Event')\n\t\t},\n\n\t\t/** @return {string} What an ungrouped event group is called. */\n\t\tungroupedText() {\n\t\t\treturn t('nextcloud-vue', 'Other')\n\t\t},\n\n\t\t/** @return {string} What an unusable channel says with no reason given. */\n\t\tunavailableText() {\n\t\t\treturn t('nextcloud-vue', 'Not available on this instance')\n\t\t},\n\n\t\t/** @return {string} What an immediate event says. */\n\t\timmediateText() {\n\t\t\treturn t('nextcloud-vue', 'Always sent straight away, never held for a digest')\n\t\t},\n\n\t\t/** @return {Array<object>} The digest choices, in the order they escalate. */\n\t\tdigestModes() {\n\t\t\treturn [\n\t\t\t\t{ id: 'off', label: t('nextcloud-vue', 'Send each one as it happens') },\n\t\t\t\t{ id: 'daily', label: t('nextcloud-vue', 'Once a day') },\n\t\t\t\t{ id: 'weekly', label: t('nextcloud-vue', 'Once a week') },\n\t\t\t]\n\t\t},\n\n\t\t/** @return {string} What the digest control is called. */\n\t\tdigestLabel() {\n\t\t\treturn t('nextcloud-vue', 'Bundle these')\n\t\t},\n\n\t\t/** @return {string} What the time control is called. */\n\t\tdigestTimeLabel() {\n\t\t\treturn t('nextcloud-vue', 'Time of day')\n\t\t},\n\n\t\t/** @return {string} What a digest says about the events it cannot hold. */\n\t\tdigestImmediateNote() {\n\t\t\treturn t('nextcloud-vue', 'Events marked as urgent are still sent straight away.')\n\t\t},\n\n\t\t/** @return {string} What the test button says. */\n\t\ttestSendText() {\n\t\t\treturn t('nextcloud-vue', 'Send a test')\n\t\t},\n\n\t\t/** @return {string} What the add-a-scope control says when nothing is chosen. */\n\t\taddScopeText() {\n\t\t\treturn t('nextcloud-vue', 'Set this for one kind of case')\n\t\t},\n\n\t\t/** @return {string} What the admin screen says about a person's own value. */\n\t\tadminNote() {\n\t\t\treturn t('nextcloud-vue', \"These are the group's defaults. Somebody who has set their own value keeps it, unless a channel is forced.\")\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * The id of a cell's source line, which describes its checkbox.\n\t\t *\n\t\t * @param {object} event The event.\n\t\t * @param {object} channel The channel.\n\t\t * @param {string} scope The row's scope.\n\t\t * @return {string} The id.\n\t\t */\n\t\tsourceIdFor(event, channel, scope) {\n\t\t\treturn `cn-nm-src-${this.$?.uid ?? 0}-${event.id}-${scope}-${channel.id}`.replace(/[^A-Za-z0-9_-]/g, '_')\n\t\t},\n\n\t\tt,\n\n\t\t/**\n\t\t * Whether a channel can be used at all, for any event.\n\t\t *\n\t\t * @param {object} channel The channel.\n\t\t * @return {object} Its availability.\n\t\t */\n\t\tchannelState(channel) {\n\t\t\treturn channelAvailability({ channel })\n\t\t},\n\n\t\t/**\n\t\t * One cell, resolved.\n\t\t *\n\t\t * @param {object} event The event.\n\t\t * @param {object} channel The channel.\n\t\t * @return {object} The resolution.\n\t\t */\n\t\tcell(event, channel) {\n\t\t\treturn this.cellFor(event, channel, GLOBAL_SCOPE)\n\t\t},\n\n\t\t/**\n\t\t * One cell on one row, resolved.\n\t\t *\n\t\t * The scoped value is a NARROWER ANSWER TO THE SAME QUESTION, not a\n\t\t * separate preference, so an unset scoped cell falls back to the\n\t\t * person's global value and from there to the group and the default.\n\t\t * Reading an unset scoped cell as false would override both.\n\t\t *\n\t\t * @param {object} event The event.\n\t\t * @param {object} channel The channel.\n\t\t * @param {string} scope The scope of this row.\n\t\t * @return {object} The resolution.\n\t\t */\n\t\tcellFor(event, channel, scope = GLOBAL_SCOPE) {\n\t\t\tconst narrow = valueForScope({\n\t\t\t\tvalues: this.scopedValues,\n\t\t\t\teventId: event?.id,\n\t\t\t\tchannelId: channel?.id,\n\t\t\t\tscope,\n\t\t\t})\n\n\t\t\treturn resolvePreference({\n\t\t\t\tappDefault: event?.appDefault === true,\n\t\t\t\tgroupValue: this.at(this.groupValues, event, channel),\n\t\t\t\tpersonalValue: narrow.value === null\n\t\t\t\t\t? this.at(this.personalValues, event, channel)\n\t\t\t\t\t: narrow.value,\n\t\t\t\tforced: this.at(this.forcedValues, event, channel),\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The rows one group renders: each event, then its scopes.\n\t\t *\n\t\t * Only events in the catalogue. A stored preference for an event the\n\t\t * catalogue no longer has is not listed, because a switch for\n\t\t * something that can no longer happen is a lie.\n\t\t *\n\t\t * @param {object} group The group.\n\t\t * @return {Array<object>} `{ event, scope, isGlobal }`.\n\t\t */\n\t\trowsFor(group) {\n\t\t\tconst rows = []\n\t\t\tfor (const event of group.events) {\n\t\t\t\tfor (const row of scopeRowsFor({ values: this.scopedValues, eventId: event.id })) {\n\t\t\t\t\trows.push({ event, scope: row.scope, isGlobal: row.isGlobal })\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn rows\n\t\t},\n\n\t\t/**\n\t\t * The scopes this row can still be narrowed to.\n\t\t *\n\t\t * @param {object} event The event.\n\t\t * @return {Array<object>} The remaining choices.\n\t\t */\n\t\taddableScopes(event) {\n\t\t\tconst taken = new Set(scopeRowsFor({ values: this.scopedValues, eventId: event?.id })\n\t\t\t\t.map((row) => row.scope))\n\t\t\treturn this.scopeChoices.filter((choice) => taken.has(String(choice?.id)) === false)\n\t\t},\n\n\t\t/**\n\t\t * What a scope is called on screen.\n\t\t *\n\t\t * @param {string} scope The scope.\n\t\t * @return {string} Its label, or the id when nobody named it.\n\t\t */\n\t\tscopeLabel(scope) {\n\t\t\tconst choice = this.scopeChoices.find((entry) => String(entry?.id) === scope)\n\t\t\treturn String(choice?.label ?? scope)\n\t\t},\n\n\t\t/**\n\t\t * Whether the platform refuses this channel for this event.\n\t\t *\n\t\t * @param {object} event The event.\n\t\t * @param {object} channel The channel.\n\t\t * @return {object} The availability.\n\t\t */\n\t\tcellAvailability(event, channel) {\n\t\t\treturn channelAvailability({\n\t\t\t\tchannel,\n\t\t\t\trefusal: this.at(this.refusals, event, channel),\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * A value out of one of the nested maps.\n\t\t *\n\t\t * @param {object} map The map.\n\t\t * @param {object} event The event.\n\t\t * @param {object} channel The channel.\n\t\t * @return {(boolean|object|null)} The value, or null.\n\t\t */\n\t\tat(map, event, channel) {\n\t\t\tconst row = map?.[event?.id]\n\t\t\tconst value = row?.[channel?.id]\n\t\t\treturn value === undefined ? null : value\n\t\t},\n\n\t\t/**\n\t\t * Whether a cell cannot be changed.\n\t\t *\n\t\t * @param {object} event The event.\n\t\t * @param {object} channel The channel.\n\t\t * @return {boolean} True when it is locked.\n\t\t */\n\t\tisLocked(event, channel) {\n\t\t\treturn this.isLockedFor(event, channel, GLOBAL_SCOPE)\n\t\t},\n\n\t\t/**\n\t\t * Whether a cell on one row cannot be changed.\n\t\t *\n\t\t * @param {object} event The event.\n\t\t * @param {object} channel The channel.\n\t\t * @param {string} scope The scope of this row.\n\t\t * @return {boolean} True when it is locked.\n\t\t */\n\t\tisLockedFor(event, channel, scope = GLOBAL_SCOPE) {\n\t\t\treturn this.cellFor(event, channel, scope).editable === false\n\t\t\t\t|| this.cellAvailability(event, channel).usable === false\n\t\t},\n\n\t\t/**\n\t\t * What a cell is called to somebody who cannot see the table.\n\t\t *\n\t\t * Says the event, the channel and why it is locked when it is. A\n\t\t * disabled checkbox with no name is a dead end for a screen reader.\n\t\t *\n\t\t * @param {object} event The event.\n\t\t * @param {object} channel The channel.\n\t\t * @return {string} The accessible name.\n\t\t */\n\t\tcellLabel(event, channel) {\n\t\t\treturn this.cellLabelFor(event, channel, GLOBAL_SCOPE)\n\t\t},\n\n\t\t/**\n\t\t * What a cell is called, saying which row it is on.\n\t\t *\n\t\t * A scoped row is indented, and indentation is invisible to a screen\n\t\t * reader: without the scope in the name, two checkboxes for the same\n\t\t * event and channel are indistinguishable.\n\t\t *\n\t\t * @param {object} event The event.\n\t\t * @param {object} channel The channel.\n\t\t * @param {string} scope The scope of this row.\n\t\t * @return {string} The accessible name.\n\t\t */\n\t\tcellLabelFor(event, channel, scope = GLOBAL_SCOPE) {\n\t\t\tconst base = scope === GLOBAL_SCOPE\n\t\t\t\t? t('nextcloud-vue', '{event} over {channel}', {\n\t\t\t\t\t\tevent: event.label,\n\t\t\t\t\t\tchannel: channel.label,\n\t\t\t\t\t})\n\t\t\t\t: t('nextcloud-vue', '{event} over {channel}, for {scope}', {\n\t\t\t\t\t\tevent: event.label,\n\t\t\t\t\t\tchannel: channel.label,\n\t\t\t\t\t\tscope: this.scopeLabel(scope),\n\t\t\t\t\t})\n\t\t\tconst reason = this.lockReasonFor(event, channel, scope)\n\t\t\treturn reason === '' ? base : `${base}. ${reason}`\n\t\t},\n\n\t\t/**\n\t\t * Why a cell is locked, in words.\n\t\t *\n\t\t * @param {object} event The event.\n\t\t * @param {object} channel The channel.\n\t\t * @return {string} The reason, or the empty string.\n\t\t */\n\t\tlockReason(event, channel) {\n\t\t\treturn this.lockReasonFor(event, channel, GLOBAL_SCOPE)\n\t\t},\n\n\t\t/**\n\t\t * Why a cell on one row is locked, in words.\n\t\t *\n\t\t * @param {object} event The event.\n\t\t * @param {object} channel The channel.\n\t\t * @param {string} scope The scope of this row.\n\t\t * @return {string} The reason, or the empty string.\n\t\t */\n\t\tlockReasonFor(event, channel, scope = GLOBAL_SCOPE) {\n\t\t\tconst availability = this.cellAvailability(event, channel)\n\t\t\tif (availability.usable === false) {\n\t\t\t\treturn availability.reason || this.unavailableText\n\t\t\t}\n\n\t\t\tconst resolved = this.cellFor(event, channel, scope)\n\t\t\tif (resolved.editable === true) {\n\t\t\t\treturn ''\n\t\t\t}\n\n\t\t\tif (resolved.forcedBy !== '' && resolved.reason !== '') {\n\t\t\t\treturn t('nextcloud-vue', 'Set by {by}: {reason}', {\n\t\t\t\t\tby: resolved.forcedBy,\n\t\t\t\t\treason: resolved.reason,\n\t\t\t\t})\n\t\t\t}\n\t\t\tif (resolved.forcedBy !== '') {\n\t\t\t\treturn t('nextcloud-vue', 'Set by {by}, and cannot be changed here', { by: resolved.forcedBy })\n\t\t\t}\n\t\t\treturn t('nextcloud-vue', 'Set by an administrator, and cannot be changed here')\n\t\t},\n\n\t\t/**\n\t\t * What a cell says under its toggle.\n\t\t *\n\t\t * @param {object} event The event.\n\t\t * @param {object} channel The channel.\n\t\t * @return {string} The sentence.\n\t\t */\n\t\tsourceText(event, channel) {\n\t\t\treturn this.sourceTextFor(event, channel, GLOBAL_SCOPE)\n\t\t},\n\n\t\t/**\n\t\t * What a cell on one row says under its toggle.\n\t\t *\n\t\t * @param {object} event The event.\n\t\t * @param {object} channel The channel.\n\t\t * @param {string} scope The scope of this row.\n\t\t * @return {string} The sentence.\n\t\t */\n\t\tsourceTextFor(event, channel, scope = GLOBAL_SCOPE) {\n\t\t\tconst locked = this.lockReasonFor(event, channel, scope)\n\t\t\tif (locked !== '') {\n\t\t\t\treturn locked\n\t\t\t}\n\n\t\t\tconst narrow = valueForScope({\n\t\t\t\tvalues: this.scopedValues,\n\t\t\t\teventId: event?.id,\n\t\t\t\tchannelId: channel?.id,\n\t\t\t\tscope,\n\t\t\t})\n\t\t\tif (scope !== GLOBAL_SCOPE && narrow.scope === GLOBAL_SCOPE) {\n\t\t\t\t// Said out loud, because an unanswered narrower question looks\n\t\t\t\t// exactly like an answered one: the checkbox shows the same\n\t\t\t\t// state either way.\n\t\t\t\treturn t('nextcloud-vue', 'Follows the row above')\n\t\t\t}\n\n\t\t\tconst level = this.cellFor(event, channel, scope).level\n\t\t\tif (level === PREFERENCE_LEVELS.PERSONAL) {\n\t\t\t\treturn t('nextcloud-vue', 'Your choice')\n\t\t\t}\n\t\t\tif (level === PREFERENCE_LEVELS.GROUP) {\n\t\t\t\treturn t('nextcloud-vue', 'Follows your group')\n\t\t\t}\n\t\t\treturn t('nextcloud-vue', 'Follows the app default')\n\t\t},\n\n\t\t/**\n\t\t * Whether a group is collapsed.\n\t\t *\n\t\t * @param {string} key The group.\n\t\t * @return {boolean} True when collapsed.\n\t\t */\n\t\tisCollapsed(key) {\n\t\t\treturn this.collapsed.includes(key)\n\t\t},\n\n\t\t/**\n\t\t * Collapse a group, or open it again.\n\t\t *\n\t\t * @param {string} key The group.\n\t\t */\n\t\ttoggleGroup(key) {\n\t\t\tthis.collapsed = this.isCollapsed(key)\n\t\t\t\t? this.collapsed.filter((entry) => entry !== key)\n\t\t\t\t: [...this.collapsed, key]\n\t\t},\n\n\t\t/**\n\t\t * A cell was set.\n\t\t *\n\t\t * @param {object} event The event.\n\t\t * @param {object} channel The channel.\n\t\t * @param {Event} domEvent The change event.\n\t\t * @param {string} scope The scope of the row it is on.\n\t\t */\n\t\tonToggle(event, channel, domEvent, scope = GLOBAL_SCOPE) {\n\t\t\t// Refuse from here too, not only in the template. A disabled\n\t\t\t// attribute is a rendering; this is the rule.\n\t\t\tif (this.isLockedFor(event, channel, scope)) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t/**\n\t\t\t * @event change A cell was set.\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('change', {\n\t\t\t\teventId: event.id,\n\t\t\t\tchannelId: channel.id,\n\t\t\t\tscope,\n\t\t\t\tvalue: domEvent?.target?.checked === true,\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * A scope was added to a row, from the row itself.\n\t\t *\n\t\t * @param {object} event The event.\n\t\t * @param {Event} domEvent The change event.\n\t\t */\n\t\tonAddScope(event, domEvent) {\n\t\t\tconst scope = String(domEvent?.target?.value ?? '')\n\t\t\tif (scope === '') {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Back to the placeholder, so the control reads as an action\n\t\t\t// rather than as a setting that now says something.\n\t\t\tif (domEvent?.target) {\n\t\t\t\tdomEvent.target.value = ''\n\t\t\t}\n\n\t\t\t/**\n\t\t\t * @event add-scope A row was narrowed.\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('add-scope', { eventId: event.id, scope })\n\t\t},\n\n\t\t/**\n\t\t * What a channel's digest is set to.\n\t\t *\n\t\t * @param {object} channel The channel.\n\t\t * @return {object} `{ mode, timeOfDay }`.\n\t\t */\n\t\tdigestFor(channel) {\n\t\t\tconst set = this.digest?.[channel?.id]\n\t\t\treturn {\n\t\t\t\tmode: String(set?.mode ?? 'off'),\n\t\t\t\ttimeOfDay: String(set?.timeOfDay ?? ''),\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The digest mode was changed.\n\t\t *\n\t\t * @param {object} channel The channel.\n\t\t * @param {Event} domEvent The change event.\n\t\t */\n\t\tonDigestMode(channel, domEvent) {\n\t\t\t/**\n\t\t\t * @event digest-change Emitted when a reader changes a channel's digest mode or its time of day. Payload: `{ channelId, mode, timeOfDay }`.\n\t\t\t */\n\t\t\tthis.$emit('digest-change', {\n\t\t\t\tchannelId: channel.id,\n\t\t\t\tmode: String(domEvent?.target?.value ?? 'off'),\n\t\t\t\ttimeOfDay: this.digestFor(channel).timeOfDay,\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The digest time was changed.\n\t\t *\n\t\t * @param {object} channel The channel.\n\t\t * @param {Event} domEvent The change event.\n\t\t */\n\t\tonDigestTime(channel, domEvent) {\n\t\t\tthis.$emit('digest-change', {\n\t\t\t\tchannelId: channel.id,\n\t\t\t\tmode: this.digestFor(channel).mode,\n\t\t\t\ttimeOfDay: String(domEvent?.target?.value ?? ''),\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The last test send on a channel, or null when there has been none.\n\t\t *\n\t\t * @param {object} channel The channel.\n\t\t * @return {?object} `{ ok, message }`.\n\t\t */\n\t\ttestResultFor(channel) {\n\t\t\tconst result = this.testResults?.[channel?.id]\n\t\t\treturn result === undefined || result === null ? null : result\n\t\t},\n\n\t\t/**\n\t\t * What a test send reported.\n\t\t *\n\t\t * @param {object} channel The channel.\n\t\t * @return {string} The sentence.\n\t\t */\n\t\ttestResultText(channel) {\n\t\t\tconst result = this.testResultFor(channel)\n\t\t\tif (result === null) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tif (String(result.message ?? '') !== '') {\n\t\t\t\treturn String(result.message)\n\t\t\t}\n\t\t\t// Only when the server sent no sentence of its own. A refusal\n\t\t\t// without a reason still has to say it was a refusal.\n\t\t\treturn result.ok === true\n\t\t\t\t? t('nextcloud-vue', 'The test arrived.')\n\t\t\t\t: t('nextcloud-vue', 'The test did not arrive.')\n\t\t},\n\n\t\t/**\n\t\t * What the test button is called.\n\t\t *\n\t\t * @param {object} channel The channel.\n\t\t * @return {string} The accessible name.\n\t\t */\n\t\ttestSendLabel(channel) {\n\t\t\treturn t('nextcloud-vue', 'Send a test over {channel}', { channel: channel.label })\n\t\t},\n\n\t\t/**\n\t\t * What the add-a-scope control is called.\n\t\t *\n\t\t * @param {object} event The event.\n\t\t * @return {string} The accessible name.\n\t\t */\n\t\taddScopeLabel(event) {\n\t\t\treturn t('nextcloud-vue', 'Set {event} for one kind of case', { event: event.label })\n\t\t},\n\n\t\t/**\n\t\t * A test send was asked for.\n\t\t *\n\t\t * @param {object} channel The channel.\n\t\t */\n\t\tonTestSend(channel) {\n\t\t\t/**\n\t\t\t * @event test-send Emitted when a reader asks for a test notification on one channel. Payload: `{ channelId }`.\n\t\t\t */\n\t\t\tthis.$emit('test-send', { channelId: channel.id })\n\t\t},\n\t},\n}\n</script>\n\n<style scoped lang=\"scss\">\n.cn-notification-matrix__table {\n\tinline-size: 100%;\n\tborder-collapse: collapse;\n}\n\n.cn-notification-matrix__caption {\n\ttext-align: start;\n\tpadding-block-end: 8px;\n}\n\n.cn-notification-matrix__table th,\n.cn-notification-matrix__table td {\n\ttext-align: start;\n\tpadding: 6px 8px;\n\tborder-block-end: 1px solid var(--color-border);\n\tvertical-align: top;\n}\n\n.cn-notification-matrix__muted {\n\tdisplay: block;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85rem;\n}\n\n.cn-notification-matrix__group {\n\tbackground: none;\n\tborder: none;\n\tfont-weight: bold;\n\tpadding: 8px 0;\n\tcursor: pointer;\n\tcolor: var(--color-main-text);\n}\n\n.cn-notification-matrix__scope {\n\tpadding-inline-start: 24px !important;\n\tfont-weight: normal;\n}\n\n.cn-notification-matrix__row--scoped th,\n.cn-notification-matrix__row--scoped td {\n\tborder-block-end-style: dotted;\n}\n\n.cn-notification-matrix__test {\n\tmargin-block-start: 4px;\n}\n\n.cn-notification-matrix__note {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n/* The board look (DqPersoonlijkeInstellingen \"Meldingen\"): muted 13px/600\n headers over a hairline, the channel columns 96px with a centred 20px\n checkbox, the event rows 10px high with the soft rule. The caption stays for\n assistive technology. */\n.cn-notification-matrix--board {\n\t.cn-notification-matrix__caption {\n\t\tposition: absolute;\n\t\tinline-size: 1px;\n\t\tblock-size: 1px;\n\t\toverflow: hidden;\n\t\tclip-path: inset(50%);\n\t\twhite-space: nowrap;\n\t}\n\n\t/* A theme that colours every th (thematiq's table header token) leaves\n\t the board grid's head white. */\n\t.cn-notification-matrix__table {\n\t\t--nldesign-component-table-header-background-color: transparent;\n\t\tfont-size: 14px;\n\t}\n\n\t.cn-notification-matrix__table thead th {\n\t\tpadding: 8px 10px 8px 0;\n\t\tline-height: 1.2;\n\t\tborder-block-end: 1px solid var(--color-border);\n\t\tcolor: var(--color-text-maxcontrast);\n\t\tfont-size: 13px;\n\t\tfont-weight: 600;\n\t\tvertical-align: bottom;\n\t}\n\n\t.cn-notification-matrix__table thead th[data-channel] {\n\t\tinline-size: 96px;\n\t\tpadding-inline: 4px;\n\t\ttext-align: center;\n\t}\n\n\t.cn-notification-matrix__table tbody th,\n\t.cn-notification-matrix__table tbody td {\n\t\tpadding: 10px 10px 10px 0;\n\t\tborder-block-end: 1px solid var(--color-background-dark);\n\t\tfont-weight: 400;\n\t\tline-height: 1.4;\n\t\tvertical-align: middle;\n\t}\n\n\t.cn-notification-matrix__table tbody td {\n\t\tpadding-inline: 4px;\n\t\ttext-align: center;\n\t}\n\n\t.cn-notification-matrix__table tbody td input[type='checkbox'] {\n\t\tinline-size: 20px;\n\t\tmin-inline-size: 20px;\n\t\tblock-size: 20px;\n\t\tmin-block-size: 20px;\n\t\tmargin: 0;\n\t}\n\n\t.cn-notification-matrix__group {\n\t\tpadding: 4px 0;\n\t\tfont-size: 13px;\n\t\tfont-weight: 600;\n\t\tcolor: var(--color-text-maxcontrast);\n\t}\n\n\t.cn-notification-matrix__channels {\n\t\tmargin-block-start: 16px;\n\t}\n\n\t.cn-notification-matrix__channels-summary {\n\t\tfont-size: 14px;\n\t\tfont-weight: 600;\n\t\tcursor: pointer;\n\t}\n\n\t.cn-notification-matrix__channel {\n\t\tdisplay: flex;\n\t\tflex-wrap: wrap;\n\t\talign-items: flex-end;\n\t\tgap: 8px 16px;\n\t\tpadding-block: 10px;\n\t\tborder-block-end: 1px solid var(--color-background-dark);\n\t}\n\n\t.cn-notification-matrix__channel-name {\n\t\tflex: 1 0 100%;\n\t\tmargin: 0;\n\t\tfont-size: 14px;\n\t\tfont-weight: 600;\n\t}\n}\n</style>\n"],"names":["t"],"mappings":"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"}
1
+ {"version":3,"file":"CnNotificationMatrix.vue2.js","sources":["../../../../src/components/CnNotificationMatrix/CnNotificationMatrix.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<div class=\"cn-notification-matrix\" :class=\"{ 'cn-notification-matrix--board': isBoardLook }\" data-testid=\"cn-notification-matrix\">\n\t\t<p v-if=\"adminMode\" class=\"cn-notification-matrix__note\" data-testid=\"cn-nm-admin-note\">\n\t\t\t{{ adminNote }}\n\t\t</p>\n\n\t\t<table class=\"cn-notification-matrix__table\" data-testid=\"cn-nm-table\">\n\t\t\t<caption class=\"cn-notification-matrix__caption\">\n\t\t\t\t{{ tableCaption }}\n\t\t\t</caption>\n\t\t\t<thead>\n\t\t\t\t<tr>\n\t\t\t\t\t<th scope=\"col\">\n\t\t\t\t\t\t{{ eventHeader }}\n\t\t\t\t\t</th>\n\t\t\t\t\t<th\n\t\t\t\t\t\tv-for=\"channel in channels\"\n\t\t\t\t\t\t:key=\"`ch-${channel.id}`\"\n\t\t\t\t\t\tscope=\"col\"\n\t\t\t\t\t\tdata-testid=\"cn-nm-channel-header\"\n\t\t\t\t\t\t:data-channel=\"channel.id\">\n\t\t\t\t\t\t{{ channel.label }}\n\t\t\t\t\t\t<template v-if=\"!isBoardLook\">\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tv-if=\"channelState(channel).usable === false\"\n\t\t\t\t\t\t\t\tclass=\"cn-notification-matrix__muted\"\n\t\t\t\t\t\t\t\tdata-testid=\"cn-nm-channel-unusable\">\n\t\t\t\t\t\t\t\t{{ channelState(channel).reason || unavailableText }}\n\t\t\t\t\t\t\t</span>\n\n\t\t\t\t\t\t\t<template v-if=\"channelState(channel).usable !== false\">\n\t\t\t\t\t\t\t\t<label class=\"cn-notification-matrix__muted\">\n\t\t\t\t\t\t\t\t\t{{ digestLabel }}\n\t\t\t\t\t\t\t\t\t<select\n\t\t\t\t\t\t\t\t\t\tdata-testid=\"cn-nm-digest-mode\"\n\t\t\t\t\t\t\t\t\t\t:data-channel=\"channel.id\"\n\t\t\t\t\t\t\t\t\t\t:value=\"digestFor(channel).mode\"\n\t\t\t\t\t\t\t\t\t\t@change=\"onDigestMode(channel, $event)\">\n\t\t\t\t\t\t\t\t\t\t<option\n\t\t\t\t\t\t\t\t\t\t\tv-for=\"mode in digestModes\"\n\t\t\t\t\t\t\t\t\t\t\t:key=\"`dm-${channel.id}-${mode.id}`\"\n\t\t\t\t\t\t\t\t\t\t\t:value=\"mode.id\">\n\t\t\t\t\t\t\t\t\t\t\t{{ mode.label }}\n\t\t\t\t\t\t\t\t\t\t</option>\n\t\t\t\t\t\t\t\t\t</select>\n\t\t\t\t\t\t\t\t</label>\n\n\t\t\t\t\t\t\t\t<label\n\t\t\t\t\t\t\t\t\tv-if=\"digestFor(channel).mode !== 'off'\"\n\t\t\t\t\t\t\t\t\tclass=\"cn-notification-matrix__muted\">\n\t\t\t\t\t\t\t\t\t{{ digestTimeLabel }}\n\t\t\t\t\t\t\t\t\t<input\n\t\t\t\t\t\t\t\t\t\ttype=\"time\"\n\t\t\t\t\t\t\t\t\t\tdata-testid=\"cn-nm-digest-time\"\n\t\t\t\t\t\t\t\t\t\t:data-channel=\"channel.id\"\n\t\t\t\t\t\t\t\t\t\t:value=\"digestFor(channel).timeOfDay\"\n\t\t\t\t\t\t\t\t\t\t@change=\"onDigestTime(channel, $event)\">\n\t\t\t\t\t\t\t\t</label>\n\n\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\tv-if=\"digestFor(channel).mode !== 'off'\"\n\t\t\t\t\t\t\t\t\tclass=\"cn-notification-matrix__muted\"\n\t\t\t\t\t\t\t\t\tdata-testid=\"cn-nm-digest-immediate-note\">\n\t\t\t\t\t\t\t\t\t{{ digestImmediateNote }}\n\t\t\t\t\t\t\t\t</span>\n\n\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\tclass=\"cn-notification-matrix__test\"\n\t\t\t\t\t\t\t\t\tdata-testid=\"cn-nm-test-send\"\n\t\t\t\t\t\t\t\t\t:data-channel=\"channel.id\"\n\t\t\t\t\t\t\t\t\t:aria-label=\"testSendLabel(channel)\"\n\t\t\t\t\t\t\t\t\t@click=\"onTestSend(channel)\">\n\t\t\t\t\t\t\t\t\t{{ testSendText }}\n\t\t\t\t\t\t\t\t</button>\n\n\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\tv-if=\"testResultFor(channel) !== null\"\n\t\t\t\t\t\t\t\t\tclass=\"cn-notification-matrix__muted\"\n\t\t\t\t\t\t\t\t\tdata-testid=\"cn-nm-test-result\"\n\t\t\t\t\t\t\t\t\t:data-channel=\"channel.id\"\n\t\t\t\t\t\t\t\t\t:data-ok=\"String(testResultFor(channel).ok === true)\"\n\t\t\t\t\t\t\t\t\trole=\"status\">\n\t\t\t\t\t\t\t\t\t{{ testResultText(channel) }}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t</template>\n\t\t\t\t\t</th>\n\t\t\t\t</tr>\n\t\t\t</thead>\n\n\t\t\t<tbody v-for=\"group in groups\" :key=\"`grp-${group.key}`\">\n\t\t\t\t<tr>\n\t\t\t\t\t<th :colspan=\"channels.length + 1\" scope=\"colgroup\">\n\t\t\t\t\t\t<button\n\t\t\t\t\t\t\tclass=\"cn-notification-matrix__group\"\n\t\t\t\t\t\t\tdata-testid=\"cn-nm-group\"\n\t\t\t\t\t\t\t:data-group=\"group.key\"\n\t\t\t\t\t\t\t:aria-expanded=\"String(!isCollapsed(group.key))\"\n\t\t\t\t\t\t\t@click=\"toggleGroup(group.key)\">\n\t\t\t\t\t\t\t{{ group.label }}\n\t\t\t\t\t\t</button>\n\t\t\t\t\t</th>\n\t\t\t\t</tr>\n\n\t\t\t\t<tr\n\t\t\t\t\tv-for=\"row in rowsFor(group)\"\n\t\t\t\t\tv-show=\"!isCollapsed(group.key)\"\n\t\t\t\t\t:key=\"`ev-${row.event.id}-${row.scope}`\"\n\t\t\t\t\tdata-testid=\"cn-nm-row\"\n\t\t\t\t\t:class=\"{ 'cn-notification-matrix__row--scoped': !row.isGlobal }\"\n\t\t\t\t\t:data-event=\"row.event.id\"\n\t\t\t\t\t:data-scope=\"row.scope\">\n\t\t\t\t\t<th scope=\"row\" :class=\"{ 'cn-notification-matrix__scope': !row.isGlobal }\">\n\t\t\t\t\t\t<template v-if=\"row.isGlobal\">\n\t\t\t\t\t\t\t{{ row.event.label }}\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tv-if=\"row.event.immediate\"\n\t\t\t\t\t\t\t\tclass=\"cn-notification-matrix__muted\"\n\t\t\t\t\t\t\t\tdata-testid=\"cn-nm-immediate\">\n\t\t\t\t\t\t\t\t{{ immediateText }}\n\t\t\t\t\t\t\t</span>\n\n\t\t\t\t\t\t\t<select\n\t\t\t\t\t\t\t\tv-if=\"addableScopes(row.event).length > 0\"\n\t\t\t\t\t\t\t\tclass=\"cn-notification-matrix__muted\"\n\t\t\t\t\t\t\t\tdata-testid=\"cn-nm-add-scope\"\n\t\t\t\t\t\t\t\t:data-event=\"row.event.id\"\n\t\t\t\t\t\t\t\t:aria-label=\"addScopeLabel(row.event)\"\n\t\t\t\t\t\t\t\tvalue=\"\"\n\t\t\t\t\t\t\t\t@change=\"onAddScope(row.event, $event)\">\n\t\t\t\t\t\t\t\t<option value=\"\">\n\t\t\t\t\t\t\t\t\t{{ addScopeText }}\n\t\t\t\t\t\t\t\t</option>\n\t\t\t\t\t\t\t\t<option\n\t\t\t\t\t\t\t\t\tv-for=\"choice in addableScopes(row.event)\"\n\t\t\t\t\t\t\t\t\t:key=\"`sc-${row.event.id}-${choice.id}`\"\n\t\t\t\t\t\t\t\t\t:value=\"choice.id\">\n\t\t\t\t\t\t\t\t\t{{ choice.label }}\n\t\t\t\t\t\t\t\t</option>\n\t\t\t\t\t\t\t</select>\n\t\t\t\t\t\t</template>\n\t\t\t\t\t\t<template v-else>\n\t\t\t\t\t\t\t<span data-testid=\"cn-nm-scope-label\">{{ scopeLabel(row.scope) }}</span>\n\t\t\t\t\t\t</template>\n\t\t\t\t\t</th>\n\n\t\t\t\t\t<td\n\t\t\t\t\t\tv-for=\"channel in channels\"\n\t\t\t\t\t\t:key=\"`c-${row.event.id}-${row.scope}-${channel.id}`\"\n\t\t\t\t\t\tdata-testid=\"cn-nm-cell\"\n\t\t\t\t\t\t:data-event=\"row.event.id\"\n\t\t\t\t\t\t:data-scope=\"row.scope\"\n\t\t\t\t\t\t:data-channel=\"channel.id\"\n\t\t\t\t\t\t:data-level=\"cellFor(row.event, channel, row.scope).level\">\n\t\t\t\t\t\t<input\n\t\t\t\t\t\t\ttype=\"checkbox\"\n\t\t\t\t\t\t\tdata-testid=\"cn-nm-toggle\"\n\t\t\t\t\t\t\t:data-event=\"row.event.id\"\n\t\t\t\t\t\t\t:data-scope=\"row.scope\"\n\t\t\t\t\t\t\t:data-channel=\"channel.id\"\n\t\t\t\t\t\t\t:checked=\"cellFor(row.event, channel, row.scope).value\"\n\t\t\t\t\t\t\t:disabled=\"isLockedFor(row.event, channel, row.scope)\"\n\t\t\t\t\t\t\t:aria-label=\"cellLabelFor(row.event, channel, row.scope)\"\n\t\t\t\t\t\t\t:aria-describedby=\"isBoardLook ? sourceIdFor(row.event, channel, row.scope) : null\"\n\t\t\t\t\t\t\t@change=\"onToggle(row.event, channel, $event, row.scope)\">\n\t\t\t\t\t\t<!-- The board look draws only the checkbox; why it is set stays said, and stays visible when the cell is locked. -->\n\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t:id=\"sourceIdFor(row.event, channel, row.scope)\"\n\t\t\t\t\t\t\tclass=\"cn-notification-matrix__muted\"\n\t\t\t\t\t\t\t:class=\"{ 'hidden-visually': isBoardLook && !isLockedFor(row.event, channel, row.scope) }\"\n\t\t\t\t\t\t\tdata-testid=\"cn-nm-source\">\n\t\t\t\t\t\t\t{{ sourceTextFor(row.event, channel, row.scope) }}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</td>\n\t\t\t\t</tr>\n\t\t\t</tbody>\n\t\t</table>\n\n\t\t<!-- The board look keeps the grid to its checkboxes; each channel's bundling, its time and the test send live here. -->\n\t\t<details\n\t\t\tv-if=\"isBoardLook\"\n\t\t\tclass=\"cn-notification-matrix__channels\"\n\t\t\tdata-testid=\"cn-nm-channel-settings\">\n\t\t\t<summary class=\"cn-notification-matrix__channels-summary\">\n\t\t\t\t{{ channelSettingsTitle }}\n\t\t\t</summary>\n\t\t\t<div\n\t\t\t\tv-for=\"channel in channels\"\n\t\t\t\t:key=\"`cs-${channel.id}`\"\n\t\t\t\tclass=\"cn-notification-matrix__channel\"\n\t\t\t\t:data-channel=\"channel.id\">\n\t\t\t\t<h4 class=\"cn-notification-matrix__channel-name\">\n\t\t\t\t\t{{ channel.label }}\n\t\t\t\t</h4>\n\t\t\t\t<span\n\t\t\t\t\tv-if=\"channelState(channel).usable === false\"\n\t\t\t\t\tclass=\"cn-notification-matrix__muted\"\n\t\t\t\t\tdata-testid=\"cn-nm-channel-unusable\">\n\t\t\t\t\t{{ channelState(channel).reason || unavailableText }}\n\t\t\t\t</span>\n\n\t\t\t\t<template v-if=\"channelState(channel).usable !== false\">\n\t\t\t\t\t<label class=\"cn-notification-matrix__muted\">\n\t\t\t\t\t\t{{ digestLabel }}\n\t\t\t\t\t\t<select\n\t\t\t\t\t\t\tdata-testid=\"cn-nm-digest-mode\"\n\t\t\t\t\t\t\t:data-channel=\"channel.id\"\n\t\t\t\t\t\t\t:value=\"digestFor(channel).mode\"\n\t\t\t\t\t\t\t@change=\"onDigestMode(channel, $event)\">\n\t\t\t\t\t\t\t<option\n\t\t\t\t\t\t\t\tv-for=\"mode in digestModes\"\n\t\t\t\t\t\t\t\t:key=\"`dm-${channel.id}-${mode.id}`\"\n\t\t\t\t\t\t\t\t:value=\"mode.id\">\n\t\t\t\t\t\t\t\t{{ mode.label }}\n\t\t\t\t\t\t\t</option>\n\t\t\t\t\t\t</select>\n\t\t\t\t\t</label>\n\n\t\t\t\t\t<label\n\t\t\t\t\t\tv-if=\"digestFor(channel).mode !== 'off'\"\n\t\t\t\t\t\tclass=\"cn-notification-matrix__muted\">\n\t\t\t\t\t\t{{ digestTimeLabel }}\n\t\t\t\t\t\t<input\n\t\t\t\t\t\t\ttype=\"time\"\n\t\t\t\t\t\t\tdata-testid=\"cn-nm-digest-time\"\n\t\t\t\t\t\t\t:data-channel=\"channel.id\"\n\t\t\t\t\t\t\t:value=\"digestFor(channel).timeOfDay\"\n\t\t\t\t\t\t\t@change=\"onDigestTime(channel, $event)\">\n\t\t\t\t\t</label>\n\n\t\t\t\t\t<span\n\t\t\t\t\t\tv-if=\"digestFor(channel).mode !== 'off'\"\n\t\t\t\t\t\tclass=\"cn-notification-matrix__muted\"\n\t\t\t\t\t\tdata-testid=\"cn-nm-digest-immediate-note\">\n\t\t\t\t\t\t{{ digestImmediateNote }}\n\t\t\t\t\t</span>\n\n\t\t\t\t\t<button\n\t\t\t\t\t\tclass=\"cn-notification-matrix__test\"\n\t\t\t\t\t\tdata-testid=\"cn-nm-test-send\"\n\t\t\t\t\t\t:data-channel=\"channel.id\"\n\t\t\t\t\t\t:aria-label=\"testSendLabel(channel)\"\n\t\t\t\t\t\t@click=\"onTestSend(channel)\">\n\t\t\t\t\t\t{{ testSendText }}\n\t\t\t\t\t</button>\n\n\t\t\t\t\t<span\n\t\t\t\t\t\tv-if=\"testResultFor(channel) !== null\"\n\t\t\t\t\t\tclass=\"cn-notification-matrix__muted\"\n\t\t\t\t\t\tdata-testid=\"cn-nm-test-result\"\n\t\t\t\t\t\t:data-channel=\"channel.id\"\n\t\t\t\t\t\t:data-ok=\"String(testResultFor(channel).ok === true)\"\n\t\t\t\t\t\trole=\"status\">\n\t\t\t\t\t\t{{ testResultText(channel) }}\n\t\t\t\t\t</span>\n\t\t\t\t</template>\n\t\t\t</div>\n\t\t</details>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { normalizeLook } from '../../composables/useLook.js'\nimport {\n\tchannelAvailability,\n\tPREFERENCE_LEVELS,\n\tresolvePreference,\n} from '../../utils/notificationPreference.js'\nimport {\n\tGLOBAL_SCOPE,\n\tscopeRowsFor,\n\tvalueForScope,\n} from '../../utils/preferenceScopes.js'\n\n/**\n * CnNotificationMatrix — one screen where a person chooses what notifies them,\n * and finds out why they are getting what they are getting.\n *\n * 🔴 IT DOES NOT REPLACE `CnNotificationPreferences`, IT SITS BESIDE IT. That\n * component is a self-contained settings pane: it takes no props, fetches for\n * itself and scopes to the app whose settings modal is open, and `CnAppRoot`\n * mounts it as the default of its `#user-settings` slot. This one takes a\n * catalogue from its host, knows four levels rather than two and has a channel\n * axis. Replacing the older one in place, which an earlier revision of this\n * change did, left every app that mounts it through `CnAppRoot` rendering an\n * empty table with no error: a silent regression, not a breakage anybody would\n * see in a test.\n *\n * Events down, channels across. The catalogue is the host's, so this component\n * never knows what a \"term expires\" event is.\n *\n * 🔴 IT NEVER SHOWS A SETTING THAT DOES NOT APPLY. That is the whole point of\n * the screen and it is the one way it can fail while looking perfect. Three\n * cases, each handled rather than hidden:\n *\n * - A FORCED cell is locked, says who forced it and why. An administrator\n * has taken the choice away; a toggle the person can move which then does\n * nothing is worse than a locked one, because they believe they acted.\n * - A channel the instance has not configured is disabled with the reason.\n * - A channel REFUSED for this kind and recipient says the rule. An internal\n * notice to an outside recipient is a rule working, not a gap to fix, and\n * \"no channels available\" would send somebody to the wrong place.\n *\n * 🔴 IT IS A REAL TABLE. A grid of checkboxes with no row and column headers\n * is unreadable to a screen reader: every cell is \"checkbox, checked\" with no\n * way to know which event or which channel. The header cells are `th` with\n * scopes, and each toggle carries a name that says both.\n *\n * 🔴 A PREFERENCE IS SET GLOBALLY OR FOR ONE CASE DOMAIN. \"Tell me when a term\n * expires\" is usually too much and \"never\" is too little, so the event row is\n * the global one and scoped rows sit indented under it. The narrower row wins\n * only where it applies: everywhere else the global row still stands, because\n * a scope that replaced it would silence every other domain the moment\n * somebody narrowed one.\n *\n * 🔴 ONLY EVENTS IN THE CATALOGUE ARE LISTED. A stored preference for an event\n * that no longer exists is not rendered, because a switch for something that\n * can no longer happen is a lie; the store drops it on the next write.\n *\n * 🔴 A TEST SEND REPORTS WHAT HAPPENED, INCLUDING A REFUSAL. Nobody should have\n * to wait for a real event to find out a channel is broken, and a button that\n * silently did nothing would read as proof the channel works.\n *\n * @event {object} change — A cell was set. Payload: `{ eventId, channelId, scope, value }`.\n * @event {object} add-scope — A scope was added to a row. Payload: `{ eventId, scope }`.\n * @event {object} digest-change — A channel's digest was set. Payload: `{ channelId, mode, timeOfDay }`.\n * @event {object} test-send — A test was asked for. Payload: `{ channelId }`.\n */\n/**\n * An event's shipped default in one channel: one boolean for every channel,\n * or the channel's own entry when the default differs per channel.\n *\n * @param {object} event The event.\n * @param {object} channel The channel.\n * @return {boolean} The default.\n * @spec openspec/changes/notification-matrix-default-per-channel/specs/notification-matrix/spec.md\n */\nfunction appDefaultFor(event, channel) {\n\tconst value = event?.appDefault\n\tif (value !== null && typeof value === 'object') {\n\t\treturn value[channel?.id] === true\n\t}\n\treturn value === true\n}\n\nexport default {\n\tname: 'CnNotificationMatrix',\n\n\tinject: {\n\t\t/** The look CnAppRoot (or the page) draws in. */\n\t\tcnLook: { default: 'nextcloud' },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The events, each `{ id, label, group, groupLabel, immediate,\n\t\t * appDefault }`. From the host's catalogue. `appDefault` is one\n\t\t * boolean for every channel, or `{ [channelId]: boolean }` when the\n\t\t * default differs per channel (an opt-in e-mail is off by default).\n\t\t */\n\t\tevents: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** The channels, each `{ id, label, configured, unconfiguredReason }`. */\n\t\tchannels: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** The group's values, as `{ [eventId]: { [channelId]: boolean } }`. */\n\t\tgroupValues: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** This person's values, same shape. */\n\t\tpersonalValues: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * What an administrator has forced, as\n\t\t * `{ [eventId]: { [channelId]: { value, by, reason } } }`.\n\t\t *\n\t\t * A level ABOVE the person's own rather than another default: it is\n\t\t * the administrator removing the choice, not setting it.\n\t\t */\n\t\tforcedValues: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * What the platform refuses for this recipient, same shape, each\n\t\t * `{ reason }`. A refusal is not an absence: it is a rule, and the\n\t\t * screen says which.\n\t\t */\n\t\trefusals: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Whether this is the admin screen writing the group's defaults. The\n\t\t * screen says a person's own value wins, so an administrator is not\n\t\t * surprised when somebody does not get what they set.\n\t\t */\n\t\tadminMode: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * This person's narrower values, as\n\t\t * `{ [eventId]: { [channelId]: { [scope]: boolean } } }`.\n\t\t *\n\t\t * Separate from `personalValues` rather than replacing it, so the\n\t\t * global row keeps the flat shape every existing caller passes. A row\n\t\t * left unset here falls through to the global value, which is what\n\t\t * makes an absent scoped row an unanswered question rather than a no.\n\t\t */\n\t\tscopedValues: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * The scopes a row can be narrowed to, each `{ id, label }`. Case\n\t\t * domains or record types, named by whoever configured the app.\n\t\t */\n\t\tscopeChoices: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * The digest per channel, as `{ [channelId]: { mode, timeOfDay } }`\n\t\t * where mode is `off`, `daily` or `weekly`.\n\t\t */\n\t\tdigest: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * The last test send per channel, as\n\t\t * `{ [channelId]: { ok, message } }`. A refusal belongs here as much\n\t\t * as a success: both are results.\n\t\t */\n\t\ttestResults: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\t},\n\n\temits: ['change', 'add-scope', 'digest-change', 'test-send'],\n\n\tdata() {\n\t\treturn {\n\t\t\t/** Groups the reader has collapsed, for as long as they are here. */\n\t\t\tcollapsed: [],\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Whether the matrix draws the board's grid.\n\t\t *\n\t\t * @return {boolean} True in the board look.\n\t\t * @spec openspec/changes/notification-matrix-board-grid/specs/notification-matrix/spec.md\n\t\t */\n\t\tisBoardLook() {\n\t\t\treturn normalizeLook(this.cnLook) === 'board'\n\t\t},\n\n\t\t/** @return {string} The disclosure under the board grid. */\n\t\tchannelSettingsTitle() {\n\t\t\treturn t('nextcloud-vue', 'Bundling and test message')\n\t\t},\n\n\t\t/** @return {Array<object>} The events, in their groups. */\n\t\tgroups() {\n\t\t\tconst byKey = new Map()\n\t\t\tfor (const event of this.events) {\n\t\t\t\tconst key = String(event?.group ?? '')\n\t\t\t\tif (byKey.has(key) === false) {\n\t\t\t\t\tbyKey.set(key, {\n\t\t\t\t\t\tkey,\n\t\t\t\t\t\tlabel: String(event?.groupLabel ?? key) || this.ungroupedText,\n\t\t\t\t\t\tevents: [],\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t\tbyKey.get(key).events.push(event)\n\t\t\t}\n\t\t\treturn [...byKey.values()]\n\t\t},\n\n\t\t/** @return {string} The table's caption. */\n\t\ttableCaption() {\n\t\t\treturn t('nextcloud-vue', 'What notifies you, and over which channel')\n\t\t},\n\n\t\t/** @return {string} The events column header. */\n\t\teventHeader() {\n\t\t\treturn t('nextcloud-vue', 'Event')\n\t\t},\n\n\t\t/** @return {string} What an ungrouped event group is called. */\n\t\tungroupedText() {\n\t\t\treturn t('nextcloud-vue', 'Other')\n\t\t},\n\n\t\t/** @return {string} What an unusable channel says with no reason given. */\n\t\tunavailableText() {\n\t\t\treturn t('nextcloud-vue', 'Not available on this instance')\n\t\t},\n\n\t\t/** @return {string} What an immediate event says. */\n\t\timmediateText() {\n\t\t\treturn t('nextcloud-vue', 'Always sent straight away, never held for a digest')\n\t\t},\n\n\t\t/** @return {Array<object>} The digest choices, in the order they escalate. */\n\t\tdigestModes() {\n\t\t\treturn [\n\t\t\t\t{ id: 'off', label: t('nextcloud-vue', 'Send each one as it happens') },\n\t\t\t\t{ id: 'daily', label: t('nextcloud-vue', 'Once a day') },\n\t\t\t\t{ id: 'weekly', label: t('nextcloud-vue', 'Once a week') },\n\t\t\t]\n\t\t},\n\n\t\t/** @return {string} What the digest control is called. */\n\t\tdigestLabel() {\n\t\t\treturn t('nextcloud-vue', 'Bundle these')\n\t\t},\n\n\t\t/** @return {string} What the time control is called. */\n\t\tdigestTimeLabel() {\n\t\t\treturn t('nextcloud-vue', 'Time of day')\n\t\t},\n\n\t\t/** @return {string} What a digest says about the events it cannot hold. */\n\t\tdigestImmediateNote() {\n\t\t\treturn t('nextcloud-vue', 'Events marked as urgent are still sent straight away.')\n\t\t},\n\n\t\t/** @return {string} What the test button says. */\n\t\ttestSendText() {\n\t\t\treturn t('nextcloud-vue', 'Send a test')\n\t\t},\n\n\t\t/** @return {string} What the add-a-scope control says when nothing is chosen. */\n\t\taddScopeText() {\n\t\t\treturn t('nextcloud-vue', 'Set this for one kind of case')\n\t\t},\n\n\t\t/** @return {string} What the admin screen says about a person's own value. */\n\t\tadminNote() {\n\t\t\treturn t('nextcloud-vue', \"These are the group's defaults. Somebody who has set their own value keeps it, unless a channel is forced.\")\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * The id of a cell's source line, which describes its checkbox.\n\t\t *\n\t\t * @param {object} event The event.\n\t\t * @param {object} channel The channel.\n\t\t * @param {string} scope The row's scope.\n\t\t * @return {string} The id.\n\t\t */\n\t\tsourceIdFor(event, channel, scope) {\n\t\t\treturn `cn-nm-src-${this.$?.uid ?? 0}-${event.id}-${scope}-${channel.id}`.replace(/[^A-Za-z0-9_-]/g, '_')\n\t\t},\n\n\t\tt,\n\n\t\t/**\n\t\t * Whether a channel can be used at all, for any event.\n\t\t *\n\t\t * @param {object} channel The channel.\n\t\t * @return {object} Its availability.\n\t\t */\n\t\tchannelState(channel) {\n\t\t\treturn channelAvailability({ channel })\n\t\t},\n\n\t\t/**\n\t\t * One cell, resolved.\n\t\t *\n\t\t * @param {object} event The event.\n\t\t * @param {object} channel The channel.\n\t\t * @return {object} The resolution.\n\t\t */\n\t\tcell(event, channel) {\n\t\t\treturn this.cellFor(event, channel, GLOBAL_SCOPE)\n\t\t},\n\n\t\t/**\n\t\t * One cell on one row, resolved.\n\t\t *\n\t\t * The scoped value is a NARROWER ANSWER TO THE SAME QUESTION, not a\n\t\t * separate preference, so an unset scoped cell falls back to the\n\t\t * person's global value and from there to the group and the default.\n\t\t * Reading an unset scoped cell as false would override both.\n\t\t *\n\t\t * @param {object} event The event.\n\t\t * @param {object} channel The channel.\n\t\t * @param {string} scope The scope of this row.\n\t\t * @return {object} The resolution.\n\t\t */\n\t\tcellFor(event, channel, scope = GLOBAL_SCOPE) {\n\t\t\tconst narrow = valueForScope({\n\t\t\t\tvalues: this.scopedValues,\n\t\t\t\teventId: event?.id,\n\t\t\t\tchannelId: channel?.id,\n\t\t\t\tscope,\n\t\t\t})\n\n\t\t\treturn resolvePreference({\n\t\t\t\tappDefault: appDefaultFor(event, channel),\n\t\t\t\tgroupValue: this.at(this.groupValues, event, channel),\n\t\t\t\tpersonalValue: narrow.value === null\n\t\t\t\t\t? this.at(this.personalValues, event, channel)\n\t\t\t\t\t: narrow.value,\n\t\t\t\tforced: this.at(this.forcedValues, event, channel),\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The rows one group renders: each event, then its scopes.\n\t\t *\n\t\t * Only events in the catalogue. A stored preference for an event the\n\t\t * catalogue no longer has is not listed, because a switch for\n\t\t * something that can no longer happen is a lie.\n\t\t *\n\t\t * @param {object} group The group.\n\t\t * @return {Array<object>} `{ event, scope, isGlobal }`.\n\t\t */\n\t\trowsFor(group) {\n\t\t\tconst rows = []\n\t\t\tfor (const event of group.events) {\n\t\t\t\tfor (const row of scopeRowsFor({ values: this.scopedValues, eventId: event.id })) {\n\t\t\t\t\trows.push({ event, scope: row.scope, isGlobal: row.isGlobal })\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn rows\n\t\t},\n\n\t\t/**\n\t\t * The scopes this row can still be narrowed to.\n\t\t *\n\t\t * @param {object} event The event.\n\t\t * @return {Array<object>} The remaining choices.\n\t\t */\n\t\taddableScopes(event) {\n\t\t\tconst taken = new Set(scopeRowsFor({ values: this.scopedValues, eventId: event?.id })\n\t\t\t\t.map((row) => row.scope))\n\t\t\treturn this.scopeChoices.filter((choice) => taken.has(String(choice?.id)) === false)\n\t\t},\n\n\t\t/**\n\t\t * What a scope is called on screen.\n\t\t *\n\t\t * @param {string} scope The scope.\n\t\t * @return {string} Its label, or the id when nobody named it.\n\t\t */\n\t\tscopeLabel(scope) {\n\t\t\tconst choice = this.scopeChoices.find((entry) => String(entry?.id) === scope)\n\t\t\treturn String(choice?.label ?? scope)\n\t\t},\n\n\t\t/**\n\t\t * Whether the platform refuses this channel for this event.\n\t\t *\n\t\t * @param {object} event The event.\n\t\t * @param {object} channel The channel.\n\t\t * @return {object} The availability.\n\t\t */\n\t\tcellAvailability(event, channel) {\n\t\t\treturn channelAvailability({\n\t\t\t\tchannel,\n\t\t\t\trefusal: this.at(this.refusals, event, channel),\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * A value out of one of the nested maps.\n\t\t *\n\t\t * @param {object} map The map.\n\t\t * @param {object} event The event.\n\t\t * @param {object} channel The channel.\n\t\t * @return {(boolean|object|null)} The value, or null.\n\t\t */\n\t\tat(map, event, channel) {\n\t\t\tconst row = map?.[event?.id]\n\t\t\tconst value = row?.[channel?.id]\n\t\t\treturn value === undefined ? null : value\n\t\t},\n\n\t\t/**\n\t\t * Whether a cell cannot be changed.\n\t\t *\n\t\t * @param {object} event The event.\n\t\t * @param {object} channel The channel.\n\t\t * @return {boolean} True when it is locked.\n\t\t */\n\t\tisLocked(event, channel) {\n\t\t\treturn this.isLockedFor(event, channel, GLOBAL_SCOPE)\n\t\t},\n\n\t\t/**\n\t\t * Whether a cell on one row cannot be changed.\n\t\t *\n\t\t * @param {object} event The event.\n\t\t * @param {object} channel The channel.\n\t\t * @param {string} scope The scope of this row.\n\t\t * @return {boolean} True when it is locked.\n\t\t */\n\t\tisLockedFor(event, channel, scope = GLOBAL_SCOPE) {\n\t\t\treturn this.cellFor(event, channel, scope).editable === false\n\t\t\t\t|| this.cellAvailability(event, channel).usable === false\n\t\t},\n\n\t\t/**\n\t\t * What a cell is called to somebody who cannot see the table.\n\t\t *\n\t\t * Says the event, the channel and why it is locked when it is. A\n\t\t * disabled checkbox with no name is a dead end for a screen reader.\n\t\t *\n\t\t * @param {object} event The event.\n\t\t * @param {object} channel The channel.\n\t\t * @return {string} The accessible name.\n\t\t */\n\t\tcellLabel(event, channel) {\n\t\t\treturn this.cellLabelFor(event, channel, GLOBAL_SCOPE)\n\t\t},\n\n\t\t/**\n\t\t * What a cell is called, saying which row it is on.\n\t\t *\n\t\t * A scoped row is indented, and indentation is invisible to a screen\n\t\t * reader: without the scope in the name, two checkboxes for the same\n\t\t * event and channel are indistinguishable.\n\t\t *\n\t\t * @param {object} event The event.\n\t\t * @param {object} channel The channel.\n\t\t * @param {string} scope The scope of this row.\n\t\t * @return {string} The accessible name.\n\t\t */\n\t\tcellLabelFor(event, channel, scope = GLOBAL_SCOPE) {\n\t\t\tconst base = scope === GLOBAL_SCOPE\n\t\t\t\t? t('nextcloud-vue', '{event} over {channel}', {\n\t\t\t\t\t\tevent: event.label,\n\t\t\t\t\t\tchannel: channel.label,\n\t\t\t\t\t})\n\t\t\t\t: t('nextcloud-vue', '{event} over {channel}, for {scope}', {\n\t\t\t\t\t\tevent: event.label,\n\t\t\t\t\t\tchannel: channel.label,\n\t\t\t\t\t\tscope: this.scopeLabel(scope),\n\t\t\t\t\t})\n\t\t\tconst reason = this.lockReasonFor(event, channel, scope)\n\t\t\treturn reason === '' ? base : `${base}. ${reason}`\n\t\t},\n\n\t\t/**\n\t\t * Why a cell is locked, in words.\n\t\t *\n\t\t * @param {object} event The event.\n\t\t * @param {object} channel The channel.\n\t\t * @return {string} The reason, or the empty string.\n\t\t */\n\t\tlockReason(event, channel) {\n\t\t\treturn this.lockReasonFor(event, channel, GLOBAL_SCOPE)\n\t\t},\n\n\t\t/**\n\t\t * Why a cell on one row is locked, in words.\n\t\t *\n\t\t * @param {object} event The event.\n\t\t * @param {object} channel The channel.\n\t\t * @param {string} scope The scope of this row.\n\t\t * @return {string} The reason, or the empty string.\n\t\t */\n\t\tlockReasonFor(event, channel, scope = GLOBAL_SCOPE) {\n\t\t\tconst availability = this.cellAvailability(event, channel)\n\t\t\tif (availability.usable === false) {\n\t\t\t\treturn availability.reason || this.unavailableText\n\t\t\t}\n\n\t\t\tconst resolved = this.cellFor(event, channel, scope)\n\t\t\tif (resolved.editable === true) {\n\t\t\t\treturn ''\n\t\t\t}\n\n\t\t\tif (resolved.forcedBy !== '' && resolved.reason !== '') {\n\t\t\t\treturn t('nextcloud-vue', 'Set by {by}: {reason}', {\n\t\t\t\t\tby: resolved.forcedBy,\n\t\t\t\t\treason: resolved.reason,\n\t\t\t\t})\n\t\t\t}\n\t\t\tif (resolved.forcedBy !== '') {\n\t\t\t\treturn t('nextcloud-vue', 'Set by {by}, and cannot be changed here', { by: resolved.forcedBy })\n\t\t\t}\n\t\t\treturn t('nextcloud-vue', 'Set by an administrator, and cannot be changed here')\n\t\t},\n\n\t\t/**\n\t\t * What a cell says under its toggle.\n\t\t *\n\t\t * @param {object} event The event.\n\t\t * @param {object} channel The channel.\n\t\t * @return {string} The sentence.\n\t\t */\n\t\tsourceText(event, channel) {\n\t\t\treturn this.sourceTextFor(event, channel, GLOBAL_SCOPE)\n\t\t},\n\n\t\t/**\n\t\t * What a cell on one row says under its toggle.\n\t\t *\n\t\t * @param {object} event The event.\n\t\t * @param {object} channel The channel.\n\t\t * @param {string} scope The scope of this row.\n\t\t * @return {string} The sentence.\n\t\t */\n\t\tsourceTextFor(event, channel, scope = GLOBAL_SCOPE) {\n\t\t\tconst locked = this.lockReasonFor(event, channel, scope)\n\t\t\tif (locked !== '') {\n\t\t\t\treturn locked\n\t\t\t}\n\n\t\t\tconst narrow = valueForScope({\n\t\t\t\tvalues: this.scopedValues,\n\t\t\t\teventId: event?.id,\n\t\t\t\tchannelId: channel?.id,\n\t\t\t\tscope,\n\t\t\t})\n\t\t\tif (scope !== GLOBAL_SCOPE && narrow.scope === GLOBAL_SCOPE) {\n\t\t\t\t// Said out loud, because an unanswered narrower question looks\n\t\t\t\t// exactly like an answered one: the checkbox shows the same\n\t\t\t\t// state either way.\n\t\t\t\treturn t('nextcloud-vue', 'Follows the row above')\n\t\t\t}\n\n\t\t\tconst level = this.cellFor(event, channel, scope).level\n\t\t\tif (level === PREFERENCE_LEVELS.PERSONAL) {\n\t\t\t\treturn t('nextcloud-vue', 'Your choice')\n\t\t\t}\n\t\t\tif (level === PREFERENCE_LEVELS.GROUP) {\n\t\t\t\treturn t('nextcloud-vue', 'Follows your group')\n\t\t\t}\n\t\t\treturn t('nextcloud-vue', 'Follows the app default')\n\t\t},\n\n\t\t/**\n\t\t * Whether a group is collapsed.\n\t\t *\n\t\t * @param {string} key The group.\n\t\t * @return {boolean} True when collapsed.\n\t\t */\n\t\tisCollapsed(key) {\n\t\t\treturn this.collapsed.includes(key)\n\t\t},\n\n\t\t/**\n\t\t * Collapse a group, or open it again.\n\t\t *\n\t\t * @param {string} key The group.\n\t\t */\n\t\ttoggleGroup(key) {\n\t\t\tthis.collapsed = this.isCollapsed(key)\n\t\t\t\t? this.collapsed.filter((entry) => entry !== key)\n\t\t\t\t: [...this.collapsed, key]\n\t\t},\n\n\t\t/**\n\t\t * A cell was set.\n\t\t *\n\t\t * @param {object} event The event.\n\t\t * @param {object} channel The channel.\n\t\t * @param {Event} domEvent The change event.\n\t\t * @param {string} scope The scope of the row it is on.\n\t\t */\n\t\tonToggle(event, channel, domEvent, scope = GLOBAL_SCOPE) {\n\t\t\t// Refuse from here too, not only in the template. A disabled\n\t\t\t// attribute is a rendering; this is the rule.\n\t\t\tif (this.isLockedFor(event, channel, scope)) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t/**\n\t\t\t * @event change A cell was set.\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('change', {\n\t\t\t\teventId: event.id,\n\t\t\t\tchannelId: channel.id,\n\t\t\t\tscope,\n\t\t\t\tvalue: domEvent?.target?.checked === true,\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * A scope was added to a row, from the row itself.\n\t\t *\n\t\t * @param {object} event The event.\n\t\t * @param {Event} domEvent The change event.\n\t\t */\n\t\tonAddScope(event, domEvent) {\n\t\t\tconst scope = String(domEvent?.target?.value ?? '')\n\t\t\tif (scope === '') {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Back to the placeholder, so the control reads as an action\n\t\t\t// rather than as a setting that now says something.\n\t\t\tif (domEvent?.target) {\n\t\t\t\tdomEvent.target.value = ''\n\t\t\t}\n\n\t\t\t/**\n\t\t\t * @event add-scope A row was narrowed.\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('add-scope', { eventId: event.id, scope })\n\t\t},\n\n\t\t/**\n\t\t * What a channel's digest is set to.\n\t\t *\n\t\t * @param {object} channel The channel.\n\t\t * @return {object} `{ mode, timeOfDay }`.\n\t\t */\n\t\tdigestFor(channel) {\n\t\t\tconst set = this.digest?.[channel?.id]\n\t\t\treturn {\n\t\t\t\tmode: String(set?.mode ?? 'off'),\n\t\t\t\ttimeOfDay: String(set?.timeOfDay ?? ''),\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The digest mode was changed.\n\t\t *\n\t\t * @param {object} channel The channel.\n\t\t * @param {Event} domEvent The change event.\n\t\t */\n\t\tonDigestMode(channel, domEvent) {\n\t\t\t/**\n\t\t\t * @event digest-change Emitted when a reader changes a channel's digest mode or its time of day. Payload: `{ channelId, mode, timeOfDay }`.\n\t\t\t */\n\t\t\tthis.$emit('digest-change', {\n\t\t\t\tchannelId: channel.id,\n\t\t\t\tmode: String(domEvent?.target?.value ?? 'off'),\n\t\t\t\ttimeOfDay: this.digestFor(channel).timeOfDay,\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The digest time was changed.\n\t\t *\n\t\t * @param {object} channel The channel.\n\t\t * @param {Event} domEvent The change event.\n\t\t */\n\t\tonDigestTime(channel, domEvent) {\n\t\t\tthis.$emit('digest-change', {\n\t\t\t\tchannelId: channel.id,\n\t\t\t\tmode: this.digestFor(channel).mode,\n\t\t\t\ttimeOfDay: String(domEvent?.target?.value ?? ''),\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The last test send on a channel, or null when there has been none.\n\t\t *\n\t\t * @param {object} channel The channel.\n\t\t * @return {?object} `{ ok, message }`.\n\t\t */\n\t\ttestResultFor(channel) {\n\t\t\tconst result = this.testResults?.[channel?.id]\n\t\t\treturn result === undefined || result === null ? null : result\n\t\t},\n\n\t\t/**\n\t\t * What a test send reported.\n\t\t *\n\t\t * @param {object} channel The channel.\n\t\t * @return {string} The sentence.\n\t\t */\n\t\ttestResultText(channel) {\n\t\t\tconst result = this.testResultFor(channel)\n\t\t\tif (result === null) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tif (String(result.message ?? '') !== '') {\n\t\t\t\treturn String(result.message)\n\t\t\t}\n\t\t\t// Only when the server sent no sentence of its own. A refusal\n\t\t\t// without a reason still has to say it was a refusal.\n\t\t\treturn result.ok === true\n\t\t\t\t? t('nextcloud-vue', 'The test arrived.')\n\t\t\t\t: t('nextcloud-vue', 'The test did not arrive.')\n\t\t},\n\n\t\t/**\n\t\t * What the test button is called.\n\t\t *\n\t\t * @param {object} channel The channel.\n\t\t * @return {string} The accessible name.\n\t\t */\n\t\ttestSendLabel(channel) {\n\t\t\treturn t('nextcloud-vue', 'Send a test over {channel}', { channel: channel.label })\n\t\t},\n\n\t\t/**\n\t\t * What the add-a-scope control is called.\n\t\t *\n\t\t * @param {object} event The event.\n\t\t * @return {string} The accessible name.\n\t\t */\n\t\taddScopeLabel(event) {\n\t\t\treturn t('nextcloud-vue', 'Set {event} for one kind of case', { event: event.label })\n\t\t},\n\n\t\t/**\n\t\t * A test send was asked for.\n\t\t *\n\t\t * @param {object} channel The channel.\n\t\t */\n\t\tonTestSend(channel) {\n\t\t\t/**\n\t\t\t * @event test-send Emitted when a reader asks for a test notification on one channel. Payload: `{ channelId }`.\n\t\t\t */\n\t\t\tthis.$emit('test-send', { channelId: channel.id })\n\t\t},\n\t},\n}\n</script>\n\n<style scoped lang=\"scss\">\n.cn-notification-matrix__table {\n\tinline-size: 100%;\n\tborder-collapse: collapse;\n}\n\n.cn-notification-matrix__caption {\n\ttext-align: start;\n\tpadding-block-end: 8px;\n}\n\n.cn-notification-matrix__table th,\n.cn-notification-matrix__table td {\n\ttext-align: start;\n\tpadding: 6px 8px;\n\tborder-block-end: 1px solid var(--color-border);\n\tvertical-align: top;\n}\n\n.cn-notification-matrix__muted {\n\tdisplay: block;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85rem;\n}\n\n.cn-notification-matrix__group {\n\tbackground: none;\n\tborder: none;\n\tfont-weight: bold;\n\tpadding: 8px 0;\n\tcursor: pointer;\n\tcolor: var(--color-main-text);\n}\n\n.cn-notification-matrix__scope {\n\tpadding-inline-start: 24px !important;\n\tfont-weight: normal;\n}\n\n.cn-notification-matrix__row--scoped th,\n.cn-notification-matrix__row--scoped td {\n\tborder-block-end-style: dotted;\n}\n\n.cn-notification-matrix__test {\n\tmargin-block-start: 4px;\n}\n\n.cn-notification-matrix__note {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n/* The board look (DqPersoonlijkeInstellingen \"Meldingen\"): muted 13px/600\n headers over a hairline, the channel columns 96px with a centred 20px\n checkbox, the event rows 10px high with the soft rule. The caption stays for\n assistive technology. */\n.cn-notification-matrix--board {\n\t.cn-notification-matrix__caption {\n\t\tposition: absolute;\n\t\tinline-size: 1px;\n\t\tblock-size: 1px;\n\t\toverflow: hidden;\n\t\tclip-path: inset(50%);\n\t\twhite-space: nowrap;\n\t}\n\n\t/* A theme that colours every th (thematiq's table header token) leaves\n\t the board grid's head white. */\n\t.cn-notification-matrix__table {\n\t\t--nldesign-component-table-header-background-color: transparent;\n\t\tfont-size: 14px;\n\t}\n\n\t.cn-notification-matrix__table thead th {\n\t\tpadding: 8px 10px 8px 0;\n\t\tline-height: 1.2;\n\t\tborder-block-end: 1px solid var(--color-border);\n\t\tcolor: var(--color-text-maxcontrast);\n\t\tfont-size: 13px;\n\t\tfont-weight: 600;\n\t\tvertical-align: bottom;\n\t}\n\n\t.cn-notification-matrix__table thead th[data-channel] {\n\t\tinline-size: 96px;\n\t\tpadding-inline: 4px;\n\t\ttext-align: center;\n\t}\n\n\t.cn-notification-matrix__table tbody th,\n\t.cn-notification-matrix__table tbody td {\n\t\tpadding: 10px 10px 10px 0;\n\t\tborder-block-end: 1px solid var(--color-background-dark);\n\t\tfont-weight: 400;\n\t\tline-height: 1.4;\n\t\tvertical-align: middle;\n\t}\n\n\t.cn-notification-matrix__table tbody td {\n\t\tpadding-inline: 4px;\n\t\ttext-align: center;\n\t}\n\n\t.cn-notification-matrix__table tbody td input[type='checkbox'] {\n\t\tinline-size: 20px;\n\t\tmin-inline-size: 20px;\n\t\tblock-size: 20px;\n\t\tmin-block-size: 20px;\n\t\t/* A block box, so the line's baseline adds no height: the row is\n\t\t 42px as on the board (10px, the 1px-low checkbox, 10px, the rule). */\n\t\tdisplay: block;\n\t\tmargin: 1px auto 0;\n\t}\n\n\t.cn-notification-matrix__group {\n\t\tpadding: 4px 0;\n\t\tfont-size: 13px;\n\t\tfont-weight: 600;\n\t\tcolor: var(--color-text-maxcontrast);\n\t}\n\n\t.cn-notification-matrix__channels {\n\t\tmargin-block-start: 16px;\n\t}\n\n\t.cn-notification-matrix__channels-summary {\n\t\tfont-size: 14px;\n\t\tfont-weight: 600;\n\t\tcursor: pointer;\n\t}\n\n\t.cn-notification-matrix__channel {\n\t\tdisplay: flex;\n\t\tflex-wrap: wrap;\n\t\talign-items: flex-end;\n\t\tgap: 8px 16px;\n\t\tpadding-block: 10px;\n\t\tborder-block-end: 1px solid var(--color-background-dark);\n\t}\n\n\t.cn-notification-matrix__channel-name {\n\t\tflex: 1 0 100%;\n\t\tmargin: 0;\n\t\tfont-size: 14px;\n\t\tfont-weight: 600;\n\t}\n}\n</style>\n"],"names":["t"],"mappings":"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"}