@conduction/nextcloud-vue 2.148.0 → 2.149.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 (101) hide show
  1. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue.js +1 -1
  2. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +24 -7
  3. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
  4. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
  5. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue.js +1 -1
  6. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js +12 -1
  7. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js.map +1 -1
  8. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js +2 -1
  9. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js.map +1 -1
  10. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue.js +1 -1
  11. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue2.js +23 -1
  12. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue2.js.map +1 -1
  13. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue3.js +1 -1
  14. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue3.js.map +1 -1
  15. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +34 -1
  16. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  17. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  18. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue.js +1 -1
  19. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue2.js.map +1 -1
  20. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue3.js.map +1 -1
  21. package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue.js +1 -1
  22. package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue2.js.map +1 -1
  23. package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue3.js.map +1 -1
  24. package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue.js +1 -1
  25. package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue2.js +64 -8
  26. package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue2.js.map +1 -1
  27. package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue3.js +14 -4
  28. package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue3.js.map +1 -1
  29. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  30. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +26 -3
  31. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  32. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +10 -2
  33. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  34. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js +1 -1
  35. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +84 -1
  36. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
  37. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +61 -33
  38. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
  39. package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue.js +1 -1
  40. package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue2.js +49 -2
  41. package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue2.js.map +1 -1
  42. package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue3.js +90 -59
  43. package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue3.js.map +1 -1
  44. package/dist/esm/components/CnFollowingWidgetHost/CnFollowingWidgetHost.vue2.js +3 -1
  45. package/dist/esm/components/CnFollowingWidgetHost/CnFollowingWidgetHost.vue2.js.map +1 -1
  46. package/dist/esm/components/CnFollowingWidgetHost/CnFollowingWidgetHost.vue3.js.map +1 -1
  47. package/dist/esm/components/CnNotesCard/CnNotesCard.vue.js +1 -1
  48. package/dist/esm/components/CnNotesCard/CnNotesCard.vue2.js +10 -0
  49. package/dist/esm/components/CnNotesCard/CnNotesCard.vue2.js.map +1 -1
  50. package/dist/esm/components/CnNotesCard/CnNotesCard.vue3.js.map +1 -1
  51. package/dist/esm/components/CnObjectCard/CnObjectCard.vue.js +1 -1
  52. package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js +11 -0
  53. package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js.map +1 -1
  54. package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js +28 -21
  55. package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js.map +1 -1
  56. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
  57. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +1 -1
  58. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  59. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  60. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  61. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +3 -15
  62. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  63. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +123 -115
  64. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  65. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue.js +1 -1
  66. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js +4 -0
  67. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js.map +1 -1
  68. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js.map +1 -1
  69. package/dist/esm/l10n/en.json.js +2 -1
  70. package/dist/esm/l10n/en.json.js.map +1 -1
  71. package/dist/esm/l10n/nl.json.js +2 -1
  72. package/dist/esm/l10n/nl.json.js.map +1 -1
  73. package/dist/esm/library-style.js +1 -1
  74. package/dist/esm/nextcloud-vue.css +677 -444
  75. package/dist/nextcloud-vue.cjs.js +1336 -942
  76. package/dist/nextcloud-vue.cjs.js.map +2 -2
  77. package/dist/nextcloud-vue.css +677 -444
  78. package/l10n/en.json +2 -1
  79. package/l10n/nl.json +2 -1
  80. package/package.json +1 -1
  81. package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +24 -7
  82. package/src/components/CnAuditTrailWidget/CnAuditTrailWidget.vue +13 -1
  83. package/src/components/CnChecklistRunWidget/CnChecklistRunWidget.vue +29 -2
  84. package/src/components/CnDataTable/CnDataTable.vue +34 -1
  85. package/src/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue +6 -1
  86. package/src/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue +1 -0
  87. package/src/components/CnDuplicateWarning/CnDuplicateWarning.vue +74 -10
  88. package/src/components/CnFactsWidget/CnFactsWidget.vue +35 -3
  89. package/src/components/CnFieldListWidget/CnFieldListWidget.vue +131 -3
  90. package/src/components/CnFlowRunsWidget/CnFlowRunsWidget.vue +103 -5
  91. package/src/components/CnFollowingWidgetHost/CnFollowingWidgetHost.vue +3 -1
  92. package/src/components/CnNotesCard/CnNotesCard.vue +10 -0
  93. package/src/components/CnObjectCard/CnObjectCard.vue +16 -0
  94. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +1 -1
  95. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +19 -19
  96. package/src/components/CnRegisterMapping/CnRegisterMapping.vue +4 -0
  97. package/src/css/dialog.css +17 -0
  98. package/src/css/look-board-detail.css +52 -3
  99. package/src/css/look-board-index.css +37 -1
  100. package/src/css/look-board.css +45 -5
  101. package/src/css/table.css +5 -2
@@ -2,7 +2,7 @@ import script from './CnDetailWidgetHeaderItems.vue2.js';
2
2
  import { render } from './CnDetailWidgetHeaderItems.vue3.js';
3
3
 
4
4
  script.render = render;
5
- script.__scopeId = "data-v-c81df510";
5
+ script.__scopeId = "data-v-03e0f8bc";
6
6
  const __sfc_main = script;
7
7
 
8
8
  export { __sfc_main as default };
@@ -1 +1 @@
1
- {"version":3,"file":"CnDetailWidgetHeaderItems.vue2.js","sources":["../../../../src/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue"],"sourcesContent":["<!--\n SPDX-FileCopyrightText: 2026 Conduction B.V.\n SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<span class=\"cn-detail-widget-header-items\">\n\t\t<CnStatusBadge\n\t\t\tv-if=\"header.pill\"\n\t\t\t:label=\"header.pill.label\"\n\t\t\t:variant=\"header.pill.variant\"\n\t\t\tdata-testid=\"cn-detail-widget-header-pill\" />\n\t\t<span\n\t\t\tv-if=\"header.note\"\n\t\t\tclass=\"cn-detail-page__catalog-note\"\n\t\t\tdata-testid=\"cn-detail-widget-header-note\">{{ header.note }}</span>\n\t\t<template v-for=\"(action, index) in header.actions\" :key=\"`${index}-${action.label}`\">\n\t\t\t<!-- `variant: 'primary'`: the card's main action as a filled button\n\t\t\t (a task table's \"New task\"); otherwise the quiet header link. -->\n\t\t\t<NcButton\n\t\t\t\tv-if=\"action.variant === 'primary'\"\n\t\t\t\tvariant=\"primary\"\n\t\t\t\tclass=\"cn-detail-widget-header-items__primary\"\n\t\t\t\tdata-testid=\"cn-detail-widget-header-action\"\n\t\t\t\t@click=\"action.run()\">\n\t\t\t\t<template v-if=\"action.icon\" #icon>\n\t\t\t\t\t<CnIcon :name=\"action.icon\" :size=\"20\" />\n\t\t\t\t</template>\n\t\t\t\t{{ action.label }}\n\t\t\t</NcButton>\n\t\t\t<button\n\t\t\t\tv-else\n\t\t\t\ttype=\"button\"\n\t\t\t\tclass=\"cn-detail-page__catalog-action\"\n\t\t\t\t:class=\"{ 'cn-detail-page__catalog-action--add': action.add === true }\"\n\t\t\t\tdata-testid=\"cn-detail-widget-header-action\"\n\t\t\t\t@click=\"action.run()\">\n\t\t\t\t<CnIcon v-if=\"action.icon\" :name=\"action.icon\" :size=\"16\" />\n\t\t\t\t{{ action.label }}\n\t\t\t</button>\n\t\t</template>\n\t</span>\n</template>\n\n<script>\nimport { NcButton } from '@nextcloud/vue'\nimport CnIcon from '../CnIcon/CnIcon.vue'\nimport CnStatusBadge from '../CnStatusBadge/CnStatusBadge.vue'\n\n/**\n * The extras a widget reports for its card's title row through its `header`\n * event: a status pill, a note and its actions. Drawn by CnDetailWidgetHost in\n * the `#actions` slot of the card it wraps a widget in.\n */\nexport default {\n\tname: 'CnDetailWidgetHeaderItems',\n\n\tcomponents: { CnIcon, CnStatusBadge, NcButton },\n\n\tprops: {\n\t\t/** The reported header: `{ pill, note, actions: [{ label, icon?, variant?, add?, run }] }`; `add: true` marks a list's Add. */\n\t\theader: {\n\t\t\ttype: Object,\n\t\t\trequired: true,\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-detail-widget-header-items {\n\tdisplay: inline-flex;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 12px;\n}\n\n.cn-detail-page__catalog-action {\n\talign-items: center;\n\tbackground: var(--color-primary-element-light);\n\tborder: 0;\n\tborder-radius: 16px;\n\tcolor: var(--color-primary-element-light-text, var(--color-primary-element));\n\tcursor: pointer;\n\tdisplay: inline-flex;\n\tfont: inherit;\n\tfont-size: 13px;\n\tfont-weight: 700;\n\tgap: 6px;\n\tmargin: 0;\n\tmin-height: 32px;\n\tpadding: 0 14px;\n\twhite-space: nowrap;\n}\n</style>\n"],"names":["CnIcon","CnStatusBadge"],"mappings":";;;;AAgDA;;AAEC;AACA;;AAED,aAAe;AACd,CAAA,IAAI,EAAE,2BAA2B;;AAEjC,CAAA,UAAU,EAAE,UAAEA,UAAM,iBAAEC,YAAa,EAAE,UAAU;;AAE/C,CAAA,KAAK,EAAE;;AAEN,EAAA,MAAM,EAAE;GACP,IAAI,EAAE,MAAM;GACZ,QAAQ,EAAE,IAAI;GACd;EACD;AACF;;;;"}
1
+ {"version":3,"file":"CnDetailWidgetHeaderItems.vue2.js","sources":["../../../../src/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue"],"sourcesContent":["<!--\n SPDX-FileCopyrightText: 2026 Conduction B.V.\n SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<span class=\"cn-detail-widget-header-items\">\n\t\t<CnStatusBadge\n\t\t\tv-if=\"header.pill\"\n\t\t\t:label=\"header.pill.label\"\n\t\t\t:variant=\"header.pill.variant\"\n\t\t\tdata-testid=\"cn-detail-widget-header-pill\" />\n\t\t<span\n\t\t\tv-if=\"header.note\"\n\t\t\tclass=\"cn-detail-page__catalog-note\"\n\t\t\tdata-testid=\"cn-detail-widget-header-note\">{{ header.note }}</span>\n\t\t<template v-for=\"(action, index) in header.actions\" :key=\"`${index}-${action.label}`\">\n\t\t\t<!-- `variant: 'primary'`: the card's main action as a filled button\n\t\t\t (a task table's \"New task\"); otherwise the quiet header link. -->\n\t\t\t<NcButton\n\t\t\t\tv-if=\"action.variant === 'primary'\"\n\t\t\t\tvariant=\"primary\"\n\t\t\t\tclass=\"cn-detail-widget-header-items__primary\"\n\t\t\t\tdata-testid=\"cn-detail-widget-header-action\"\n\t\t\t\t@click=\"action.run()\">\n\t\t\t\t<template v-if=\"action.icon\" #icon>\n\t\t\t\t\t<CnIcon :name=\"action.icon\" :size=\"20\" />\n\t\t\t\t</template>\n\t\t\t\t{{ action.label }}\n\t\t\t</NcButton>\n\t\t\t<button\n\t\t\t\tv-else\n\t\t\t\ttype=\"button\"\n\t\t\t\tclass=\"cn-detail-page__catalog-action\"\n\t\t\t\t:class=\"{ 'cn-detail-page__catalog-action--add': action.add === true }\"\n\t\t\t\tdata-testid=\"cn-detail-widget-header-action\"\n\t\t\t\t@click=\"action.run()\">\n\t\t\t\t<CnIcon v-if=\"action.icon\" :name=\"action.icon\" :size=\"16\" />\n\t\t\t\t{{ action.label }}\n\t\t\t</button>\n\t\t</template>\n\t</span>\n</template>\n\n<script>\nimport { NcButton } from '@nextcloud/vue'\nimport CnIcon from '../CnIcon/CnIcon.vue'\nimport CnStatusBadge from '../CnStatusBadge/CnStatusBadge.vue'\n\n/**\n * The extras a widget reports for its card's title row through its `header`\n * event: a status pill, a note and its actions. Drawn by CnDetailWidgetHost in\n * the `#actions` slot of the card it wraps a widget in.\n */\nexport default {\n\tname: 'CnDetailWidgetHeaderItems',\n\n\tcomponents: { CnIcon, CnStatusBadge, NcButton },\n\n\tprops: {\n\t\t/** The reported header: `{ pill, note, actions: [{ label, icon?, variant?, add?, run }] }`; `add: true` marks a list's Add. */\n\t\theader: {\n\t\t\ttype: Object,\n\t\t\trequired: true,\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-detail-widget-header-items {\n\tdisplay: inline-flex;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 12px;\n}\n\n.cn-detail-page__catalog-action {\n\talign-items: center;\n\tbackground: var(--color-primary-element-light);\n\tborder: 0;\n\tborder-radius: 16px;\n\tcolor: var(--color-primary-element-light-text, var(--color-primary-element));\n\tcursor: pointer;\n\tdisplay: inline-flex;\n\tfont: inherit;\n\tfont-size: 13px;\n\tfont-weight: 700;\n\tgap: 6px;\n\tmin-height: 32px;\n\tpadding: 0 14px;\n\twhite-space: nowrap;\n}\n\n/* server.css gives every button `margin: 3px 3px 3px 0` at (0,2,1); the\n doubled class outranks it. */\n.cn-detail-page__catalog-action.cn-detail-page__catalog-action {\n\tmargin: 0;\n}\n</style>\n"],"names":["CnIcon","CnStatusBadge"],"mappings":";;;;AAgDA;;AAEC;AACA;;AAED,aAAe;AACd,CAAA,IAAI,EAAE,2BAA2B;;AAEjC,CAAA,UAAU,EAAE,UAAEA,UAAM,iBAAEC,YAAa,EAAE,UAAU;;AAE/C,CAAA,KAAK,EAAE;;AAEN,EAAA,MAAM,EAAE;GACP,IAAI,EAAE,MAAM;GACZ,QAAQ,EAAE,IAAI;GACd;EACD;AACF;;;;"}
@@ -1 +1 @@
1
- {"version":3,"file":"CnDetailWidgetHeaderItems.vue3.js","sources":["../../../../src/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue?vue&type=template&id=c81df510&lang.js"],"sourcesContent":["<!--\n SPDX-FileCopyrightText: 2026 Conduction B.V.\n SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<span class=\"cn-detail-widget-header-items\">\n\t\t<CnStatusBadge\n\t\t\tv-if=\"header.pill\"\n\t\t\t:label=\"header.pill.label\"\n\t\t\t:variant=\"header.pill.variant\"\n\t\t\tdata-testid=\"cn-detail-widget-header-pill\" />\n\t\t<span\n\t\t\tv-if=\"header.note\"\n\t\t\tclass=\"cn-detail-page__catalog-note\"\n\t\t\tdata-testid=\"cn-detail-widget-header-note\">{{ header.note }}</span>\n\t\t<template v-for=\"(action, index) in header.actions\" :key=\"`${index}-${action.label}`\">\n\t\t\t<!-- `variant: 'primary'`: the card's main action as a filled button\n\t\t\t (a task table's \"New task\"); otherwise the quiet header link. -->\n\t\t\t<NcButton\n\t\t\t\tv-if=\"action.variant === 'primary'\"\n\t\t\t\tvariant=\"primary\"\n\t\t\t\tclass=\"cn-detail-widget-header-items__primary\"\n\t\t\t\tdata-testid=\"cn-detail-widget-header-action\"\n\t\t\t\t@click=\"action.run()\">\n\t\t\t\t<template v-if=\"action.icon\" #icon>\n\t\t\t\t\t<CnIcon :name=\"action.icon\" :size=\"20\" />\n\t\t\t\t</template>\n\t\t\t\t{{ action.label }}\n\t\t\t</NcButton>\n\t\t\t<button\n\t\t\t\tv-else\n\t\t\t\ttype=\"button\"\n\t\t\t\tclass=\"cn-detail-page__catalog-action\"\n\t\t\t\t:class=\"{ 'cn-detail-page__catalog-action--add': action.add === true }\"\n\t\t\t\tdata-testid=\"cn-detail-widget-header-action\"\n\t\t\t\t@click=\"action.run()\">\n\t\t\t\t<CnIcon v-if=\"action.icon\" :name=\"action.icon\" :size=\"16\" />\n\t\t\t\t{{ action.label }}\n\t\t\t</button>\n\t\t</template>\n\t</span>\n</template>\n\n<script>\nimport { NcButton } from '@nextcloud/vue'\nimport CnIcon from '../CnIcon/CnIcon.vue'\nimport CnStatusBadge from '../CnStatusBadge/CnStatusBadge.vue'\n\n/**\n * The extras a widget reports for its card's title row through its `header`\n * event: a status pill, a note and its actions. Drawn by CnDetailWidgetHost in\n * the `#actions` slot of the card it wraps a widget in.\n */\nexport default {\n\tname: 'CnDetailWidgetHeaderItems',\n\n\tcomponents: { CnIcon, CnStatusBadge, NcButton },\n\n\tprops: {\n\t\t/** The reported header: `{ pill, note, actions: [{ label, icon?, variant?, add?, run }] }`; `add: true` marks a list's Add. */\n\t\theader: {\n\t\t\ttype: Object,\n\t\t\trequired: true,\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-detail-widget-header-items {\n\tdisplay: inline-flex;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 12px;\n}\n\n.cn-detail-page__catalog-action {\n\talign-items: center;\n\tbackground: var(--color-primary-element-light);\n\tborder: 0;\n\tborder-radius: 16px;\n\tcolor: var(--color-primary-element-light-text, var(--color-primary-element));\n\tcursor: pointer;\n\tdisplay: inline-flex;\n\tfont: inherit;\n\tfont-size: 13px;\n\tfont-weight: 700;\n\tgap: 6px;\n\tmargin: 0;\n\tmin-height: 32px;\n\tpadding: 0 14px;\n\twhite-space: nowrap;\n}\n</style>\n"],"names":["_createBlock","_openBlock","_createElementBlock","_toDisplayString","_createVNode","_normalizeClass"],"mappings":";;AASO,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,+BAAmB,GAAA;;;OAQxB,EAAC,8BAA8B;AACpC,EAAA,aAAQ,EAAA,8BAAA;;;;;;;;;;AAPL,IAAA,CAAA,MAAA,CAAA,MAAA,CAAA,IAAA;sBADHA,WAI8C,CAAA,wBAAA,EAAA;;AAF7C,UAAA,KAAK,EAAE,MAAA,CAAA,MAAM,CAAC,IAAE,CAAA,KAAA;AAChB,UAAA,OAAO,EAAE,MAAA,CAAA,MAAM,CAAC,IAAI,CAAC,OAAO;AAC7B,UAAA,aAAW,EAAC,8BAA8B;;;AAEpC,IAAA,CAAA,MAAA,CAAA,MAAM,CAAC,IAAI;AADlB,SAAAC,SAAA,EAAA,EAAAC,kBAAA,CAGS,MAHT,EAAA,UAGS,EAAAC,eAAA,CAAA,MAAA,CAAA,MAAA,CAAA,IAAA,CAAA,EAAA,CAAA,YAAA;;AACP,KAAAF,SAAA,CAAA,IAAA,CAAA,EAAAC,kBAAA,4BAAkC,MAAA,CAAA,MAAA,CAAA,OAAA,EAAlB,CAAA,MAAM,EAAG,KAAI,KAAA;;gBAAK,KAAA,CAAA,CAAA,EAAA,MAAA,CAAA,KAAA,CAAA,CAAA;;AAI5B,QAAA,CAAA,MAAM,CAAC,OAAO,KAAA,SAAA;0BADpBF,WAUU,CAAA,mBAAA,EAAA;;AARV,cAAA,OAAO,EAAC,SAAS;AAChB,cAAA,KAAI,EAAC,wCAAmC;AACzC,cAAA,aAAU,EAAA,gCAAA;cACT,OAAK,EAAA,MAAA,KAAE,MAAM,IAAG,EAAA,CAAA;;+BAGZ,MACL;gCADK,GACL,GAAAG,eAAA,CAAG,MAAM,CAAC,KAAE,CAAA,EAAA,CAAA,YAAA;;;;AAHH,cAAA,CAAA,MAAA,CAAA,IAAA;;0BAAA,MAAA;gCACR,MAAI;sBAAJC,WAAI,CAAA,iBAAA,EAAA;wBAAA,IAAA,EAAA,MAAA,CAAA,IAAA;AAAA,wBAAA,IAAA,EAAA,EAAA;;;;;;;0BAILF;;AAEA,cAAA,IAAI,EAAC,QAAQ;AACb,cAAA,KAAK,EAACG,cAAA,CAAA,CAAA,gCAAgC,EACrB,EAAA,qCAAA,EAAA,MAAA,CAAA,GAAA,KAAA,IAAA,EAAA,CAAA,CAAA;AACjB,cAAA,aAAO,EAAA,gCAAA;cACN,OAAK,EAAA,MAAA,KAAE,MAAA,CAAA,GAAA,EAAA,CAAA;;AACL,cAAA,CAAA,MAAA,CAAA,IAAA;gCAAHL,WAAG,CAAA,iBAAA,EAAA;;oBAAA,IAAA,EAAA,MAAA,CAAA,IAAA;AAAA,oBAAA,IAAA,EAAA,EAAA;;;8BAAA;;;;;;;;;"}
1
+ {"version":3,"file":"CnDetailWidgetHeaderItems.vue3.js","sources":["../../../../src/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue?vue&type=template&id=03e0f8bc&lang.js"],"sourcesContent":["<!--\n SPDX-FileCopyrightText: 2026 Conduction B.V.\n SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<span class=\"cn-detail-widget-header-items\">\n\t\t<CnStatusBadge\n\t\t\tv-if=\"header.pill\"\n\t\t\t:label=\"header.pill.label\"\n\t\t\t:variant=\"header.pill.variant\"\n\t\t\tdata-testid=\"cn-detail-widget-header-pill\" />\n\t\t<span\n\t\t\tv-if=\"header.note\"\n\t\t\tclass=\"cn-detail-page__catalog-note\"\n\t\t\tdata-testid=\"cn-detail-widget-header-note\">{{ header.note }}</span>\n\t\t<template v-for=\"(action, index) in header.actions\" :key=\"`${index}-${action.label}`\">\n\t\t\t<!-- `variant: 'primary'`: the card's main action as a filled button\n\t\t\t (a task table's \"New task\"); otherwise the quiet header link. -->\n\t\t\t<NcButton\n\t\t\t\tv-if=\"action.variant === 'primary'\"\n\t\t\t\tvariant=\"primary\"\n\t\t\t\tclass=\"cn-detail-widget-header-items__primary\"\n\t\t\t\tdata-testid=\"cn-detail-widget-header-action\"\n\t\t\t\t@click=\"action.run()\">\n\t\t\t\t<template v-if=\"action.icon\" #icon>\n\t\t\t\t\t<CnIcon :name=\"action.icon\" :size=\"20\" />\n\t\t\t\t</template>\n\t\t\t\t{{ action.label }}\n\t\t\t</NcButton>\n\t\t\t<button\n\t\t\t\tv-else\n\t\t\t\ttype=\"button\"\n\t\t\t\tclass=\"cn-detail-page__catalog-action\"\n\t\t\t\t:class=\"{ 'cn-detail-page__catalog-action--add': action.add === true }\"\n\t\t\t\tdata-testid=\"cn-detail-widget-header-action\"\n\t\t\t\t@click=\"action.run()\">\n\t\t\t\t<CnIcon v-if=\"action.icon\" :name=\"action.icon\" :size=\"16\" />\n\t\t\t\t{{ action.label }}\n\t\t\t</button>\n\t\t</template>\n\t</span>\n</template>\n\n<script>\nimport { NcButton } from '@nextcloud/vue'\nimport CnIcon from '../CnIcon/CnIcon.vue'\nimport CnStatusBadge from '../CnStatusBadge/CnStatusBadge.vue'\n\n/**\n * The extras a widget reports for its card's title row through its `header`\n * event: a status pill, a note and its actions. Drawn by CnDetailWidgetHost in\n * the `#actions` slot of the card it wraps a widget in.\n */\nexport default {\n\tname: 'CnDetailWidgetHeaderItems',\n\n\tcomponents: { CnIcon, CnStatusBadge, NcButton },\n\n\tprops: {\n\t\t/** The reported header: `{ pill, note, actions: [{ label, icon?, variant?, add?, run }] }`; `add: true` marks a list's Add. */\n\t\theader: {\n\t\t\ttype: Object,\n\t\t\trequired: true,\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-detail-widget-header-items {\n\tdisplay: inline-flex;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 12px;\n}\n\n.cn-detail-page__catalog-action {\n\talign-items: center;\n\tbackground: var(--color-primary-element-light);\n\tborder: 0;\n\tborder-radius: 16px;\n\tcolor: var(--color-primary-element-light-text, var(--color-primary-element));\n\tcursor: pointer;\n\tdisplay: inline-flex;\n\tfont: inherit;\n\tfont-size: 13px;\n\tfont-weight: 700;\n\tgap: 6px;\n\tmin-height: 32px;\n\tpadding: 0 14px;\n\twhite-space: nowrap;\n}\n\n/* server.css gives every button `margin: 3px 3px 3px 0` at (0,2,1); the\n doubled class outranks it. */\n.cn-detail-page__catalog-action.cn-detail-page__catalog-action {\n\tmargin: 0;\n}\n</style>\n"],"names":["_createBlock","_openBlock","_createElementBlock","_toDisplayString","_createVNode","_normalizeClass"],"mappings":";;AASO,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,+BAAmB,GAAA;;;OAQxB,EAAC,8BAA8B;AACpC,EAAA,aAAQ,EAAA,8BAAA;;;;;;;;;;AAPL,IAAA,CAAA,MAAA,CAAA,MAAA,CAAA,IAAA;sBADHA,WAI8C,CAAA,wBAAA,EAAA;;AAF7C,UAAA,KAAK,EAAE,MAAA,CAAA,MAAM,CAAC,IAAE,CAAA,KAAA;AAChB,UAAA,OAAO,EAAE,MAAA,CAAA,MAAM,CAAC,IAAI,CAAC,OAAO;AAC7B,UAAA,aAAW,EAAC,8BAA8B;;;AAEpC,IAAA,CAAA,MAAA,CAAA,MAAM,CAAC,IAAI;AADlB,SAAAC,SAAA,EAAA,EAAAC,kBAAA,CAGS,MAHT,EAAA,UAGS,EAAAC,eAAA,CAAA,MAAA,CAAA,MAAA,CAAA,IAAA,CAAA,EAAA,CAAA,YAAA;;AACP,KAAAF,SAAA,CAAA,IAAA,CAAA,EAAAC,kBAAA,4BAAkC,MAAA,CAAA,MAAA,CAAA,OAAA,EAAlB,CAAA,MAAM,EAAG,KAAI,KAAA;;gBAAK,KAAA,CAAA,CAAA,EAAA,MAAA,CAAA,KAAA,CAAA,CAAA;;AAI5B,QAAA,CAAA,MAAM,CAAC,OAAO,KAAA,SAAA;0BADpBF,WAUU,CAAA,mBAAA,EAAA;;AARV,cAAA,OAAO,EAAC,SAAS;AAChB,cAAA,KAAI,EAAC,wCAAmC;AACzC,cAAA,aAAU,EAAA,gCAAA;cACT,OAAK,EAAA,MAAA,KAAE,MAAM,IAAG,EAAA,CAAA;;+BAGZ,MACL;gCADK,GACL,GAAAG,eAAA,CAAG,MAAM,CAAC,KAAE,CAAA,EAAA,CAAA,YAAA;;;;AAHH,cAAA,CAAA,MAAA,CAAA,IAAA;;0BAAA,MAAA;gCACR,MAAI;sBAAJC,WAAI,CAAA,iBAAA,EAAA;wBAAA,IAAA,EAAA,MAAA,CAAA,IAAA;AAAA,wBAAA,IAAA,EAAA,EAAA;;;;;;;0BAILF;;AAEA,cAAA,IAAI,EAAC,QAAQ;AACb,cAAA,KAAK,EAACG,cAAA,CAAA,CAAA,gCAAgC,EACrB,EAAA,qCAAA,EAAA,MAAA,CAAA,GAAA,KAAA,IAAA,EAAA,CAAA,CAAA;AACjB,cAAA,aAAO,EAAA,gCAAA;cACN,OAAK,EAAA,MAAA,KAAE,MAAA,CAAA,GAAA,EAAA,CAAA;;AACL,cAAA,CAAA,MAAA,CAAA,IAAA;gCAAHL,WAAG,CAAA,iBAAA,EAAA;;oBAAA,IAAA,EAAA,MAAA,CAAA,IAAA;AAAA,oBAAA,IAAA,EAAA,EAAA;;;8BAAA;;;;;;;;;"}
@@ -2,7 +2,7 @@ import script from './CnDeviceFramesWidget.vue2.js';
2
2
  import { render } from './CnDeviceFramesWidget.vue3.js';
3
3
 
4
4
  script.render = render;
5
- script.__scopeId = "data-v-05f76090";
5
+ script.__scopeId = "data-v-64bde7d3";
6
6
  const __sfc_main = script;
7
7
 
8
8
  export { __sfc_main as default };
@@ -1 +1 @@
1
- {"version":3,"file":"CnDeviceFramesWidget.vue2.js","sources":["../../../../src/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-device-frames-widget\" data-testid=\"cn-device-frames-widget\">\n\t\t<section\n\t\t\tv-for=\"frame in frames\"\n\t\t\t:key=\"frame.key\"\n\t\t\tclass=\"cn-device-frames-widget__frame\"\n\t\t\t:style=\"frameStyle\"\n\t\t\t:aria-labelledby=\"headingId(frame.key)\"\n\t\t\t:data-testid=\"`cn-device-frame-${frame.key}`\">\n\t\t\t<header class=\"cn-device-frames-widget__head\">\n\t\t\t\t<span\n\t\t\t\t\tv-if=\"frame.status\"\n\t\t\t\t\tclass=\"cn-device-frames-widget__status\"\n\t\t\t\t\t:class=\"`cn-device-frames-widget__status--${frame.status.tone}`\"\n\t\t\t\t\tdata-testid=\"cn-device-frame-status\">\n\t\t\t\t\t<component :is=\"frame.status.icon\" :size=\"16\" aria-hidden=\"true\" />\n\t\t\t\t\t{{ frame.status.text }}\n\t\t\t\t</span>\n\t\t\t\t<h3 :id=\"headingId(frame.key)\" class=\"cn-device-frames-widget__title\">\n\t\t\t\t\t{{ frame.title }}\n\t\t\t\t</h3>\n\t\t\t</header>\n\t\t\t<div class=\"cn-device-frames-widget__body\">\n\t\t\t\t<CnSectionsWidget\n\t\t\t\t\t:content=\"{ sections: frame.sections, cards: false, hideEmpty: true }\"\n\t\t\t\t\t:availableWidgets=\"availableWidgets\"\n\t\t\t\t\t:objectId=\"objectId\"\n\t\t\t\t\t:objectData=\"objectData\"\n\t\t\t\t\t:objectType=\"objectType\"\n\t\t\t\t\t:schemaObject=\"schemaObject\"\n\t\t\t\t\t:register=\"register\"\n\t\t\t\t\t:schema=\"schema\"\n\t\t\t\t\t:store=\"store\"\n\t\t\t\t\t:surface=\"surface\"\n\t\t\t\t\t:integrationContext=\"integrationContext\"\n\t\t\t\t\t:cnRegistry=\"cnRegistry\" />\n\t\t\t</div>\n\t\t</section>\n\t</div>\n</template>\n\n<script>\nimport { markRaw } from 'vue'\nimport CloudCheckOutline from 'vue-material-design-icons/CloudCheckOutline.vue'\nimport CloudOffOutline from 'vue-material-design-icons/CloudOffOutline.vue'\nimport InformationOutline from 'vue-material-design-icons/InformationOutline.vue'\nimport CnSectionsWidget from '../CnSectionsWidget/CnSectionsWidget.vue'\n\n/** Counter for unique heading ids across instances. */\nlet instanceCounter = 0\n\n/**\n * CnDeviceFramesWidget - the `device-frames` widget: several small pages side\n * by side, each in a phone frame with its own header (board DqInspectie,\n * \"Inspectie in het veld\": the day offline, a checklist question with a\n * photo, and the sync queue).\n *\n * Each frame stacks widgets of the surface, as the `sections` widget does,\n * under a header with the frame's title and an optional status line. The\n * status can follow the connection (\"Offline · synced at 07:42\" while the\n * device is offline). The frames wrap onto the next row when the page is\n * narrower, and a frame takes the full width on a phone.\n *\n * ```json\n * {\n * \"type\": \"device-frames\",\n * \"content\": {\n * \"width\": 350,\n * \"status\": { \"followConnection\": true, \"offlineText\": \"Offline · synced at 07:42\", \"onlineText\": \"Online\" },\n * \"frames\": [\n * { \"key\": \"today\", \"title\": \"Today\", \"widgetIds\": [\"today-runs\", \"offline-note\"] },\n * { \"key\": \"question\", \"title\": \"Kerkplein 7\", \"widgetIds\": [\"checklist-run\"] },\n * { \"key\": \"sync\", \"title\": \"Synchronise\", \"widgetIds\": [\"sync-queue\"] }\n * ]\n * }\n * }\n * ```\n *\n * @spec openspec/changes/device-frames-widget/specs/device-frames-widget/spec.md#requirement-a-page-can-show-several-small-pages-as-phone-frames\n */\nexport default {\n\tname: 'CnDeviceFramesWidget',\n\n\tcomponents: { CnSectionsWidget },\n\n\tinject: {\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget configuration.\n\t\t *\n\t\t * - `frames` — `[{ key?, title, widgetIds? | sections?, status? }]`, in order. `widgetIds` names\n\t\t * widgets of the surface, stacked; `sections` takes the `sections` widget's entries instead.\n\t\t * - `status` — the status line of every frame (a frame's own `status` wins): `{ text?, tone?,\n\t\t * followConnection?, onlineText?, offlineText? }`. With `followConnection` the line reads\n\t\t * `offlineText` while the device is offline and `onlineText` otherwise. `tone` is `warning`\n\t\t * (default while offline), `success` or `info`.\n\t\t * - `width` — the frame width in pixels (default 350).\n\t\t *\n\t\t * @type {{frames?: Array<object>, status?: object, width?: number}}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** Sibling widget definitions on the surface, for `widgetIds`. */\n\t\tavailableWidgets: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** The bound record's id, forwarded to the children. */\n\t\tobjectId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The loaded record, forwarded to the children. */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The resolved object-type slug. */\n\t\tobjectType: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The resolved JSON Schema object. */\n\t\tschemaObject: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** OpenRegister register slug of the surface. */\n\t\tregister: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** OpenRegister schema slug of the surface. */\n\t\tschema: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The effective object store. */\n\t\tstore: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Rendering surface forwarded to the children. */\n\t\tsurface: {\n\t\t\ttype: String,\n\t\t\tdefault: 'app-dashboard',\n\t\t},\n\n\t\t/** Object context forwarded to integration children. */\n\t\tintegrationContext: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The consumer's component registry, for custom child widget types. */\n\t\tcnRegistry: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\t},\n\n\tdata() {\n\t\tinstanceCounter += 1\n\t\treturn {\n\t\t\tuid: instanceCounter,\n\t\t\tonline: typeof navigator === 'undefined' || navigator.onLine !== false,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** @return {object} The frame width. */\n\t\tframeStyle() {\n\t\t\tconst width = Number(this.content?.width)\n\t\t\treturn { '--cn-device-frame-width': `${Number.isFinite(width) && width >= 240 ? width : 350}px` }\n\t\t},\n\n\t\t/**\n\t\t * The frames to draw: a key, a translated title, the sections and the\n\t\t * status line.\n\t\t *\n\t\t * @return {Array<{key: string, title: string, sections: Array<object>, status: object|null}>}\n\t\t */\n\t\tframes() {\n\t\t\tconst list = Array.isArray(this.content?.frames) ? this.content.frames : []\n\t\t\treturn list\n\t\t\t\t.filter((frame) => frame && typeof frame === 'object')\n\t\t\t\t.map((frame, index) => {\n\t\t\t\t\tconst sections = Array.isArray(frame.sections)\n\t\t\t\t\t\t? frame.sections\n\t\t\t\t\t\t: (Array.isArray(frame.widgetIds) ? frame.widgetIds : []).filter((id) => typeof id === 'string' && id !== '').map((widgetId) => ({ widgetId }))\n\t\t\t\t\treturn {\n\t\t\t\t\t\tkey: String(frame.key || `frame-${index}`).replace(/[^\\w-]/g, '-'),\n\t\t\t\t\t\ttitle: this.tr(frame.title || ''),\n\t\t\t\t\t\tsections,\n\t\t\t\t\t\tstatus: this.statusOf(frame.status || this.content?.status || null),\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t},\n\t},\n\n\tmounted() {\n\t\tif (typeof window !== 'undefined') {\n\t\t\twindow.addEventListener('online', this.onConnection)\n\t\t\twindow.addEventListener('offline', this.onConnection)\n\t\t}\n\t},\n\n\tbeforeUnmount() {\n\t\tif (typeof window !== 'undefined') {\n\t\t\twindow.removeEventListener('online', this.onConnection)\n\t\t\twindow.removeEventListener('offline', this.onConnection)\n\t\t}\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Translate through the app's translate.\n\t\t *\n\t\t * @param {string} key The source string.\n\t\t * @return {string}\n\t\t */\n\t\ttr(key) {\n\t\t\treturn key && typeof this.cnTranslate === 'function' ? this.cnTranslate(key) : (key || '')\n\t\t},\n\n\t\t/**\n\t\t * The status line of a frame, or null.\n\t\t *\n\t\t * @param {object|null} status The configured status.\n\t\t * @return {{text: string, tone: string, icon: object}|null}\n\t\t */\n\t\tstatusOf(status) {\n\t\t\tif (!status || typeof status !== 'object') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tlet text = status.text\n\t\t\tlet tone = status.tone\n\t\t\tif (status.followConnection === true) {\n\t\t\t\ttext = this.online ? status.onlineText : status.offlineText\n\t\t\t\ttone = tone || (this.online ? 'success' : 'warning')\n\t\t\t}\n\t\t\tif (typeof text !== 'string' || text === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst known = ['warning', 'success', 'info'].includes(tone) ? tone : 'info'\n\t\t\tconst icon = known === 'warning' ? CloudOffOutline : (known === 'success' ? CloudCheckOutline : InformationOutline)\n\t\t\treturn { text: this.tr(text), tone: known, icon: markRaw(icon) }\n\t\t},\n\n\t\t/** Follow the browser's connection state. */\n\t\tonConnection() {\n\t\t\tthis.online = typeof navigator === 'undefined' || navigator.onLine !== false\n\t\t},\n\n\t\t/**\n\t\t * The id of a frame's heading.\n\t\t *\n\t\t * @param {string} key The frame key.\n\t\t * @return {string}\n\t\t */\n\t\theadingId(key) {\n\t\t\treturn `cn-device-frame-${this.uid}-${key}`\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-device-frames-widget {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: flex-start;\n\tgap: 20px;\n}\n\n.cn-device-frames-widget__frame {\n\tdisplay: flex;\n\tflex: none;\n\tflex-direction: column;\n\twidth: var(--cn-device-frame-width, 350px);\n\tmax-width: 100%;\n\tmin-height: 600px;\n\tbox-sizing: border-box;\n\toverflow: hidden;\n\tborder: 10px solid var(--color-main-text);\n\tborder-radius: 36px;\n\tbackground: var(--color-background-hover);\n}\n\n.cn-device-frames-widget__head {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 6px;\n\tpadding: 14px 18px 10px;\n\tborder-bottom: 1px solid var(--color-border);\n\tbackground: var(--color-main-background);\n}\n\n.cn-device-frames-widget__status {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tfont-size: 13px;\n\tfont-weight: 600;\n}\n\n.cn-device-frames-widget__status--warning {\n\tcolor: var(--color-warning-text, var(--color-main-text));\n}\n\n.cn-device-frames-widget__status--success {\n\tcolor: var(--color-success-text, var(--color-main-text));\n}\n\n.cn-device-frames-widget__status--info {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-device-frames-widget__title {\n\tmargin: 0;\n\tfont-size: 20px;\n\tfont-weight: 700;\n}\n\n.cn-device-frames-widget__body {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n\tpadding: 14px;\n}\n\n/* On a phone the frame is the screen: no bezel, the full width. */\n@media (max-width: 640px) {\n\t.cn-device-frames-widget__frame {\n\t\twidth: 100%;\n\t\tmin-height: 0;\n\t\tborder-width: 0;\n\t\tborder-radius: var(--cn-board-card-radius, 12px);\n\t}\n}\n</style>\n"],"names":["CnSectionsWidget"],"mappings":"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"}
1
+ {"version":3,"file":"CnDeviceFramesWidget.vue2.js","sources":["../../../../src/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-device-frames-widget\" data-testid=\"cn-device-frames-widget\">\n\t\t<section\n\t\t\tv-for=\"frame in frames\"\n\t\t\t:key=\"frame.key\"\n\t\t\tclass=\"cn-device-frames-widget__frame\"\n\t\t\t:style=\"frameStyle\"\n\t\t\t:aria-labelledby=\"headingId(frame.key)\"\n\t\t\t:data-testid=\"`cn-device-frame-${frame.key}`\">\n\t\t\t<header class=\"cn-device-frames-widget__head\">\n\t\t\t\t<span\n\t\t\t\t\tv-if=\"frame.status\"\n\t\t\t\t\tclass=\"cn-device-frames-widget__status\"\n\t\t\t\t\t:class=\"`cn-device-frames-widget__status--${frame.status.tone}`\"\n\t\t\t\t\tdata-testid=\"cn-device-frame-status\">\n\t\t\t\t\t<component :is=\"frame.status.icon\" :size=\"16\" aria-hidden=\"true\" />\n\t\t\t\t\t{{ frame.status.text }}\n\t\t\t\t</span>\n\t\t\t\t<h3 :id=\"headingId(frame.key)\" class=\"cn-device-frames-widget__title\">\n\t\t\t\t\t{{ frame.title }}\n\t\t\t\t</h3>\n\t\t\t</header>\n\t\t\t<div class=\"cn-device-frames-widget__body\">\n\t\t\t\t<CnSectionsWidget\n\t\t\t\t\t:content=\"{ sections: frame.sections, cards: false, hideEmpty: true }\"\n\t\t\t\t\t:availableWidgets=\"availableWidgets\"\n\t\t\t\t\t:objectId=\"objectId\"\n\t\t\t\t\t:objectData=\"objectData\"\n\t\t\t\t\t:objectType=\"objectType\"\n\t\t\t\t\t:schemaObject=\"schemaObject\"\n\t\t\t\t\t:register=\"register\"\n\t\t\t\t\t:schema=\"schema\"\n\t\t\t\t\t:store=\"store\"\n\t\t\t\t\t:surface=\"surface\"\n\t\t\t\t\t:integrationContext=\"integrationContext\"\n\t\t\t\t\t:cnRegistry=\"cnRegistry\" />\n\t\t\t</div>\n\t\t</section>\n\t</div>\n</template>\n\n<script>\nimport { markRaw } from 'vue'\nimport CloudCheckOutline from 'vue-material-design-icons/CloudCheckOutline.vue'\nimport CloudOffOutline from 'vue-material-design-icons/CloudOffOutline.vue'\nimport InformationOutline from 'vue-material-design-icons/InformationOutline.vue'\nimport CnSectionsWidget from '../CnSectionsWidget/CnSectionsWidget.vue'\n\n/** Counter for unique heading ids across instances. */\nlet instanceCounter = 0\n\n/**\n * CnDeviceFramesWidget - the `device-frames` widget: several small pages side\n * by side, each in a phone frame with its own header (board DqInspectie,\n * \"Inspectie in het veld\": the day offline, a checklist question with a\n * photo, and the sync queue).\n *\n * Each frame stacks widgets of the surface, as the `sections` widget does,\n * under a header with the frame's title and an optional status line. The\n * status can follow the connection (\"Offline · synced at 07:42\" while the\n * device is offline). The frames wrap onto the next row when the page is\n * narrower, and a frame takes the full width on a phone.\n *\n * ```json\n * {\n * \"type\": \"device-frames\",\n * \"content\": {\n * \"width\": 350,\n * \"status\": { \"followConnection\": true, \"offlineText\": \"Offline · synced at 07:42\", \"onlineText\": \"Online\" },\n * \"frames\": [\n * { \"key\": \"today\", \"title\": \"Today\", \"widgetIds\": [\"today-runs\", \"offline-note\"] },\n * { \"key\": \"question\", \"title\": \"Kerkplein 7\", \"widgetIds\": [\"checklist-run\"] },\n * { \"key\": \"sync\", \"title\": \"Synchronise\", \"widgetIds\": [\"sync-queue\"] }\n * ]\n * }\n * }\n * ```\n *\n * @spec openspec/changes/device-frames-widget/specs/device-frames-widget/spec.md#requirement-a-page-can-show-several-small-pages-as-phone-frames\n */\nexport default {\n\tname: 'CnDeviceFramesWidget',\n\n\tcomponents: { CnSectionsWidget },\n\n\tinject: {\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget configuration.\n\t\t *\n\t\t * - `frames` — `[{ key?, title, widgetIds? | sections?, status? }]`, in order. `widgetIds` names\n\t\t * widgets of the surface, stacked; `sections` takes the `sections` widget's entries instead.\n\t\t * - `status` — the status line of every frame (a frame's own `status` wins): `{ text?, tone?,\n\t\t * followConnection?, onlineText?, offlineText? }`. With `followConnection` the line reads\n\t\t * `offlineText` while the device is offline and `onlineText` otherwise. `tone` is `warning`\n\t\t * (default while offline), `success` or `info`.\n\t\t * - `width` — the frame width in pixels (default 350).\n\t\t *\n\t\t * @type {{frames?: Array<object>, status?: object, width?: number}}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** Sibling widget definitions on the surface, for `widgetIds`. */\n\t\tavailableWidgets: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** The bound record's id, forwarded to the children. */\n\t\tobjectId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The loaded record, forwarded to the children. */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The resolved object-type slug. */\n\t\tobjectType: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The resolved JSON Schema object. */\n\t\tschemaObject: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** OpenRegister register slug of the surface. */\n\t\tregister: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** OpenRegister schema slug of the surface. */\n\t\tschema: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The effective object store. */\n\t\tstore: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Rendering surface forwarded to the children. */\n\t\tsurface: {\n\t\t\ttype: String,\n\t\t\tdefault: 'app-dashboard',\n\t\t},\n\n\t\t/** Object context forwarded to integration children. */\n\t\tintegrationContext: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The consumer's component registry, for custom child widget types. */\n\t\tcnRegistry: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\t},\n\n\tdata() {\n\t\tinstanceCounter += 1\n\t\treturn {\n\t\t\tuid: instanceCounter,\n\t\t\tonline: typeof navigator === 'undefined' || navigator.onLine !== false,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** @return {object} The frame width. */\n\t\tframeStyle() {\n\t\t\tconst width = Number(this.content?.width)\n\t\t\treturn { '--cn-device-frame-width': `${Number.isFinite(width) && width >= 240 ? width : 350}px` }\n\t\t},\n\n\t\t/**\n\t\t * The frames to draw: a key, a translated title, the sections and the\n\t\t * status line.\n\t\t *\n\t\t * @return {Array<{key: string, title: string, sections: Array<object>, status: object|null}>}\n\t\t */\n\t\tframes() {\n\t\t\tconst list = Array.isArray(this.content?.frames) ? this.content.frames : []\n\t\t\treturn list\n\t\t\t\t.filter((frame) => frame && typeof frame === 'object')\n\t\t\t\t.map((frame, index) => {\n\t\t\t\t\tconst sections = Array.isArray(frame.sections)\n\t\t\t\t\t\t? frame.sections\n\t\t\t\t\t\t: (Array.isArray(frame.widgetIds) ? frame.widgetIds : []).filter((id) => typeof id === 'string' && id !== '').map((widgetId) => ({ widgetId }))\n\t\t\t\t\treturn {\n\t\t\t\t\t\tkey: String(frame.key || `frame-${index}`).replace(/[^\\w-]/g, '-'),\n\t\t\t\t\t\ttitle: this.tr(frame.title || ''),\n\t\t\t\t\t\tsections,\n\t\t\t\t\t\tstatus: this.statusOf(frame.status || this.content?.status || null),\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t},\n\t},\n\n\tmounted() {\n\t\tif (typeof window !== 'undefined') {\n\t\t\twindow.addEventListener('online', this.onConnection)\n\t\t\twindow.addEventListener('offline', this.onConnection)\n\t\t}\n\t},\n\n\tbeforeUnmount() {\n\t\tif (typeof window !== 'undefined') {\n\t\t\twindow.removeEventListener('online', this.onConnection)\n\t\t\twindow.removeEventListener('offline', this.onConnection)\n\t\t}\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Translate through the app's translate.\n\t\t *\n\t\t * @param {string} key The source string.\n\t\t * @return {string}\n\t\t */\n\t\ttr(key) {\n\t\t\treturn key && typeof this.cnTranslate === 'function' ? this.cnTranslate(key) : (key || '')\n\t\t},\n\n\t\t/**\n\t\t * The status line of a frame, or null.\n\t\t *\n\t\t * @param {object|null} status The configured status.\n\t\t * @return {{text: string, tone: string, icon: object}|null}\n\t\t */\n\t\tstatusOf(status) {\n\t\t\tif (!status || typeof status !== 'object') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tlet text = status.text\n\t\t\tlet tone = status.tone\n\t\t\tif (status.followConnection === true) {\n\t\t\t\ttext = this.online ? status.onlineText : status.offlineText\n\t\t\t\ttone = tone || (this.online ? 'success' : 'warning')\n\t\t\t}\n\t\t\tif (typeof text !== 'string' || text === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst known = ['warning', 'success', 'info'].includes(tone) ? tone : 'info'\n\t\t\tconst icon = known === 'warning' ? CloudOffOutline : (known === 'success' ? CloudCheckOutline : InformationOutline)\n\t\t\treturn { text: this.tr(text), tone: known, icon: markRaw(icon) }\n\t\t},\n\n\t\t/** Follow the browser's connection state. */\n\t\tonConnection() {\n\t\t\tthis.online = typeof navigator === 'undefined' || navigator.onLine !== false\n\t\t},\n\n\t\t/**\n\t\t * The id of a frame's heading.\n\t\t *\n\t\t * @param {string} key The frame key.\n\t\t * @return {string}\n\t\t */\n\t\theadingId(key) {\n\t\t\treturn `cn-device-frame-${this.uid}-${key}`\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-device-frames-widget {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: flex-start;\n\tgap: 20px;\n}\n\n.cn-device-frames-widget__frame {\n\tdisplay: flex;\n\tflex: none;\n\tflex-direction: column;\n\twidth: var(--cn-device-frame-width, 350px);\n\tmax-width: 100%;\n\tmin-height: 600px;\n\tbox-sizing: border-box;\n\toverflow: hidden;\n\tborder: 10px solid var(--color-main-text);\n\tborder-radius: 36px;\n\tbackground: var(--color-background-hover);\n}\n\n.cn-device-frames-widget__head {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 6px;\n\tpadding: 14px 18px 10px;\n\tborder-bottom: 1px solid var(--color-border);\n\tbackground: var(--color-main-background);\n}\n\n.cn-device-frames-widget__status {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tfont-size: 13px;\n\tfont-weight: 600;\n}\n\n.cn-device-frames-widget__status--warning {\n\tcolor: var(--color-warning-text, var(--color-main-text));\n}\n\n.cn-device-frames-widget__status--success {\n\tcolor: var(--color-success-text, var(--color-main-text));\n}\n\n.cn-device-frames-widget__status--info {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-device-frames-widget__title {\n\tmargin: 0;\n\tfont-size: 20px;\n\tfont-weight: 700;\n\tline-height: 1.2;\n}\n\n.cn-device-frames-widget__body {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n\tpadding: 14px;\n}\n\n/* On a phone the frame is the screen: no bezel, the full width. */\n@media (max-width: 640px) {\n\t.cn-device-frames-widget__frame {\n\t\twidth: 100%;\n\t\tmin-height: 0;\n\t\tborder-width: 0;\n\t\tborder-radius: var(--cn-board-card-radius, 12px);\n\t}\n}\n</style>\n"],"names":["CnSectionsWidget"],"mappings":";;;;;;AAqDA;AACA,IAAI,kBAAkB,EAAA;;AAEtB;AACC;;;;AAIA;;;;;AAKA;AACA;AACA;;;;;;;AAOA;AACA;AACA;;;;AAIA;AACA;AACA;;AAED,aAAe;CACd,IAAI,EAAE,sBAAsB;;CAE5B,UAAU,EAAE,oBAAEA,UAAe,EAAG;;AAEhC,CAAA,MAAM,EAAE;AACP,EAAA,WAAW,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,GAAG,KAAK,KAAK;EAC5C;;AAED,CAAA,KAAK,EAAE;AACN;;AAEC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;AAGD,EAAA,OAAO,EAAE;GACR,IAAI,EAAE,MAAM;AACZ,GAAA,OAAO,EAAE,OAAO,EAAE,CAAC;GACnB;;;AAGD,EAAA,gBAAgB,EAAE;GACjB,IAAI,EAAE,KAAK;AACX,GAAA,OAAO,EAAE,MAAM,EAAE;GACjB;;;AAGD,EAAA,QAAQ,EAAE;AACT,GAAA,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC;GACtB,OAAO,EAAE,EAAE;GACX;;AAED;EACA,UAAU,EAAE;GACX,IAAI,EAAE,MAAM;GACZ,OAAO,EAAE,IAAI;GACb;;AAED;EACA,UAAU,EAAE;GACX,IAAI,EAAE,MAAM;GACZ,OAAO,EAAE,EAAE;GACX;;AAED;EACA,YAAY,EAAE;GACb,IAAI,EAAE,MAAM;GACZ,OAAO,EAAE,IAAI;GACb;;AAED;AACA,EAAA,QAAQ,EAAE;AACT,GAAA,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC;GACtB,OAAO,EAAE,EAAE;GACX;;AAED;AACA,EAAA,MAAM,EAAE;AACP,GAAA,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC;GACtB,OAAO,EAAE,EAAE;GACX;;;AAGD,EAAA,KAAK,EAAE;GACN,IAAI,EAAE,MAAM;GACZ,OAAO,EAAE,IAAI;GACb;;AAED;AACA,EAAA,OAAO,EAAE;GACR,IAAI,EAAE,MAAM;GACZ,OAAO,EAAE,eAAe;GACxB;;;AAGD,EAAA,kBAAkB,EAAE;GACnB,IAAI,EAAE,MAAM;GACZ,OAAO,EAAE,IAAI;GACb;;;EAGD,UAAU,EAAE;GACX,IAAI,EAAE,MAAM;AACZ,GAAA,OAAO,EAAE,OAAO,EAAE,CAAC;GACnB;EACD;;AAED,CAAA,IAAI,GAAG;AACN,EAAA,eAAc,IAAK,EAAA;AACnB,EAAA,OAAO;AACN,GAAA,GAAG,EAAE,eAAe;AACpB,GAAA,MAAM,EAAE,OAAO,SAAU,KAAI,WAAY,IAAG,SAAS,CAAC,MAAO,KAAI,KAAK;AACvE,GAAA;EACA;;AAED,CAAA,QAAQ,EAAE;AACT;EACA,UAAU,GAAG;AACZ,GAAA,MAAM,KAAI,GAAI,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,EAAA;AACxC,GAAA,OAAO,EAAE,yBAAyB,EAAE,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC,KAAK,KAAK,KAAI,IAAK,GAAI,GAAE,KAAI,GAAI,GAAG,CAAC,EAAE,CAAE,EAAA;GAChG;;AAED;;;AAGC;AACA;;AAED,EAAA,MAAM,GAAG;AACR,GAAA,MAAM,IAAG,GAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAA,GAAI,IAAI,CAAC,OAAO,CAAC,MAAK,GAAI,GAAC;AAC1E,GAAA,OAAO,IAAG;KACR,MAAM,CAAC,CAAC,KAAK,KAAK,KAAM,IAAG,OAAO,KAAI,KAAM,QAAQ,CAAA;AACrD,KAAC,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,KAAK;AACtB,KAAA,MAAM,WAAW,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAA;AAC5C,QAAE,KAAK,CAAC,QAAO;QACb,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,SAAS,CAAA,GAAI,KAAK,CAAC,SAAQ,GAAI,EAAE,EAAE,MAAM,CAAC,CAAC,EAAE,KAAK,OAAO,EAAG,KAAI,QAAO,IAAK,EAAC,KAAM,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,QAAQ,MAAM,EAAE,QAAS,EAAC,CAAC,EAAA;AAC/I,KAAA,OAAO;AACN,MAAA,GAAG,EAAE,MAAM,CAAC,KAAK,CAAC,GAAI,IAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC;AAClE,MAAA,KAAK,EAAE,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,KAAI,IAAK,EAAE,CAAC;AACjC,MAAA,QAAQ;AACR,MAAA,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAK,IAAK,IAAI,CAAC,OAAO,EAAE,MAAK,IAAK,IAAI,CAAC;AACpE,MAAA;KACA,CAAA;GACF;EACD;;AAED,CAAA,OAAO,GAAG;AACT,EAAA,IAAI,OAAO,MAAK,KAAM,WAAW,EAAE;GAClC,MAAM,CAAC,gBAAgB,CAAC,QAAQ,EAAE,IAAI,CAAC,YAAY,EAAA;GACnD,MAAM,CAAC,gBAAgB,CAAC,SAAS,EAAE,IAAI,CAAC,YAAY,EAAA;AACrD,GAAA;EACA;;AAED,CAAA,aAAa,GAAG;AACf,EAAA,IAAI,OAAO,MAAK,KAAM,WAAW,EAAE;GAClC,MAAM,CAAC,mBAAmB,CAAC,QAAQ,EAAE,IAAI,CAAC,YAAY,EAAA;GACtD,MAAM,CAAC,mBAAmB,CAAC,SAAS,EAAE,IAAI,CAAC,YAAY,EAAA;AACxD,GAAA;EACA;;AAED,CAAA,OAAO,EAAE;AACR;AACC;AACA;AACA;AACA;;AAED,EAAA,EAAE,CAAC,GAAG,EAAE;AACP,GAAA,OAAO,GAAI,IAAG,OAAO,IAAI,CAAC,WAAU,KAAM,aAAa,IAAI,CAAC,WAAW,CAAC,GAAG,CAAA,IAAK,GAAE,IAAK,EAAE,CAAA;GACzF;;AAED;;AAEC;AACA;;;AAGD,EAAA,QAAQ,CAAC,MAAM,EAAE;AAChB,GAAA,IAAI,CAAC,MAAK,IAAK,OAAO,MAAK,KAAM,QAAQ,EAAE;AAC1C,IAAA,OAAO,IAAG;AACX,IAAA;AACA,GAAA,IAAI,OAAO,MAAM,CAAC,KAAG;AACrB,GAAA,IAAI,OAAO,MAAM,CAAC,KAAG;AACrB,GAAA,IAAI,MAAM,CAAC,gBAAe,KAAM,IAAI,EAAE;IACrC,IAAK,GAAE,IAAI,CAAC,MAAO,GAAE,MAAM,CAAC,UAAW,GAAE,MAAM,CAAC,YAAU;AAC1D,IAAA,IAAG,GAAI,IAAK,KAAI,IAAI,CAAC,MAAK,GAAI,YAAY,SAAS,EAAA;AACpD,IAAA;AACA,GAAA,IAAI,OAAO,IAAG,KAAM,QAAO,IAAK,SAAS,EAAE,EAAE;AAC5C,IAAA,OAAO,IAAG;AACX,IAAA;AACA,GAAA,MAAM,KAAM,GAAE,CAAC,SAAS,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAE,GAAE,IAAG,GAAI,OAAK;GAC1E,MAAM,OAAO,UAAU,SAAQ,GAAI,mBAAmB,UAAU,YAAY,iBAAgB,GAAI,kBAAkB,EAAA;GAClH,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,CAAE,EAAA;GAC/D;;AAED;AACA,EAAA,YAAY,GAAG;AACd,GAAA,IAAI,CAAC,MAAK,GAAI,OAAO,SAAU,KAAI,WAAU,IAAK,SAAS,CAAC,MAAO,KAAI,MAAI;GAC3E;;AAED;;AAEC;;AAEA;;AAED,EAAA,SAAS,CAAC,GAAG,EAAE;AACd,GAAA,OAAO,CAAC,gBAAgB,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA;GAC1C;EACD;AACF;;;;"}
@@ -1 +1 @@
1
- {"version":3,"file":"CnDeviceFramesWidget.vue3.js","sources":["../../../../src/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue?vue&type=template&id=05f76090&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-device-frames-widget\" data-testid=\"cn-device-frames-widget\">\n\t\t<section\n\t\t\tv-for=\"frame in frames\"\n\t\t\t:key=\"frame.key\"\n\t\t\tclass=\"cn-device-frames-widget__frame\"\n\t\t\t:style=\"frameStyle\"\n\t\t\t:aria-labelledby=\"headingId(frame.key)\"\n\t\t\t:data-testid=\"`cn-device-frame-${frame.key}`\">\n\t\t\t<header class=\"cn-device-frames-widget__head\">\n\t\t\t\t<span\n\t\t\t\t\tv-if=\"frame.status\"\n\t\t\t\t\tclass=\"cn-device-frames-widget__status\"\n\t\t\t\t\t:class=\"`cn-device-frames-widget__status--${frame.status.tone}`\"\n\t\t\t\t\tdata-testid=\"cn-device-frame-status\">\n\t\t\t\t\t<component :is=\"frame.status.icon\" :size=\"16\" aria-hidden=\"true\" />\n\t\t\t\t\t{{ frame.status.text }}\n\t\t\t\t</span>\n\t\t\t\t<h3 :id=\"headingId(frame.key)\" class=\"cn-device-frames-widget__title\">\n\t\t\t\t\t{{ frame.title }}\n\t\t\t\t</h3>\n\t\t\t</header>\n\t\t\t<div class=\"cn-device-frames-widget__body\">\n\t\t\t\t<CnSectionsWidget\n\t\t\t\t\t:content=\"{ sections: frame.sections, cards: false, hideEmpty: true }\"\n\t\t\t\t\t:availableWidgets=\"availableWidgets\"\n\t\t\t\t\t:objectId=\"objectId\"\n\t\t\t\t\t:objectData=\"objectData\"\n\t\t\t\t\t:objectType=\"objectType\"\n\t\t\t\t\t:schemaObject=\"schemaObject\"\n\t\t\t\t\t:register=\"register\"\n\t\t\t\t\t:schema=\"schema\"\n\t\t\t\t\t:store=\"store\"\n\t\t\t\t\t:surface=\"surface\"\n\t\t\t\t\t:integrationContext=\"integrationContext\"\n\t\t\t\t\t:cnRegistry=\"cnRegistry\" />\n\t\t\t</div>\n\t\t</section>\n\t</div>\n</template>\n\n<script>\nimport { markRaw } from 'vue'\nimport CloudCheckOutline from 'vue-material-design-icons/CloudCheckOutline.vue'\nimport CloudOffOutline from 'vue-material-design-icons/CloudOffOutline.vue'\nimport InformationOutline from 'vue-material-design-icons/InformationOutline.vue'\nimport CnSectionsWidget from '../CnSectionsWidget/CnSectionsWidget.vue'\n\n/** Counter for unique heading ids across instances. */\nlet instanceCounter = 0\n\n/**\n * CnDeviceFramesWidget - the `device-frames` widget: several small pages side\n * by side, each in a phone frame with its own header (board DqInspectie,\n * \"Inspectie in het veld\": the day offline, a checklist question with a\n * photo, and the sync queue).\n *\n * Each frame stacks widgets of the surface, as the `sections` widget does,\n * under a header with the frame's title and an optional status line. The\n * status can follow the connection (\"Offline · synced at 07:42\" while the\n * device is offline). The frames wrap onto the next row when the page is\n * narrower, and a frame takes the full width on a phone.\n *\n * ```json\n * {\n * \"type\": \"device-frames\",\n * \"content\": {\n * \"width\": 350,\n * \"status\": { \"followConnection\": true, \"offlineText\": \"Offline · synced at 07:42\", \"onlineText\": \"Online\" },\n * \"frames\": [\n * { \"key\": \"today\", \"title\": \"Today\", \"widgetIds\": [\"today-runs\", \"offline-note\"] },\n * { \"key\": \"question\", \"title\": \"Kerkplein 7\", \"widgetIds\": [\"checklist-run\"] },\n * { \"key\": \"sync\", \"title\": \"Synchronise\", \"widgetIds\": [\"sync-queue\"] }\n * ]\n * }\n * }\n * ```\n *\n * @spec openspec/changes/device-frames-widget/specs/device-frames-widget/spec.md#requirement-a-page-can-show-several-small-pages-as-phone-frames\n */\nexport default {\n\tname: 'CnDeviceFramesWidget',\n\n\tcomponents: { CnSectionsWidget },\n\n\tinject: {\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget configuration.\n\t\t *\n\t\t * - `frames` — `[{ key?, title, widgetIds? | sections?, status? }]`, in order. `widgetIds` names\n\t\t * widgets of the surface, stacked; `sections` takes the `sections` widget's entries instead.\n\t\t * - `status` — the status line of every frame (a frame's own `status` wins): `{ text?, tone?,\n\t\t * followConnection?, onlineText?, offlineText? }`. With `followConnection` the line reads\n\t\t * `offlineText` while the device is offline and `onlineText` otherwise. `tone` is `warning`\n\t\t * (default while offline), `success` or `info`.\n\t\t * - `width` — the frame width in pixels (default 350).\n\t\t *\n\t\t * @type {{frames?: Array<object>, status?: object, width?: number}}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** Sibling widget definitions on the surface, for `widgetIds`. */\n\t\tavailableWidgets: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** The bound record's id, forwarded to the children. */\n\t\tobjectId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The loaded record, forwarded to the children. */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The resolved object-type slug. */\n\t\tobjectType: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The resolved JSON Schema object. */\n\t\tschemaObject: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** OpenRegister register slug of the surface. */\n\t\tregister: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** OpenRegister schema slug of the surface. */\n\t\tschema: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The effective object store. */\n\t\tstore: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Rendering surface forwarded to the children. */\n\t\tsurface: {\n\t\t\ttype: String,\n\t\t\tdefault: 'app-dashboard',\n\t\t},\n\n\t\t/** Object context forwarded to integration children. */\n\t\tintegrationContext: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The consumer's component registry, for custom child widget types. */\n\t\tcnRegistry: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\t},\n\n\tdata() {\n\t\tinstanceCounter += 1\n\t\treturn {\n\t\t\tuid: instanceCounter,\n\t\t\tonline: typeof navigator === 'undefined' || navigator.onLine !== false,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** @return {object} The frame width. */\n\t\tframeStyle() {\n\t\t\tconst width = Number(this.content?.width)\n\t\t\treturn { '--cn-device-frame-width': `${Number.isFinite(width) && width >= 240 ? width : 350}px` }\n\t\t},\n\n\t\t/**\n\t\t * The frames to draw: a key, a translated title, the sections and the\n\t\t * status line.\n\t\t *\n\t\t * @return {Array<{key: string, title: string, sections: Array<object>, status: object|null}>}\n\t\t */\n\t\tframes() {\n\t\t\tconst list = Array.isArray(this.content?.frames) ? this.content.frames : []\n\t\t\treturn list\n\t\t\t\t.filter((frame) => frame && typeof frame === 'object')\n\t\t\t\t.map((frame, index) => {\n\t\t\t\t\tconst sections = Array.isArray(frame.sections)\n\t\t\t\t\t\t? frame.sections\n\t\t\t\t\t\t: (Array.isArray(frame.widgetIds) ? frame.widgetIds : []).filter((id) => typeof id === 'string' && id !== '').map((widgetId) => ({ widgetId }))\n\t\t\t\t\treturn {\n\t\t\t\t\t\tkey: String(frame.key || `frame-${index}`).replace(/[^\\w-]/g, '-'),\n\t\t\t\t\t\ttitle: this.tr(frame.title || ''),\n\t\t\t\t\t\tsections,\n\t\t\t\t\t\tstatus: this.statusOf(frame.status || this.content?.status || null),\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t},\n\t},\n\n\tmounted() {\n\t\tif (typeof window !== 'undefined') {\n\t\t\twindow.addEventListener('online', this.onConnection)\n\t\t\twindow.addEventListener('offline', this.onConnection)\n\t\t}\n\t},\n\n\tbeforeUnmount() {\n\t\tif (typeof window !== 'undefined') {\n\t\t\twindow.removeEventListener('online', this.onConnection)\n\t\t\twindow.removeEventListener('offline', this.onConnection)\n\t\t}\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Translate through the app's translate.\n\t\t *\n\t\t * @param {string} key The source string.\n\t\t * @return {string}\n\t\t */\n\t\ttr(key) {\n\t\t\treturn key && typeof this.cnTranslate === 'function' ? this.cnTranslate(key) : (key || '')\n\t\t},\n\n\t\t/**\n\t\t * The status line of a frame, or null.\n\t\t *\n\t\t * @param {object|null} status The configured status.\n\t\t * @return {{text: string, tone: string, icon: object}|null}\n\t\t */\n\t\tstatusOf(status) {\n\t\t\tif (!status || typeof status !== 'object') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tlet text = status.text\n\t\t\tlet tone = status.tone\n\t\t\tif (status.followConnection === true) {\n\t\t\t\ttext = this.online ? status.onlineText : status.offlineText\n\t\t\t\ttone = tone || (this.online ? 'success' : 'warning')\n\t\t\t}\n\t\t\tif (typeof text !== 'string' || text === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst known = ['warning', 'success', 'info'].includes(tone) ? tone : 'info'\n\t\t\tconst icon = known === 'warning' ? CloudOffOutline : (known === 'success' ? CloudCheckOutline : InformationOutline)\n\t\t\treturn { text: this.tr(text), tone: known, icon: markRaw(icon) }\n\t\t},\n\n\t\t/** Follow the browser's connection state. */\n\t\tonConnection() {\n\t\t\tthis.online = typeof navigator === 'undefined' || navigator.onLine !== false\n\t\t},\n\n\t\t/**\n\t\t * The id of a frame's heading.\n\t\t *\n\t\t * @param {string} key The frame key.\n\t\t * @return {string}\n\t\t */\n\t\theadingId(key) {\n\t\t\treturn `cn-device-frame-${this.uid}-${key}`\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-device-frames-widget {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: flex-start;\n\tgap: 20px;\n}\n\n.cn-device-frames-widget__frame {\n\tdisplay: flex;\n\tflex: none;\n\tflex-direction: column;\n\twidth: var(--cn-device-frame-width, 350px);\n\tmax-width: 100%;\n\tmin-height: 600px;\n\tbox-sizing: border-box;\n\toverflow: hidden;\n\tborder: 10px solid var(--color-main-text);\n\tborder-radius: 36px;\n\tbackground: var(--color-background-hover);\n}\n\n.cn-device-frames-widget__head {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 6px;\n\tpadding: 14px 18px 10px;\n\tborder-bottom: 1px solid var(--color-border);\n\tbackground: var(--color-main-background);\n}\n\n.cn-device-frames-widget__status {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tfont-size: 13px;\n\tfont-weight: 600;\n}\n\n.cn-device-frames-widget__status--warning {\n\tcolor: var(--color-warning-text, var(--color-main-text));\n}\n\n.cn-device-frames-widget__status--success {\n\tcolor: var(--color-success-text, var(--color-main-text));\n}\n\n.cn-device-frames-widget__status--info {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-device-frames-widget__title {\n\tmargin: 0;\n\tfont-size: 20px;\n\tfont-weight: 700;\n}\n\n.cn-device-frames-widget__body {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n\tpadding: 14px;\n}\n\n/* On a phone the frame is the screen: no bezel, the full width. */\n@media (max-width: 640px) {\n\t.cn-device-frames-widget__frame {\n\t\twidth: 100%;\n\t\tmin-height: 0;\n\t\tborder-width: 0;\n\t\tborder-radius: var(--cn-board-card-radius, 12px);\n\t}\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_createElementVNode","_createBlock","_resolveDynamicComponent","_createTextVNode","_toDisplayString"],"mappings":";;;AAWM,EAAA,KAAK,EAAC,yBAAS;AAAA,EAAA,aAAA,EAAA,yBAAA;;;AAQV,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,+BAAwB,GAAA;;AAajC,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,+BAAc,GAAA;;;;;AArBzB,EAAA,QAAAA,SAAA,EAAA,EAAAC,kBAAA,QAAA;sBACAA,8CACgB,QAAM,CAAA,MAAA,EAAA,CAAf,KAAK,KAAA;2BADZA;QAEC,GAAG,EAAE,KAAK,CAAC,GAAG;aACV,EAAA,gCAAA;AACH,QAAA,KAAI,iBAAE,QAAU,CAAA,UAAA,CAAA;AAChB,QAAA,iBAAc,EAAE,QAAA,CAAA,SAAS,CAAC,KAAK,CAAC,GAAG,CAAA;QACnC,aAAU,EAAA,CAAA,gBAAA,EAAqB,KAAK,CAAC,GAAG,CAAA,CAAA;;AACxC,QAAAC,kBAAA,CAYO,UAZP,UAYO,EAAA;AAVD,UAAA,CAAA,KAAK,CAAC,MAAM;4BADlBD,kBAOM,CAAA,MAAA,EAAA;;AALN,gBAAA,KAAK,kBAAA,iCAAA,EAAA,CAAA,iCAAA,EACuC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAA,CAAA,CAAA,CAAA;AAC7D,gBAAA,aAAW,EAAC,wBAAI;;AAChB,iBAAAD,SAAA,EAAA,EAAAG,WAAA,CAAGC,uBAAA,CAAA,KAAA,CAAA,MAAA,CAAA,IAAA,CAAA,EAAA;AAAA,kBAAA,IAAA,EAAA,EAAA;AAAA,kBAAA,aAAA,EAAA,MAAA;;AAAA,gBAAAC,eAAA,CAAA,GACH,GAAGC,eAAA,CAAA,KAAG,CAAA,MAAA,CAAA,IAAA,CAAA,EAAA,CAAA,YAAA;;;UAEPJ,kBAEK,CAAA,IAAA,EAAA;AAFA,YAAA,EAAE,EAAE,QAAA,CAAA,SAAO,CAAA,KAAA,CAAA,GAAA,CAAA;AAAA,YAAA,KAAA,EAAA,gCAAA;AACZ,WAAA,EAAAI,eAAA,CAAA,KAAK,CAAC,KAAK,CAAA,EAAA,CAAA,oBAAA,UAAA,CAAA;;;;YAKb,OAAO,EAAA,EAAA,QAAA,EAAc,KAAK,CAAA,QAAA,EAAA,KAAA,EAAA,KAAA,EAAA,SAAA,EAAA,IAAA,EAAA;AAC1B,YAAA,gBAAgB,EAAE,MAAA,CAAA,gBAAA;AAClB,YAAA,QAAQ,EAAE,MAAI,CAAA,QAAA;AACd,YAAA,UAAU,EAAE,MAAA,CAAA,UAAA;AACZ,YAAA,UAAU,EAAE,MAAI,CAAA,UAAA;AAChB,YAAA,YAAY,EAAE,MAAY,CAAA,YAAA;AAC1B,YAAA,QAAQ,EAAE,MAAQ,CAAA,QAAA;AAClB,YAAA,MAAE,EAAA,MAAA,CAAA,MAAA;AACF,YAAA,KAAK,EAAA,MAAA,CAAA,KAAA;AACL,YAAA,OAAA,EAAA,MAAA,CAAA,OAAA;;;;;;;;;;;;"}
1
+ {"version":3,"file":"CnDeviceFramesWidget.vue3.js","sources":["../../../../src/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue?vue&type=template&id=64bde7d3&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-device-frames-widget\" data-testid=\"cn-device-frames-widget\">\n\t\t<section\n\t\t\tv-for=\"frame in frames\"\n\t\t\t:key=\"frame.key\"\n\t\t\tclass=\"cn-device-frames-widget__frame\"\n\t\t\t:style=\"frameStyle\"\n\t\t\t:aria-labelledby=\"headingId(frame.key)\"\n\t\t\t:data-testid=\"`cn-device-frame-${frame.key}`\">\n\t\t\t<header class=\"cn-device-frames-widget__head\">\n\t\t\t\t<span\n\t\t\t\t\tv-if=\"frame.status\"\n\t\t\t\t\tclass=\"cn-device-frames-widget__status\"\n\t\t\t\t\t:class=\"`cn-device-frames-widget__status--${frame.status.tone}`\"\n\t\t\t\t\tdata-testid=\"cn-device-frame-status\">\n\t\t\t\t\t<component :is=\"frame.status.icon\" :size=\"16\" aria-hidden=\"true\" />\n\t\t\t\t\t{{ frame.status.text }}\n\t\t\t\t</span>\n\t\t\t\t<h3 :id=\"headingId(frame.key)\" class=\"cn-device-frames-widget__title\">\n\t\t\t\t\t{{ frame.title }}\n\t\t\t\t</h3>\n\t\t\t</header>\n\t\t\t<div class=\"cn-device-frames-widget__body\">\n\t\t\t\t<CnSectionsWidget\n\t\t\t\t\t:content=\"{ sections: frame.sections, cards: false, hideEmpty: true }\"\n\t\t\t\t\t:availableWidgets=\"availableWidgets\"\n\t\t\t\t\t:objectId=\"objectId\"\n\t\t\t\t\t:objectData=\"objectData\"\n\t\t\t\t\t:objectType=\"objectType\"\n\t\t\t\t\t:schemaObject=\"schemaObject\"\n\t\t\t\t\t:register=\"register\"\n\t\t\t\t\t:schema=\"schema\"\n\t\t\t\t\t:store=\"store\"\n\t\t\t\t\t:surface=\"surface\"\n\t\t\t\t\t:integrationContext=\"integrationContext\"\n\t\t\t\t\t:cnRegistry=\"cnRegistry\" />\n\t\t\t</div>\n\t\t</section>\n\t</div>\n</template>\n\n<script>\nimport { markRaw } from 'vue'\nimport CloudCheckOutline from 'vue-material-design-icons/CloudCheckOutline.vue'\nimport CloudOffOutline from 'vue-material-design-icons/CloudOffOutline.vue'\nimport InformationOutline from 'vue-material-design-icons/InformationOutline.vue'\nimport CnSectionsWidget from '../CnSectionsWidget/CnSectionsWidget.vue'\n\n/** Counter for unique heading ids across instances. */\nlet instanceCounter = 0\n\n/**\n * CnDeviceFramesWidget - the `device-frames` widget: several small pages side\n * by side, each in a phone frame with its own header (board DqInspectie,\n * \"Inspectie in het veld\": the day offline, a checklist question with a\n * photo, and the sync queue).\n *\n * Each frame stacks widgets of the surface, as the `sections` widget does,\n * under a header with the frame's title and an optional status line. The\n * status can follow the connection (\"Offline · synced at 07:42\" while the\n * device is offline). The frames wrap onto the next row when the page is\n * narrower, and a frame takes the full width on a phone.\n *\n * ```json\n * {\n * \"type\": \"device-frames\",\n * \"content\": {\n * \"width\": 350,\n * \"status\": { \"followConnection\": true, \"offlineText\": \"Offline · synced at 07:42\", \"onlineText\": \"Online\" },\n * \"frames\": [\n * { \"key\": \"today\", \"title\": \"Today\", \"widgetIds\": [\"today-runs\", \"offline-note\"] },\n * { \"key\": \"question\", \"title\": \"Kerkplein 7\", \"widgetIds\": [\"checklist-run\"] },\n * { \"key\": \"sync\", \"title\": \"Synchronise\", \"widgetIds\": [\"sync-queue\"] }\n * ]\n * }\n * }\n * ```\n *\n * @spec openspec/changes/device-frames-widget/specs/device-frames-widget/spec.md#requirement-a-page-can-show-several-small-pages-as-phone-frames\n */\nexport default {\n\tname: 'CnDeviceFramesWidget',\n\n\tcomponents: { CnSectionsWidget },\n\n\tinject: {\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget configuration.\n\t\t *\n\t\t * - `frames` — `[{ key?, title, widgetIds? | sections?, status? }]`, in order. `widgetIds` names\n\t\t * widgets of the surface, stacked; `sections` takes the `sections` widget's entries instead.\n\t\t * - `status` — the status line of every frame (a frame's own `status` wins): `{ text?, tone?,\n\t\t * followConnection?, onlineText?, offlineText? }`. With `followConnection` the line reads\n\t\t * `offlineText` while the device is offline and `onlineText` otherwise. `tone` is `warning`\n\t\t * (default while offline), `success` or `info`.\n\t\t * - `width` — the frame width in pixels (default 350).\n\t\t *\n\t\t * @type {{frames?: Array<object>, status?: object, width?: number}}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** Sibling widget definitions on the surface, for `widgetIds`. */\n\t\tavailableWidgets: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** The bound record's id, forwarded to the children. */\n\t\tobjectId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The loaded record, forwarded to the children. */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The resolved object-type slug. */\n\t\tobjectType: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The resolved JSON Schema object. */\n\t\tschemaObject: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** OpenRegister register slug of the surface. */\n\t\tregister: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** OpenRegister schema slug of the surface. */\n\t\tschema: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The effective object store. */\n\t\tstore: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Rendering surface forwarded to the children. */\n\t\tsurface: {\n\t\t\ttype: String,\n\t\t\tdefault: 'app-dashboard',\n\t\t},\n\n\t\t/** Object context forwarded to integration children. */\n\t\tintegrationContext: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The consumer's component registry, for custom child widget types. */\n\t\tcnRegistry: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\t},\n\n\tdata() {\n\t\tinstanceCounter += 1\n\t\treturn {\n\t\t\tuid: instanceCounter,\n\t\t\tonline: typeof navigator === 'undefined' || navigator.onLine !== false,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** @return {object} The frame width. */\n\t\tframeStyle() {\n\t\t\tconst width = Number(this.content?.width)\n\t\t\treturn { '--cn-device-frame-width': `${Number.isFinite(width) && width >= 240 ? width : 350}px` }\n\t\t},\n\n\t\t/**\n\t\t * The frames to draw: a key, a translated title, the sections and the\n\t\t * status line.\n\t\t *\n\t\t * @return {Array<{key: string, title: string, sections: Array<object>, status: object|null}>}\n\t\t */\n\t\tframes() {\n\t\t\tconst list = Array.isArray(this.content?.frames) ? this.content.frames : []\n\t\t\treturn list\n\t\t\t\t.filter((frame) => frame && typeof frame === 'object')\n\t\t\t\t.map((frame, index) => {\n\t\t\t\t\tconst sections = Array.isArray(frame.sections)\n\t\t\t\t\t\t? frame.sections\n\t\t\t\t\t\t: (Array.isArray(frame.widgetIds) ? frame.widgetIds : []).filter((id) => typeof id === 'string' && id !== '').map((widgetId) => ({ widgetId }))\n\t\t\t\t\treturn {\n\t\t\t\t\t\tkey: String(frame.key || `frame-${index}`).replace(/[^\\w-]/g, '-'),\n\t\t\t\t\t\ttitle: this.tr(frame.title || ''),\n\t\t\t\t\t\tsections,\n\t\t\t\t\t\tstatus: this.statusOf(frame.status || this.content?.status || null),\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t},\n\t},\n\n\tmounted() {\n\t\tif (typeof window !== 'undefined') {\n\t\t\twindow.addEventListener('online', this.onConnection)\n\t\t\twindow.addEventListener('offline', this.onConnection)\n\t\t}\n\t},\n\n\tbeforeUnmount() {\n\t\tif (typeof window !== 'undefined') {\n\t\t\twindow.removeEventListener('online', this.onConnection)\n\t\t\twindow.removeEventListener('offline', this.onConnection)\n\t\t}\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Translate through the app's translate.\n\t\t *\n\t\t * @param {string} key The source string.\n\t\t * @return {string}\n\t\t */\n\t\ttr(key) {\n\t\t\treturn key && typeof this.cnTranslate === 'function' ? this.cnTranslate(key) : (key || '')\n\t\t},\n\n\t\t/**\n\t\t * The status line of a frame, or null.\n\t\t *\n\t\t * @param {object|null} status The configured status.\n\t\t * @return {{text: string, tone: string, icon: object}|null}\n\t\t */\n\t\tstatusOf(status) {\n\t\t\tif (!status || typeof status !== 'object') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tlet text = status.text\n\t\t\tlet tone = status.tone\n\t\t\tif (status.followConnection === true) {\n\t\t\t\ttext = this.online ? status.onlineText : status.offlineText\n\t\t\t\ttone = tone || (this.online ? 'success' : 'warning')\n\t\t\t}\n\t\t\tif (typeof text !== 'string' || text === '') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst known = ['warning', 'success', 'info'].includes(tone) ? tone : 'info'\n\t\t\tconst icon = known === 'warning' ? CloudOffOutline : (known === 'success' ? CloudCheckOutline : InformationOutline)\n\t\t\treturn { text: this.tr(text), tone: known, icon: markRaw(icon) }\n\t\t},\n\n\t\t/** Follow the browser's connection state. */\n\t\tonConnection() {\n\t\t\tthis.online = typeof navigator === 'undefined' || navigator.onLine !== false\n\t\t},\n\n\t\t/**\n\t\t * The id of a frame's heading.\n\t\t *\n\t\t * @param {string} key The frame key.\n\t\t * @return {string}\n\t\t */\n\t\theadingId(key) {\n\t\t\treturn `cn-device-frame-${this.uid}-${key}`\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-device-frames-widget {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: flex-start;\n\tgap: 20px;\n}\n\n.cn-device-frames-widget__frame {\n\tdisplay: flex;\n\tflex: none;\n\tflex-direction: column;\n\twidth: var(--cn-device-frame-width, 350px);\n\tmax-width: 100%;\n\tmin-height: 600px;\n\tbox-sizing: border-box;\n\toverflow: hidden;\n\tborder: 10px solid var(--color-main-text);\n\tborder-radius: 36px;\n\tbackground: var(--color-background-hover);\n}\n\n.cn-device-frames-widget__head {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 6px;\n\tpadding: 14px 18px 10px;\n\tborder-bottom: 1px solid var(--color-border);\n\tbackground: var(--color-main-background);\n}\n\n.cn-device-frames-widget__status {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tfont-size: 13px;\n\tfont-weight: 600;\n}\n\n.cn-device-frames-widget__status--warning {\n\tcolor: var(--color-warning-text, var(--color-main-text));\n}\n\n.cn-device-frames-widget__status--success {\n\tcolor: var(--color-success-text, var(--color-main-text));\n}\n\n.cn-device-frames-widget__status--info {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-device-frames-widget__title {\n\tmargin: 0;\n\tfont-size: 20px;\n\tfont-weight: 700;\n\tline-height: 1.2;\n}\n\n.cn-device-frames-widget__body {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n\tpadding: 14px;\n}\n\n/* On a phone the frame is the screen: no bezel, the full width. */\n@media (max-width: 640px) {\n\t.cn-device-frames-widget__frame {\n\t\twidth: 100%;\n\t\tmin-height: 0;\n\t\tborder-width: 0;\n\t\tborder-radius: var(--cn-board-card-radius, 12px);\n\t}\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_createElementVNode","_createBlock","_resolveDynamicComponent","_createTextVNode","_toDisplayString"],"mappings":";;;AAWM,EAAA,KAAK,EAAC,yBAAS;AAAA,EAAA,aAAA,EAAA,yBAAA;;;AAQV,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,+BAAwB,GAAA;;AAajC,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,+BAAc,GAAA;;;;;AArBzB,EAAA,QAAAA,SAAA,EAAA,EAAAC,kBAAA,QAAA;sBACAA,8CACgB,QAAM,CAAA,MAAA,EAAA,CAAf,KAAK,KAAA;2BADZA;QAEC,GAAG,EAAE,KAAK,CAAC,GAAG;aACV,EAAA,gCAAA;AACH,QAAA,KAAI,iBAAE,QAAU,CAAA,UAAA,CAAA;AAChB,QAAA,iBAAc,EAAE,QAAA,CAAA,SAAS,CAAC,KAAK,CAAC,GAAG,CAAA;QACnC,aAAU,EAAA,CAAA,gBAAA,EAAqB,KAAK,CAAC,GAAG,CAAA,CAAA;;AACxC,QAAAC,kBAAA,CAYO,UAZP,UAYO,EAAA;AAVD,UAAA,CAAA,KAAK,CAAC,MAAM;4BADlBD,kBAOM,CAAA,MAAA,EAAA;;AALN,gBAAA,KAAK,kBAAA,iCAAA,EAAA,CAAA,iCAAA,EACuC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAA,CAAA,CAAA,CAAA;AAC7D,gBAAA,aAAW,EAAC,wBAAI;;AAChB,iBAAAD,SAAA,EAAA,EAAAG,WAAA,CAAGC,uBAAA,CAAA,KAAA,CAAA,MAAA,CAAA,IAAA,CAAA,EAAA;AAAA,kBAAA,IAAA,EAAA,EAAA;AAAA,kBAAA,aAAA,EAAA,MAAA;;AAAA,gBAAAC,eAAA,CAAA,GACH,GAAGC,eAAA,CAAA,KAAG,CAAA,MAAA,CAAA,IAAA,CAAA,EAAA,CAAA,YAAA;;;UAEPJ,kBAEK,CAAA,IAAA,EAAA;AAFA,YAAA,EAAE,EAAE,QAAA,CAAA,SAAO,CAAA,KAAA,CAAA,GAAA,CAAA;AAAA,YAAA,KAAA,EAAA,gCAAA;AACZ,WAAA,EAAAI,eAAA,CAAA,KAAK,CAAC,KAAK,CAAA,EAAA,CAAA,oBAAA,UAAA,CAAA;;;;YAKb,OAAO,EAAA,EAAA,QAAA,EAAc,KAAK,CAAA,QAAA,EAAA,KAAA,EAAA,KAAA,EAAA,SAAA,EAAA,IAAA,EAAA;AAC1B,YAAA,gBAAgB,EAAE,MAAA,CAAA,gBAAA;AAClB,YAAA,QAAQ,EAAE,MAAI,CAAA,QAAA;AACd,YAAA,UAAU,EAAE,MAAA,CAAA,UAAA;AACZ,YAAA,UAAU,EAAE,MAAI,CAAA,UAAA;AAChB,YAAA,YAAY,EAAE,MAAY,CAAA,YAAA;AAC1B,YAAA,QAAQ,EAAE,MAAQ,CAAA,QAAA;AAClB,YAAA,MAAE,EAAA,MAAA,CAAA,MAAA;AACF,YAAA,KAAK,EAAA,MAAA,CAAA,KAAA;AACL,YAAA,OAAA,EAAA,MAAA,CAAA,OAAA;;;;;;;;;;;;"}
@@ -2,7 +2,7 @@ import script from './CnDuplicateWarning.vue2.js';
2
2
  import { render } from './CnDuplicateWarning.vue3.js';
3
3
 
4
4
  script.render = render;
5
- script.__scopeId = "data-v-7cb31ec5";
5
+ script.__scopeId = "data-v-22af78f5";
6
6
  const __sfc_main = script;
7
7
 
8
8
  export { __sfc_main as default };
@@ -2,7 +2,7 @@ import { translatePlural, translate } from '@nextcloud/l10n';
2
2
  import { generateUrl } from '@nextcloud/router';
3
3
  import { NcButton, NcNoteCard, NcTextField } from '@nextcloud/vue';
4
4
  import { duplicateAffordances } from '../../utils/duplicateWarning.js';
5
- import { fillTemplate } from '../../utils/fillTemplate.js';
5
+ import { fillTemplate, firstFilled } from '../../utils/fillTemplate.js';
6
6
  import __sfc_main from '../CnDialog/CnDialog.vue.js';
7
7
 
8
8
  /**
@@ -78,12 +78,46 @@ var script = {
78
78
  default: '{title}',
79
79
  },
80
80
 
81
- /** A record's meta line, a template filled from it. */
81
+ /**
82
+ * A record's meta line, a template filled from it, or several: the
83
+ * first that fills is shown ("{identifier} · {caseType.title}", then
84
+ * "{identifier}" for a record that lacks the type).
85
+ *
86
+ * @type {string|Array<string>}
87
+ */
82
88
  rowMeta: {
83
- type: String,
89
+ type: [String, Array],
84
90
  default: '{identifier}',
85
91
  },
86
92
 
93
+ /**
94
+ * What a match was found on, `{fields}` (joined as a list: "Aanvrager
95
+ * en Onderwerp") and `{percentage}` filled, through the host
96
+ * translate. Empty: "Aanvrager, Onderwerp (82%)".
97
+ */
98
+ matchedText: {
99
+ type: String,
100
+ default: '',
101
+ },
102
+
103
+ /** A line above the title, as the board dialogs draw one. */
104
+ eyebrow: {
105
+ type: String,
106
+ default: '',
107
+ },
108
+
109
+ /** The help line under the reason field. */
110
+ reasonHint: {
111
+ type: String,
112
+ default: '',
113
+ },
114
+
115
+ /** A muted note under the matches and the reason, for what Cancel and a block mean. */
116
+ footnote: {
117
+ type: String,
118
+ default: '',
119
+ },
120
+
87
121
  /** A record's page, a template with `{uuid}`; empty draws no link. */
88
122
  recordUrl: {
89
123
  type: String,
@@ -177,15 +211,14 @@ var script = {
177
211
  const record = this.records.find((row) => String((row && (row.id || row.uuid || (row['@self'] && row['@self'].id))) || '') === uuid);
178
212
  const fields = Array.isArray(match && match.matchedOn) ? match.matchedOn : [];
179
213
  const score = Number(match && match.score);
214
+ const words = fields.map((field) => (this.fieldLabels[field] ? this.cnTranslate(this.fieldLabels[field]) : field));
215
+ const percentage = Number.isFinite(score) ? Math.max(0, Math.min(100, Math.round(score * 100))) : 0;
180
216
  return {
181
217
  uuid,
182
218
  title: record ? (fillTemplate(this.rowTitle, record).trim() || uuid) : translate('nextcloud-vue', 'A record you may not open'),
183
- meta: record ? fillTemplate(this.rowMeta, record).trim() : '',
219
+ meta: record ? firstFilled(this.rowMeta, record, (key) => key).trim() : '',
184
220
  href: record && this.recordUrl ? generateUrl(fillTemplate(this.recordUrl, { uuid: encodeURIComponent(uuid) })) : '',
185
- matchedOn: fields.length
186
- ? fields.map((field) => (this.fieldLabels[field] ? this.cnTranslate(this.fieldLabels[field]) : field)).join(', ')
187
- : translate('nextcloud-vue', 'Matched on the score, not on a single field'),
188
- percentage: Number.isFinite(score) ? Math.max(0, Math.min(100, Math.round(score * 100))) : 0,
221
+ matched: this.matchedLine(words, percentage),
189
222
  }
190
223
  })
191
224
  },
@@ -204,6 +237,29 @@ var script = {
204
237
  return value ? this.cnTranslate(value) : ''
205
238
  },
206
239
 
240
+ /**
241
+ * What a match was found on, and how strongly.
242
+ *
243
+ * @param {Array<string>} words The matched fields, in the host's words.
244
+ * @param {number} percentage The score as a percentage.
245
+ * @return {string}
246
+ */
247
+ matchedLine(words, percentage) {
248
+ if (words.length === 0) {
249
+ return `${translate('nextcloud-vue', 'Matched on the score, not on a single field')} (${percentage}%)`
250
+ }
251
+ if (!this.matchedText) {
252
+ return `${words.join(', ')} (${percentage}%)`
253
+ }
254
+ let fields = words.join(', ');
255
+ try {
256
+ fields = new Intl.ListFormat(document.documentElement.lang || undefined, { type: 'conjunction' }).format(words);
257
+ } catch {
258
+ // An engine without Intl.ListFormat keeps the comma list.
259
+ }
260
+ return fillTemplate(this.cnTranslate(this.matchedText), { fields, percentage: String(percentage) })
261
+ },
262
+
207
263
  /** File it over the warning, carrying the reason and what it was filed over. */
208
264
  fileAnyway() {
209
265
  if (!this.canFile) {
@@ -1 +1 @@
1
- {"version":3,"file":"CnDuplicateWarning.vue2.js","sources":["../../../../src/components/CnDuplicateWarning/CnDuplicateWarning.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<CnDialog\n\t\t:name=\"text(title) || t('nextcloud-vue', 'This may already exist')\"\n\t\twidth=\"form\"\n\t\tdata-testid=\"cn-duplicate-warning\"\n\t\t@closing=\"close\">\n\t\t<div class=\"cn-duplicate-warning\">\n\t\t\t<NcNoteCard :type=\"affordances.blocked ? 'error' : 'warning'\" data-testid=\"cn-duplicate-warning-note\">\n\t\t\t\t{{ headlineText }}\n\t\t\t</NcNoteCard>\n\n\t\t\t<ul class=\"cn-duplicate-warning__matches\">\n\t\t\t\t<li v-for=\"row in rows\"\n\t\t\t\t\t:key=\"row.uuid\"\n\t\t\t\t\tclass=\"cn-duplicate-warning__match\"\n\t\t\t\t\tdata-testid=\"cn-duplicate-warning-match\">\n\t\t\t\t\t<a v-if=\"row.href\" :href=\"row.href\" class=\"cn-duplicate-warning__title\">{{ row.title }}</a>\n\t\t\t\t\t<span v-else class=\"cn-duplicate-warning__title\">{{ row.title }}</span>\n\t\t\t\t\t<span v-if=\"row.meta\" class=\"cn-duplicate-warning__meta\">{{ row.meta }}</span>\n\t\t\t\t\t<span class=\"cn-duplicate-warning__meta\">{{ row.matchedOn }} ({{ row.percentage }}%)</span>\n\t\t\t\t</li>\n\t\t\t</ul>\n\n\t\t\t<NcTextField v-if=\"affordances.needsReason\"\n\t\t\t\t:label=\"text(reasonLabel) || t('nextcloud-vue', 'Why file it anyway')\"\n\t\t\t\t:modelValue=\"reason\"\n\t\t\t\tdata-testid=\"cn-duplicate-warning-reason\"\n\t\t\t\t@update:modelValue=\"reason = $event\" />\n\n\t\t\t<p v-if=\"affordances.blocked && !affordances.mayContinue\"\n\t\t\t\tclass=\"cn-duplicate-warning__hint\"\n\t\t\t\tdata-testid=\"cn-duplicate-warning-blocked\">\n\t\t\t\t{{ text(blockedText) || t('nextcloud-vue', 'This cannot be filed twice. Open the one it matches, or ask a coordinator.') }}\n\t\t\t</p>\n\t\t</div>\n\n\t\t<template #actions>\n\t\t\t<NcButton variant=\"tertiary\" data-testid=\"cn-duplicate-warning-cancel\" @click=\"close\">\n\t\t\t\t{{ t('nextcloud-vue', 'Cancel') }}\n\t\t\t</NcButton>\n\t\t\t<NcButton v-if=\"affordances.mayContinue\"\n\t\t\t\tvariant=\"primary\"\n\t\t\t\t:disabled=\"!canFile\"\n\t\t\t\tdata-testid=\"cn-duplicate-warning-continue\"\n\t\t\t\t@click=\"fileAnyway\">\n\t\t\t\t{{ text(continueLabel) || t('nextcloud-vue', 'File it anyway') }}\n\t\t\t</NcButton>\n\t\t</template>\n\t</CnDialog>\n</template>\n\n<script>\nimport { translatePlural as n, translate as t } from '@nextcloud/l10n'\nimport { generateUrl } from '@nextcloud/router'\nimport { NcButton, NcNoteCard, NcTextField } from '@nextcloud/vue'\nimport { duplicateAffordances } from '../../utils/duplicateWarning.js'\nimport { fillTemplate } from '../../utils/fillTemplate.js'\nimport { CnDialog } from '../CnDialog/index.js'\n\n/**\n * CnDuplicateWarning — \"this may already exist\", before a record is filed.\n * It renders OpenRegister's scored matches (`{ uuid, score, matchedOn }`),\n * never scoring anything itself, with each matched record's title, a meta\n * line, what it matched on and how strongly. Under a `warn` policy anyone may\n * file it anyway; under `block` only a user who `mayOverride`, and they say\n * why. A match the user may not read is reported without disclosing it.\n * dossiq's case intake is the first consumer; pipelinq contacts share it.\n *\n * Open it in a template, or from code with `openDuplicateWarning(props)`,\n * which resolves with `{ reason, over }` or null.\n *\n * ```vue\n * <CnDuplicateWarning\n * :matches=\"matches\"\n * :records=\"readableCases\"\n * policy=\"block\"\n * :may-override=\"isCoordinator\"\n * record-url=\"/apps/dossiq/cases/{uuid}\"\n * :field-labels=\"{ requester: 'Requester', title: 'Subject' }\"\n * @file=\"save\"\n * @close=\"cancel\" />\n * ```\n */\nexport default {\n\tname: 'CnDuplicateWarning',\n\n\tcomponents: { CnDialog, NcButton, NcNoteCard, NcTextField },\n\n\tinject: {\n\t\t/** The host app's translate (CnAppRoot `cnTranslate`). */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The scored matches, as OpenRegister answered: `{ uuid, score, matchedOn }`.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\tmatches: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * The matched records the user may read, matched by `id`, `uuid` or `@self.id`.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\trecords: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** `warn` lets anyone file it anyway; `block` only a user who `mayOverride`. */\n\t\tpolicy: {\n\t\t\ttype: String,\n\t\t\tdefault: 'warn',\n\t\t},\n\n\t\t/** Whether this user may file over a block (and must say why). */\n\t\tmayOverride: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** A record's title, a template filled from it. */\n\t\trowTitle: {\n\t\t\ttype: String,\n\t\t\tdefault: '{title}',\n\t\t},\n\n\t\t/** A record's meta line, a template filled from it. */\n\t\trowMeta: {\n\t\t\ttype: String,\n\t\t\tdefault: '{identifier}',\n\t\t},\n\n\t\t/** A record's page, a template with `{uuid}`; empty draws no link. */\n\t\trecordUrl: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Words for the matched fields (`{ requester: \"Requester\" }`), through the host translate.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tfieldLabels: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** The dialog title; defaults to \"This may already exist\". */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The note's sentence, `{count}` filled; defaults to \"%n existing record looks like this one.\". */\n\t\theadline: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The sentence under a block the user may not pass. */\n\t\tblockedText: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The reason field's label. */\n\t\treasonLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The continue button's label; defaults to \"File it anyway\". */\n\t\tcontinueLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\temits: ['close', 'file'],\n\n\tdata() {\n\t\treturn {\n\t\t\treason: '',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * What to draw and offer.\n\t\t *\n\t\t * @spec openspec/changes/duplicate-warning/specs/duplicate-warning/spec.md#requirement-a-generic-duplicate-warning\n\t\t * @return {{show: boolean, blocked: boolean, mayContinue: boolean, needsReason: boolean}}\n\t\t */\n\t\taffordances() {\n\t\t\treturn duplicateAffordances({ matches: this.matches, policy: this.policy, mayOverride: this.mayOverride })\n\t\t},\n\n\t\t/** @return {boolean} Whether \"file it anyway\" may be pressed. */\n\t\tcanFile() {\n\t\t\treturn this.affordances.mayContinue && (!this.affordances.needsReason || this.reason.trim() !== '')\n\t\t},\n\n\t\t/** @return {string} The note's sentence. */\n\t\theadlineText() {\n\t\t\tconst count = this.matches.length\n\t\t\tif (this.headline) {\n\t\t\t\treturn fillTemplate(this.cnTranslate(this.headline), { count })\n\t\t\t}\n\t\t\treturn n('nextcloud-vue', '%n existing record looks like this one.', '%n existing records look like this one.', count)\n\t\t},\n\n\t\t/**\n\t\t * One row per match: the readable record's title and meta and link, or\n\t\t * a neutral name for one the user may not read.\n\t\t *\n\t\t * @spec openspec/changes/duplicate-warning/specs/duplicate-warning/spec.md#requirement-a-generic-duplicate-warning\n\t\t * @return {Array<object>}\n\t\t */\n\t\trows() {\n\t\t\treturn this.matches.map((match) => {\n\t\t\t\tconst uuid = String((match && match.uuid) || '')\n\t\t\t\tconst record = this.records.find((row) => String((row && (row.id || row.uuid || (row['@self'] && row['@self'].id))) || '') === uuid)\n\t\t\t\tconst fields = Array.isArray(match && match.matchedOn) ? match.matchedOn : []\n\t\t\t\tconst score = Number(match && match.score)\n\t\t\t\treturn {\n\t\t\t\t\tuuid,\n\t\t\t\t\ttitle: record ? (fillTemplate(this.rowTitle, record).trim() || uuid) : t('nextcloud-vue', 'A record you may not open'),\n\t\t\t\t\tmeta: record ? fillTemplate(this.rowMeta, record).trim() : '',\n\t\t\t\t\thref: record && this.recordUrl ? generateUrl(fillTemplate(this.recordUrl, { uuid: encodeURIComponent(uuid) })) : '',\n\t\t\t\t\tmatchedOn: fields.length\n\t\t\t\t\t\t? fields.map((field) => (this.fieldLabels[field] ? this.cnTranslate(this.fieldLabels[field]) : field)).join(', ')\n\t\t\t\t\t\t: t('nextcloud-vue', 'Matched on the score, not on a single field'),\n\t\t\t\t\tpercentage: Number.isFinite(score) ? Math.max(0, Math.min(100, Math.round(score * 100))) : 0,\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * A prop text through the host translate.\n\t\t *\n\t\t * @param {string} value The text.\n\t\t * @return {string}\n\t\t */\n\t\ttext(value) {\n\t\t\treturn value ? this.cnTranslate(value) : ''\n\t\t},\n\n\t\t/** File it over the warning, carrying the reason and what it was filed over. */\n\t\tfileAnyway() {\n\t\t\tif (!this.canFile) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\t/** @event file The user files it anyway. Payload: `{ reason, over }`, `over` the matched uuids. */\n\t\t\tthis.$emit('file', { reason: this.reason.trim(), over: this.matches.map((m) => String((m && m.uuid) || '')).filter(Boolean) })\n\t\t},\n\n\t\t/** Go back without filing. */\n\t\tclose() {\n\t\t\t/** @event close The user went back without filing. */\n\t\t\tthis.$emit('close')\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-duplicate-warning {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n}\n\n.cn-duplicate-warning__matches {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n\tmargin: 0;\n\tpadding: 0;\n\tlist-style: none;\n}\n\n.cn-duplicate-warning__match {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 4px 12px;\n\tpadding: 8px 12px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 8px);\n}\n\n.cn-duplicate-warning__title {\n\tfont-weight: 600;\n}\n\n.cn-duplicate-warning__meta,\n.cn-duplicate-warning__hint {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-duplicate-warning__hint {\n\tmargin: 0;\n}\n</style>\n"],"names":["CnDialog","n","t"],"mappings":"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"}
1
+ {"version":3,"file":"CnDuplicateWarning.vue2.js","sources":["../../../../src/components/CnDuplicateWarning/CnDuplicateWarning.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<CnDialog\n\t\t:name=\"text(title) || t('nextcloud-vue', 'This may already exist')\"\n\t\t:eyebrow=\"text(eyebrow)\"\n\t\twidth=\"form\"\n\t\tdata-testid=\"cn-duplicate-warning\"\n\t\t@closing=\"close\">\n\t\t<div class=\"cn-duplicate-warning\">\n\t\t\t<NcNoteCard :type=\"affordances.blocked && !affordances.mayContinue ? 'error' : 'warning'\" data-testid=\"cn-duplicate-warning-note\">\n\t\t\t\t{{ headlineText }}\n\t\t\t</NcNoteCard>\n\n\t\t\t<ul class=\"cn-duplicate-warning__matches\">\n\t\t\t\t<li v-for=\"row in rows\"\n\t\t\t\t\t:key=\"row.uuid\"\n\t\t\t\t\tclass=\"cn-duplicate-warning__match\"\n\t\t\t\t\tdata-testid=\"cn-duplicate-warning-match\">\n\t\t\t\t\t<a v-if=\"row.href\" :href=\"row.href\" class=\"cn-duplicate-warning__title\">{{ row.title }}</a>\n\t\t\t\t\t<span v-else class=\"cn-duplicate-warning__title\">{{ row.title }}</span>\n\t\t\t\t\t<span v-if=\"row.meta\" class=\"cn-duplicate-warning__meta\">{{ row.meta }}</span>\n\t\t\t\t\t<span class=\"cn-duplicate-warning__meta\">{{ row.matched }}</span>\n\t\t\t\t</li>\n\t\t\t</ul>\n\n\t\t\t<NcTextField v-if=\"affordances.needsReason\"\n\t\t\t\t:label=\"text(reasonLabel) || t('nextcloud-vue', 'Why file it anyway')\"\n\t\t\t\t:modelValue=\"reason\"\n\t\t\t\t:helperText=\"text(reasonHint)\"\n\t\t\t\tdata-testid=\"cn-duplicate-warning-reason\"\n\t\t\t\t@update:modelValue=\"reason = $event\" />\n\n\t\t\t<p v-if=\"affordances.blocked && !affordances.mayContinue\"\n\t\t\t\tclass=\"cn-duplicate-warning__hint\"\n\t\t\t\tdata-testid=\"cn-duplicate-warning-blocked\">\n\t\t\t\t{{ text(blockedText) || t('nextcloud-vue', 'This cannot be filed twice. Open the one it matches, or ask a coordinator.') }}\n\t\t\t</p>\n\n\t\t\t<p v-if=\"footnote\"\n\t\t\t\tclass=\"cn-duplicate-warning__hint\"\n\t\t\t\tdata-testid=\"cn-duplicate-warning-footnote\">\n\t\t\t\t{{ text(footnote) }}\n\t\t\t</p>\n\t\t</div>\n\n\t\t<template #actions>\n\t\t\t<NcButton variant=\"tertiary\" data-testid=\"cn-duplicate-warning-cancel\" @click=\"close\">\n\t\t\t\t{{ t('nextcloud-vue', 'Cancel') }}\n\t\t\t</NcButton>\n\t\t\t<NcButton v-if=\"affordances.mayContinue\"\n\t\t\t\tvariant=\"primary\"\n\t\t\t\t:disabled=\"!canFile\"\n\t\t\t\tdata-testid=\"cn-duplicate-warning-continue\"\n\t\t\t\t@click=\"fileAnyway\">\n\t\t\t\t{{ text(continueLabel) || t('nextcloud-vue', 'File it anyway') }}\n\t\t\t</NcButton>\n\t\t</template>\n\t</CnDialog>\n</template>\n\n<script>\nimport { translatePlural as n, translate as t } from '@nextcloud/l10n'\nimport { generateUrl } from '@nextcloud/router'\nimport { NcButton, NcNoteCard, NcTextField } from '@nextcloud/vue'\nimport { duplicateAffordances } from '../../utils/duplicateWarning.js'\nimport { fillTemplate, firstFilled } from '../../utils/fillTemplate.js'\nimport { CnDialog } from '../CnDialog/index.js'\n\n/**\n * CnDuplicateWarning — \"this may already exist\", before a record is filed.\n * It renders OpenRegister's scored matches (`{ uuid, score, matchedOn }`),\n * never scoring anything itself, with each matched record's title, a meta\n * line, what it matched on and how strongly. Under a `warn` policy anyone may\n * file it anyway; under `block` only a user who `mayOverride`, and they say\n * why. A match the user may not read is reported without disclosing it.\n * dossiq's case intake is the first consumer; pipelinq contacts share it.\n *\n * Open it in a template, or from code with `openDuplicateWarning(props)`,\n * which resolves with `{ reason, over }` or null.\n *\n * ```vue\n * <CnDuplicateWarning\n * :matches=\"matches\"\n * :records=\"readableCases\"\n * policy=\"block\"\n * :may-override=\"isCoordinator\"\n * record-url=\"/apps/dossiq/cases/{uuid}\"\n * :field-labels=\"{ requester: 'Requester', title: 'Subject' }\"\n * @file=\"save\"\n * @close=\"cancel\" />\n * ```\n */\nexport default {\n\tname: 'CnDuplicateWarning',\n\n\tcomponents: { CnDialog, NcButton, NcNoteCard, NcTextField },\n\n\tinject: {\n\t\t/** The host app's translate (CnAppRoot `cnTranslate`). */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The scored matches, as OpenRegister answered: `{ uuid, score, matchedOn }`.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\tmatches: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * The matched records the user may read, matched by `id`, `uuid` or `@self.id`.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\trecords: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** `warn` lets anyone file it anyway; `block` only a user who `mayOverride`. */\n\t\tpolicy: {\n\t\t\ttype: String,\n\t\t\tdefault: 'warn',\n\t\t},\n\n\t\t/** Whether this user may file over a block (and must say why). */\n\t\tmayOverride: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** A record's title, a template filled from it. */\n\t\trowTitle: {\n\t\t\ttype: String,\n\t\t\tdefault: '{title}',\n\t\t},\n\n\t\t/**\n\t\t * A record's meta line, a template filled from it, or several: the\n\t\t * first that fills is shown (\"{identifier} · {caseType.title}\", then\n\t\t * \"{identifier}\" for a record that lacks the type).\n\t\t *\n\t\t * @type {string|Array<string>}\n\t\t */\n\t\trowMeta: {\n\t\t\ttype: [String, Array],\n\t\t\tdefault: '{identifier}',\n\t\t},\n\n\t\t/**\n\t\t * What a match was found on, `{fields}` (joined as a list: \"Aanvrager\n\t\t * en Onderwerp\") and `{percentage}` filled, through the host\n\t\t * translate. Empty: \"Aanvrager, Onderwerp (82%)\".\n\t\t */\n\t\tmatchedText: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** A line above the title, as the board dialogs draw one. */\n\t\teyebrow: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The help line under the reason field. */\n\t\treasonHint: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** A muted note under the matches and the reason, for what Cancel and a block mean. */\n\t\tfootnote: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** A record's page, a template with `{uuid}`; empty draws no link. */\n\t\trecordUrl: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Words for the matched fields (`{ requester: \"Requester\" }`), through the host translate.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tfieldLabels: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** The dialog title; defaults to \"This may already exist\". */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The note's sentence, `{count}` filled; defaults to \"%n existing record looks like this one.\". */\n\t\theadline: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The sentence under a block the user may not pass. */\n\t\tblockedText: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The reason field's label. */\n\t\treasonLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The continue button's label; defaults to \"File it anyway\". */\n\t\tcontinueLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\temits: ['close', 'file'],\n\n\tdata() {\n\t\treturn {\n\t\t\treason: '',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * What to draw and offer.\n\t\t *\n\t\t * @spec openspec/changes/duplicate-warning/specs/duplicate-warning/spec.md#requirement-a-generic-duplicate-warning\n\t\t * @return {{show: boolean, blocked: boolean, mayContinue: boolean, needsReason: boolean}}\n\t\t */\n\t\taffordances() {\n\t\t\treturn duplicateAffordances({ matches: this.matches, policy: this.policy, mayOverride: this.mayOverride })\n\t\t},\n\n\t\t/** @return {boolean} Whether \"file it anyway\" may be pressed. */\n\t\tcanFile() {\n\t\t\treturn this.affordances.mayContinue && (!this.affordances.needsReason || this.reason.trim() !== '')\n\t\t},\n\n\t\t/** @return {string} The note's sentence. */\n\t\theadlineText() {\n\t\t\tconst count = this.matches.length\n\t\t\tif (this.headline) {\n\t\t\t\treturn fillTemplate(this.cnTranslate(this.headline), { count })\n\t\t\t}\n\t\t\treturn n('nextcloud-vue', '%n existing record looks like this one.', '%n existing records look like this one.', count)\n\t\t},\n\n\t\t/**\n\t\t * One row per match: the readable record's title and meta and link, or\n\t\t * a neutral name for one the user may not read.\n\t\t *\n\t\t * @spec openspec/changes/duplicate-warning/specs/duplicate-warning/spec.md#requirement-a-generic-duplicate-warning\n\t\t * @return {Array<object>}\n\t\t */\n\t\trows() {\n\t\t\treturn this.matches.map((match) => {\n\t\t\t\tconst uuid = String((match && match.uuid) || '')\n\t\t\t\tconst record = this.records.find((row) => String((row && (row.id || row.uuid || (row['@self'] && row['@self'].id))) || '') === uuid)\n\t\t\t\tconst fields = Array.isArray(match && match.matchedOn) ? match.matchedOn : []\n\t\t\t\tconst score = Number(match && match.score)\n\t\t\t\tconst words = fields.map((field) => (this.fieldLabels[field] ? this.cnTranslate(this.fieldLabels[field]) : field))\n\t\t\t\tconst percentage = Number.isFinite(score) ? Math.max(0, Math.min(100, Math.round(score * 100))) : 0\n\t\t\t\treturn {\n\t\t\t\t\tuuid,\n\t\t\t\t\ttitle: record ? (fillTemplate(this.rowTitle, record).trim() || uuid) : t('nextcloud-vue', 'A record you may not open'),\n\t\t\t\t\tmeta: record ? firstFilled(this.rowMeta, record, (key) => key).trim() : '',\n\t\t\t\t\thref: record && this.recordUrl ? generateUrl(fillTemplate(this.recordUrl, { uuid: encodeURIComponent(uuid) })) : '',\n\t\t\t\t\tmatched: this.matchedLine(words, percentage),\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * A prop text through the host translate.\n\t\t *\n\t\t * @param {string} value The text.\n\t\t * @return {string}\n\t\t */\n\t\ttext(value) {\n\t\t\treturn value ? this.cnTranslate(value) : ''\n\t\t},\n\n\t\t/**\n\t\t * What a match was found on, and how strongly.\n\t\t *\n\t\t * @param {Array<string>} words The matched fields, in the host's words.\n\t\t * @param {number} percentage The score as a percentage.\n\t\t * @return {string}\n\t\t */\n\t\tmatchedLine(words, percentage) {\n\t\t\tif (words.length === 0) {\n\t\t\t\treturn `${t('nextcloud-vue', 'Matched on the score, not on a single field')} (${percentage}%)`\n\t\t\t}\n\t\t\tif (!this.matchedText) {\n\t\t\t\treturn `${words.join(', ')} (${percentage}%)`\n\t\t\t}\n\t\t\tlet fields = words.join(', ')\n\t\t\ttry {\n\t\t\t\tfields = new Intl.ListFormat(document.documentElement.lang || undefined, { type: 'conjunction' }).format(words)\n\t\t\t} catch {\n\t\t\t\t// An engine without Intl.ListFormat keeps the comma list.\n\t\t\t}\n\t\t\treturn fillTemplate(this.cnTranslate(this.matchedText), { fields, percentage: String(percentage) })\n\t\t},\n\n\t\t/** File it over the warning, carrying the reason and what it was filed over. */\n\t\tfileAnyway() {\n\t\t\tif (!this.canFile) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\t/** @event file The user files it anyway. Payload: `{ reason, over }`, `over` the matched uuids. */\n\t\t\tthis.$emit('file', { reason: this.reason.trim(), over: this.matches.map((m) => String((m && m.uuid) || '')).filter(Boolean) })\n\t\t},\n\n\t\t/** Go back without filing. */\n\t\tclose() {\n\t\t\t/** @event close The user went back without filing. */\n\t\t\tthis.$emit('close')\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-duplicate-warning {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n}\n\n.cn-duplicate-warning__matches {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n\tmargin: 0;\n\tpadding: 0;\n\tlist-style: none;\n}\n\n.cn-duplicate-warning__match {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 4px 12px;\n\tpadding: 8px 12px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 8px);\n}\n\n.cn-duplicate-warning__title {\n\tfont-weight: 600;\n}\n\n.cn-duplicate-warning__meta,\n.cn-duplicate-warning__hint {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-duplicate-warning__hint {\n\tmargin: 0;\n}\n</style>\n"],"names":["CnDialog","n","t"],"mappings":"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"}
@@ -17,6 +17,11 @@ const _hoisted_7 = {
17
17
  class: "cn-duplicate-warning__hint",
18
18
  "data-testid": "cn-duplicate-warning-blocked"
19
19
  };
20
+ const _hoisted_8 = {
21
+ key: 2,
22
+ class: "cn-duplicate-warning__hint",
23
+ "data-testid": "cn-duplicate-warning-footnote"
24
+ };
20
25
 
21
26
  function render(_ctx, _cache, $props, $setup, $data, $options) {
22
27
  const _component_NcNoteCard = resolveComponent("NcNoteCard");
@@ -26,6 +31,7 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
26
31
 
27
32
  return (openBlock(), createBlock(_component_CnDialog, {
28
33
  name: $options.text($props.title) || $options.t('nextcloud-vue', 'This may already exist'),
34
+ eyebrow: $options.text($props.eyebrow),
29
35
  width: "form",
30
36
  "data-testid": "cn-duplicate-warning",
31
37
  onClosing: $options.close
@@ -59,7 +65,7 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
59
65
  default: withCtx(() => [
60
66
  createElementVNode("div", _hoisted_1, [
61
67
  createVNode(_component_NcNoteCard, {
62
- type: $options.affordances.blocked ? 'error' : 'warning',
68
+ type: $options.affordances.blocked && !$options.affordances.mayContinue ? 'error' : 'warning',
63
69
  "data-testid": "cn-duplicate-warning-note"
64
70
  }, {
65
71
  default: withCtx(() => [
@@ -84,7 +90,7 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
84
90
  (row.meta)
85
91
  ? (openBlock(), createElementBlock("span", _hoisted_5, toDisplayString(row.meta), 1 /* TEXT */))
86
92
  : createCommentVNode("v-if", true),
87
- createElementVNode("span", _hoisted_6, toDisplayString(row.matchedOn) + " (" + toDisplayString(row.percentage) + "%)", 1 /* TEXT */)
93
+ createElementVNode("span", _hoisted_6, toDisplayString(row.matched), 1 /* TEXT */)
88
94
  ]))
89
95
  }), 128 /* KEYED_FRAGMENT */))
90
96
  ]),
@@ -93,17 +99,21 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
93
99
  key: 0,
94
100
  label: $options.text($props.reasonLabel) || $options.t('nextcloud-vue', 'Why file it anyway'),
95
101
  modelValue: $data.reason,
102
+ helperText: $options.text($props.reasonHint),
96
103
  "data-testid": "cn-duplicate-warning-reason",
97
104
  "onUpdate:modelValue": _cache[0] || (_cache[0] = $event => ($data.reason = $event))
98
- }, null, 8 /* PROPS */, ["label", "modelValue"]))
105
+ }, null, 8 /* PROPS */, ["label", "modelValue", "helperText"]))
99
106
  : createCommentVNode("v-if", true),
100
107
  ($options.affordances.blocked && !$options.affordances.mayContinue)
101
108
  ? (openBlock(), createElementBlock("p", _hoisted_7, toDisplayString($options.text($props.blockedText) || $options.t('nextcloud-vue', 'This cannot be filed twice. Open the one it matches, or ask a coordinator.')), 1 /* TEXT */))
109
+ : createCommentVNode("v-if", true),
110
+ ($props.footnote)
111
+ ? (openBlock(), createElementBlock("p", _hoisted_8, toDisplayString($options.text($props.footnote)), 1 /* TEXT */))
102
112
  : createCommentVNode("v-if", true)
103
113
  ])
104
114
  ]),
105
115
  _: 1 /* STABLE */
106
- }, 8 /* PROPS */, ["name", "onClosing"]))
116
+ }, 8 /* PROPS */, ["name", "eyebrow", "onClosing"]))
107
117
  }
108
118
 
109
119
  export { render };
@@ -1 +1 @@
1
- {"version":3,"file":"CnDuplicateWarning.vue3.js","sources":["../../../../src/components/CnDuplicateWarning/CnDuplicateWarning.vue?vue&type=template&id=7cb31ec5&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<CnDialog\n\t\t:name=\"text(title) || t('nextcloud-vue', 'This may already exist')\"\n\t\twidth=\"form\"\n\t\tdata-testid=\"cn-duplicate-warning\"\n\t\t@closing=\"close\">\n\t\t<div class=\"cn-duplicate-warning\">\n\t\t\t<NcNoteCard :type=\"affordances.blocked ? 'error' : 'warning'\" data-testid=\"cn-duplicate-warning-note\">\n\t\t\t\t{{ headlineText }}\n\t\t\t</NcNoteCard>\n\n\t\t\t<ul class=\"cn-duplicate-warning__matches\">\n\t\t\t\t<li v-for=\"row in rows\"\n\t\t\t\t\t:key=\"row.uuid\"\n\t\t\t\t\tclass=\"cn-duplicate-warning__match\"\n\t\t\t\t\tdata-testid=\"cn-duplicate-warning-match\">\n\t\t\t\t\t<a v-if=\"row.href\" :href=\"row.href\" class=\"cn-duplicate-warning__title\">{{ row.title }}</a>\n\t\t\t\t\t<span v-else class=\"cn-duplicate-warning__title\">{{ row.title }}</span>\n\t\t\t\t\t<span v-if=\"row.meta\" class=\"cn-duplicate-warning__meta\">{{ row.meta }}</span>\n\t\t\t\t\t<span class=\"cn-duplicate-warning__meta\">{{ row.matchedOn }} ({{ row.percentage }}%)</span>\n\t\t\t\t</li>\n\t\t\t</ul>\n\n\t\t\t<NcTextField v-if=\"affordances.needsReason\"\n\t\t\t\t:label=\"text(reasonLabel) || t('nextcloud-vue', 'Why file it anyway')\"\n\t\t\t\t:modelValue=\"reason\"\n\t\t\t\tdata-testid=\"cn-duplicate-warning-reason\"\n\t\t\t\t@update:modelValue=\"reason = $event\" />\n\n\t\t\t<p v-if=\"affordances.blocked && !affordances.mayContinue\"\n\t\t\t\tclass=\"cn-duplicate-warning__hint\"\n\t\t\t\tdata-testid=\"cn-duplicate-warning-blocked\">\n\t\t\t\t{{ text(blockedText) || t('nextcloud-vue', 'This cannot be filed twice. Open the one it matches, or ask a coordinator.') }}\n\t\t\t</p>\n\t\t</div>\n\n\t\t<template #actions>\n\t\t\t<NcButton variant=\"tertiary\" data-testid=\"cn-duplicate-warning-cancel\" @click=\"close\">\n\t\t\t\t{{ t('nextcloud-vue', 'Cancel') }}\n\t\t\t</NcButton>\n\t\t\t<NcButton v-if=\"affordances.mayContinue\"\n\t\t\t\tvariant=\"primary\"\n\t\t\t\t:disabled=\"!canFile\"\n\t\t\t\tdata-testid=\"cn-duplicate-warning-continue\"\n\t\t\t\t@click=\"fileAnyway\">\n\t\t\t\t{{ text(continueLabel) || t('nextcloud-vue', 'File it anyway') }}\n\t\t\t</NcButton>\n\t\t</template>\n\t</CnDialog>\n</template>\n\n<script>\nimport { translatePlural as n, translate as t } from '@nextcloud/l10n'\nimport { generateUrl } from '@nextcloud/router'\nimport { NcButton, NcNoteCard, NcTextField } from '@nextcloud/vue'\nimport { duplicateAffordances } from '../../utils/duplicateWarning.js'\nimport { fillTemplate } from '../../utils/fillTemplate.js'\nimport { CnDialog } from '../CnDialog/index.js'\n\n/**\n * CnDuplicateWarning — \"this may already exist\", before a record is filed.\n * It renders OpenRegister's scored matches (`{ uuid, score, matchedOn }`),\n * never scoring anything itself, with each matched record's title, a meta\n * line, what it matched on and how strongly. Under a `warn` policy anyone may\n * file it anyway; under `block` only a user who `mayOverride`, and they say\n * why. A match the user may not read is reported without disclosing it.\n * dossiq's case intake is the first consumer; pipelinq contacts share it.\n *\n * Open it in a template, or from code with `openDuplicateWarning(props)`,\n * which resolves with `{ reason, over }` or null.\n *\n * ```vue\n * <CnDuplicateWarning\n * :matches=\"matches\"\n * :records=\"readableCases\"\n * policy=\"block\"\n * :may-override=\"isCoordinator\"\n * record-url=\"/apps/dossiq/cases/{uuid}\"\n * :field-labels=\"{ requester: 'Requester', title: 'Subject' }\"\n * @file=\"save\"\n * @close=\"cancel\" />\n * ```\n */\nexport default {\n\tname: 'CnDuplicateWarning',\n\n\tcomponents: { CnDialog, NcButton, NcNoteCard, NcTextField },\n\n\tinject: {\n\t\t/** The host app's translate (CnAppRoot `cnTranslate`). */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The scored matches, as OpenRegister answered: `{ uuid, score, matchedOn }`.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\tmatches: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * The matched records the user may read, matched by `id`, `uuid` or `@self.id`.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\trecords: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** `warn` lets anyone file it anyway; `block` only a user who `mayOverride`. */\n\t\tpolicy: {\n\t\t\ttype: String,\n\t\t\tdefault: 'warn',\n\t\t},\n\n\t\t/** Whether this user may file over a block (and must say why). */\n\t\tmayOverride: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** A record's title, a template filled from it. */\n\t\trowTitle: {\n\t\t\ttype: String,\n\t\t\tdefault: '{title}',\n\t\t},\n\n\t\t/** A record's meta line, a template filled from it. */\n\t\trowMeta: {\n\t\t\ttype: String,\n\t\t\tdefault: '{identifier}',\n\t\t},\n\n\t\t/** A record's page, a template with `{uuid}`; empty draws no link. */\n\t\trecordUrl: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Words for the matched fields (`{ requester: \"Requester\" }`), through the host translate.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tfieldLabels: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** The dialog title; defaults to \"This may already exist\". */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The note's sentence, `{count}` filled; defaults to \"%n existing record looks like this one.\". */\n\t\theadline: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The sentence under a block the user may not pass. */\n\t\tblockedText: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The reason field's label. */\n\t\treasonLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The continue button's label; defaults to \"File it anyway\". */\n\t\tcontinueLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\temits: ['close', 'file'],\n\n\tdata() {\n\t\treturn {\n\t\t\treason: '',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * What to draw and offer.\n\t\t *\n\t\t * @spec openspec/changes/duplicate-warning/specs/duplicate-warning/spec.md#requirement-a-generic-duplicate-warning\n\t\t * @return {{show: boolean, blocked: boolean, mayContinue: boolean, needsReason: boolean}}\n\t\t */\n\t\taffordances() {\n\t\t\treturn duplicateAffordances({ matches: this.matches, policy: this.policy, mayOverride: this.mayOverride })\n\t\t},\n\n\t\t/** @return {boolean} Whether \"file it anyway\" may be pressed. */\n\t\tcanFile() {\n\t\t\treturn this.affordances.mayContinue && (!this.affordances.needsReason || this.reason.trim() !== '')\n\t\t},\n\n\t\t/** @return {string} The note's sentence. */\n\t\theadlineText() {\n\t\t\tconst count = this.matches.length\n\t\t\tif (this.headline) {\n\t\t\t\treturn fillTemplate(this.cnTranslate(this.headline), { count })\n\t\t\t}\n\t\t\treturn n('nextcloud-vue', '%n existing record looks like this one.', '%n existing records look like this one.', count)\n\t\t},\n\n\t\t/**\n\t\t * One row per match: the readable record's title and meta and link, or\n\t\t * a neutral name for one the user may not read.\n\t\t *\n\t\t * @spec openspec/changes/duplicate-warning/specs/duplicate-warning/spec.md#requirement-a-generic-duplicate-warning\n\t\t * @return {Array<object>}\n\t\t */\n\t\trows() {\n\t\t\treturn this.matches.map((match) => {\n\t\t\t\tconst uuid = String((match && match.uuid) || '')\n\t\t\t\tconst record = this.records.find((row) => String((row && (row.id || row.uuid || (row['@self'] && row['@self'].id))) || '') === uuid)\n\t\t\t\tconst fields = Array.isArray(match && match.matchedOn) ? match.matchedOn : []\n\t\t\t\tconst score = Number(match && match.score)\n\t\t\t\treturn {\n\t\t\t\t\tuuid,\n\t\t\t\t\ttitle: record ? (fillTemplate(this.rowTitle, record).trim() || uuid) : t('nextcloud-vue', 'A record you may not open'),\n\t\t\t\t\tmeta: record ? fillTemplate(this.rowMeta, record).trim() : '',\n\t\t\t\t\thref: record && this.recordUrl ? generateUrl(fillTemplate(this.recordUrl, { uuid: encodeURIComponent(uuid) })) : '',\n\t\t\t\t\tmatchedOn: fields.length\n\t\t\t\t\t\t? fields.map((field) => (this.fieldLabels[field] ? this.cnTranslate(this.fieldLabels[field]) : field)).join(', ')\n\t\t\t\t\t\t: t('nextcloud-vue', 'Matched on the score, not on a single field'),\n\t\t\t\t\tpercentage: Number.isFinite(score) ? Math.max(0, Math.min(100, Math.round(score * 100))) : 0,\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * A prop text through the host translate.\n\t\t *\n\t\t * @param {string} value The text.\n\t\t * @return {string}\n\t\t */\n\t\ttext(value) {\n\t\t\treturn value ? this.cnTranslate(value) : ''\n\t\t},\n\n\t\t/** File it over the warning, carrying the reason and what it was filed over. */\n\t\tfileAnyway() {\n\t\t\tif (!this.canFile) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\t/** @event file The user files it anyway. Payload: `{ reason, over }`, `over` the matched uuids. */\n\t\t\tthis.$emit('file', { reason: this.reason.trim(), over: this.matches.map((m) => String((m && m.uuid) || '')).filter(Boolean) })\n\t\t},\n\n\t\t/** Go back without filing. */\n\t\tclose() {\n\t\t\t/** @event close The user went back without filing. */\n\t\t\tthis.$emit('close')\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-duplicate-warning {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n}\n\n.cn-duplicate-warning__matches {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n\tmargin: 0;\n\tpadding: 0;\n\tlist-style: none;\n}\n\n.cn-duplicate-warning__match {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 4px 12px;\n\tpadding: 8px 12px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 8px);\n}\n\n.cn-duplicate-warning__title {\n\tfont-weight: 600;\n}\n\n.cn-duplicate-warning__meta,\n.cn-duplicate-warning__hint {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-duplicate-warning__hint {\n\tmargin: 0;\n}\n</style>\n"],"names":["_createBlock","_createVNode","_createElementVNode","_createElementBlock","_Fragment","_renderList","_toDisplayString","_openBlock"],"mappings":"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"}
1
+ {"version":3,"file":"CnDuplicateWarning.vue3.js","sources":["../../../../src/components/CnDuplicateWarning/CnDuplicateWarning.vue?vue&type=template&id=22af78f5&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<CnDialog\n\t\t:name=\"text(title) || t('nextcloud-vue', 'This may already exist')\"\n\t\t:eyebrow=\"text(eyebrow)\"\n\t\twidth=\"form\"\n\t\tdata-testid=\"cn-duplicate-warning\"\n\t\t@closing=\"close\">\n\t\t<div class=\"cn-duplicate-warning\">\n\t\t\t<NcNoteCard :type=\"affordances.blocked && !affordances.mayContinue ? 'error' : 'warning'\" data-testid=\"cn-duplicate-warning-note\">\n\t\t\t\t{{ headlineText }}\n\t\t\t</NcNoteCard>\n\n\t\t\t<ul class=\"cn-duplicate-warning__matches\">\n\t\t\t\t<li v-for=\"row in rows\"\n\t\t\t\t\t:key=\"row.uuid\"\n\t\t\t\t\tclass=\"cn-duplicate-warning__match\"\n\t\t\t\t\tdata-testid=\"cn-duplicate-warning-match\">\n\t\t\t\t\t<a v-if=\"row.href\" :href=\"row.href\" class=\"cn-duplicate-warning__title\">{{ row.title }}</a>\n\t\t\t\t\t<span v-else class=\"cn-duplicate-warning__title\">{{ row.title }}</span>\n\t\t\t\t\t<span v-if=\"row.meta\" class=\"cn-duplicate-warning__meta\">{{ row.meta }}</span>\n\t\t\t\t\t<span class=\"cn-duplicate-warning__meta\">{{ row.matched }}</span>\n\t\t\t\t</li>\n\t\t\t</ul>\n\n\t\t\t<NcTextField v-if=\"affordances.needsReason\"\n\t\t\t\t:label=\"text(reasonLabel) || t('nextcloud-vue', 'Why file it anyway')\"\n\t\t\t\t:modelValue=\"reason\"\n\t\t\t\t:helperText=\"text(reasonHint)\"\n\t\t\t\tdata-testid=\"cn-duplicate-warning-reason\"\n\t\t\t\t@update:modelValue=\"reason = $event\" />\n\n\t\t\t<p v-if=\"affordances.blocked && !affordances.mayContinue\"\n\t\t\t\tclass=\"cn-duplicate-warning__hint\"\n\t\t\t\tdata-testid=\"cn-duplicate-warning-blocked\">\n\t\t\t\t{{ text(blockedText) || t('nextcloud-vue', 'This cannot be filed twice. Open the one it matches, or ask a coordinator.') }}\n\t\t\t</p>\n\n\t\t\t<p v-if=\"footnote\"\n\t\t\t\tclass=\"cn-duplicate-warning__hint\"\n\t\t\t\tdata-testid=\"cn-duplicate-warning-footnote\">\n\t\t\t\t{{ text(footnote) }}\n\t\t\t</p>\n\t\t</div>\n\n\t\t<template #actions>\n\t\t\t<NcButton variant=\"tertiary\" data-testid=\"cn-duplicate-warning-cancel\" @click=\"close\">\n\t\t\t\t{{ t('nextcloud-vue', 'Cancel') }}\n\t\t\t</NcButton>\n\t\t\t<NcButton v-if=\"affordances.mayContinue\"\n\t\t\t\tvariant=\"primary\"\n\t\t\t\t:disabled=\"!canFile\"\n\t\t\t\tdata-testid=\"cn-duplicate-warning-continue\"\n\t\t\t\t@click=\"fileAnyway\">\n\t\t\t\t{{ text(continueLabel) || t('nextcloud-vue', 'File it anyway') }}\n\t\t\t</NcButton>\n\t\t</template>\n\t</CnDialog>\n</template>\n\n<script>\nimport { translatePlural as n, translate as t } from '@nextcloud/l10n'\nimport { generateUrl } from '@nextcloud/router'\nimport { NcButton, NcNoteCard, NcTextField } from '@nextcloud/vue'\nimport { duplicateAffordances } from '../../utils/duplicateWarning.js'\nimport { fillTemplate, firstFilled } from '../../utils/fillTemplate.js'\nimport { CnDialog } from '../CnDialog/index.js'\n\n/**\n * CnDuplicateWarning — \"this may already exist\", before a record is filed.\n * It renders OpenRegister's scored matches (`{ uuid, score, matchedOn }`),\n * never scoring anything itself, with each matched record's title, a meta\n * line, what it matched on and how strongly. Under a `warn` policy anyone may\n * file it anyway; under `block` only a user who `mayOverride`, and they say\n * why. A match the user may not read is reported without disclosing it.\n * dossiq's case intake is the first consumer; pipelinq contacts share it.\n *\n * Open it in a template, or from code with `openDuplicateWarning(props)`,\n * which resolves with `{ reason, over }` or null.\n *\n * ```vue\n * <CnDuplicateWarning\n * :matches=\"matches\"\n * :records=\"readableCases\"\n * policy=\"block\"\n * :may-override=\"isCoordinator\"\n * record-url=\"/apps/dossiq/cases/{uuid}\"\n * :field-labels=\"{ requester: 'Requester', title: 'Subject' }\"\n * @file=\"save\"\n * @close=\"cancel\" />\n * ```\n */\nexport default {\n\tname: 'CnDuplicateWarning',\n\n\tcomponents: { CnDialog, NcButton, NcNoteCard, NcTextField },\n\n\tinject: {\n\t\t/** The host app's translate (CnAppRoot `cnTranslate`). */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The scored matches, as OpenRegister answered: `{ uuid, score, matchedOn }`.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\tmatches: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * The matched records the user may read, matched by `id`, `uuid` or `@self.id`.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\trecords: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** `warn` lets anyone file it anyway; `block` only a user who `mayOverride`. */\n\t\tpolicy: {\n\t\t\ttype: String,\n\t\t\tdefault: 'warn',\n\t\t},\n\n\t\t/** Whether this user may file over a block (and must say why). */\n\t\tmayOverride: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** A record's title, a template filled from it. */\n\t\trowTitle: {\n\t\t\ttype: String,\n\t\t\tdefault: '{title}',\n\t\t},\n\n\t\t/**\n\t\t * A record's meta line, a template filled from it, or several: the\n\t\t * first that fills is shown (\"{identifier} · {caseType.title}\", then\n\t\t * \"{identifier}\" for a record that lacks the type).\n\t\t *\n\t\t * @type {string|Array<string>}\n\t\t */\n\t\trowMeta: {\n\t\t\ttype: [String, Array],\n\t\t\tdefault: '{identifier}',\n\t\t},\n\n\t\t/**\n\t\t * What a match was found on, `{fields}` (joined as a list: \"Aanvrager\n\t\t * en Onderwerp\") and `{percentage}` filled, through the host\n\t\t * translate. Empty: \"Aanvrager, Onderwerp (82%)\".\n\t\t */\n\t\tmatchedText: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** A line above the title, as the board dialogs draw one. */\n\t\teyebrow: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The help line under the reason field. */\n\t\treasonHint: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** A muted note under the matches and the reason, for what Cancel and a block mean. */\n\t\tfootnote: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** A record's page, a template with `{uuid}`; empty draws no link. */\n\t\trecordUrl: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Words for the matched fields (`{ requester: \"Requester\" }`), through the host translate.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tfieldLabels: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** The dialog title; defaults to \"This may already exist\". */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The note's sentence, `{count}` filled; defaults to \"%n existing record looks like this one.\". */\n\t\theadline: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The sentence under a block the user may not pass. */\n\t\tblockedText: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The reason field's label. */\n\t\treasonLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The continue button's label; defaults to \"File it anyway\". */\n\t\tcontinueLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\temits: ['close', 'file'],\n\n\tdata() {\n\t\treturn {\n\t\t\treason: '',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * What to draw and offer.\n\t\t *\n\t\t * @spec openspec/changes/duplicate-warning/specs/duplicate-warning/spec.md#requirement-a-generic-duplicate-warning\n\t\t * @return {{show: boolean, blocked: boolean, mayContinue: boolean, needsReason: boolean}}\n\t\t */\n\t\taffordances() {\n\t\t\treturn duplicateAffordances({ matches: this.matches, policy: this.policy, mayOverride: this.mayOverride })\n\t\t},\n\n\t\t/** @return {boolean} Whether \"file it anyway\" may be pressed. */\n\t\tcanFile() {\n\t\t\treturn this.affordances.mayContinue && (!this.affordances.needsReason || this.reason.trim() !== '')\n\t\t},\n\n\t\t/** @return {string} The note's sentence. */\n\t\theadlineText() {\n\t\t\tconst count = this.matches.length\n\t\t\tif (this.headline) {\n\t\t\t\treturn fillTemplate(this.cnTranslate(this.headline), { count })\n\t\t\t}\n\t\t\treturn n('nextcloud-vue', '%n existing record looks like this one.', '%n existing records look like this one.', count)\n\t\t},\n\n\t\t/**\n\t\t * One row per match: the readable record's title and meta and link, or\n\t\t * a neutral name for one the user may not read.\n\t\t *\n\t\t * @spec openspec/changes/duplicate-warning/specs/duplicate-warning/spec.md#requirement-a-generic-duplicate-warning\n\t\t * @return {Array<object>}\n\t\t */\n\t\trows() {\n\t\t\treturn this.matches.map((match) => {\n\t\t\t\tconst uuid = String((match && match.uuid) || '')\n\t\t\t\tconst record = this.records.find((row) => String((row && (row.id || row.uuid || (row['@self'] && row['@self'].id))) || '') === uuid)\n\t\t\t\tconst fields = Array.isArray(match && match.matchedOn) ? match.matchedOn : []\n\t\t\t\tconst score = Number(match && match.score)\n\t\t\t\tconst words = fields.map((field) => (this.fieldLabels[field] ? this.cnTranslate(this.fieldLabels[field]) : field))\n\t\t\t\tconst percentage = Number.isFinite(score) ? Math.max(0, Math.min(100, Math.round(score * 100))) : 0\n\t\t\t\treturn {\n\t\t\t\t\tuuid,\n\t\t\t\t\ttitle: record ? (fillTemplate(this.rowTitle, record).trim() || uuid) : t('nextcloud-vue', 'A record you may not open'),\n\t\t\t\t\tmeta: record ? firstFilled(this.rowMeta, record, (key) => key).trim() : '',\n\t\t\t\t\thref: record && this.recordUrl ? generateUrl(fillTemplate(this.recordUrl, { uuid: encodeURIComponent(uuid) })) : '',\n\t\t\t\t\tmatched: this.matchedLine(words, percentage),\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * A prop text through the host translate.\n\t\t *\n\t\t * @param {string} value The text.\n\t\t * @return {string}\n\t\t */\n\t\ttext(value) {\n\t\t\treturn value ? this.cnTranslate(value) : ''\n\t\t},\n\n\t\t/**\n\t\t * What a match was found on, and how strongly.\n\t\t *\n\t\t * @param {Array<string>} words The matched fields, in the host's words.\n\t\t * @param {number} percentage The score as a percentage.\n\t\t * @return {string}\n\t\t */\n\t\tmatchedLine(words, percentage) {\n\t\t\tif (words.length === 0) {\n\t\t\t\treturn `${t('nextcloud-vue', 'Matched on the score, not on a single field')} (${percentage}%)`\n\t\t\t}\n\t\t\tif (!this.matchedText) {\n\t\t\t\treturn `${words.join(', ')} (${percentage}%)`\n\t\t\t}\n\t\t\tlet fields = words.join(', ')\n\t\t\ttry {\n\t\t\t\tfields = new Intl.ListFormat(document.documentElement.lang || undefined, { type: 'conjunction' }).format(words)\n\t\t\t} catch {\n\t\t\t\t// An engine without Intl.ListFormat keeps the comma list.\n\t\t\t}\n\t\t\treturn fillTemplate(this.cnTranslate(this.matchedText), { fields, percentage: String(percentage) })\n\t\t},\n\n\t\t/** File it over the warning, carrying the reason and what it was filed over. */\n\t\tfileAnyway() {\n\t\t\tif (!this.canFile) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\t/** @event file The user files it anyway. Payload: `{ reason, over }`, `over` the matched uuids. */\n\t\t\tthis.$emit('file', { reason: this.reason.trim(), over: this.matches.map((m) => String((m && m.uuid) || '')).filter(Boolean) })\n\t\t},\n\n\t\t/** Go back without filing. */\n\t\tclose() {\n\t\t\t/** @event close The user went back without filing. */\n\t\t\tthis.$emit('close')\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-duplicate-warning {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 12px;\n}\n\n.cn-duplicate-warning__matches {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 8px;\n\tmargin: 0;\n\tpadding: 0;\n\tlist-style: none;\n}\n\n.cn-duplicate-warning__match {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 4px 12px;\n\tpadding: 8px 12px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-large, 8px);\n}\n\n.cn-duplicate-warning__title {\n\tfont-weight: 600;\n}\n\n.cn-duplicate-warning__meta,\n.cn-duplicate-warning__hint {\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-duplicate-warning__hint {\n\tmargin: 0;\n}\n</style>\n"],"names":["_createBlock","_createVNode","_createElementVNode","_createElementBlock","_Fragment","_renderList","_toDisplayString","_openBlock"],"mappings":"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"}
@@ -3,7 +3,7 @@ export { FACTS_REVEAL_CHANNEL } from './CnFactsWidget.vue2.js';
3
3
  import { render } from './CnFactsWidget.vue3.js';
4
4
 
5
5
  script.render = render;
6
- script.__scopeId = "data-v-48761b13";
6
+ script.__scopeId = "data-v-e627249e";
7
7
  const __sfc_main = script;
8
8
 
9
9
  export { __sfc_main as default };