@conduction/nextcloud-vue 2.63.0 → 2.64.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 (100) hide show
  1. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +154 -7
  2. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
  3. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +119 -42
  4. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
  5. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +87 -0
  6. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  7. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +26 -24
  8. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  9. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +61 -16
  10. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
  11. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
  12. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js +20 -0
  13. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js.map +1 -1
  14. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js +3 -2
  15. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js.map +1 -1
  16. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +33 -2
  17. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  18. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  19. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +29 -0
  20. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  21. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +150 -133
  22. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  23. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +37 -1
  24. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  25. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +12 -4
  26. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  27. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +66 -14
  28. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  29. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +23 -14
  30. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  31. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js +81 -4
  32. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js.map +1 -1
  33. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js +22 -5
  34. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js.map +1 -1
  35. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +30 -0
  36. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  37. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +2 -2
  38. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  39. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +5 -1
  40. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  41. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +4 -2
  42. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  43. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js +63 -2
  44. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
  45. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js +96 -37
  46. package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
  47. package/dist/esm/components/CnStagesWidget/stagesModel.js +5 -2
  48. package/dist/esm/components/CnStagesWidget/stagesModel.js.map +1 -1
  49. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +52 -2
  50. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  51. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +6 -6
  52. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  53. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +14 -2
  54. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  55. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +2 -1
  56. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  57. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js +73 -1
  58. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js.map +1 -1
  59. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js +14 -2
  60. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js.map +1 -1
  61. package/dist/esm/l10n/en.json.js +2 -1
  62. package/dist/esm/l10n/en.json.js.map +1 -1
  63. package/dist/esm/l10n/nl.json.js +2 -1
  64. package/dist/esm/l10n/nl.json.js.map +1 -1
  65. package/dist/esm/nextcloud-vue.css +331 -81
  66. package/dist/esm/utils/schemaRefSlug.js +19 -3
  67. package/dist/esm/utils/schemaRefSlug.js.map +1 -1
  68. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  69. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  70. package/dist/nextcloud-vue.cjs.js +6933 -5958
  71. package/dist/nextcloud-vue.cjs.js.map +2 -2
  72. package/dist/nextcloud-vue.css +331 -81
  73. package/l10n/en.json +2 -0
  74. package/l10n/nl.json +2 -0
  75. package/package.json +1 -1
  76. package/src/components/CnAppNav/CnAppNav.vue +297 -11
  77. package/src/components/CnAppRoot/CnAppRoot.md +4 -0
  78. package/src/components/CnAppRoot/CnAppRoot.vue +94 -1
  79. package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +61 -16
  80. package/src/components/CnAuditTrailWidget/CnAuditTrailWidget.vue +22 -1
  81. package/src/components/CnCellRenderer/CnCellRenderer.vue +33 -2
  82. package/src/components/CnDashboardPage/CnDashboardPage.vue +43 -2
  83. package/src/components/CnDataTable/CnDataTable.vue +45 -1
  84. package/src/components/CnDetailPage/CnDetailPage.md +4 -0
  85. package/src/components/CnDetailPage/CnDetailPage.vue +60 -1
  86. package/src/components/CnHeaderWidget/CnHeaderWidget.vue +116 -6
  87. package/src/components/CnIndexPage/CnIndexPage.vue +32 -2
  88. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +7 -1
  89. package/src/components/CnStagesWidget/CnStagesWidget.vue +180 -2
  90. package/src/components/CnStagesWidget/stagesModel.js +5 -2
  91. package/src/components/CnStatWidget/CnStatWidget.vue +57 -3
  92. package/src/components/CnTabsWidget/CnTabsWidget.vue +15 -2
  93. package/src/components/CnWidgetWrapper/CnWidgetWrapper.md +4 -0
  94. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +107 -1
  95. package/src/css/detail-page.css +10 -4
  96. package/src/css/kpi-card.css +24 -1
  97. package/src/css/table.css +18 -6
  98. package/src/schemas/app-manifest-v2.schema.json +113 -3
  99. package/src/utils/schemaRefSlug.js +19 -3
  100. package/src/utils/validateManifestV2.compiled.js +2 -2
@@ -16,6 +16,7 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
16
16
  createVNode(_component_CnTabs, {
17
17
  "aria-label": $options.stripLabel,
18
18
  moreLabel: $options.moreLabel,
19
+ variant: $options.stripVariant,
19
20
  class: "cn-tabs-widget__tabs",
20
21
  "onUpdate:activeIndex": $options.onTabChange
21
22
  }, {
@@ -112,7 +113,7 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
112
113
  }), 128 /* KEYED_FRAGMENT */))
113
114
  ]),
114
115
  _: 1 /* STABLE */
115
- }, 8 /* PROPS */, ["aria-label", "moreLabel", "onUpdate:activeIndex"])
116
+ }, 8 /* PROPS */, ["aria-label", "moreLabel", "variant", "onUpdate:activeIndex"])
116
117
  ]))
117
118
  }
118
119
 
@@ -1 +1 @@
1
- {"version":3,"file":"CnTabsWidget.vue3.js","sources":["../../../../src/components/CnTabsWidget/CnTabsWidget.vue?vue&type=template&id=1dbd6122&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-tabs-widget\">\n\t\t<CnTabs\n\t\t\t:aria-label=\"stripLabel\"\n\t\t\t:moreLabel=\"moreLabel\"\n\t\t\tclass=\"cn-tabs-widget__tabs\"\n\t\t\t@update:activeIndex=\"onTabChange\">\n\t\t\t<!-- One Actions menu for the whole widget, bound to whichever child\n\t\t\t is showing. This is the point of the component: six tabbed\n\t\t\t widgets used to mean six card headers stacked down the page. -->\n\t\t\t<template #nav-end>\n\t\t\t\t<CnActionsMenu\n\t\t\t\t\t:showRefresh=\"showRefresh\"\n\t\t\t\t\t:showRequestFeature=\"showRequestFeature\"\n\t\t\t\t\t:showDocumentation=\"showDocumentation\"\n\t\t\t\t\t:documentationUrl=\"documentationUrl\"\n\t\t\t\t\t:widgetId=\"activeWidgetId\"\n\t\t\t\t\t:title=\"activeTitle\"\n\t\t\t\t\t:surface=\"`widget:${activeWidgetId}`\"\n\t\t\t\t\trefreshChannel=\"cn:widget:refresh\"\n\t\t\t\t\ttestidBase=\"cn-tabs-widget\">\n\t\t\t\t\t<!-- The open panel's own items, below the built-in trio.\n\t\t\t\t\t A panel draws no header, so these would otherwise have\n\t\t\t\t\t nowhere to go: the data widget's Metadata and full edit\n\t\t\t\t\t dialog vanished outright when the panel stopped drawing\n\t\t\t\t\t a card. They are rendered here rather than rebuilt here\n\t\t\t\t\t because `run` closes over the widget that published it,\n\t\t\t\t\t which is what keeps the dialog's per-widget config and\n\t\t\t\t\t the widget's own save path. See utils/panelActions.js.\n\n\t\t\t\t\t Only the ACTIVE panel's items. `lazy` keeps a visited\n\t\t\t\t\t tab mounted, so several panels publish at once and an\n\t\t\t\t\t unfiltered list would offer actions for a sheet nobody\n\t\t\t\t\t is looking at. -->\n\t\t\t\t\t<template v-if=\"activePanelActions.length\" #action-items>\n\t\t\t\t\t\t<NcActionButton\n\t\t\t\t\t\t\tv-for=\"action in activePanelActions\"\n\t\t\t\t\t\t\t:key=\"action.key\"\n\t\t\t\t\t\t\t:closeAfterClick=\"true\"\n\t\t\t\t\t\t\t@click=\"action.run()\">\n\t\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t\t<CnIcon :name=\"action.icon\" :size=\"20\" />\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t{{ action.label }}\n\t\t\t\t\t\t</NcActionButton>\n\t\t\t\t\t</template>\n\t\t\t\t</CnActionsMenu>\n\t\t\t</template>\n\n\t\t\t<CnTab\n\t\t\t\tv-for=\"(entry, index) in resolvedTabs\"\n\t\t\t\t:key=\"entry.key\"\n\t\t\t\t:active=\"index === activeIndex\"\n\t\t\t\t:title=\"entry.label\"\n\t\t\t\t:count=\"entry.count\"\n\t\t\t\t:overflow=\"entry.overflow\"\n\t\t\t\tlazy\n\t\t\t\t@click=\"activeIndex = index\">\n\t\t\t\t<template #title>\n\t\t\t\t\t<span class=\"cn-tabs-widget__title\">\n\t\t\t\t\t\t<CnIcon\n\t\t\t\t\t\t\tv-if=\"entry.icon\"\n\t\t\t\t\t\t\t:name=\"entry.icon\"\n\t\t\t\t\t\t\t:size=\"18\"\n\t\t\t\t\t\t\tclass=\"cn-tabs-widget__title-icon\" />\n\t\t\t\t\t\t{{ entry.label }}\n\t\t\t\t\t</span>\n\t\t\t\t</template>\n\n\t\t\t\t<CnDetailWidgetHost\n\t\t\t\t\tv-if=\"entry.widget\"\n\t\t\t\t\t:widget=\"entry.widget\"\n\t\t\t\t\tchrome=\"bare\"\n\t\t\t\t\t:objectId=\"objectId\"\n\t\t\t\t\t:object=\"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\t\t@geoSaved=\"onGeoSaved\"\n\t\t\t\t\t@openIntegration=\"onOpenIntegration\" />\n\t\t\t\t<NcEmptyContent v-else :name=\"missingLabel(entry)\" />\n\t\t\t</CnTab>\n\t\t</CnTabs>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcActionButton, NcEmptyContent } from '@nextcloud/vue'\nimport CnDetailWidgetHost from '../CnDetailWidgetHost/CnDetailWidgetHost.vue'\nimport CnIcon from '../CnIcon/CnIcon.vue'\nimport CnTab from '../CnTabs/CnTab.vue'\nimport CnTabs from '../CnTabs/CnTabs.vue'\nimport { resolveTabCount } from '../../utils/detailActionModel.js'\nimport { PANEL_ACTION_SINK } from '../../utils/panelActions.js'\nimport { widgetTitleOf } from '../../utils/widgetDispatch.js'\nimport { CnActionsMenu } from '../CnActionsMenu/index.js'\n\n/**\n * CnTabsWidget — a widget that holds other widgets, one per tab.\n *\n * A detail page that carries notes, files, related records, sub-records, mail\n * and appointments renders six cards, each with its own header and its own\n * Actions menu, stacked down the page. All six say roughly the same thing about\n * one record, and only one of them is being read at a time. This widget puts\n * them behind a tab strip instead.\n *\n * ## What the tabs take over\n *\n * The strip owns the title and the card. A child renders through\n * `CnDetailWidgetHost` with `chrome=\"bare\"`, so it draws no header and no\n * border of its own, and its content fills the panel exactly as it would have\n * filled its card. The tab label is what the widget's card header used to say.\n *\n * The Actions menu moves OUT of the panels and into the bar, beside the strip\n * rather than inside it, and rebinds to whichever child is showing. So there is\n * one menu, and it always acts on what you are looking at.\n *\n * ## Configuring it\n *\n * `content.tabs[]` names the children and their labels, so a deployment can\n * relabel or reorder tabs, or drop one, without touching code:\n *\n * ```js\n * content: {\n * tabs: [\n * { widgetId: 'case-notes', label: 'Notes', icon: 'NoteTextOutline' },\n * { widgetId: 'case-files', label: 'Files and attachments' },\n * ],\n * }\n * ```\n *\n * `label` and `icon` are optional and fall back to the child widget's own\n * title and icon, so the common case is a list of `widgetId`s.\n *\n * ## Panels are lazy, and stay mounted\n *\n * Each panel is a `CnTab` with `lazy`, so a child only mounts when its tab is\n * first opened. Six eager panels that each fetch on `mounted()` would fire six\n * requests on page load to answer five questions nobody asked. Once opened, a\n * panel stays mounted, so switching back never refetches.\n *\n * ## A named tab whose widget is missing\n *\n * The tab still renders, and its panel says which widget id did not resolve.\n * Dropping the tab would be worse: `content.tabs[]` is hand-authored config,\n * and a typo that silently removes a tab is a typo nobody finds.\n */\nexport default {\n\tname: 'CnTabsWidget',\n\n\tcomponents: {\n\t\tCnActionsMenu,\n\t\tCnDetailWidgetHost,\n\t\tCnIcon,\n\t\tCnTab,\n\t\tCnTabs,\n\t\tNcActionButton,\n\t\tNcEmptyContent,\n\t},\n\n\t/**\n\t * Offer the panels a way to put their own menu items in the strip's menu.\n\t *\n\t * Re-provided one level down by `CnDetailWidgetHost`, which knows the id to\n\t * key by; see utils/panelActions.js for why the items travel up instead of\n\t * being rebuilt here.\n\t *\n\t * @return {object} The provided sink.\n\t */\n\tprovide() {\n\t\treturn {\n\t\t\t[PANEL_ACTION_SINK]: {\n\t\t\t\t/**\n\t\t\t\t * Publish a panel's items, under one SOURCE.\n\t\t\t\t *\n\t\t\t\t * A panel has two possible publishers: the host, for an action\n\t\t\t\t * the host itself would have drawn (the catalog Add), and the\n\t\t\t\t * widget inside it, for its own menu items. Keyed by source so\n\t\t\t\t * the two coexist; a single slot per widget meant whichever\n\t\t\t\t * published last silently replaced the other.\n\t\t\t\t *\n\t\t\t\t * Replaces the map rather than mutating it so the computed that\n\t\t\t\t * reads it re-evaluates on any change.\n\t\t\t\t *\n\t\t\t\t * @param {string} id The publishing widget's id.\n\t\t\t\t * @param {object[]} items Its PanelAction descriptors.\n\t\t\t\t * @param {string} [source] Who is publishing: `host` or `widget`.\n\t\t\t\t * @return {void}\n\t\t\t\t */\n\t\t\t\tset: (id, items, source = 'widget') => {\n\t\t\t\t\t// `activePanelActions` spreads what is stored, so a\n\t\t\t\t\t// non-array becomes one menu item per character.\n\t\t\t\t\tif (!id || !Array.isArray(items)) {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tconst forId = { ...(this.panelActionsByWidget[id] || {}), [source]: items }\n\t\t\t\t\tthis.panelActionsByWidget = { ...this.panelActionsByWidget, [id]: forId }\n\t\t\t\t},\n\n\t\t\t\t/**\n\t\t\t\t * Withdraw one source's items, on unmount or when that\n\t\t\t\t * publisher's own menu comes back. The other source's items\n\t\t\t\t * stay.\n\t\t\t\t *\n\t\t\t\t * @param {string} id The publishing widget's id.\n\t\t\t\t * @param {string} [source] Who is withdrawing.\n\t\t\t\t * @return {void}\n\t\t\t\t */\n\t\t\t\tclear: (id, source = 'widget') => {\n\t\t\t\t\tconst forId = this.panelActionsByWidget[id]\n\t\t\t\t\tif (!forId || !(source in forId)) {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tconst { [source]: _removed, ...keptSources } = forId\n\t\t\t\t\tif (Object.keys(keptSources).length) {\n\t\t\t\t\t\tthis.panelActionsByWidget = { ...this.panelActionsByWidget, [id]: keptSources }\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tconst { [id]: _gone, ...rest } = this.panelActionsByWidget\n\t\t\t\t\tthis.panelActionsByWidget = rest\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget's config: `{ tabs, ariaLabel }`.\n\t\t *\n\t\t * `tabs[]` entries are `{ widgetId, label?, icon?, count?, countField?, overflow? }`.\n\t\t * `label` and `icon` fall back to the referenced widget's own title and\n\t\t * icon. `count` is a number shown after the label; `countField` reads\n\t\t * it off the record instead (a list counts its items). `overflow: true`\n\t\t * lists the tab under \"More\".\n\t\t *\n\t\t * `maxVisibleTabs` caps the strip: later tabs go under \"More\".\n\t\t * `hideEmpty: true` moves a tab whose count is 0 there too.\n\t\t * `moreLabel` names that menu.\n\t\t *\n\t\t * @type {{ tabs?: Array<{widgetId: string, label?: string, icon?: string, count?: number, countField?: string, overflow?: boolean}>, ariaLabel?: string, maxVisibleTabs?: number, hideEmpty?: boolean, moreLabel?: string }}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Every widget definition available on the surface, for `content.tabs[]`\n\t\t * to reference by id.\n\t\t *\n\t\t * The surface passes its whole list rather than the resolved children\n\t\t * because a tab may name a widget that does not exist, and this component\n\t\t * has to be able to say so.\n\t\t *\n\t\t * @type {object[]}\n\t\t */\n\t\tavailableWidgets: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** The bound record's id. */\n\t\tobjectId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The loaded record, or null while it is still being fetched. */\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, needed by a `data` child. */\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 integration children (AD-19). */\n\t\tsurface: {\n\t\t\ttype: String,\n\t\t\tdefault: 'detail-page',\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\n\t\t/** Show the Refresh entry in the hoisted Actions menu. */\n\t\tshowRefresh: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Show the Request-a-feature entry in the hoisted Actions menu. */\n\t\tshowRequestFeature: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Show the Documentation entry in the hoisted Actions menu. */\n\t\tshowDocumentation: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Documentation URL for the hoisted Actions menu. */\n\t\tdocumentationUrl: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\temits: ['geo-saved', 'open-integration'],\n\n\tdata() {\n\t\treturn {\n\t\t\tactiveIndex: 0,\n\t\t\t// Items published by the panels, keyed by widget id. Keyed rather\n\t\t\t// than a flat list because `lazy` keeps a visited tab mounted, so\n\t\t\t// more than one panel publishes at a time.\n\t\t\tpanelActionsByWidget: {},\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The open panel's own menu items, or an empty list.\n\t\t *\n\t\t * @return {object[]} PanelAction descriptors for the active tab.\n\t\t */\n\t\tactivePanelActions() {\n\t\t\tconst forId = this.panelActionsByWidget[this.activeWidgetId]\n\t\t\tif (!forId) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\t// Host first, then the widget's own: the catalog Add is about the\n\t\t\t// panel as a whole, the widget's items about what is in it.\n\t\t\treturn [...(forId.host || []), ...(forId.widget || [])]\n\t\t},\n\n\t\t/**\n\t\t * The configured tabs, each paired with the widget definition it names.\n\t\t *\n\t\t * A tab goes under the strip's \"More\" menu when it says `overflow`, when\n\t\t * `content.hideEmpty` is set and its count is 0, or when\n\t\t * `content.maxVisibleTabs` places are already taken. `count` is a\n\t\t * literal, or read off the record through `countField`.\n\t\t *\n\t\t * @return {object[]} `{ key, widgetId, label, icon, widget, count, overflow }` per tab.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-tab-counts-and-overflow\n\t\t */\n\t\tresolvedTabs() {\n\t\t\tconst tabs = Array.isArray(this.content?.tabs) ? this.content.tabs : []\n\t\t\tconst max = Number(this.content?.maxVisibleTabs)\n\t\t\tconst hideEmpty = this.content?.hideEmpty === true\n\t\t\t// How many tabs have taken a place in the strip so far. A tab that\n\t\t\t// is already going under \"More\" does not use one up.\n\t\t\tlet placed = 0\n\t\t\treturn tabs.map((tab, index) => {\n\t\t\t\tconst widgetId = typeof tab === 'string' ? tab : tab?.widgetId\n\t\t\t\tconst widget = this.availableWidgets.find((w) => w && w.id === widgetId) || null\n\t\t\t\tconst count = resolveTabCount(typeof tab === 'object' ? tab : null, this.objectData)\n\t\t\t\tlet overflow = (tab && tab.overflow === true) || (hideEmpty && count === 0)\n\t\t\t\tif (!overflow) {\n\t\t\t\t\tif (Number.isFinite(max) && max > 0 && placed >= max) {\n\t\t\t\t\t\toverflow = true\n\t\t\t\t\t} else {\n\t\t\t\t\t\tplaced += 1\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn {\n\t\t\t\t\tkey: `${widgetId || 'tab'}-${index}`,\n\t\t\t\t\twidgetId,\n\t\t\t\t\tlabel: (tab && tab.label) || widgetTitleOf(widget) || widgetId || '',\n\t\t\t\t\ticon: (tab && tab.icon) || widget?.icon || '',\n\t\t\t\t\twidget,\n\t\t\t\t\tcount,\n\t\t\t\t\toverflow,\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Name of the menu that holds the tabs that are not in the strip.\n\t\t *\n\t\t * @return {string|undefined} The configured label, or undefined for the default.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-tab-counts-and-overflow\n\t\t */\n\t\tmoreLabel() {\n\t\t\tconst label = this.content?.moreLabel\n\t\t\treturn typeof label === 'string' && label !== '' ? label : undefined\n\t\t},\n\n\t\t/**\n\t\t * The tab currently showing.\n\t\t *\n\t\t * @return {object|null} The resolved tab, or null when there are none.\n\t\t */\n\t\tactiveTab() {\n\t\t\treturn this.resolvedTabs[this.activeIndex] || null\n\t\t},\n\n\t\t/**\n\t\t * The active child's id, so the hoisted Refresh reaches THAT child's\n\t\t * fetch over `cn:widget:refresh` rather than a sibling's.\n\t\t *\n\t\t * @return {string} The active widget id.\n\t\t */\n\t\tactiveWidgetId() {\n\t\t\treturn this.activeTab?.widgetId || ''\n\t\t},\n\n\t\t/**\n\t\t * The active child's label, so the Actions menu names what it acts on.\n\t\t *\n\t\t * @return {string} The active tab's label.\n\t\t */\n\t\tactiveTitle() {\n\t\t\treturn this.activeTab?.label || ''\n\t\t},\n\n\t\t/**\n\t\t * Accessible name for the tab strip.\n\t\t *\n\t\t * @return {string} The aria-label.\n\t\t */\n\t\tstripLabel() {\n\t\t\treturn this.content?.ariaLabel || t('nextcloud-vue', 'Details')\n\t\t},\n\t},\n\n\t/**\n\t * Open on the first tab that is in the strip. Tab 0 is the default, and\n\t * when tab 0 sits under \"More\" the strip would open on a tab the author\n\t * chose to tuck away.\n\t *\n\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-tab-counts-and-overflow\n\t */\n\tcreated() {\n\t\tconst first = this.resolvedTabs.findIndex((tab) => !tab.overflow)\n\t\tif (first > 0) {\n\t\t\tthis.activeIndex = first\n\t\t}\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Re-emit a geo child's save so the surface can reload the record.\n\t\t *\n\t\t * @param {object} geo The saved geometry.\n\t\t * @return {void}\n\t\t */\n\t\tonGeoSaved(geo) {\n\t\t\t/**\n\t\t\t * @event geo-saved Re-emitted from a geo child that saved a geometry.\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('geo-saved', geo)\n\t\t},\n\n\t\t/**\n\t\t * Re-emit a related child's request to open an integration.\n\t\t *\n\t\t * @param {string} integrationId The integration to open.\n\t\t * @return {void}\n\t\t */\n\t\tonOpenIntegration(integrationId) {\n\t\t\t/**\n\t\t\t * @event open-integration Re-emitted from a related child asking to open an integration.\n\t\t\t * @type {string}\n\t\t\t */\n\t\t\tthis.$emit('open-integration', integrationId)\n\t\t},\n\n\t\t/**\n\t\t * Follow the strip's own selection, so keyboard navigation moves the\n\t\t * hoisted Actions menu too.\n\t\t *\n\t\t * @param {number} index The newly selected tab index.\n\t\t * @return {void}\n\t\t */\n\t\tonTabChange(index) {\n\t\t\tif (typeof index === 'number' && index >= 0) {\n\t\t\t\tthis.activeIndex = index\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Empty-state text for a tab whose widget id resolves to nothing.\n\t\t *\n\t\t * @param {object} entry The resolved tab.\n\t\t * @return {string} The message.\n\t\t */\n\t\tmissingLabel(entry) {\n\t\t\treturn t('nextcloud-vue', 'No widget found for \"{id}\"', { id: entry.widgetId || '' })\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n/* The panel is the card; the strip is not inside it. There is no title bar\n above the tabs either: the open tab names the panel, so a title row would\n say the same thing twice and cost a row of height on a card that is mostly\n content.\n\n The card chrome used to wrap the strip as well, which drew a border and a\n pair of rounded corners ABOVE the tabs and boxed them in. Folder tabs are\n drawn as the edge of the sheet they open, so a second edge around them\n reads as a header the widget does not have. Border, radius and background\n now live on the panel below, and the strip sits bare on the page. */\n/* The class is doubled on purpose, and the four properties below are RESET\n rather than simply omitted.\n\n Nextcloud serves every enabled app's assets on every page, each app bundles\n this library's compiled CSS, and the Vue scope id is derived from the file\n PATH, so `data-v-1dbd6122` is byte-identical across library versions. An app\n still on an older release therefore ships a rule with exactly this selector\n and the OLD declarations, and it lands on the pages of an app already on the\n new one. Measured on a dossiq case page: three stale copies of the pre-fix\n rule, one from hermiq's `companion.css` (its companion bundle loads on every\n page by design) and two inline from other apps' bundles. Same specificity,\n so source order decided, and the card border came back around the strip.\n\n Dropping a declaration cannot beat a rule that sets it, so `border` and the\n rest are explicitly zeroed. Doubling the class takes this selector to\n (0,3,0) against the stale (0,2,0), which wins on specificity rather than on\n `!important` or on load order this library does not control. The same\n technique, for the same reason, is used on `.cn-tabs__nav .cn-tabs__nav-item`\n in CnTabs to beat Nextcloud's own button margin. */\n.cn-tabs-widget.cn-tabs-widget {\n\tbackground-color: transparent;\n\tborder: 0;\n\tborder-radius: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\theight: 100%;\n\tmin-height: 0;\n\toverflow: visible;\n\tpadding: 0;\n}\n\n/* No inset, so the first tab starts at the panel's own left edge. The 8px\n here was there to clear the card's rounded top corner; that corner is gone\n from the strip now, and the inset it existed for left the first tab floating\n 8px inside the sheet it opens.\n\n The bar's own bottom rule is switched off and redrawn as a pseudo-element\n that stops one corner radius short of the right edge. CnTabs draws its rule\n across the bar's whole width, which is the sheet's top edge everywhere\n except at the far right, where the panel's top-right corner curves away\n below it: the rule ran straight on to the widget's edge and read as a\n border sticking out past a rounded corner (Ruben, 2026-09-13). Ending it\n where the curve begins makes the rule and the curve one continuous edge. */\n.cn-tabs-widget__tabs :deep(.cn-tabs__bar) {\n\tborder-bottom: none;\n\tpadding: 0;\n\tposition: relative;\n}\n\n.cn-tabs-widget__tabs :deep(.cn-tabs__bar::after) {\n\tborder-bottom: 1px solid var(--color-border);\n\tcontent: '';\n\tinset-block-end: 0;\n\tinset-inline: 0 var(--border-radius-large);\n\tpointer-events: none;\n\tposition: absolute;\n}\n\n/* The open tab erases the slice of the rule directly above the panel with its\n own background-coloured bottom edge, which only works if it paints ABOVE the\n pseudo-element: a positioned pseudo-element otherwise paints over every\n in-flow sibling, rule included. */\n.cn-tabs-widget__tabs :deep(.cn-tabs__nav-item--active) {\n\tposition: relative;\n\tz-index: 1;\n}\n\n.cn-tabs-widget__tabs {\n\tdisplay: flex;\n\tflex-direction: column;\n\t/* Grow to the widget's height, so the panel fills the grid cell the\n\t layout gave it. Without this the strip plus the open panel took only\n\t the height of the panel's content, and a cell of nine rows ended in\n\t 130px of nothing under a sheet that stopped short of its own card. */\n\tflex: 1 1 auto;\n\tmin-height: 0;\n}\n\n/* The panel area is the scroll region, so the strip stays put while a long\n child scrolls under it.\n\n Padded on all four sides. The top was once zeroed to protect the join with\n the open tab, but padding sits inside the painted box — only a margin would\n have opened that gap, and a zero top put every panel's content against the\n strip. */\n.cn-tabs-widget__tabs :deep(.cn-tabs__content) {\n\tbackground-color: var(--color-main-background);\n\t/* Three sides only: the bar's rule (redrawn above, stopping at the corner)\n\t is this sheet's top edge, and the open tab erases the slice of it\n\t directly above the panel so the two read as one surface. A border-top\n\t here would put a second line under that tab which the tab cannot paint\n\t over. */\n\tborder: 1px solid var(--color-border);\n\tborder-top: none;\n\t/* Three rounded corners. The top-left stays square because the first tab\n\t is drawn joined to the panel there, and a curve under it would curl the\n\t sheet away from that tab. The top-right has no tab above it: the strip\n\t ends where the last tab ends and the Actions menu floats free, so a\n\t square corner there read as a sheet cut off, not as a tab's edge. */\n\tborder-radius: 0 var(--border-radius-large) var(--border-radius-large) var(--border-radius-large);\n\tflex: 1 1 auto;\n\tmin-height: 0;\n\toverflow: auto;\n\tpadding: 12px;\n}\n\n.cn-tabs-widget__tabs :deep(.cn-tab) {\n\theight: 100%;\n}\n\n.cn-tabs-widget__title {\n\talign-items: center;\n\tdisplay: inline-flex;\n\tgap: 6px;\n}\n\n.cn-tabs-widget__title-icon {\n\tflex: 0 0 auto;\n}\n</style>\n"],"names":["_openBlock","_createElementBlock","_Fragment","_renderList","_createBlock","_createVNode","_createElementVNode","_toDisplayString"],"mappings":"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"}
1
+ {"version":3,"file":"CnTabsWidget.vue3.js","sources":["../../../../src/components/CnTabsWidget/CnTabsWidget.vue?vue&type=template&id=1dbd6122&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-tabs-widget\">\n\t\t<CnTabs\n\t\t\t:aria-label=\"stripLabel\"\n\t\t\t:moreLabel=\"moreLabel\"\n\t\t\t:variant=\"stripVariant\"\n\t\t\tclass=\"cn-tabs-widget__tabs\"\n\t\t\t@update:activeIndex=\"onTabChange\">\n\t\t\t<!-- One Actions menu for the whole widget, bound to whichever child\n\t\t\t is showing. This is the point of the component: six tabbed\n\t\t\t widgets used to mean six card headers stacked down the page. -->\n\t\t\t<template #nav-end>\n\t\t\t\t<CnActionsMenu\n\t\t\t\t\t:showRefresh=\"showRefresh\"\n\t\t\t\t\t:showRequestFeature=\"showRequestFeature\"\n\t\t\t\t\t:showDocumentation=\"showDocumentation\"\n\t\t\t\t\t:documentationUrl=\"documentationUrl\"\n\t\t\t\t\t:widgetId=\"activeWidgetId\"\n\t\t\t\t\t:title=\"activeTitle\"\n\t\t\t\t\t:surface=\"`widget:${activeWidgetId}`\"\n\t\t\t\t\trefreshChannel=\"cn:widget:refresh\"\n\t\t\t\t\ttestidBase=\"cn-tabs-widget\">\n\t\t\t\t\t<!-- The open panel's own items, below the built-in trio.\n\t\t\t\t\t A panel draws no header, so these would otherwise have\n\t\t\t\t\t nowhere to go: the data widget's Metadata and full edit\n\t\t\t\t\t dialog vanished outright when the panel stopped drawing\n\t\t\t\t\t a card. They are rendered here rather than rebuilt here\n\t\t\t\t\t because `run` closes over the widget that published it,\n\t\t\t\t\t which is what keeps the dialog's per-widget config and\n\t\t\t\t\t the widget's own save path. See utils/panelActions.js.\n\n\t\t\t\t\t Only the ACTIVE panel's items. `lazy` keeps a visited\n\t\t\t\t\t tab mounted, so several panels publish at once and an\n\t\t\t\t\t unfiltered list would offer actions for a sheet nobody\n\t\t\t\t\t is looking at. -->\n\t\t\t\t\t<template v-if=\"activePanelActions.length\" #action-items>\n\t\t\t\t\t\t<NcActionButton\n\t\t\t\t\t\t\tv-for=\"action in activePanelActions\"\n\t\t\t\t\t\t\t:key=\"action.key\"\n\t\t\t\t\t\t\t:closeAfterClick=\"true\"\n\t\t\t\t\t\t\t@click=\"action.run()\">\n\t\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t\t<CnIcon :name=\"action.icon\" :size=\"20\" />\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t{{ action.label }}\n\t\t\t\t\t\t</NcActionButton>\n\t\t\t\t\t</template>\n\t\t\t\t</CnActionsMenu>\n\t\t\t</template>\n\n\t\t\t<CnTab\n\t\t\t\tv-for=\"(entry, index) in resolvedTabs\"\n\t\t\t\t:key=\"entry.key\"\n\t\t\t\t:active=\"index === activeIndex\"\n\t\t\t\t:title=\"entry.label\"\n\t\t\t\t:count=\"entry.count\"\n\t\t\t\t:overflow=\"entry.overflow\"\n\t\t\t\tlazy\n\t\t\t\t@click=\"activeIndex = index\">\n\t\t\t\t<template #title>\n\t\t\t\t\t<span class=\"cn-tabs-widget__title\">\n\t\t\t\t\t\t<CnIcon\n\t\t\t\t\t\t\tv-if=\"entry.icon\"\n\t\t\t\t\t\t\t:name=\"entry.icon\"\n\t\t\t\t\t\t\t:size=\"18\"\n\t\t\t\t\t\t\tclass=\"cn-tabs-widget__title-icon\" />\n\t\t\t\t\t\t{{ entry.label }}\n\t\t\t\t\t</span>\n\t\t\t\t</template>\n\n\t\t\t\t<CnDetailWidgetHost\n\t\t\t\t\tv-if=\"entry.widget\"\n\t\t\t\t\t:widget=\"entry.widget\"\n\t\t\t\t\tchrome=\"bare\"\n\t\t\t\t\t:objectId=\"objectId\"\n\t\t\t\t\t:object=\"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\t\t@geoSaved=\"onGeoSaved\"\n\t\t\t\t\t@openIntegration=\"onOpenIntegration\" />\n\t\t\t\t<NcEmptyContent v-else :name=\"missingLabel(entry)\" />\n\t\t\t</CnTab>\n\t\t</CnTabs>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcActionButton, NcEmptyContent } from '@nextcloud/vue'\nimport CnDetailWidgetHost from '../CnDetailWidgetHost/CnDetailWidgetHost.vue'\nimport CnIcon from '../CnIcon/CnIcon.vue'\nimport CnTab from '../CnTabs/CnTab.vue'\nimport CnTabs from '../CnTabs/CnTabs.vue'\nimport { resolveTabCount } from '../../utils/detailActionModel.js'\nimport { PANEL_ACTION_SINK } from '../../utils/panelActions.js'\nimport { widgetTitleOf } from '../../utils/widgetDispatch.js'\nimport { CnActionsMenu } from '../CnActionsMenu/index.js'\n\n/**\n * CnTabsWidget — a widget that holds other widgets, one per tab.\n *\n * A detail page that carries notes, files, related records, sub-records, mail\n * and appointments renders six cards, each with its own header and its own\n * Actions menu, stacked down the page. All six say roughly the same thing about\n * one record, and only one of them is being read at a time. This widget puts\n * them behind a tab strip instead.\n *\n * ## What the tabs take over\n *\n * The strip owns the title and the card. A child renders through\n * `CnDetailWidgetHost` with `chrome=\"bare\"`, so it draws no header and no\n * border of its own, and its content fills the panel exactly as it would have\n * filled its card. The tab label is what the widget's card header used to say.\n *\n * The Actions menu moves OUT of the panels and into the bar, beside the strip\n * rather than inside it, and rebinds to whichever child is showing. So there is\n * one menu, and it always acts on what you are looking at.\n *\n * ## Configuring it\n *\n * `content.tabs[]` names the children and their labels, so a deployment can\n * relabel or reorder tabs, or drop one, without touching code:\n *\n * ```js\n * content: {\n * tabs: [\n * { widgetId: 'case-notes', label: 'Notes', icon: 'NoteTextOutline' },\n * { widgetId: 'case-files', label: 'Files and attachments' },\n * ],\n * }\n * ```\n *\n * `label` and `icon` are optional and fall back to the child widget's own\n * title and icon, so the common case is a list of `widgetId`s.\n *\n * ## Panels are lazy, and stay mounted\n *\n * Each panel is a `CnTab` with `lazy`, so a child only mounts when its tab is\n * first opened. Six eager panels that each fetch on `mounted()` would fire six\n * requests on page load to answer five questions nobody asked. Once opened, a\n * panel stays mounted, so switching back never refetches.\n *\n * ## A named tab whose widget is missing\n *\n * The tab still renders, and its panel says which widget id did not resolve.\n * Dropping the tab would be worse: `content.tabs[]` is hand-authored config,\n * and a typo that silently removes a tab is a typo nobody finds.\n */\nexport default {\n\tname: 'CnTabsWidget',\n\n\tcomponents: {\n\t\tCnActionsMenu,\n\t\tCnDetailWidgetHost,\n\t\tCnIcon,\n\t\tCnTab,\n\t\tCnTabs,\n\t\tNcActionButton,\n\t\tNcEmptyContent,\n\t},\n\n\t/**\n\t * Offer the panels a way to put their own menu items in the strip's menu.\n\t *\n\t * Re-provided one level down by `CnDetailWidgetHost`, which knows the id to\n\t * key by; see utils/panelActions.js for why the items travel up instead of\n\t * being rebuilt here.\n\t *\n\t * @return {object} The provided sink.\n\t */\n\tprovide() {\n\t\treturn {\n\t\t\t[PANEL_ACTION_SINK]: {\n\t\t\t\t/**\n\t\t\t\t * Publish a panel's items, under one SOURCE.\n\t\t\t\t *\n\t\t\t\t * A panel has two possible publishers: the host, for an action\n\t\t\t\t * the host itself would have drawn (the catalog Add), and the\n\t\t\t\t * widget inside it, for its own menu items. Keyed by source so\n\t\t\t\t * the two coexist; a single slot per widget meant whichever\n\t\t\t\t * published last silently replaced the other.\n\t\t\t\t *\n\t\t\t\t * Replaces the map rather than mutating it so the computed that\n\t\t\t\t * reads it re-evaluates on any change.\n\t\t\t\t *\n\t\t\t\t * @param {string} id The publishing widget's id.\n\t\t\t\t * @param {object[]} items Its PanelAction descriptors.\n\t\t\t\t * @param {string} [source] Who is publishing: `host` or `widget`.\n\t\t\t\t * @return {void}\n\t\t\t\t */\n\t\t\t\tset: (id, items, source = 'widget') => {\n\t\t\t\t\t// `activePanelActions` spreads what is stored, so a\n\t\t\t\t\t// non-array becomes one menu item per character.\n\t\t\t\t\tif (!id || !Array.isArray(items)) {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tconst forId = { ...(this.panelActionsByWidget[id] || {}), [source]: items }\n\t\t\t\t\tthis.panelActionsByWidget = { ...this.panelActionsByWidget, [id]: forId }\n\t\t\t\t},\n\n\t\t\t\t/**\n\t\t\t\t * Withdraw one source's items, on unmount or when that\n\t\t\t\t * publisher's own menu comes back. The other source's items\n\t\t\t\t * stay.\n\t\t\t\t *\n\t\t\t\t * @param {string} id The publishing widget's id.\n\t\t\t\t * @param {string} [source] Who is withdrawing.\n\t\t\t\t * @return {void}\n\t\t\t\t */\n\t\t\t\tclear: (id, source = 'widget') => {\n\t\t\t\t\tconst forId = this.panelActionsByWidget[id]\n\t\t\t\t\tif (!forId || !(source in forId)) {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tconst { [source]: _removed, ...keptSources } = forId\n\t\t\t\t\tif (Object.keys(keptSources).length) {\n\t\t\t\t\t\tthis.panelActionsByWidget = { ...this.panelActionsByWidget, [id]: keptSources }\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tconst { [id]: _gone, ...rest } = this.panelActionsByWidget\n\t\t\t\t\tthis.panelActionsByWidget = rest\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget's config: `{ tabs, ariaLabel }`.\n\t\t *\n\t\t * `tabs[]` entries are `{ widgetId, label?, icon?, count?, countField?, overflow? }`.\n\t\t * `label` and `icon` fall back to the referenced widget's own title and\n\t\t * icon. `count` is a number shown after the label; `countField` reads\n\t\t * it off the record instead (a list counts its items). `overflow: true`\n\t\t * lists the tab under \"More\".\n\t\t *\n\t\t * `maxVisibleTabs` caps the strip: later tabs go under \"More\".\n\t\t * `hideEmpty: true` moves a tab whose count is 0 there too.\n\t\t * `moreLabel` names that menu. `variant: \"segmented\"` draws the strip\n\t\t * as a pill switch (CnTabs' segmented variant); `line` is the default.\n\t\t *\n\t\t * @type {{ tabs?: Array<{widgetId: string, label?: string, icon?: string, count?: number, countField?: string, overflow?: boolean}>, ariaLabel?: string, maxVisibleTabs?: number, hideEmpty?: boolean, moreLabel?: string, variant?: ('line'|'segmented') }}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Every widget definition available on the surface, for `content.tabs[]`\n\t\t * to reference by id.\n\t\t *\n\t\t * The surface passes its whole list rather than the resolved children\n\t\t * because a tab may name a widget that does not exist, and this component\n\t\t * has to be able to say so.\n\t\t *\n\t\t * @type {object[]}\n\t\t */\n\t\tavailableWidgets: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** The bound record's id. */\n\t\tobjectId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The loaded record, or null while it is still being fetched. */\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, needed by a `data` child. */\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 integration children (AD-19). */\n\t\tsurface: {\n\t\t\ttype: String,\n\t\t\tdefault: 'detail-page',\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\n\t\t/** Show the Refresh entry in the hoisted Actions menu. */\n\t\tshowRefresh: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Show the Request-a-feature entry in the hoisted Actions menu. */\n\t\tshowRequestFeature: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Show the Documentation entry in the hoisted Actions menu. */\n\t\tshowDocumentation: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Documentation URL for the hoisted Actions menu. */\n\t\tdocumentationUrl: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\temits: ['geo-saved', 'open-integration'],\n\n\tdata() {\n\t\treturn {\n\t\t\tactiveIndex: 0,\n\t\t\t// Items published by the panels, keyed by widget id. Keyed rather\n\t\t\t// than a flat list because `lazy` keeps a visited tab mounted, so\n\t\t\t// more than one panel publishes at a time.\n\t\t\tpanelActionsByWidget: {},\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The open panel's own menu items, or an empty list.\n\t\t *\n\t\t * @return {object[]} PanelAction descriptors for the active tab.\n\t\t */\n\t\tactivePanelActions() {\n\t\t\tconst forId = this.panelActionsByWidget[this.activeWidgetId]\n\t\t\tif (!forId) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\t// Host first, then the widget's own: the catalog Add is about the\n\t\t\t// panel as a whole, the widget's items about what is in it.\n\t\t\treturn [...(forId.host || []), ...(forId.widget || [])]\n\t\t},\n\n\t\t/**\n\t\t * The configured tabs, each paired with the widget definition it names.\n\t\t *\n\t\t * A tab goes under the strip's \"More\" menu when it says `overflow`, when\n\t\t * `content.hideEmpty` is set and its count is 0, or when\n\t\t * `content.maxVisibleTabs` places are already taken. `count` is a\n\t\t * literal, or read off the record through `countField`.\n\t\t *\n\t\t * @return {object[]} `{ key, widgetId, label, icon, widget, count, overflow }` per tab.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-tab-counts-and-overflow\n\t\t */\n\t\tresolvedTabs() {\n\t\t\tconst tabs = Array.isArray(this.content?.tabs) ? this.content.tabs : []\n\t\t\tconst max = Number(this.content?.maxVisibleTabs)\n\t\t\tconst hideEmpty = this.content?.hideEmpty === true\n\t\t\t// How many tabs have taken a place in the strip so far. A tab that\n\t\t\t// is already going under \"More\" does not use one up.\n\t\t\tlet placed = 0\n\t\t\treturn tabs.map((tab, index) => {\n\t\t\t\tconst widgetId = typeof tab === 'string' ? tab : tab?.widgetId\n\t\t\t\tconst widget = this.availableWidgets.find((w) => w && w.id === widgetId) || null\n\t\t\t\tconst count = resolveTabCount(typeof tab === 'object' ? tab : null, this.objectData)\n\t\t\t\tlet overflow = (tab && tab.overflow === true) || (hideEmpty && count === 0)\n\t\t\t\tif (!overflow) {\n\t\t\t\t\tif (Number.isFinite(max) && max > 0 && placed >= max) {\n\t\t\t\t\t\toverflow = true\n\t\t\t\t\t} else {\n\t\t\t\t\t\tplaced += 1\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn {\n\t\t\t\t\tkey: `${widgetId || 'tab'}-${index}`,\n\t\t\t\t\twidgetId,\n\t\t\t\t\tlabel: (tab && tab.label) || widgetTitleOf(widget) || widgetId || '',\n\t\t\t\t\ticon: (tab && tab.icon) || widget?.icon || '',\n\t\t\t\t\twidget,\n\t\t\t\t\tcount,\n\t\t\t\t\toverflow,\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Name of the menu that holds the tabs that are not in the strip.\n\t\t *\n\t\t * @return {string|undefined} The configured label, or undefined for the default.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-tab-counts-and-overflow\n\t\t */\n\t\tmoreLabel() {\n\t\t\tconst label = this.content?.moreLabel\n\t\t\treturn typeof label === 'string' && label !== '' ? label : undefined\n\t\t},\n\n\t\t/**\n\t\t * The tab currently showing.\n\t\t *\n\t\t * @return {object|null} The resolved tab, or null when there are none.\n\t\t */\n\t\tactiveTab() {\n\t\t\treturn this.resolvedTabs[this.activeIndex] || null\n\t\t},\n\n\t\t/**\n\t\t * The active child's id, so the hoisted Refresh reaches THAT child's\n\t\t * fetch over `cn:widget:refresh` rather than a sibling's.\n\t\t *\n\t\t * @return {string} The active widget id.\n\t\t */\n\t\tactiveWidgetId() {\n\t\t\treturn this.activeTab?.widgetId || ''\n\t\t},\n\n\t\t/**\n\t\t * The active child's label, so the Actions menu names what it acts on.\n\t\t *\n\t\t * @return {string} The active tab's label.\n\t\t */\n\t\tactiveTitle() {\n\t\t\treturn this.activeTab?.label || ''\n\t\t},\n\n\t\t/**\n\t\t * The strip's look: `segmented` when the content asks for it, else\n\t\t * CnTabs' default `line`. An unknown value falls back to `line`.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-tabs-widget-renders-a-segmented-strip\n\t\t * @return {('line'|'segmented')} The CnTabs variant.\n\t\t */\n\t\tstripVariant() {\n\t\t\treturn this.content?.variant === 'segmented' ? 'segmented' : 'line'\n\t\t},\n\n\t\t/**\n\t\t * Accessible name for the tab strip.\n\t\t *\n\t\t * @return {string} The aria-label.\n\t\t */\n\t\tstripLabel() {\n\t\t\treturn this.content?.ariaLabel || t('nextcloud-vue', 'Details')\n\t\t},\n\t},\n\n\t/**\n\t * Open on the first tab that is in the strip. Tab 0 is the default, and\n\t * when tab 0 sits under \"More\" the strip would open on a tab the author\n\t * chose to tuck away.\n\t *\n\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-tab-counts-and-overflow\n\t */\n\tcreated() {\n\t\tconst first = this.resolvedTabs.findIndex((tab) => !tab.overflow)\n\t\tif (first > 0) {\n\t\t\tthis.activeIndex = first\n\t\t}\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Re-emit a geo child's save so the surface can reload the record.\n\t\t *\n\t\t * @param {object} geo The saved geometry.\n\t\t * @return {void}\n\t\t */\n\t\tonGeoSaved(geo) {\n\t\t\t/**\n\t\t\t * @event geo-saved Re-emitted from a geo child that saved a geometry.\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('geo-saved', geo)\n\t\t},\n\n\t\t/**\n\t\t * Re-emit a related child's request to open an integration.\n\t\t *\n\t\t * @param {string} integrationId The integration to open.\n\t\t * @return {void}\n\t\t */\n\t\tonOpenIntegration(integrationId) {\n\t\t\t/**\n\t\t\t * @event open-integration Re-emitted from a related child asking to open an integration.\n\t\t\t * @type {string}\n\t\t\t */\n\t\t\tthis.$emit('open-integration', integrationId)\n\t\t},\n\n\t\t/**\n\t\t * Follow the strip's own selection, so keyboard navigation moves the\n\t\t * hoisted Actions menu too.\n\t\t *\n\t\t * @param {number} index The newly selected tab index.\n\t\t * @return {void}\n\t\t */\n\t\tonTabChange(index) {\n\t\t\tif (typeof index === 'number' && index >= 0) {\n\t\t\t\tthis.activeIndex = index\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Empty-state text for a tab whose widget id resolves to nothing.\n\t\t *\n\t\t * @param {object} entry The resolved tab.\n\t\t * @return {string} The message.\n\t\t */\n\t\tmissingLabel(entry) {\n\t\t\treturn t('nextcloud-vue', 'No widget found for \"{id}\"', { id: entry.widgetId || '' })\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n/* The panel is the card; the strip is not inside it. There is no title bar\n above the tabs either: the open tab names the panel, so a title row would\n say the same thing twice and cost a row of height on a card that is mostly\n content.\n\n The card chrome used to wrap the strip as well, which drew a border and a\n pair of rounded corners ABOVE the tabs and boxed them in. Folder tabs are\n drawn as the edge of the sheet they open, so a second edge around them\n reads as a header the widget does not have. Border, radius and background\n now live on the panel below, and the strip sits bare on the page. */\n/* The class is doubled on purpose, and the four properties below are RESET\n rather than simply omitted.\n\n Nextcloud serves every enabled app's assets on every page, each app bundles\n this library's compiled CSS, and the Vue scope id is derived from the file\n PATH, so `data-v-1dbd6122` is byte-identical across library versions. An app\n still on an older release therefore ships a rule with exactly this selector\n and the OLD declarations, and it lands on the pages of an app already on the\n new one. Measured on a dossiq case page: three stale copies of the pre-fix\n rule, one from hermiq's `companion.css` (its companion bundle loads on every\n page by design) and two inline from other apps' bundles. Same specificity,\n so source order decided, and the card border came back around the strip.\n\n Dropping a declaration cannot beat a rule that sets it, so `border` and the\n rest are explicitly zeroed. Doubling the class takes this selector to\n (0,3,0) against the stale (0,2,0), which wins on specificity rather than on\n `!important` or on load order this library does not control. The same\n technique, for the same reason, is used on `.cn-tabs__nav .cn-tabs__nav-item`\n in CnTabs to beat Nextcloud's own button margin. */\n.cn-tabs-widget.cn-tabs-widget {\n\tbackground-color: transparent;\n\tborder: 0;\n\tborder-radius: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\theight: 100%;\n\tmin-height: 0;\n\toverflow: visible;\n\tpadding: 0;\n}\n\n/* No inset, so the first tab starts at the panel's own left edge. The 8px\n here was there to clear the card's rounded top corner; that corner is gone\n from the strip now, and the inset it existed for left the first tab floating\n 8px inside the sheet it opens.\n\n The bar's own bottom rule is switched off and redrawn as a pseudo-element\n that stops one corner radius short of the right edge. CnTabs draws its rule\n across the bar's whole width, which is the sheet's top edge everywhere\n except at the far right, where the panel's top-right corner curves away\n below it: the rule ran straight on to the widget's edge and read as a\n border sticking out past a rounded corner (Ruben, 2026-09-13). Ending it\n where the curve begins makes the rule and the curve one continuous edge. */\n.cn-tabs-widget__tabs :deep(.cn-tabs__bar) {\n\tborder-bottom: none;\n\tpadding: 0;\n\tposition: relative;\n}\n\n.cn-tabs-widget__tabs :deep(.cn-tabs__bar::after) {\n\tborder-bottom: 1px solid var(--color-border);\n\tcontent: '';\n\tinset-block-end: 0;\n\tinset-inline: 0 var(--border-radius-large);\n\tpointer-events: none;\n\tposition: absolute;\n}\n\n/* The open tab erases the slice of the rule directly above the panel with its\n own background-coloured bottom edge, which only works if it paints ABOVE the\n pseudo-element: a positioned pseudo-element otherwise paints over every\n in-flow sibling, rule included. */\n.cn-tabs-widget__tabs :deep(.cn-tabs__nav-item--active) {\n\tposition: relative;\n\tz-index: 1;\n}\n\n.cn-tabs-widget__tabs {\n\tdisplay: flex;\n\tflex-direction: column;\n\t/* Grow to the widget's height, so the panel fills the grid cell the\n\t layout gave it. Without this the strip plus the open panel took only\n\t the height of the panel's content, and a cell of nine rows ended in\n\t 130px of nothing under a sheet that stopped short of its own card. */\n\tflex: 1 1 auto;\n\tmin-height: 0;\n}\n\n/* The panel area is the scroll region, so the strip stays put while a long\n child scrolls under it.\n\n Padded on all four sides. The top was once zeroed to protect the join with\n the open tab, but padding sits inside the painted box — only a margin would\n have opened that gap, and a zero top put every panel's content against the\n strip. */\n.cn-tabs-widget__tabs :deep(.cn-tabs__content) {\n\tbackground-color: var(--color-main-background);\n\t/* Three sides only: the bar's rule (redrawn above, stopping at the corner)\n\t is this sheet's top edge, and the open tab erases the slice of it\n\t directly above the panel so the two read as one surface. A border-top\n\t here would put a second line under that tab which the tab cannot paint\n\t over. */\n\tborder: 1px solid var(--color-border);\n\tborder-top: none;\n\t/* Three rounded corners. The top-left stays square because the first tab\n\t is drawn joined to the panel there, and a curve under it would curl the\n\t sheet away from that tab. The top-right has no tab above it: the strip\n\t ends where the last tab ends and the Actions menu floats free, so a\n\t square corner there read as a sheet cut off, not as a tab's edge. */\n\tborder-radius: 0 var(--border-radius-large) var(--border-radius-large) var(--border-radius-large);\n\tflex: 1 1 auto;\n\tmin-height: 0;\n\toverflow: auto;\n\tpadding: 12px;\n}\n\n.cn-tabs-widget__tabs :deep(.cn-tab) {\n\theight: 100%;\n}\n\n.cn-tabs-widget__title {\n\talign-items: center;\n\tdisplay: inline-flex;\n\tgap: 6px;\n}\n\n.cn-tabs-widget__title-icon {\n\tflex: 0 0 auto;\n}\n</style>\n"],"names":["_createVNode","_createElementBlock","_Fragment","_renderList","_createBlock","_openBlock","_createElementVNode","_toDisplayString"],"mappings":"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"}
@@ -193,6 +193,22 @@ var script = {
193
193
  default: () => [],
194
194
  },
195
195
 
196
+ /**
197
+ * One text link in the header's actions group, before the overflow
198
+ * menu: `{ label, route?, params?, query?, href? }`. A `route` is a
199
+ * named router link (needs a router), an `href` a plain anchor. The
200
+ * label goes through the host translate function. Null (the
201
+ * default) draws nothing. Manifest: `headerLink` on the widget
202
+ * definition or its layout entry.
203
+ *
204
+ * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-widget-header-carries-a-text-link
205
+ * @type {{label: string, route?: string, params?: object, query?: object, href?: string}|null}
206
+ */
207
+ headerLink: {
208
+ type: Object,
209
+ default: null,
210
+ },
211
+
196
212
  /**
197
213
  * Whether the header's overflow action menu (Refresh / Documentation /
198
214
  * Request-a-feature + any `#action-items`) renders. Shown by default;
@@ -476,6 +492,62 @@ var script = {
476
492
  return this.translate ?? this.cnTranslate
477
493
  },
478
494
 
495
+ /**
496
+ * The header link's tag: `router-link` for a route (when a router is
497
+ * mounted), `a` for an href, '' when there is no usable link.
498
+ *
499
+ * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-widget-header-carries-a-text-link
500
+ * @return {string}
501
+ */
502
+ headerLinkTag() {
503
+ const link = this.headerLink;
504
+ if (!link || typeof link.label !== 'string' || link.label === '') {
505
+ return ''
506
+ }
507
+ if (link.route && this.$router) {
508
+ return 'router-link'
509
+ }
510
+ if (typeof link.href === 'string' && link.href !== '') {
511
+ return 'a'
512
+ }
513
+ return ''
514
+ },
515
+
516
+ /**
517
+ * The header link's attributes for its tag.
518
+ *
519
+ * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-widget-header-carries-a-text-link
520
+ * @return {object}
521
+ */
522
+ headerLinkAttrs() {
523
+ const link = this.headerLink || {};
524
+ if (this.headerLinkTag === 'router-link') {
525
+ const to = { name: link.route };
526
+ if (link.params && typeof link.params === 'object') {
527
+ to.params = link.params;
528
+ }
529
+ if (link.query && typeof link.query === 'object') {
530
+ to.query = link.query;
531
+ }
532
+ return { to }
533
+ }
534
+ if (this.headerLinkTag === 'a') {
535
+ return { href: link.href }
536
+ }
537
+ return {}
538
+ },
539
+
540
+ /**
541
+ * The header link's text, through the host translate function.
542
+ *
543
+ * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-widget-header-carries-a-text-link
544
+ * @return {string}
545
+ */
546
+ headerLinkLabel() {
547
+ const label = this.headerLink && this.headerLink.label;
548
+ return label ? this.effectiveTranslate(label) : ''
549
+ },
550
+
479
551
  /**
480
552
  * The RENDERED widget title — `displayTitle` run through the host
481
553
  * translate function. Kept separate from `displayTitle` on purpose:
@@ -719,7 +791,7 @@ var script = {
719
791
  * @return {boolean} True when the group would hold a visible control.
720
792
  */
721
793
  actionsAreVisible() {
722
- return this.showActions || this.actionsSlotRenders()
794
+ return this.showActions || this.actionsSlotRenders() || Boolean(this.headerLinkTag)
723
795
  },
724
796
 
725
797
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"CnWidgetWrapper.vue2.js","sources":["../../../../src/components/CnWidgetWrapper/CnWidgetWrapper.vue"],"sourcesContent":["<!--\n CnWidgetWrapper — Container shell around a dashboard widget.\n\n Provides header (icon + title), scrollable content area, and optional\n footer with action buttons. Applies style configuration for borders,\n backgrounds, and padding. The built-in overflow Actions menu (Refresh /\n Documentation / Request a feature) is delegated to the shared\n CnActionsMenu so widgets and page surfaces stay in lockstep.\n-->\n<template>\n\t<div\n\t\tclass=\"cn-widget-wrapper\"\n\t\t:class=\"{\n\t\t\t'cn-widget-wrapper--borderless': noBorder,\n\t\t\t'cn-widget-wrapper--flush': noPadding,\n\t\t\t'cn-widget-wrapper--chromeless': chromeless,\n\t\t\t'cn-widget-wrapper--nc-dashboard': chrome === 'nc-dashboard',\n\t\t\t'cn-widget-wrapper--high-contrast': highContrast,\n\t\t}\"\n\t\t:data-cn-high-contrast=\"highContrast ? 'true' : undefined\"\n\t\t:style=\"wrapperStyles\">\n\t\t<!-- Header.\n\t\t Rendered when a title earns it, or when controls that are actually\n\t\t on screen need a home. Three conditions rather than one, and each of\n\t\t the three is a bug this component has already shipped: gating on the\n\t\t title alone took the Save button for an inline edit away along with\n\t\t the doubled title; reading `showActions`, which defaults to true,\n\t\t handed a header to every headerless KPI tile; and reading the\n\t\t `actions` slot's mere PRESENCE gave a chromeless panel a 59px band\n\t\t holding nothing, because `CnObjectDataWidget` provides that template\n\t\t always and fills it only while an edit is unsaved.\n\t\t `headerIsWorthIt()` carries the reasoning. -->\n\t\t<div v-if=\"headerIsWorthIt()\"\n\t\t\tclass=\"cn-widget-wrapper__header\"\n\t\t\t:class=\"{ 'cn-widget-wrapper__header--actions-only': !titleVisible }\"\n\t\t\t:style=\"[headerStyles, titleIconStyle]\">\n\t\t\t<!-- Title icon — left: rendered before the title group. Moved\n\t\t\t INSIDE header-left so the header's `space-between` cannot pull\n\t\t\t it away from the title it belongs to; header-left's `gap` is\n\t\t\t what now separates icon from title (they used to touch). -->\n\t\t\t<div v-if=\"titleVisible\" class=\"cn-widget-wrapper__header-left\">\n\t\t\t\t<div v-if=\"$slots['title-icon'] && titleIconPosition === 'left'\"\n\t\t\t\t\tclass=\"cn-widget-wrapper__title-icon\">\n\t\t\t\t\t<slot name=\"title-icon\" />\n\t\t\t\t</div>\n\t\t\t\t<img\n\t\t\t\t\tv-if=\"iconUrl\"\n\t\t\t\t\t:src=\"iconUrl\"\n\t\t\t\t\t:alt=\"resolvedTitle\"\n\t\t\t\t\tclass=\"cn-widget-wrapper__icon\">\n\t\t\t\t<span\n\t\t\t\t\tv-else-if=\"iconClass\"\n\t\t\t\t\t:class=\"iconClass\"\n\t\t\t\t\tclass=\"cn-widget-wrapper__icon\" />\n\t\t\t\t<h3 :id=\"titleId\" class=\"cn-widget-wrapper__title\">\n\t\t\t\t\t{{ resolvedTitle }}\n\t\t\t\t</h3>\n\t\t\t\t<!-- @slot title-meta Rendered inside the header's left group,\n\t\t\t\t after the title and before the spacer. Use for small\n\t\t\t\t contextual chips (e.g. a date-range badge on chart\n\t\t\t\t widgets). Kept inside header-left so the chip sits next\n\t\t\t\t to the title rather than floating right next to the\n\t\t\t\t actions menu. -->\n\t\t\t\t<div v-if=\"$slots['title-meta']\" class=\"cn-widget-wrapper__title-meta\">\n\t\t\t\t\t<slot name=\"title-meta\" />\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<!-- `showActions` gates the overflow MENU, not the slot beside it.\n\t\t\t It used to gate both, so a caller who switched the menu off\n\t\t\t also lost the Save button for an inline edit, silently. -->\n\t\t\t<div v-if=\"actionsAreVisible()\" class=\"cn-widget-wrapper__actions\">\n\t\t\t\t<!-- @slot actions Custom action buttons rendered before the\n\t\t\t\t built-in overflow menu. -->\n\t\t\t\t<slot name=\"actions\" />\n\t\t\t\t<CnActionsMenu\n\t\t\t\t\tv-if=\"showActions\"\n\t\t\t\t\t:showRefresh=\"effectiveShowRefresh\"\n\t\t\t\t\t:showRequestFeature=\"effectiveShowRequestFeature\"\n\t\t\t\t\t:showReportBug=\"showReportBug\"\n\t\t\t\t\t:showDocumentation=\"showDocumentation\"\n\t\t\t\t\t:documentationUrl=\"documentationUrl\"\n\t\t\t\t\t:docsAnchor=\"docsAnchor\"\n\t\t\t\t\t:reportBugUrl=\"reportBugUrl\"\n\t\t\t\t\t:documentationLabel=\"documentationLabel\"\n\t\t\t\t\t:refreshLabel=\"refreshLabel\"\n\t\t\t\t\t:requestFeatureLabel=\"requestFeatureLabel\"\n\t\t\t\t\t:actionsMenuLabel=\"actionsMenuLabel\"\n\t\t\t\t\t:refreshing=\"refreshing\"\n\t\t\t\t\t:widgetId=\"resolvedWidgetId\"\n\t\t\t\t\t:title=\"displayTitle\"\n\t\t\t\t\t:surface=\"`widget:${resolvedWidgetId}`\"\n\t\t\t\t\t:specRef=\"specRef\"\n\t\t\t\t\trefreshChannel=\"cn:widget:refresh\"\n\t\t\t\t\ttestidBase=\"cn-widget-wrapper\"\n\t\t\t\t\t@refresh=\"onActionsRefresh\"\n\t\t\t\t\t@requestFeature=\"onActionsRequestFeature\">\n\t\t\t\t\t<!-- @slot action-items Additional NcActionButton-family\n\t\t\t\t\t items rendered inside the overflow menu, after the\n\t\t\t\t\t built-in Refresh / Documentation / Request-a-feature\n\t\t\t\t\t group. -->\n\t\t\t\t\t<template v-if=\"hasActionItemsSlot\" #action-items>\n\t\t\t\t\t\t<slot name=\"action-items\" />\n\t\t\t\t\t</template>\n\t\t\t\t</CnActionsMenu>\n\t\t\t</div>\n\t\t\t<!-- Title icon — right: rendered after actions, far right -->\n\t\t\t<div v-if=\"$slots['title-icon'] && titleIconPosition === 'right'\"\n\t\t\t\tclass=\"cn-widget-wrapper__title-icon\">\n\t\t\t\t<slot name=\"title-icon\" />\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- Headerless meta (e.g. a KPI date chip on a flush card). When the\n\t\t header is hidden but a `title-meta` chip is provided, float it in the\n\t\t top-right corner over the content instead of dropping it — so a\n\t\t compact flush KPI tile can carry a date-range chip without growing a\n\t\t full header bar. -->\n\t\t<div\n\t\t\tv-if=\"!titleVisible && $slots['title-meta']\"\n\t\t\tclass=\"cn-widget-wrapper__floating-meta\">\n\t\t\t<slot name=\"title-meta\" />\n\t\t</div>\n\n\t\t<!-- Content -->\n\t\t<!--\n\t\t The content area is `overflow: auto`, so it can become a scrollable\n\t\t region. WCAG 2.1.1 (Keyboard) requires scrollable regions to be\n\t\t keyboard-focusable so keyboard-only users can scroll them, and 4.1.2\n\t\t requires an accessible name. We expose it as a labelled region and\n\t\t give it tabindex=\"0\". When the header (and its title) is hidden we\n\t\t fall back to an explicit aria-label from the widget title so the\n\t\t region is never anonymous.\n\t\t-->\n\t\t<div\n\t\t\tclass=\"cn-widget-wrapper__content\"\n\t\t\ttabindex=\"0\"\n\t\t\trole=\"region\"\n\t\t\t:aria-labelledby=\"titleVisible ? titleId : null\"\n\t\t\t:aria-label=\"titleVisible ? null : resolvedTitle\">\n\t\t\t<slot />\n\t\t</div>\n\n\t\t<!-- Footer -->\n\t\t<div v-if=\"$slots.footer || (buttons && buttons.length > 0)\" class=\"cn-widget-wrapper__footer\">\n\t\t\t<slot name=\"footer\">\n\t\t\t\t<a\n\t\t\t\t\tv-for=\"button in buttons\"\n\t\t\t\t\t:key=\"button.link\"\n\t\t\t\t\t:href=\"button.link\"\n\t\t\t\t\tclass=\"cn-widget-wrapper__footer-link\">\n\t\t\t\t\t{{ button.text }}\n\t\t\t\t</a>\n\t\t\t</slot>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { slotRenders } from '../../utils/slotContent.js'\nimport { CnActionsMenu } from '../CnActionsMenu/index.js'\n\n/**\n * CnWidgetWrapper — Widget container with header, content, and footer.\n *\n * ```vue\n * <CnWidgetWrapper title=\"My Cases\" :icon-url=\"casesIconUrl\">\n * <MyCasesChart :data=\"chartData\" />\n * </CnWidgetWrapper>\n * ```\n *\n * With NC widget object\n * ```vue\n * <CnWidgetWrapper\n * :title=\"widget.title\"\n * :icon-url=\"widget.iconUrl\"\n * :icon-class=\"widget.iconClass\"\n * :documentation-url=\"widget.documentationUrl\"\n * :buttons=\"widget.buttons\">\n * <CnWidgetRenderer :widget=\"widget\" />\n * </CnWidgetWrapper>\n * ```\n */\n/**\n * Semantic variant → CSS colour token for the widget header icon. Tokens\n * only — the nldesign app themes by overriding the Nextcloud variables, so a\n * literal hex here would ignore government theming entirely (ADR-062 / NL\n * Design System).\n *\n * @type {Record<string, string>}\n */\nconst TITLE_ICON_VARIANT_COLORS = {\n\tprimary: 'var(--color-primary-element)',\n\tsuccess: 'var(--color-success)',\n\twarning: 'var(--color-warning)',\n\terror: 'var(--color-error)',\n\tinfo: 'var(--color-info, var(--color-primary-element))',\n\tneutral: 'var(--color-text-maxcontrast)',\n}\n\nexport default {\n\tname: 'CnWidgetWrapper',\n\n\tcomponents: {\n\t\tCnActionsMenu,\n\t},\n\n\tinject: {\n\t\t/**\n\t\t * Host translate function provided by CnAppRoot as\n\t\t * `cnTranslate: this.translate` (bound to the host app's id). The\n\t\t * manifest-authored widget title is run through it so the card\n\t\t * heading follows the user's language. Defaults to an identity\n\t\t * function so an untranslated key renders as itself.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/** Widget title */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Widget'),\n\t\t},\n\n\t\t/** Whether to show the header with title */\n\t\tshowTitle: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Chrome variant for the wrapper card.\n\t\t * - `'default'` — the library's own card chrome (opaque background,\n\t\t * 1px border, compact header).\n\t\t * - `'nc-dashboard'` — matches the native Nextcloud Dashboard panel\n\t\t * exactly using the same design tokens: translucent blurred\n\t\t * background (`--color-main-background-blur` + `--filter-background-blur`),\n\t\t * `--border-radius-container-large` corners, no border/shadow, a 16px\n\t\t * header with a 20px/700 title and 32px leading icon, and content\n\t\t * inset 16px on the sides + bottom. `styleConfig` overrides still\n\t\t * layer on top so a user can customise any token.\n\t\t *\n\t\t * @type {'default'|'nc-dashboard'}\n\t\t */\n\t\tchrome: {\n\t\t\ttype: String,\n\t\t\tdefault: 'default',\n\t\t\tvalidator: (v) => ['default', 'nc-dashboard'].includes(v),\n\t\t},\n\n\t\t/**\n\t\t * Remove border and background — makes the wrapper transparent.\n\t\t * Useful for widgets that are self-contained cards (e.g. CnStatsBlock).\n\t\t */\n\t\tborderless: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Remove content padding — allows content to go edge-to-edge.\n\t\t * Useful for list-style widgets where items should span the full width.\n\t\t */\n\t\tflush: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Draw NO card of this widget's own: no border, no background, no\n\t\t * content padding, no title row and no header divider. For a surface\n\t\t * that already supplies the card and the heading — a tab panel being\n\t\t * the case that matters, where a nested card repeats both.\n\t\t *\n\t\t * The wrapper ELEMENT still renders. That is the contract, not an\n\t\t * oversight: `CnObjectDataWidget` measures its overflow against\n\t\t * `.cn-widget-wrapper__content` (`closest()` plus a ResizeObserver on\n\t\t * that node), `src/css/table.css` gives a table inside it its\n\t\t * edge-to-edge treatment, and `src/css/detail-page.css` and\n\t\t * `dashboard.css` size their content areas through it. Dropping the\n\t\t * node would take all of that out silently — `closest()` returning\n\t\t * null reads as \"nothing overflows\", never as an error.\n\t\t *\n\t\t * Controls in the `actions` slot are NOT chrome and keep their header:\n\t\t * the Save button that commits an inline edit lives there, and a panel\n\t\t * that hides it is silently unsaveable.\n\t\t *\n\t\t * `chromeless` is what an integration provider already asks for through\n\t\t * `CnDetailWidgetHost` (`bareWidget`), so this is the same word for the\n\t\t * same thing on every path into the wrapper.\n\t\t */\n\t\tchromeless: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Icon URL (image) */\n\t\ticonUrl: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Icon CSS class (e.g., Nextcloud icon class) */\n\t\ticonClass: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Position of the title-icon slot in the header.\n\t\t * 'left' places it before the title; 'right' places it after the actions.\n\t\t */\n\t\ttitleIconPosition: {\n\t\t\ttype: String,\n\t\t\tdefault: 'right',\n\t\t\tvalidator: (v) => ['left', 'right'].includes(v),\n\t\t},\n\n\t\t/**\n\t\t * Explicit CSS colour for the header icon. Overrides `titleIconVariant`.\n\t\t * Must be a CSS custom property or a theme token — never a literal hex\n\t\t * (NL Design System: the nldesign app re-themes by overriding the\n\t\t * Nextcloud variables, and a hardcoded colour ignores that).\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\ttitleIconColor: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Semantic colour for the header icon. Every widget's icon is\n\t\t * coloured — `primary` (the theme colour) is the default, and a\n\t\t * widget whose subject already carries a semantic meaning names it\n\t\t * here so the icon says so at a glance (a Depublished list is\n\t\t * `error`, a Concepts list `warning`, a Published list `success`).\n\t\t *\n\t\t * @type {'primary'|'success'|'warning'|'error'|'info'|'neutral'}\n\t\t */\n\t\ttitleIconVariant: {\n\t\t\ttype: String,\n\t\t\tdefault: 'primary',\n\t\t\tvalidator: (v) => ['primary', 'success', 'warning', 'error', 'info', 'neutral'].includes(v),\n\t\t},\n\n\t\t/** Footer action buttons: [{ text, link }] */\n\t\tbuttons: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Whether the header's overflow action menu (Refresh / Documentation /\n\t\t * Request-a-feature + any `#action-items`) renders. Shown by default;\n\t\t * set `false` for compact surfaces — e.g. a KPI tile whose only header\n\t\t * affordance is a date chip — to drop the menu and free header width.\n\t\t */\n\t\tshowActions: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Style configuration for the wrapper.\n\t\t *\n\t\t * @type {{ backgroundColor: string, borderStyle: string, borderWidth: number, borderColor: string, borderRadius: number, padding: { top: number, right: number, bottom: number, left: number } }}\n\t\t */\n\t\tstyleConfig: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Hide the built-in Refresh item from the overflow action menu.\n\t\t * The Refresh item is shown by default — set this when the widget\n\t\t * has no refreshable data source (e.g. a static tile). Alias for\n\t\t * the inverse `:show-refresh=\"false\"`; either form opts out.\n\t\t */\n\t\thideRefresh: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Hide the built-in Request-a-feature item from the overflow\n\t\t * action menu. Shown by default; set when the consuming app has\n\t\t * no public issue tracker to link out to. Alias for the inverse\n\t\t * `:show-request-feature=\"false\"`; either form opts out.\n\t\t */\n\t\thideRequestFeature: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Whether to show the built-in Refresh item. Tri-state:\n\t\t * - `true` / `false` — force the action on or off.\n\t\t * - `null` (the default) — **auto**: show the action only when a\n\t\t * parent has attached an `@refresh` listener (i.e. something will\n\t\t * actually handle the refresh). This keeps widgets that can't\n\t\t * refresh — e.g. a prop-driven `CnObjectDataWidget` — from showing\n\t\t * a dead button. Widgets that refresh themselves via the\n\t\t * `cn:widget:refresh` event bus (with no `@refresh` listener)\n\t\t * should set `:show-refresh=\"true\"` explicitly.\n\t\t *\n\t\t * `hideRefresh` (or `:show-refresh=\"false\"`) always wins.\n\t\t *\n\t\t * @type {boolean|null}\n\t\t */\n\t\tshowRefresh: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Inverse of `hideRequestFeature`. Defaults to `true` so the\n\t\t * action renders. Set `:show-request-feature=\"false\"` to hide it.\n\t\t * Either flag hides the action.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tshowRequestFeature: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Whether the built-in \"Report a bug\" item renders. On by default —\n\t\t * the trio Request a feature / Report a bug / Documentation is the\n\t\t * contract for every widget; this exists for the rare surface that\n\t\t * must suppress one deliberately.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tshowReportBug: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Whether the built-in \"Documentation\" item renders. On by default,\n\t\t * for the same reason as `showReportBug`. The item's target is\n\t\t * resolved by the shared menu (see `docsAnchor`), so leaving it on\n\t\t * costs the host nothing.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tshowDocumentation: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Explicit documentation link for this widget, opened in a new tab.\n\t\t * Usually unnecessary: leave it empty and set `docsAnchor` instead,\n\t\t * so the link is built from the app-wide documentation base and\n\t\t * lands on this widget's own section.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tdocumentationUrl: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Optional pre-translated label for the Documentation action.\n\t\t * Defaults to the lib's translation of \"Documentation\".\n\t\t */\n\t\tdocumentationLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Documentation'),\n\t\t},\n\n\t\t/**\n\t\t * Widget id for the built-in default Refresh / Request-a-feature\n\t\t * handlers (B2). Forwarded as the `surface: \"widget:<id>\"` value\n\t\t * on the Actions menu (the English headline fallback for its forge\n\t\t * deep-links) AND as the `widgetId` field on the\n\t\t * `cn:widget:refresh` event-bus payload. When unset, the wrapper\n\t\t * falls back to a slugified `displayTitle`, which works but is\n\t\t * less stable than an explicit id.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\twidgetId: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * This widget's own section in the app's documentation, appended to\n\t\t * the app-wide documentation base URL (provided by CnAppRoot) to build\n\t\t * the Actions menu's Documentation deep-link. A bare slug becomes a\n\t\t * `#fragment`. Supply it per widget type — without it the item still\n\t\t * renders, but lands on the docs homepage rather than this widget.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tdocsAnchor: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Explicit \"Report a bug\" target for the Actions menu. Empty (the\n\t\t * default) builds a new-issue deep-link on the app's own forge.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\treportBugUrl: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Declares that this widget wants a high contrast treatment. The flag\n\t\t * reaches the theme as `data-cn-high-contrast` and a `--high-contrast`\n\t\t * class; the component picks no colour of its own (ADR-003). A theme\n\t\t * that says nothing about the flag renders the widget unchanged.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\thighContrast: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Optional `specRef` slug, forwarded to the Actions menu. Accepted\n\t\t * for backward compatibility with the removed in-product suggestion\n\t\t * modal; the forge issue form asks for its own context.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tspecRef: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Whether a refresh is currently in flight. When bound by the host\n\t\t * (e.g. `:refreshing=\"loading\"` around its refetch), the Refresh\n\t\t * item is disabled and shows a loading spinner for exactly as long\n\t\t * as this stays true — so the spinner reflects the real refresh time.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\trefreshing: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Optional pre-translated label for the Refresh action. Defaults\n\t\t * to the lib's translation of \"Refresh\" so callers usually don't\n\t\t * need to set this.\n\t\t */\n\t\trefreshLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Refresh'),\n\t\t},\n\n\t\t/**\n\t\t * Optional pre-translated label for the Request-a-feature action.\n\t\t * Defaults to the lib's translation of \"Request a feature\".\n\t\t */\n\t\trequestFeatureLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Request a feature'),\n\t\t},\n\n\t\t/**\n\t\t * Pre-translated aria-label / tooltip for the overflow menu\n\t\t * trigger. Defaults to \"Actions\".\n\t\t */\n\t\tactionsMenuLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Actions'),\n\t\t},\n\n\t\t/**\n\t\t * Translate function. Falls back to the injected `cnTranslate`,\n\t\t * which itself defaults to an identity function.\n\t\t *\n\t\t * @type {((key: string) => string)|null}\n\t\t */\n\t\ttranslate: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\temits: ['refresh', 'request-feature'],\n\n\tcomputed: {\n\t\tdisplayTitle() {\n\t\t\treturn this.title || 'Widget'\n\t\t},\n\n\t\t/**\n\t\t * The header's icon colour, published as ONE custom property on the\n\t\t * header rather than an inline `color` repeated on each icon element.\n\t\t * Everything under the header — the icon-class span, the `title-icon`\n\t\t * slot's content, an SVG inside it — inherits from that single\n\t\t * declaration, so the icon and any decoration around it cannot end up\n\t\t * different colours.\n\t\t *\n\t\t * `titleIconColor` wins when the host set one; otherwise the\n\t\t * `titleIconVariant` token does, which is why EVERY widget icon is\n\t\t * coloured and not just the ones an app remembered to configure. Every\n\t\t * value is a CSS variable, so the nldesign app's overrides re-theme\n\t\t * them for free (NL Design System).\n\t\t *\n\t\t * @return {object} A style object declaring `--cn-widget-icon-color`.\n\t\t */\n\t\ttitleIconStyle() {\n\t\t\treturn {\n\t\t\t\t'--cn-widget-icon-color': this.titleIconColor\n\t\t\t\t\t|| TITLE_ICON_VARIANT_COLORS[this.titleIconVariant]\n\t\t\t\t\t|| TITLE_ICON_VARIANT_COLORS.primary,\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Effective translate function: the explicit `translate` prop when\n\t\t * given, else the injected `cnTranslate` (identity by default).\n\t\t *\n\t\t * @return {(key: string) => string}\n\t\t */\n\t\teffectiveTranslate() {\n\t\t\treturn this.translate ?? this.cnTranslate\n\t\t},\n\n\t\t/**\n\t\t * The RENDERED widget title — `displayTitle` run through the host\n\t\t * translate function. Kept separate from `displayTitle` on purpose:\n\t\t * `resolvedWidgetId` slugifies the raw title, so identifiers (DOM\n\t\t * ids, refresh channels, action payloads) stay locale-independent\n\t\t * while only the visible/AT-facing text localises.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedTitle() {\n\t\t\treturn this.title ? this.effectiveTranslate(this.title) : this.displayTitle\n\t\t},\n\n\t\t/**\n\t\t * Stable DOM id for the header title `<h3>`, used as the\n\t\t * `aria-labelledby` target of the scrollable content region so the\n\t\t * region gets an accessible name from the widget title (WCAG 4.1.2).\n\t\t * Derived from `resolvedWidgetId` so it is stable and unique per\n\t\t * widget instance on the page.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\ttitleId() {\n\t\t\treturn `cn-widget-wrapper-title-${this.resolvedWidgetId}`\n\t\t},\n\n\t\t/**\n\t\t * Effective Refresh visibility. `hideRefresh: true` (or\n\t\t * `:show-refresh=\"false\"`) always hides it. When `showRefresh` is\n\t\t * left unset (`null`), auto-detect: show the action only when a\n\t\t * parent attached an `@refresh` listener — otherwise the refresh\n\t\t * would do nothing. `hideRefresh` remains a back-compat alias.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\teffectiveShowRefresh() {\n\t\t\tif (this.hideRefresh) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif (this.showRefresh !== null) {\n\t\t\t\treturn this.showRefresh\n\t\t\t}\n\t\t\t// `$.vnode.props`, not `$attrs`: `refresh` is a declared emit, and\n\t\t\t// Vue keeps declared emits out of `$attrs`.\n\t\t\treturn Boolean(this.$.vnode.props?.onRefresh)\n\t\t},\n\n\t\t/**\n\t\t * Effective Request-a-feature visibility — same OR-of-opt-outs\n\t\t * pattern as `effectiveShowRefresh`.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\teffectiveShowRequestFeature() {\n\t\t\treturn this.showRequestFeature && !this.hideRequestFeature\n\t\t},\n\n\t\t/**\n\t\t * Stable id used in event-bus payloads and the\n\t\t * `surface: \"widget:<id>\"` field on the auto-mounted feature\n\t\t * modal. Prefers the explicit `widgetId` prop; falls back to a\n\t\t * slugified `displayTitle` so widgets without an explicit id\n\t\t * still get a usable identifier.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedWidgetId() {\n\t\t\tif (this.widgetId) {\n\t\t\t\treturn this.widgetId\n\t\t\t}\n\t\t\treturn this.displayTitle.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')\n\t\t},\n\n\t\t/**\n\t\t * Whether the caller supplied an `action-items` slot. Forwarded\n\t\t * conditionally so the shared CnActionsMenu doesn't treat an\n\t\t * always-present pass-through template as content (which would\n\t\t * keep the overflow menu visible even when every built-in is\n\t\t * opted out).\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\thasActionItemsSlot() {\n\t\t\treturn Boolean(this.$slots['action-items']) || Boolean(this.$slots && this.$slots['action-items'])\n\t\t},\n\n\t\t/**\n\t\t * Whether the caller put controls in the `actions` slot.\n\t\t *\n\t\t * Gates the header together with `showTitle`, so a surface that wants no\n\t\t * title still keeps controls that have nowhere else to go — the Save\n\t\t * button `CnObjectDataWidget` renders there being the case that matters.\n\t\t *\n\t\t * Deliberately NOT `showActions`: that prop defaults to true and governs\n\t\t * the overflow menu, so reading it here would give a header to every\n\t\t * headerless KPI tile that never had one.\n\t\t *\n\t\t * @return {boolean} true when the slot is filled.\n\t\t */\n\t\thasActionsSlot() {\n\t\t\treturn Boolean(this.$slots.actions)\n\t\t},\n\n\t\t/**\n\t\t * Whether the card outline is drawn. `chromeless` implies `borderless`\n\t\t * so a caller names the intent once instead of remembering the three\n\t\t * props it decomposes into.\n\t\t *\n\t\t * @return {boolean} true when border and background are suppressed.\n\t\t */\n\t\tnoBorder() {\n\t\t\treturn this.borderless || this.chromeless\n\t\t},\n\n\t\t/**\n\t\t * Whether the content area goes edge to edge. `chromeless` implies\n\t\t * `flush` for the same reason as `noBorder`.\n\t\t *\n\t\t * @return {boolean} true when content padding is suppressed.\n\t\t */\n\t\tnoPadding() {\n\t\t\treturn this.flush || this.chromeless\n\t\t},\n\n\t\t/**\n\t\t * Whether the title row renders. `chromeless` overrides `showTitle`,\n\t\t * which defaults to true: a surface that already names the widget must\n\t\t * not have to also remember to turn the title off.\n\t\t *\n\t\t * Read everywhere the template used to read `showTitle` directly,\n\t\t * including the two ARIA bindings on the content region — a hidden\n\t\t * title cannot be the region's `aria-labelledby` target.\n\t\t *\n\t\t * @return {boolean} true when the header's title group renders.\n\t\t */\n\t\ttitleVisible() {\n\t\t\treturn this.showTitle && !this.chromeless\n\t\t},\n\n\t\twrapperStyles() {\n\t\t\tconst styles = {}\n\n\t\t\tif (this.styleConfig.backgroundColor) {\n\t\t\t\tstyles.backgroundColor = this.styleConfig.backgroundColor\n\t\t\t}\n\n\t\t\tif (this.styleConfig.borderStyle && this.styleConfig.borderStyle !== 'none') {\n\t\t\t\tstyles.border = `${this.styleConfig.borderWidth || 1}px ${this.styleConfig.borderStyle} ${this.styleConfig.borderColor || 'var(--color-border)'}`\n\t\t\t}\n\n\t\t\tif (this.styleConfig.borderRadius !== undefined) {\n\t\t\t\tstyles.borderRadius = `${this.styleConfig.borderRadius}px`\n\t\t\t}\n\n\t\t\tif (this.styleConfig.padding) {\n\t\t\t\tconst p = this.styleConfig.padding\n\t\t\t\tstyles.padding = `${p.top || 0}px ${p.right || 0}px ${p.bottom || 0}px ${p.left || 0}px`\n\t\t\t}\n\n\t\t\treturn styles\n\t\t},\n\n\t\t/**\n\t\t * Inline styles for the header bar, derived from\n\t\t * `styleConfig.headerStyle.{backgroundColor, textColor}`. Lets a host\n\t\t * give individual widgets a custom header colour without a per-app CSS\n\t\t * workaround. Empty object when no header style is configured.\n\t\t *\n\t\t * @return {object} the header style bindings.\n\t\t */\n\t\theaderStyles() {\n\t\t\tconst hs = this.styleConfig.headerStyle\n\t\t\tif (!hs || typeof hs !== 'object') {\n\t\t\t\treturn {}\n\t\t\t}\n\t\t\tconst styles = {}\n\t\t\tif (hs.backgroundColor) {\n\t\t\t\tstyles.backgroundColor = hs.backgroundColor\n\t\t\t}\n\t\t\tif (hs.textColor) {\n\t\t\t\tstyles.color = hs.textColor\n\t\t\t}\n\t\t\treturn styles\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Whether the `actions` slot renders anything a reader can see.\n\t\t *\n\t\t * `CnObjectDataWidget` always PROVIDES the template and fills it only\n\t\t * while an edit is unsaved, so the slot has to be called and its vnodes\n\t\t * inspected. Reading `$slots.actions` as a boolean counts the template\n\t\t * itself as content, which is what gave a titleless tab panel a 59px\n\t\t * band holding nothing but a second Actions menu.\n\t\t *\n\t\t * A method rather than a computed on purpose. The answer changes when the\n\t\t * PARENT's state changes, and a computed reading `$slots` does not\n\t\t * re-track that; a method re-runs on every render, and a slot whose\n\t\t * content depends on parent state already forces this component to\n\t\t * re-render.\n\t\t *\n\t\t * @return {boolean} True when the slot yields visible content.\n\t\t */\n\t\tactionsSlotRenders() {\n\t\t\treturn slotRenders(this.$slots.actions)\n\t\t},\n\n\t\t/**\n\t\t * Whether the header band is worth the vertical space it costs.\n\t\t *\n\t\t * A VISIBLE title always earns it, which `chromeless` rules out. Otherwise\n\t\t * it is earned by controls that are actually on screen: content in the\n\t\t * `actions` slot, or the overflow menu when a caller both asked for it and\n\t\t * provided the slot it sits beside.\n\t\t *\n\t\t * That last clause looks redundant and is not. `showActions` defaults to\n\t\t * TRUE, so reading it alone hands a header to every headerless KPI tile\n\t\t * in the fleet, which is what the floating title-meta test caught when\n\t\t * this was first written the short way.\n\t\t *\n\t\t * @return {boolean} True when the header should render.\n\t\t */\n\t\theaderIsWorthIt() {\n\t\t\tif (this.titleVisible) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tif (this.actionsSlotRenders()) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn this.showActions && this.hasActionsSlot\n\t\t},\n\n\t\t/**\n\t\t * Whether the actions group holds anything.\n\t\t *\n\t\t * The overflow menu and the slot beside it are separate conditions now.\n\t\t * `showActions` used to gate both, so a caller who only wanted the menu\n\t\t * gone also lost the button that commits an inline edit.\n\t\t *\n\t\t * @return {boolean} True when the group would hold a visible control.\n\t\t */\n\t\tactionsAreVisible() {\n\t\t\treturn this.showActions || this.actionsSlotRenders()\n\t\t},\n\n\t\t/**\n\t\t * Re-emit the shared CnActionsMenu `@refresh` to the host, passing\n\t\t * the synthetic event through unchanged so a host listener can\n\t\t * still `preventDefault()` the built-in default (event-bus emit on\n\t\t * `cn:widget:refresh`).\n\t\t *\n\t\t * @param {{ widgetId: string, title: string }} payload Action payload.\n\t\t * @param {{ defaultPrevented: boolean, preventDefault: () => void }} ev Synthetic event.\n\t\t * @return {void}\n\t\t */\n\t\tonActionsRefresh(payload, ev) {\n\t\t\t/**\n\t\t\t * @event refresh User clicked the Refresh item in the overflow\n\t\t\t * action menu. Payload: `{ widgetId, title }`. Handlers may call\n\t\t\t * the second arg's `preventDefault()` to suppress the built-in\n\t\t\t * default (event-bus emit on `cn:widget:refresh`).\n\t\t\t * @type {{ widgetId: string, title: string }}\n\t\t\t */\n\t\t\tthis.$emit('refresh', payload, ev)\n\t\t},\n\n\t\t/**\n\t\t * Re-emit the shared CnActionsMenu `@request-feature` to the host,\n\t\t * passing the synthetic event through so a host listener can\n\t\t * `preventDefault()` the built-in default (opening the forge's\n\t\t * feature-request issue form).\n\t\t *\n\t\t * @param {{ widgetId: string, title: string }} payload Action payload.\n\t\t * @param {{ defaultPrevented: boolean, preventDefault: () => void }} ev Synthetic event.\n\t\t * @return {void}\n\t\t */\n\t\tonActionsRequestFeature(payload, ev) {\n\t\t\t/**\n\t\t\t * @event request-feature User clicked the Request a feature\n\t\t\t * item. Payload: `{ widgetId, title }`. Handlers may call the\n\t\t\t * second arg's `preventDefault()` to suppress the built-in\n\t\t\t * default (opening the forge's feature-request issue form).\n\t\t\t * @type {{ widgetId: string, title: string }}\n\t\t\t */\n\t\t\tthis.$emit('request-feature', payload, ev)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-widget-wrapper {\n\tposition: relative;\n\theight: 100%;\n\tdisplay: flex;\n\tflex-direction: column;\n\tbackground: var(--color-main-background);\n\tborder: 1px solid var(--color-border);\n\t/* NC design system: every content card is rounded — same token as\n\t CnDetailCard so detail pages read as ONE card family (ADR-062). */\n\tborder-radius: var(--border-radius-large, 8px);\n\toverflow: hidden;\n}\n\n/* Headerless date chip — floated over the top-right of a flush card. */\n.cn-widget-wrapper__floating-meta {\n\tposition: absolute;\n\ttop: 8px;\n\tinset-inline-end: 10px;\n\tz-index: 2;\n}\n\n.cn-widget-wrapper__content {\n\tflex: 1;\n\toverflow: auto;\n\tmin-height: 0;\n\tpadding: 16px;\n}\n\n/* A TABLE GOES EDGE TO EDGE. ALWAYS.\n A table drawn inside 16px of padding renders a border inside a border — two\n rectangles a few pixels apart, which reads as a mistake because it is one.\n The wrapper already has a `flush` prop that zeroes this padding, but a prop\n is something an app has to remember on every table it ever adds, and the\n apps did not: the padding was visible in pipelinq's table widgets.\n So it is structural instead. `:has()` keys on the table component's own root\n class, which means a table is flush by construction and no caller can\n reintroduce the gap by forgetting a prop. */\n.cn-widget-wrapper__content:has(> .cn-table-container),\n.cn-widget-wrapper__content:has(> .cn-widget-object-table) {\n\tpadding: 0;\n}\n\n/* Keyboard focus ring for the now-focusable scrollable content region. */\n.cn-widget-wrapper__content:focus-visible {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: -2px;\n}\n\n.cn-widget-wrapper--borderless {\n\tborder: none;\n\tbackground: transparent;\n}\n\n.cn-widget-wrapper--borderless .cn-widget-wrapper__content {\n\tpadding: 0;\n}\n\n.cn-widget-wrapper--flush .cn-widget-wrapper__content {\n\tpadding: 0;\n}\n\n/* A form or a search list keeps its padding, the reverse of the table rule\n above. A dashboard renders every widget `flush`, and without this their\n fields touch the card's edges. Borderless and chromeless cards, and the\n Nextcloud dashboard variant, keep their own padding. */\n.cn-widget-wrapper--flush:not(.cn-widget-wrapper--borderless, .cn-widget-wrapper--nc-dashboard) > .cn-widget-wrapper__content:has(> .cn-form-widget),\n.cn-widget-wrapper--flush:not(.cn-widget-wrapper--borderless, .cn-widget-wrapper--nc-dashboard) > .cn-widget-wrapper__content:has(> .cn-kb-search-widget) {\n\tpadding: 16px;\n}\n\n/* `chromeless` — border and background are already gone through borderless,\n and the content padding through flush. What is left on the root is the\n corner radius, which still clips: with `overflow: hidden` above, a child\n that paints its own background (a table header) gets rounded corners inside\n a square panel, which is the last visible piece of a card that is supposed\n to be absent. `CnDetailCard.cn-detail-card--chromeless` zeroes the same\n thing for the same reason. */\n.cn-widget-wrapper--chromeless {\n\tborder-radius: 0;\n}\n\n/*\n * `chrome=\"nc-dashboard\"` — reproduce the native Nextcloud Dashboard panel\n * (apps/dashboard) exactly, using the same design tokens so an un-customised\n * widget is pixel-identical to a core dashboard panel. `styleConfig` inline\n * overrides (wrapperStyles / headerStyles) still win over these class rules,\n * so users can override any token. Combined selector raises specificity above\n * the base `.cn-widget-wrapper` rules it supersedes.\n */\n.cn-widget-wrapper__header {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n\t/* The header is `space-between`, so without an explicit gap a right-hand\n\t title-icon sits flush against whatever precedes it. The left-hand icon's\n\t spacing comes from header-left's own gap. */\n\tgap: 8px;\n\tpadding: 12px 16px;\n\tborder-bottom: 1px solid var(--color-border);\n\tflex-shrink: 0;\n}\n\n/* Actions with no title beside them. `header-left` is not rendered at all in\n this mode, and the header is `space-between`, so a single child would sit\n flush LEFT where every other surface puts its actions on the right. */\n.cn-widget-wrapper__header--actions-only {\n\tjustify-content: flex-end;\n}\n\n/* `chromeless` — the surface around this widget already drew the card, so\n nothing here may draw a second one. Border, background and content padding\n are already gone (chromeless implies borderless + flush); what is left is\n the header, which still renders when there are controls to hold. Its bottom\n rule is the card's own divider and reads as a second card edge inside the\n panel, and its 16px side inset leaves those controls hanging off the grid\n that now starts at the panel's edge. Both go.\n\n Placed AFTER the rules it overrides, which is what stylelint's\n no-descending-specificity asks for. The cascade did not need it (this\n selector is more specific either way), a reader does: an override written\n above the thing it overrides has to be read twice. */\n.cn-widget-wrapper--chromeless .cn-widget-wrapper__header {\n\tborder-bottom: none;\n\tpadding-inline: 0;\n}\n\n.cn-widget-wrapper__header-left {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\tmin-width: 0;\n\t/* Grow so the title hugs the (optional) title-icon on the left instead\n\t of centering between icon and actions (header is space-between). */\n\tflex: 1 1 auto;\n}\n\n.cn-widget-wrapper__icon {\n\twidth: 24px;\n\theight: 24px;\n\tflex-shrink: 0;\n\tcolor: var(--cn-widget-icon-color, var(--color-primary-element));\n}\n\n.cn-widget-wrapper__title {\n\tfont-weight: 600;\n\tfont-size: 14px;\n\tmargin: 0;\n\twhite-space: nowrap;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n}\n\n.cn-widget-wrapper.cn-widget-wrapper--nc-dashboard {\n\tbackground: var(--color-main-background-blur, var(--color-main-background));\n\t-webkit-backdrop-filter: var(--filter-background-blur, none);\n\tbackdrop-filter: var(--filter-background-blur, none);\n\tborder: none;\n\tborder-radius: var(--border-radius-container-large, 16px);\n}\n\n.cn-widget-wrapper--nc-dashboard .cn-widget-wrapper__header {\n\tpadding: 16px;\n\tborder-bottom: none;\n}\n\n.cn-widget-wrapper--nc-dashboard .cn-widget-wrapper__header-left {\n\tgap: 16px;\n}\n\n.cn-widget-wrapper--nc-dashboard .cn-widget-wrapper__icon {\n\twidth: 32px;\n\theight: 32px;\n\tbackground-size: 32px;\n}\n\n.cn-widget-wrapper--nc-dashboard .cn-widget-wrapper__title {\n\tfont-size: 20px;\n\tfont-weight: 700;\n\tline-height: 24px;\n}\n\n.cn-widget-wrapper--nc-dashboard .cn-widget-wrapper__content {\n\tpadding: 0 16px 16px;\n}\n\n.cn-widget-wrapper__title-meta {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 4px;\n\tfont-size: 12px;\n\tcolor: var(--color-text-maxcontrast);\n\twhite-space: nowrap;\n\tflex-shrink: 0;\n}\n\n.cn-widget-wrapper__actions {\n\tdisplay: flex;\n\tgap: 4px;\n\tflex-shrink: 0;\n}\n\n/* Every widget's header icon is coloured — `--cn-widget-icon-color` is set on\n the header from titleIconStyle, and defaults to the theme colour if a\n wrapper is ever rendered without it. Material-design-icon components paint\n with `fill: currentColor`, so this reaches them; the :deep rule below makes\n it reach a raw <svg> in the slot too. */\n.cn-widget-wrapper__title-icon {\n\tdisplay: flex;\n\talign-items: center;\n\tflex-shrink: 0;\n\t/* COLOURED BY DEFAULT. `--cn-widget-icon-color` is set on the header from\n\t titleIconStyle; the theme colour is the fallback for a wrapper rendered\n\t without it. Before this, the prop defaulted to null and the icon inherited\n\t body text colour, which is why every widget header looked the same. */\n\tcolor: var(--cn-widget-icon-color, var(--color-primary-element));\n\n\t/* SPACE BETWEEN THE ICON AND THE TITLE.\n\t This element is a sibling of `__header-left` inside a space-between\n\t header, so it never received that element's `gap: 8px` — the icon sat\n\t flush against the title text. Matching the header-left gap rather than\n\t inventing a value keeps the two icon positions spaced identically. */\n\tmargin-inline-end: 8px;\n}\n\n/* Right-positioned title icons sit AFTER the actions, so the margin belongs on\n the other side or it pushes the icon off the header's right edge. */\n.cn-widget-wrapper__title-icon:last-child {\n\tmargin-inline-end: 0;\n\tmargin-inline-start: 8px;\n}\n\n/* Material-design-icon components paint with `fill: currentColor`, so the\n colour above reaches them; this makes it reach a raw <svg> in the slot too. */\n.cn-widget-wrapper__title-icon :deep(svg) {\n\tfill: currentcolor;\n}\n\n.cn-widget-wrapper__footer {\n\tdisplay: flex;\n\tjustify-content: flex-end;\n\tgap: 8px;\n\tpadding: 8px 16px;\n\tborder-top: 1px solid var(--color-border);\n\tflex-shrink: 0;\n}\n\n.cn-widget-wrapper__footer-link {\n\tfont-size: 13px;\n\tcolor: var(--color-primary-element);\n\ttext-decoration: none;\n}\n\n.cn-widget-wrapper__footer-link:hover {\n\ttext-decoration: underline;\n}\n</style>\n"],"names":["CnActionsMenu","t"],"mappings":"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"}
1
+ {"version":3,"file":"CnWidgetWrapper.vue2.js","sources":["../../../../src/components/CnWidgetWrapper/CnWidgetWrapper.vue"],"sourcesContent":["<!--\n CnWidgetWrapper — Container shell around a dashboard widget.\n\n Provides header (icon + title), scrollable content area, and optional\n footer with action buttons. Applies style configuration for borders,\n backgrounds, and padding. The built-in overflow Actions menu (Refresh /\n Documentation / Request a feature) is delegated to the shared\n CnActionsMenu so widgets and page surfaces stay in lockstep.\n-->\n<template>\n\t<div\n\t\tclass=\"cn-widget-wrapper\"\n\t\t:class=\"{\n\t\t\t'cn-widget-wrapper--borderless': noBorder,\n\t\t\t'cn-widget-wrapper--flush': noPadding,\n\t\t\t'cn-widget-wrapper--chromeless': chromeless,\n\t\t\t'cn-widget-wrapper--nc-dashboard': chrome === 'nc-dashboard',\n\t\t\t'cn-widget-wrapper--high-contrast': highContrast,\n\t\t}\"\n\t\t:data-cn-high-contrast=\"highContrast ? 'true' : undefined\"\n\t\t:style=\"wrapperStyles\">\n\t\t<!-- Header.\n\t\t Rendered when a title earns it, or when controls that are actually\n\t\t on screen need a home. Three conditions rather than one, and each of\n\t\t the three is a bug this component has already shipped: gating on the\n\t\t title alone took the Save button for an inline edit away along with\n\t\t the doubled title; reading `showActions`, which defaults to true,\n\t\t handed a header to every headerless KPI tile; and reading the\n\t\t `actions` slot's mere PRESENCE gave a chromeless panel a 59px band\n\t\t holding nothing, because `CnObjectDataWidget` provides that template\n\t\t always and fills it only while an edit is unsaved.\n\t\t `headerIsWorthIt()` carries the reasoning. -->\n\t\t<div v-if=\"headerIsWorthIt()\"\n\t\t\tclass=\"cn-widget-wrapper__header\"\n\t\t\t:class=\"{ 'cn-widget-wrapper__header--actions-only': !titleVisible }\"\n\t\t\t:style=\"[headerStyles, titleIconStyle]\">\n\t\t\t<!-- Title icon — left: rendered before the title group. Moved\n\t\t\t INSIDE header-left so the header's `space-between` cannot pull\n\t\t\t it away from the title it belongs to; header-left's `gap` is\n\t\t\t what now separates icon from title (they used to touch). -->\n\t\t\t<div v-if=\"titleVisible\" class=\"cn-widget-wrapper__header-left\">\n\t\t\t\t<div v-if=\"$slots['title-icon'] && titleIconPosition === 'left'\"\n\t\t\t\t\tclass=\"cn-widget-wrapper__title-icon\">\n\t\t\t\t\t<slot name=\"title-icon\" />\n\t\t\t\t</div>\n\t\t\t\t<img\n\t\t\t\t\tv-if=\"iconUrl\"\n\t\t\t\t\t:src=\"iconUrl\"\n\t\t\t\t\t:alt=\"resolvedTitle\"\n\t\t\t\t\tclass=\"cn-widget-wrapper__icon\">\n\t\t\t\t<span\n\t\t\t\t\tv-else-if=\"iconClass\"\n\t\t\t\t\t:class=\"iconClass\"\n\t\t\t\t\tclass=\"cn-widget-wrapper__icon\" />\n\t\t\t\t<h3 :id=\"titleId\" class=\"cn-widget-wrapper__title\">\n\t\t\t\t\t{{ resolvedTitle }}\n\t\t\t\t</h3>\n\t\t\t\t<!-- @slot title-meta Rendered inside the header's left group,\n\t\t\t\t after the title and before the spacer. Use for small\n\t\t\t\t contextual chips (e.g. a date-range badge on chart\n\t\t\t\t widgets). Kept inside header-left so the chip sits next\n\t\t\t\t to the title rather than floating right next to the\n\t\t\t\t actions menu. -->\n\t\t\t\t<div v-if=\"$slots['title-meta']\" class=\"cn-widget-wrapper__title-meta\">\n\t\t\t\t\t<slot name=\"title-meta\" />\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<!-- `showActions` gates the overflow MENU, not the slot beside it.\n\t\t\t It used to gate both, so a caller who switched the menu off\n\t\t\t also lost the Save button for an inline edit, silently. -->\n\t\t\t<div v-if=\"actionsAreVisible()\" class=\"cn-widget-wrapper__actions\">\n\t\t\t\t<!-- One text link in the header (`headerLink`): \"All deadlines\",\n\t\t\t\t \"To the board\". A route is a router link, an href a plain\n\t\t\t\t anchor. Sits before the slot and the overflow menu. -->\n\t\t\t\t<component\n\t\t\t\t\t:is=\"headerLinkTag\"\n\t\t\t\t\tv-if=\"headerLinkTag\"\n\t\t\t\t\tclass=\"cn-widget-wrapper__header-link\"\n\t\t\t\t\tdata-testid=\"cn-widget-wrapper-header-link\"\n\t\t\t\t\tv-bind=\"headerLinkAttrs\">\n\t\t\t\t\t{{ headerLinkLabel }}\n\t\t\t\t</component>\n\t\t\t\t<!-- @slot actions Custom action buttons rendered before the\n\t\t\t\t built-in overflow menu. -->\n\t\t\t\t<slot name=\"actions\" />\n\t\t\t\t<CnActionsMenu\n\t\t\t\t\tv-if=\"showActions\"\n\t\t\t\t\t:showRefresh=\"effectiveShowRefresh\"\n\t\t\t\t\t:showRequestFeature=\"effectiveShowRequestFeature\"\n\t\t\t\t\t:showReportBug=\"showReportBug\"\n\t\t\t\t\t:showDocumentation=\"showDocumentation\"\n\t\t\t\t\t:documentationUrl=\"documentationUrl\"\n\t\t\t\t\t:docsAnchor=\"docsAnchor\"\n\t\t\t\t\t:reportBugUrl=\"reportBugUrl\"\n\t\t\t\t\t:documentationLabel=\"documentationLabel\"\n\t\t\t\t\t:refreshLabel=\"refreshLabel\"\n\t\t\t\t\t:requestFeatureLabel=\"requestFeatureLabel\"\n\t\t\t\t\t:actionsMenuLabel=\"actionsMenuLabel\"\n\t\t\t\t\t:refreshing=\"refreshing\"\n\t\t\t\t\t:widgetId=\"resolvedWidgetId\"\n\t\t\t\t\t:title=\"displayTitle\"\n\t\t\t\t\t:surface=\"`widget:${resolvedWidgetId}`\"\n\t\t\t\t\t:specRef=\"specRef\"\n\t\t\t\t\trefreshChannel=\"cn:widget:refresh\"\n\t\t\t\t\ttestidBase=\"cn-widget-wrapper\"\n\t\t\t\t\t@refresh=\"onActionsRefresh\"\n\t\t\t\t\t@requestFeature=\"onActionsRequestFeature\">\n\t\t\t\t\t<!-- @slot action-items Additional NcActionButton-family\n\t\t\t\t\t items rendered inside the overflow menu, after the\n\t\t\t\t\t built-in Refresh / Documentation / Request-a-feature\n\t\t\t\t\t group. -->\n\t\t\t\t\t<template v-if=\"hasActionItemsSlot\" #action-items>\n\t\t\t\t\t\t<slot name=\"action-items\" />\n\t\t\t\t\t</template>\n\t\t\t\t</CnActionsMenu>\n\t\t\t</div>\n\t\t\t<!-- Title icon — right: rendered after actions, far right -->\n\t\t\t<div v-if=\"$slots['title-icon'] && titleIconPosition === 'right'\"\n\t\t\t\tclass=\"cn-widget-wrapper__title-icon\">\n\t\t\t\t<slot name=\"title-icon\" />\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- Headerless meta (e.g. a KPI date chip on a flush card). When the\n\t\t header is hidden but a `title-meta` chip is provided, float it in the\n\t\t top-right corner over the content instead of dropping it — so a\n\t\t compact flush KPI tile can carry a date-range chip without growing a\n\t\t full header bar. -->\n\t\t<div\n\t\t\tv-if=\"!titleVisible && $slots['title-meta']\"\n\t\t\tclass=\"cn-widget-wrapper__floating-meta\">\n\t\t\t<slot name=\"title-meta\" />\n\t\t</div>\n\n\t\t<!-- Content -->\n\t\t<!--\n\t\t The content area is `overflow: auto`, so it can become a scrollable\n\t\t region. WCAG 2.1.1 (Keyboard) requires scrollable regions to be\n\t\t keyboard-focusable so keyboard-only users can scroll them, and 4.1.2\n\t\t requires an accessible name. We expose it as a labelled region and\n\t\t give it tabindex=\"0\". When the header (and its title) is hidden we\n\t\t fall back to an explicit aria-label from the widget title so the\n\t\t region is never anonymous.\n\t\t-->\n\t\t<div\n\t\t\tclass=\"cn-widget-wrapper__content\"\n\t\t\ttabindex=\"0\"\n\t\t\trole=\"region\"\n\t\t\t:aria-labelledby=\"titleVisible ? titleId : null\"\n\t\t\t:aria-label=\"titleVisible ? null : resolvedTitle\">\n\t\t\t<slot />\n\t\t</div>\n\n\t\t<!-- Footer -->\n\t\t<div v-if=\"$slots.footer || (buttons && buttons.length > 0)\" class=\"cn-widget-wrapper__footer\">\n\t\t\t<slot name=\"footer\">\n\t\t\t\t<a\n\t\t\t\t\tv-for=\"button in buttons\"\n\t\t\t\t\t:key=\"button.link\"\n\t\t\t\t\t:href=\"button.link\"\n\t\t\t\t\tclass=\"cn-widget-wrapper__footer-link\">\n\t\t\t\t\t{{ button.text }}\n\t\t\t\t</a>\n\t\t\t</slot>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { slotRenders } from '../../utils/slotContent.js'\nimport { CnActionsMenu } from '../CnActionsMenu/index.js'\n\n/**\n * CnWidgetWrapper — Widget container with header, content, and footer.\n *\n * ```vue\n * <CnWidgetWrapper title=\"My Cases\" :icon-url=\"casesIconUrl\">\n * <MyCasesChart :data=\"chartData\" />\n * </CnWidgetWrapper>\n * ```\n *\n * With NC widget object\n * ```vue\n * <CnWidgetWrapper\n * :title=\"widget.title\"\n * :icon-url=\"widget.iconUrl\"\n * :icon-class=\"widget.iconClass\"\n * :documentation-url=\"widget.documentationUrl\"\n * :buttons=\"widget.buttons\">\n * <CnWidgetRenderer :widget=\"widget\" />\n * </CnWidgetWrapper>\n * ```\n */\n/**\n * Semantic variant → CSS colour token for the widget header icon. Tokens\n * only — the nldesign app themes by overriding the Nextcloud variables, so a\n * literal hex here would ignore government theming entirely (ADR-062 / NL\n * Design System).\n *\n * @type {Record<string, string>}\n */\nconst TITLE_ICON_VARIANT_COLORS = {\n\tprimary: 'var(--color-primary-element)',\n\tsuccess: 'var(--color-success)',\n\twarning: 'var(--color-warning)',\n\terror: 'var(--color-error)',\n\tinfo: 'var(--color-info, var(--color-primary-element))',\n\tneutral: 'var(--color-text-maxcontrast)',\n}\n\nexport default {\n\tname: 'CnWidgetWrapper',\n\n\tcomponents: {\n\t\tCnActionsMenu,\n\t},\n\n\tinject: {\n\t\t/**\n\t\t * Host translate function provided by CnAppRoot as\n\t\t * `cnTranslate: this.translate` (bound to the host app's id). The\n\t\t * manifest-authored widget title is run through it so the card\n\t\t * heading follows the user's language. Defaults to an identity\n\t\t * function so an untranslated key renders as itself.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/** Widget title */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Widget'),\n\t\t},\n\n\t\t/** Whether to show the header with title */\n\t\tshowTitle: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Chrome variant for the wrapper card.\n\t\t * - `'default'` — the library's own card chrome (opaque background,\n\t\t * 1px border, compact header).\n\t\t * - `'nc-dashboard'` — matches the native Nextcloud Dashboard panel\n\t\t * exactly using the same design tokens: translucent blurred\n\t\t * background (`--color-main-background-blur` + `--filter-background-blur`),\n\t\t * `--border-radius-container-large` corners, no border/shadow, a 16px\n\t\t * header with a 20px/700 title and 32px leading icon, and content\n\t\t * inset 16px on the sides + bottom. `styleConfig` overrides still\n\t\t * layer on top so a user can customise any token.\n\t\t *\n\t\t * @type {'default'|'nc-dashboard'}\n\t\t */\n\t\tchrome: {\n\t\t\ttype: String,\n\t\t\tdefault: 'default',\n\t\t\tvalidator: (v) => ['default', 'nc-dashboard'].includes(v),\n\t\t},\n\n\t\t/**\n\t\t * Remove border and background — makes the wrapper transparent.\n\t\t * Useful for widgets that are self-contained cards (e.g. CnStatsBlock).\n\t\t */\n\t\tborderless: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Remove content padding — allows content to go edge-to-edge.\n\t\t * Useful for list-style widgets where items should span the full width.\n\t\t */\n\t\tflush: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Draw NO card of this widget's own: no border, no background, no\n\t\t * content padding, no title row and no header divider. For a surface\n\t\t * that already supplies the card and the heading — a tab panel being\n\t\t * the case that matters, where a nested card repeats both.\n\t\t *\n\t\t * The wrapper ELEMENT still renders. That is the contract, not an\n\t\t * oversight: `CnObjectDataWidget` measures its overflow against\n\t\t * `.cn-widget-wrapper__content` (`closest()` plus a ResizeObserver on\n\t\t * that node), `src/css/table.css` gives a table inside it its\n\t\t * edge-to-edge treatment, and `src/css/detail-page.css` and\n\t\t * `dashboard.css` size their content areas through it. Dropping the\n\t\t * node would take all of that out silently — `closest()` returning\n\t\t * null reads as \"nothing overflows\", never as an error.\n\t\t *\n\t\t * Controls in the `actions` slot are NOT chrome and keep their header:\n\t\t * the Save button that commits an inline edit lives there, and a panel\n\t\t * that hides it is silently unsaveable.\n\t\t *\n\t\t * `chromeless` is what an integration provider already asks for through\n\t\t * `CnDetailWidgetHost` (`bareWidget`), so this is the same word for the\n\t\t * same thing on every path into the wrapper.\n\t\t */\n\t\tchromeless: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Icon URL (image) */\n\t\ticonUrl: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Icon CSS class (e.g., Nextcloud icon class) */\n\t\ticonClass: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Position of the title-icon slot in the header.\n\t\t * 'left' places it before the title; 'right' places it after the actions.\n\t\t */\n\t\ttitleIconPosition: {\n\t\t\ttype: String,\n\t\t\tdefault: 'right',\n\t\t\tvalidator: (v) => ['left', 'right'].includes(v),\n\t\t},\n\n\t\t/**\n\t\t * Explicit CSS colour for the header icon. Overrides `titleIconVariant`.\n\t\t * Must be a CSS custom property or a theme token — never a literal hex\n\t\t * (NL Design System: the nldesign app re-themes by overriding the\n\t\t * Nextcloud variables, and a hardcoded colour ignores that).\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\ttitleIconColor: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Semantic colour for the header icon. Every widget's icon is\n\t\t * coloured — `primary` (the theme colour) is the default, and a\n\t\t * widget whose subject already carries a semantic meaning names it\n\t\t * here so the icon says so at a glance (a Depublished list is\n\t\t * `error`, a Concepts list `warning`, a Published list `success`).\n\t\t *\n\t\t * @type {'primary'|'success'|'warning'|'error'|'info'|'neutral'}\n\t\t */\n\t\ttitleIconVariant: {\n\t\t\ttype: String,\n\t\t\tdefault: 'primary',\n\t\t\tvalidator: (v) => ['primary', 'success', 'warning', 'error', 'info', 'neutral'].includes(v),\n\t\t},\n\n\t\t/** Footer action buttons: [{ text, link }] */\n\t\tbuttons: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * One text link in the header's actions group, before the overflow\n\t\t * menu: `{ label, route?, params?, query?, href? }`. A `route` is a\n\t\t * named router link (needs a router), an `href` a plain anchor. The\n\t\t * label goes through the host translate function. Null (the\n\t\t * default) draws nothing. Manifest: `headerLink` on the widget\n\t\t * definition or its layout entry.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-widget-header-carries-a-text-link\n\t\t * @type {{label: string, route?: string, params?: object, query?: object, href?: string}|null}\n\t\t */\n\t\theaderLink: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Whether the header's overflow action menu (Refresh / Documentation /\n\t\t * Request-a-feature + any `#action-items`) renders. Shown by default;\n\t\t * set `false` for compact surfaces — e.g. a KPI tile whose only header\n\t\t * affordance is a date chip — to drop the menu and free header width.\n\t\t */\n\t\tshowActions: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Style configuration for the wrapper.\n\t\t *\n\t\t * @type {{ backgroundColor: string, borderStyle: string, borderWidth: number, borderColor: string, borderRadius: number, padding: { top: number, right: number, bottom: number, left: number } }}\n\t\t */\n\t\tstyleConfig: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Hide the built-in Refresh item from the overflow action menu.\n\t\t * The Refresh item is shown by default — set this when the widget\n\t\t * has no refreshable data source (e.g. a static tile). Alias for\n\t\t * the inverse `:show-refresh=\"false\"`; either form opts out.\n\t\t */\n\t\thideRefresh: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Hide the built-in Request-a-feature item from the overflow\n\t\t * action menu. Shown by default; set when the consuming app has\n\t\t * no public issue tracker to link out to. Alias for the inverse\n\t\t * `:show-request-feature=\"false\"`; either form opts out.\n\t\t */\n\t\thideRequestFeature: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Whether to show the built-in Refresh item. Tri-state:\n\t\t * - `true` / `false` — force the action on or off.\n\t\t * - `null` (the default) — **auto**: show the action only when a\n\t\t * parent has attached an `@refresh` listener (i.e. something will\n\t\t * actually handle the refresh). This keeps widgets that can't\n\t\t * refresh — e.g. a prop-driven `CnObjectDataWidget` — from showing\n\t\t * a dead button. Widgets that refresh themselves via the\n\t\t * `cn:widget:refresh` event bus (with no `@refresh` listener)\n\t\t * should set `:show-refresh=\"true\"` explicitly.\n\t\t *\n\t\t * `hideRefresh` (or `:show-refresh=\"false\"`) always wins.\n\t\t *\n\t\t * @type {boolean|null}\n\t\t */\n\t\tshowRefresh: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Inverse of `hideRequestFeature`. Defaults to `true` so the\n\t\t * action renders. Set `:show-request-feature=\"false\"` to hide it.\n\t\t * Either flag hides the action.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tshowRequestFeature: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Whether the built-in \"Report a bug\" item renders. On by default —\n\t\t * the trio Request a feature / Report a bug / Documentation is the\n\t\t * contract for every widget; this exists for the rare surface that\n\t\t * must suppress one deliberately.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tshowReportBug: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Whether the built-in \"Documentation\" item renders. On by default,\n\t\t * for the same reason as `showReportBug`. The item's target is\n\t\t * resolved by the shared menu (see `docsAnchor`), so leaving it on\n\t\t * costs the host nothing.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tshowDocumentation: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Explicit documentation link for this widget, opened in a new tab.\n\t\t * Usually unnecessary: leave it empty and set `docsAnchor` instead,\n\t\t * so the link is built from the app-wide documentation base and\n\t\t * lands on this widget's own section.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tdocumentationUrl: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Optional pre-translated label for the Documentation action.\n\t\t * Defaults to the lib's translation of \"Documentation\".\n\t\t */\n\t\tdocumentationLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Documentation'),\n\t\t},\n\n\t\t/**\n\t\t * Widget id for the built-in default Refresh / Request-a-feature\n\t\t * handlers (B2). Forwarded as the `surface: \"widget:<id>\"` value\n\t\t * on the Actions menu (the English headline fallback for its forge\n\t\t * deep-links) AND as the `widgetId` field on the\n\t\t * `cn:widget:refresh` event-bus payload. When unset, the wrapper\n\t\t * falls back to a slugified `displayTitle`, which works but is\n\t\t * less stable than an explicit id.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\twidgetId: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * This widget's own section in the app's documentation, appended to\n\t\t * the app-wide documentation base URL (provided by CnAppRoot) to build\n\t\t * the Actions menu's Documentation deep-link. A bare slug becomes a\n\t\t * `#fragment`. Supply it per widget type — without it the item still\n\t\t * renders, but lands on the docs homepage rather than this widget.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tdocsAnchor: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Explicit \"Report a bug\" target for the Actions menu. Empty (the\n\t\t * default) builds a new-issue deep-link on the app's own forge.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\treportBugUrl: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Declares that this widget wants a high contrast treatment. The flag\n\t\t * reaches the theme as `data-cn-high-contrast` and a `--high-contrast`\n\t\t * class; the component picks no colour of its own (ADR-003). A theme\n\t\t * that says nothing about the flag renders the widget unchanged.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\thighContrast: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Optional `specRef` slug, forwarded to the Actions menu. Accepted\n\t\t * for backward compatibility with the removed in-product suggestion\n\t\t * modal; the forge issue form asks for its own context.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tspecRef: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Whether a refresh is currently in flight. When bound by the host\n\t\t * (e.g. `:refreshing=\"loading\"` around its refetch), the Refresh\n\t\t * item is disabled and shows a loading spinner for exactly as long\n\t\t * as this stays true — so the spinner reflects the real refresh time.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\trefreshing: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Optional pre-translated label for the Refresh action. Defaults\n\t\t * to the lib's translation of \"Refresh\" so callers usually don't\n\t\t * need to set this.\n\t\t */\n\t\trefreshLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Refresh'),\n\t\t},\n\n\t\t/**\n\t\t * Optional pre-translated label for the Request-a-feature action.\n\t\t * Defaults to the lib's translation of \"Request a feature\".\n\t\t */\n\t\trequestFeatureLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Request a feature'),\n\t\t},\n\n\t\t/**\n\t\t * Pre-translated aria-label / tooltip for the overflow menu\n\t\t * trigger. Defaults to \"Actions\".\n\t\t */\n\t\tactionsMenuLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Actions'),\n\t\t},\n\n\t\t/**\n\t\t * Translate function. Falls back to the injected `cnTranslate`,\n\t\t * which itself defaults to an identity function.\n\t\t *\n\t\t * @type {((key: string) => string)|null}\n\t\t */\n\t\ttranslate: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\temits: ['refresh', 'request-feature'],\n\n\tcomputed: {\n\t\tdisplayTitle() {\n\t\t\treturn this.title || 'Widget'\n\t\t},\n\n\t\t/**\n\t\t * The header's icon colour, published as ONE custom property on the\n\t\t * header rather than an inline `color` repeated on each icon element.\n\t\t * Everything under the header — the icon-class span, the `title-icon`\n\t\t * slot's content, an SVG inside it — inherits from that single\n\t\t * declaration, so the icon and any decoration around it cannot end up\n\t\t * different colours.\n\t\t *\n\t\t * `titleIconColor` wins when the host set one; otherwise the\n\t\t * `titleIconVariant` token does, which is why EVERY widget icon is\n\t\t * coloured and not just the ones an app remembered to configure. Every\n\t\t * value is a CSS variable, so the nldesign app's overrides re-theme\n\t\t * them for free (NL Design System).\n\t\t *\n\t\t * @return {object} A style object declaring `--cn-widget-icon-color`.\n\t\t */\n\t\ttitleIconStyle() {\n\t\t\treturn {\n\t\t\t\t'--cn-widget-icon-color': this.titleIconColor\n\t\t\t\t\t|| TITLE_ICON_VARIANT_COLORS[this.titleIconVariant]\n\t\t\t\t\t|| TITLE_ICON_VARIANT_COLORS.primary,\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Effective translate function: the explicit `translate` prop when\n\t\t * given, else the injected `cnTranslate` (identity by default).\n\t\t *\n\t\t * @return {(key: string) => string}\n\t\t */\n\t\teffectiveTranslate() {\n\t\t\treturn this.translate ?? this.cnTranslate\n\t\t},\n\n\t\t/**\n\t\t * The header link's tag: `router-link` for a route (when a router is\n\t\t * mounted), `a` for an href, '' when there is no usable link.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-widget-header-carries-a-text-link\n\t\t * @return {string}\n\t\t */\n\t\theaderLinkTag() {\n\t\t\tconst link = this.headerLink\n\t\t\tif (!link || typeof link.label !== 'string' || link.label === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tif (link.route && this.$router) {\n\t\t\t\treturn 'router-link'\n\t\t\t}\n\t\t\tif (typeof link.href === 'string' && link.href !== '') {\n\t\t\t\treturn 'a'\n\t\t\t}\n\t\t\treturn ''\n\t\t},\n\n\t\t/**\n\t\t * The header link's attributes for its tag.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-widget-header-carries-a-text-link\n\t\t * @return {object}\n\t\t */\n\t\theaderLinkAttrs() {\n\t\t\tconst link = this.headerLink || {}\n\t\t\tif (this.headerLinkTag === 'router-link') {\n\t\t\t\tconst to = { name: link.route }\n\t\t\t\tif (link.params && typeof link.params === 'object') {\n\t\t\t\t\tto.params = link.params\n\t\t\t\t}\n\t\t\t\tif (link.query && typeof link.query === 'object') {\n\t\t\t\t\tto.query = link.query\n\t\t\t\t}\n\t\t\t\treturn { to }\n\t\t\t}\n\t\t\tif (this.headerLinkTag === 'a') {\n\t\t\t\treturn { href: link.href }\n\t\t\t}\n\t\t\treturn {}\n\t\t},\n\n\t\t/**\n\t\t * The header link's text, through the host translate function.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-widget-header-carries-a-text-link\n\t\t * @return {string}\n\t\t */\n\t\theaderLinkLabel() {\n\t\t\tconst label = this.headerLink && this.headerLink.label\n\t\t\treturn label ? this.effectiveTranslate(label) : ''\n\t\t},\n\n\t\t/**\n\t\t * The RENDERED widget title — `displayTitle` run through the host\n\t\t * translate function. Kept separate from `displayTitle` on purpose:\n\t\t * `resolvedWidgetId` slugifies the raw title, so identifiers (DOM\n\t\t * ids, refresh channels, action payloads) stay locale-independent\n\t\t * while only the visible/AT-facing text localises.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedTitle() {\n\t\t\treturn this.title ? this.effectiveTranslate(this.title) : this.displayTitle\n\t\t},\n\n\t\t/**\n\t\t * Stable DOM id for the header title `<h3>`, used as the\n\t\t * `aria-labelledby` target of the scrollable content region so the\n\t\t * region gets an accessible name from the widget title (WCAG 4.1.2).\n\t\t * Derived from `resolvedWidgetId` so it is stable and unique per\n\t\t * widget instance on the page.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\ttitleId() {\n\t\t\treturn `cn-widget-wrapper-title-${this.resolvedWidgetId}`\n\t\t},\n\n\t\t/**\n\t\t * Effective Refresh visibility. `hideRefresh: true` (or\n\t\t * `:show-refresh=\"false\"`) always hides it. When `showRefresh` is\n\t\t * left unset (`null`), auto-detect: show the action only when a\n\t\t * parent attached an `@refresh` listener — otherwise the refresh\n\t\t * would do nothing. `hideRefresh` remains a back-compat alias.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\teffectiveShowRefresh() {\n\t\t\tif (this.hideRefresh) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif (this.showRefresh !== null) {\n\t\t\t\treturn this.showRefresh\n\t\t\t}\n\t\t\t// `$.vnode.props`, not `$attrs`: `refresh` is a declared emit, and\n\t\t\t// Vue keeps declared emits out of `$attrs`.\n\t\t\treturn Boolean(this.$.vnode.props?.onRefresh)\n\t\t},\n\n\t\t/**\n\t\t * Effective Request-a-feature visibility — same OR-of-opt-outs\n\t\t * pattern as `effectiveShowRefresh`.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\teffectiveShowRequestFeature() {\n\t\t\treturn this.showRequestFeature && !this.hideRequestFeature\n\t\t},\n\n\t\t/**\n\t\t * Stable id used in event-bus payloads and the\n\t\t * `surface: \"widget:<id>\"` field on the auto-mounted feature\n\t\t * modal. Prefers the explicit `widgetId` prop; falls back to a\n\t\t * slugified `displayTitle` so widgets without an explicit id\n\t\t * still get a usable identifier.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedWidgetId() {\n\t\t\tif (this.widgetId) {\n\t\t\t\treturn this.widgetId\n\t\t\t}\n\t\t\treturn this.displayTitle.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')\n\t\t},\n\n\t\t/**\n\t\t * Whether the caller supplied an `action-items` slot. Forwarded\n\t\t * conditionally so the shared CnActionsMenu doesn't treat an\n\t\t * always-present pass-through template as content (which would\n\t\t * keep the overflow menu visible even when every built-in is\n\t\t * opted out).\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\thasActionItemsSlot() {\n\t\t\treturn Boolean(this.$slots['action-items']) || Boolean(this.$slots && this.$slots['action-items'])\n\t\t},\n\n\t\t/**\n\t\t * Whether the caller put controls in the `actions` slot.\n\t\t *\n\t\t * Gates the header together with `showTitle`, so a surface that wants no\n\t\t * title still keeps controls that have nowhere else to go — the Save\n\t\t * button `CnObjectDataWidget` renders there being the case that matters.\n\t\t *\n\t\t * Deliberately NOT `showActions`: that prop defaults to true and governs\n\t\t * the overflow menu, so reading it here would give a header to every\n\t\t * headerless KPI tile that never had one.\n\t\t *\n\t\t * @return {boolean} true when the slot is filled.\n\t\t */\n\t\thasActionsSlot() {\n\t\t\treturn Boolean(this.$slots.actions)\n\t\t},\n\n\t\t/**\n\t\t * Whether the card outline is drawn. `chromeless` implies `borderless`\n\t\t * so a caller names the intent once instead of remembering the three\n\t\t * props it decomposes into.\n\t\t *\n\t\t * @return {boolean} true when border and background are suppressed.\n\t\t */\n\t\tnoBorder() {\n\t\t\treturn this.borderless || this.chromeless\n\t\t},\n\n\t\t/**\n\t\t * Whether the content area goes edge to edge. `chromeless` implies\n\t\t * `flush` for the same reason as `noBorder`.\n\t\t *\n\t\t * @return {boolean} true when content padding is suppressed.\n\t\t */\n\t\tnoPadding() {\n\t\t\treturn this.flush || this.chromeless\n\t\t},\n\n\t\t/**\n\t\t * Whether the title row renders. `chromeless` overrides `showTitle`,\n\t\t * which defaults to true: a surface that already names the widget must\n\t\t * not have to also remember to turn the title off.\n\t\t *\n\t\t * Read everywhere the template used to read `showTitle` directly,\n\t\t * including the two ARIA bindings on the content region — a hidden\n\t\t * title cannot be the region's `aria-labelledby` target.\n\t\t *\n\t\t * @return {boolean} true when the header's title group renders.\n\t\t */\n\t\ttitleVisible() {\n\t\t\treturn this.showTitle && !this.chromeless\n\t\t},\n\n\t\twrapperStyles() {\n\t\t\tconst styles = {}\n\n\t\t\tif (this.styleConfig.backgroundColor) {\n\t\t\t\tstyles.backgroundColor = this.styleConfig.backgroundColor\n\t\t\t}\n\n\t\t\tif (this.styleConfig.borderStyle && this.styleConfig.borderStyle !== 'none') {\n\t\t\t\tstyles.border = `${this.styleConfig.borderWidth || 1}px ${this.styleConfig.borderStyle} ${this.styleConfig.borderColor || 'var(--color-border)'}`\n\t\t\t}\n\n\t\t\tif (this.styleConfig.borderRadius !== undefined) {\n\t\t\t\tstyles.borderRadius = `${this.styleConfig.borderRadius}px`\n\t\t\t}\n\n\t\t\tif (this.styleConfig.padding) {\n\t\t\t\tconst p = this.styleConfig.padding\n\t\t\t\tstyles.padding = `${p.top || 0}px ${p.right || 0}px ${p.bottom || 0}px ${p.left || 0}px`\n\t\t\t}\n\n\t\t\treturn styles\n\t\t},\n\n\t\t/**\n\t\t * Inline styles for the header bar, derived from\n\t\t * `styleConfig.headerStyle.{backgroundColor, textColor}`. Lets a host\n\t\t * give individual widgets a custom header colour without a per-app CSS\n\t\t * workaround. Empty object when no header style is configured.\n\t\t *\n\t\t * @return {object} the header style bindings.\n\t\t */\n\t\theaderStyles() {\n\t\t\tconst hs = this.styleConfig.headerStyle\n\t\t\tif (!hs || typeof hs !== 'object') {\n\t\t\t\treturn {}\n\t\t\t}\n\t\t\tconst styles = {}\n\t\t\tif (hs.backgroundColor) {\n\t\t\t\tstyles.backgroundColor = hs.backgroundColor\n\t\t\t}\n\t\t\tif (hs.textColor) {\n\t\t\t\tstyles.color = hs.textColor\n\t\t\t}\n\t\t\treturn styles\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Whether the `actions` slot renders anything a reader can see.\n\t\t *\n\t\t * `CnObjectDataWidget` always PROVIDES the template and fills it only\n\t\t * while an edit is unsaved, so the slot has to be called and its vnodes\n\t\t * inspected. Reading `$slots.actions` as a boolean counts the template\n\t\t * itself as content, which is what gave a titleless tab panel a 59px\n\t\t * band holding nothing but a second Actions menu.\n\t\t *\n\t\t * A method rather than a computed on purpose. The answer changes when the\n\t\t * PARENT's state changes, and a computed reading `$slots` does not\n\t\t * re-track that; a method re-runs on every render, and a slot whose\n\t\t * content depends on parent state already forces this component to\n\t\t * re-render.\n\t\t *\n\t\t * @return {boolean} True when the slot yields visible content.\n\t\t */\n\t\tactionsSlotRenders() {\n\t\t\treturn slotRenders(this.$slots.actions)\n\t\t},\n\n\t\t/**\n\t\t * Whether the header band is worth the vertical space it costs.\n\t\t *\n\t\t * A VISIBLE title always earns it, which `chromeless` rules out. Otherwise\n\t\t * it is earned by controls that are actually on screen: content in the\n\t\t * `actions` slot, or the overflow menu when a caller both asked for it and\n\t\t * provided the slot it sits beside.\n\t\t *\n\t\t * That last clause looks redundant and is not. `showActions` defaults to\n\t\t * TRUE, so reading it alone hands a header to every headerless KPI tile\n\t\t * in the fleet, which is what the floating title-meta test caught when\n\t\t * this was first written the short way.\n\t\t *\n\t\t * @return {boolean} True when the header should render.\n\t\t */\n\t\theaderIsWorthIt() {\n\t\t\tif (this.titleVisible) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tif (this.actionsSlotRenders()) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn this.showActions && this.hasActionsSlot\n\t\t},\n\n\t\t/**\n\t\t * Whether the actions group holds anything.\n\t\t *\n\t\t * The overflow menu and the slot beside it are separate conditions now.\n\t\t * `showActions` used to gate both, so a caller who only wanted the menu\n\t\t * gone also lost the button that commits an inline edit.\n\t\t *\n\t\t * @return {boolean} True when the group would hold a visible control.\n\t\t */\n\t\tactionsAreVisible() {\n\t\t\treturn this.showActions || this.actionsSlotRenders() || Boolean(this.headerLinkTag)\n\t\t},\n\n\t\t/**\n\t\t * Re-emit the shared CnActionsMenu `@refresh` to the host, passing\n\t\t * the synthetic event through unchanged so a host listener can\n\t\t * still `preventDefault()` the built-in default (event-bus emit on\n\t\t * `cn:widget:refresh`).\n\t\t *\n\t\t * @param {{ widgetId: string, title: string }} payload Action payload.\n\t\t * @param {{ defaultPrevented: boolean, preventDefault: () => void }} ev Synthetic event.\n\t\t * @return {void}\n\t\t */\n\t\tonActionsRefresh(payload, ev) {\n\t\t\t/**\n\t\t\t * @event refresh User clicked the Refresh item in the overflow\n\t\t\t * action menu. Payload: `{ widgetId, title }`. Handlers may call\n\t\t\t * the second arg's `preventDefault()` to suppress the built-in\n\t\t\t * default (event-bus emit on `cn:widget:refresh`).\n\t\t\t * @type {{ widgetId: string, title: string }}\n\t\t\t */\n\t\t\tthis.$emit('refresh', payload, ev)\n\t\t},\n\n\t\t/**\n\t\t * Re-emit the shared CnActionsMenu `@request-feature` to the host,\n\t\t * passing the synthetic event through so a host listener can\n\t\t * `preventDefault()` the built-in default (opening the forge's\n\t\t * feature-request issue form).\n\t\t *\n\t\t * @param {{ widgetId: string, title: string }} payload Action payload.\n\t\t * @param {{ defaultPrevented: boolean, preventDefault: () => void }} ev Synthetic event.\n\t\t * @return {void}\n\t\t */\n\t\tonActionsRequestFeature(payload, ev) {\n\t\t\t/**\n\t\t\t * @event request-feature User clicked the Request a feature\n\t\t\t * item. Payload: `{ widgetId, title }`. Handlers may call the\n\t\t\t * second arg's `preventDefault()` to suppress the built-in\n\t\t\t * default (opening the forge's feature-request issue form).\n\t\t\t * @type {{ widgetId: string, title: string }}\n\t\t\t */\n\t\t\tthis.$emit('request-feature', payload, ev)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-widget-wrapper {\n\tposition: relative;\n\theight: 100%;\n\tdisplay: flex;\n\tflex-direction: column;\n\tbackground: var(--color-main-background);\n\tborder: 1px solid var(--color-border);\n\t/* NC design system: every content card is rounded — same token as\n\t CnDetailCard so detail pages read as ONE card family (ADR-062). */\n\tborder-radius: var(--border-radius-large, 8px);\n\toverflow: hidden;\n}\n\n/* Headerless date chip — floated over the top-right of a flush card. */\n.cn-widget-wrapper__floating-meta {\n\tposition: absolute;\n\ttop: 8px;\n\tinset-inline-end: 10px;\n\tz-index: 2;\n}\n\n.cn-widget-wrapper__content {\n\tflex: 1;\n\toverflow: auto;\n\tmin-height: 0;\n\tpadding: 16px;\n}\n\n/* A TABLE GOES EDGE TO EDGE. ALWAYS.\n A table drawn inside 16px of padding renders a border inside a border — two\n rectangles a few pixels apart, which reads as a mistake because it is one.\n The wrapper already has a `flush` prop that zeroes this padding, but a prop\n is something an app has to remember on every table it ever adds, and the\n apps did not: the padding was visible in pipelinq's table widgets.\n So it is structural instead. `:has()` keys on the table component's own root\n class, which means a table is flush by construction and no caller can\n reintroduce the gap by forgetting a prop. */\n.cn-widget-wrapper__content:has(> .cn-table-container),\n.cn-widget-wrapper__content:has(> .cn-widget-object-table) {\n\tpadding: 0;\n}\n\n/* Keyboard focus ring for the now-focusable scrollable content region. */\n.cn-widget-wrapper__content:focus-visible {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: -2px;\n}\n\n.cn-widget-wrapper--borderless {\n\tborder: none;\n\tbackground: transparent;\n}\n\n.cn-widget-wrapper--borderless .cn-widget-wrapper__content {\n\tpadding: 0;\n}\n\n.cn-widget-wrapper--flush .cn-widget-wrapper__content {\n\tpadding: 0;\n}\n\n/* A form or a search list keeps its padding, the reverse of the table rule\n above. A dashboard renders every widget `flush`, and without this their\n fields touch the card's edges. Borderless and chromeless cards, and the\n Nextcloud dashboard variant, keep their own padding. */\n.cn-widget-wrapper--flush:not(.cn-widget-wrapper--borderless, .cn-widget-wrapper--nc-dashboard) > .cn-widget-wrapper__content:has(> .cn-form-widget),\n.cn-widget-wrapper--flush:not(.cn-widget-wrapper--borderless, .cn-widget-wrapper--nc-dashboard) > .cn-widget-wrapper__content:has(> .cn-kb-search-widget) {\n\tpadding: 16px;\n}\n\n/* `chromeless` — border and background are already gone through borderless,\n and the content padding through flush. What is left on the root is the\n corner radius, which still clips: with `overflow: hidden` above, a child\n that paints its own background (a table header) gets rounded corners inside\n a square panel, which is the last visible piece of a card that is supposed\n to be absent. `CnDetailCard.cn-detail-card--chromeless` zeroes the same\n thing for the same reason. */\n.cn-widget-wrapper--chromeless {\n\tborder-radius: 0;\n}\n\n/*\n * `chrome=\"nc-dashboard\"` — reproduce the native Nextcloud Dashboard panel\n * (apps/dashboard) exactly, using the same design tokens so an un-customised\n * widget is pixel-identical to a core dashboard panel. `styleConfig` inline\n * overrides (wrapperStyles / headerStyles) still win over these class rules,\n * so users can override any token. Combined selector raises specificity above\n * the base `.cn-widget-wrapper` rules it supersedes.\n */\n.cn-widget-wrapper__header {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n\t/* The header is `space-between`, so without an explicit gap a right-hand\n\t title-icon sits flush against whatever precedes it. The left-hand icon's\n\t spacing comes from header-left's own gap. */\n\tgap: 8px;\n\tpadding: 12px 16px;\n\tborder-bottom: 1px solid var(--color-border);\n\tflex-shrink: 0;\n}\n\n/* Actions with no title beside them. `header-left` is not rendered at all in\n this mode, and the header is `space-between`, so a single child would sit\n flush LEFT where every other surface puts its actions on the right. */\n.cn-widget-wrapper__header--actions-only {\n\tjustify-content: flex-end;\n}\n\n/* `chromeless` — the surface around this widget already drew the card, so\n nothing here may draw a second one. Border, background and content padding\n are already gone (chromeless implies borderless + flush); what is left is\n the header, which still renders when there are controls to hold. Its bottom\n rule is the card's own divider and reads as a second card edge inside the\n panel, and its 16px side inset leaves those controls hanging off the grid\n that now starts at the panel's edge. Both go.\n\n Placed AFTER the rules it overrides, which is what stylelint's\n no-descending-specificity asks for. The cascade did not need it (this\n selector is more specific either way), a reader does: an override written\n above the thing it overrides has to be read twice. */\n.cn-widget-wrapper--chromeless .cn-widget-wrapper__header {\n\tborder-bottom: none;\n\tpadding-inline: 0;\n}\n\n.cn-widget-wrapper__header-left {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\tmin-width: 0;\n\t/* Grow so the title hugs the (optional) title-icon on the left instead\n\t of centering between icon and actions (header is space-between). */\n\tflex: 1 1 auto;\n}\n\n.cn-widget-wrapper__icon {\n\twidth: 24px;\n\theight: 24px;\n\tflex-shrink: 0;\n\tcolor: var(--cn-widget-icon-color, var(--color-primary-element));\n}\n\n.cn-widget-wrapper__title {\n\tfont-weight: 600;\n\tfont-size: 14px;\n\tmargin: 0;\n\twhite-space: nowrap;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n}\n\n.cn-widget-wrapper.cn-widget-wrapper--nc-dashboard {\n\tbackground: var(--color-main-background-blur, var(--color-main-background));\n\t-webkit-backdrop-filter: var(--filter-background-blur, none);\n\tbackdrop-filter: var(--filter-background-blur, none);\n\tborder: none;\n\tborder-radius: var(--border-radius-container-large, 16px);\n}\n\n.cn-widget-wrapper--nc-dashboard .cn-widget-wrapper__header {\n\tpadding: 16px;\n\tborder-bottom: none;\n}\n\n.cn-widget-wrapper--nc-dashboard .cn-widget-wrapper__header-left {\n\tgap: 16px;\n}\n\n.cn-widget-wrapper--nc-dashboard .cn-widget-wrapper__icon {\n\twidth: 32px;\n\theight: 32px;\n\tbackground-size: 32px;\n}\n\n.cn-widget-wrapper--nc-dashboard .cn-widget-wrapper__title {\n\tfont-size: 20px;\n\tfont-weight: 700;\n\tline-height: 24px;\n}\n\n.cn-widget-wrapper--nc-dashboard .cn-widget-wrapper__content {\n\tpadding: 0 16px 16px;\n}\n\n.cn-widget-wrapper__title-meta {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 4px;\n\tfont-size: 12px;\n\tcolor: var(--color-text-maxcontrast);\n\twhite-space: nowrap;\n\tflex-shrink: 0;\n}\n\n.cn-widget-wrapper__actions {\n\tdisplay: flex;\n\tgap: 4px;\n\tflex-shrink: 0;\n}\n\n/* The header's text link (`headerLink`). Theme hooks: --cn-widget-header-link-size\n and --cn-widget-header-link-weight. */\n.cn-widget-wrapper__header-link {\n\talign-self: center;\n\tpadding: 0 calc(1 * var(--default-grid-baseline));\n\tcolor: var(--color-primary-element);\n\tfont-size: var(--cn-widget-header-link-size, 14px);\n\tfont-weight: var(--cn-widget-header-link-weight, 600);\n\ttext-decoration: none;\n\twhite-space: nowrap;\n}\n\n.cn-widget-wrapper__header-link:hover,\n.cn-widget-wrapper__header-link:focus-visible {\n\ttext-decoration: underline;\n}\n\n.cn-widget-wrapper__header-link:focus-visible {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 2px;\n\tborder-radius: var(--border-radius-small, 4px);\n}\n\n/* Every widget's header icon is coloured — `--cn-widget-icon-color` is set on\n the header from titleIconStyle, and defaults to the theme colour if a\n wrapper is ever rendered without it. Material-design-icon components paint\n with `fill: currentColor`, so this reaches them; the :deep rule below makes\n it reach a raw <svg> in the slot too. */\n.cn-widget-wrapper__title-icon {\n\tdisplay: flex;\n\talign-items: center;\n\tflex-shrink: 0;\n\t/* COLOURED BY DEFAULT. `--cn-widget-icon-color` is set on the header from\n\t titleIconStyle; the theme colour is the fallback for a wrapper rendered\n\t without it. Before this, the prop defaulted to null and the icon inherited\n\t body text colour, which is why every widget header looked the same. */\n\tcolor: var(--cn-widget-icon-color, var(--color-primary-element));\n\n\t/* SPACE BETWEEN THE ICON AND THE TITLE.\n\t This element is a sibling of `__header-left` inside a space-between\n\t header, so it never received that element's `gap: 8px` — the icon sat\n\t flush against the title text. Matching the header-left gap rather than\n\t inventing a value keeps the two icon positions spaced identically. */\n\tmargin-inline-end: 8px;\n}\n\n/* Right-positioned title icons sit AFTER the actions, so the margin belongs on\n the other side or it pushes the icon off the header's right edge. */\n.cn-widget-wrapper__title-icon:last-child {\n\tmargin-inline-end: 0;\n\tmargin-inline-start: 8px;\n}\n\n/* Material-design-icon components paint with `fill: currentColor`, so the\n colour above reaches them; this makes it reach a raw <svg> in the slot too. */\n.cn-widget-wrapper__title-icon :deep(svg) {\n\tfill: currentcolor;\n}\n\n.cn-widget-wrapper__footer {\n\tdisplay: flex;\n\tjustify-content: flex-end;\n\tgap: 8px;\n\tpadding: 8px 16px;\n\tborder-top: 1px solid var(--color-border);\n\tflex-shrink: 0;\n}\n\n.cn-widget-wrapper__footer-link {\n\tfont-size: 13px;\n\tcolor: var(--color-primary-element);\n\ttext-decoration: none;\n}\n\n.cn-widget-wrapper__footer-link:hover {\n\ttext-decoration: underline;\n}\n</style>\n"],"names":["CnActionsMenu","t"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { resolveComponent, openBlock, createElementBlock, normalizeClass, normalizeStyle, renderSlot, createCommentVNode, createElementVNode, toDisplayString, createBlock, createSlots, withCtx, Fragment, renderList } from 'vue';
1
+ import { resolveComponent, openBlock, createElementBlock, normalizeClass, normalizeStyle, renderSlot, createCommentVNode, createElementVNode, toDisplayString, createBlock, resolveDynamicComponent, mergeProps, withCtx, createTextVNode, createSlots, Fragment, renderList } from 'vue';
2
2
 
3
3
  const _hoisted_1 = ["data-cn-high-contrast"];
4
4
  const _hoisted_2 = {
@@ -87,10 +87,22 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
87
87
  : createCommentVNode("v-if", true),
88
88
  ($options.actionsAreVisible())
89
89
  ? (openBlock(), createElementBlock("div", _hoisted_7, [
90
+ ($options.headerLinkTag)
91
+ ? (openBlock(), createBlock(resolveDynamicComponent($options.headerLinkTag), mergeProps({
92
+ key: 0,
93
+ class: "cn-widget-wrapper__header-link",
94
+ "data-testid": "cn-widget-wrapper-header-link"
95
+ }, $options.headerLinkAttrs), {
96
+ default: withCtx(() => [
97
+ createTextVNode(toDisplayString($options.headerLinkLabel), 1 /* TEXT */)
98
+ ]),
99
+ _: 1 /* STABLE */
100
+ }, 16 /* FULL_PROPS */))
101
+ : createCommentVNode("v-if", true),
90
102
  renderSlot(_ctx.$slots, "actions"),
91
103
  ($props.showActions)
92
104
  ? (openBlock(), createBlock(_component_CnActionsMenu, {
93
- key: 0,
105
+ key: 1,
94
106
  showRefresh: $options.effectiveShowRefresh,
95
107
  showRequestFeature: $options.effectiveShowRequestFeature,
96
108
  showReportBug: $props.showReportBug,