@conduction/nextcloud-vue 2.56.0-beta.7 → 2.56.0-beta.9

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 (87) hide show
  1. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +169 -1
  2. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
  3. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
  4. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +88 -4
  5. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
  6. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js +38 -25
  7. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
  8. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue2.js +21 -1
  9. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue2.js.map +1 -1
  10. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue3.js +4 -1
  11. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue3.js.map +1 -1
  12. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +40 -8
  13. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  14. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  15. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +40 -15
  16. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  17. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +2 -2
  18. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  19. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  20. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +6 -5
  21. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  22. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js +3 -1
  23. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js.map +1 -1
  24. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js.map +1 -1
  25. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue.js +10 -0
  26. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue.js.map +1 -0
  27. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue2.js +316 -0
  28. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue2.js.map +1 -0
  29. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue3.js +105 -0
  30. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue3.js.map +1 -0
  31. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +53 -0
  32. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  33. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +22 -5
  34. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  35. package/dist/esm/components/CnPageRenderer/pageTypes.js +1 -0
  36. package/dist/esm/components/CnPageRenderer/pageTypes.js.map +1 -1
  37. package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js.map +1 -1
  38. package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js.map +1 -1
  39. package/dist/esm/components/CnStorePage/CnStorePage.vue2.js.map +1 -1
  40. package/dist/esm/components/CnStorePage/CnStorePage.vue3.js.map +1 -1
  41. package/dist/esm/components/CnWikiPage/CnWikiPage.vue2.js.map +1 -1
  42. package/dist/esm/components/CnWikiPage/CnWikiPage.vue3.js.map +1 -1
  43. package/dist/esm/index.js +6 -0
  44. package/dist/esm/index.js.map +1 -1
  45. package/dist/esm/l10n/en.json.js +6 -0
  46. package/dist/esm/l10n/en.json.js.map +1 -1
  47. package/dist/esm/l10n/nl.json.js +6 -0
  48. package/dist/esm/l10n/nl.json.js.map +1 -1
  49. package/dist/esm/nextcloud-vue.css +198 -9
  50. package/dist/esm/utils/headers.js +28 -2
  51. package/dist/esm/utils/headers.js.map +1 -1
  52. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  53. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  54. package/dist/nextcloud-vue.cjs.js +9162 -8283
  55. package/dist/nextcloud-vue.cjs.js.map +2 -2
  56. package/dist/nextcloud-vue.css +198 -9
  57. package/l10n/en.json +6 -0
  58. package/l10n/nl.json +6 -0
  59. package/package.json +3 -2
  60. package/scripts/.jsdoc-baselines.json +1 -0
  61. package/scripts/manifest-schema-hash.js +41 -0
  62. package/scripts/record-manifest-schema-hash.js +52 -0
  63. package/src/components/CnAppNav/CnAppNav.vue +169 -1
  64. package/src/components/CnBannerWidget/CnBannerWidget.vue +122 -4
  65. package/src/components/CnBrandStripe/CnBrandStripe.md +16 -0
  66. package/src/components/CnBrandStripe/CnBrandStripe.vue +58 -5
  67. package/src/components/CnDashboardPage/CnDashboardPage.vue +40 -8
  68. package/src/components/CnDataTable/CnDataTable.md +1 -1
  69. package/src/components/CnDataTable/CnDataTable.vue +42 -17
  70. package/src/components/CnDetailPage/CnDetailPage.vue +4 -1
  71. package/src/components/CnHeaderWidget/CnHeaderWidget.vue +3 -1
  72. package/src/components/CnLinkCardsPage/CnLinkCardsPage.vue +517 -0
  73. package/src/components/CnLinkCardsPage/index.js +4 -0
  74. package/src/components/CnObjectSidebar/CnFilesTab.vue +65 -0
  75. package/src/components/CnPageRenderer/pageTypes.js +1 -0
  76. package/src/components/CnReportsPage/CnReportsPage.vue +7 -0
  77. package/src/components/CnStorePage/CnStorePage.vue +7 -0
  78. package/src/components/CnWikiPage/CnWikiPage.vue +7 -0
  79. package/src/components/index.js +1 -0
  80. package/src/css/detail-page.css +10 -2
  81. package/src/css/table.css +1 -2
  82. package/src/index.js +7 -0
  83. package/src/public/index.js +5 -0
  84. package/src/schemas/app-manifest-v2.schema.json +45 -3
  85. package/src/types/manifest.d.ts +1 -1
  86. package/src/utils/headers.js +28 -2
  87. package/src/utils/validateManifestV2.compiled.js +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"CnBannerWidget.vue2.js","sources":["../../../../src/components/CnBannerWidget/CnBannerWidget.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<section\n\t\tv-if=\"visible && isAttention\"\n\t\tclass=\"cn-banner-widget cn-banner-widget--attention\"\n\t\t:class=\"'cn-banner-widget--' + resolvedVariant\"\n\t\t:aria-labelledby=\"headingId\"\n\t\tdata-testid=\"cn-banner-widget-attention\">\n\t\t<div class=\"cn-banner-widget__body\">\n\t\t\t<p v-if=\"resolvedKicker\" class=\"cn-banner-widget__kicker\">\n\t\t\t\t{{ resolvedKicker }}\n\t\t\t</p>\n\t\t\t<h3 :id=\"headingId\" class=\"cn-banner-widget__title\">\n\t\t\t\t{{ displayTitle }}\n\t\t\t</h3>\n\t\t\t<p v-if=\"displayReason\" class=\"cn-banner-widget__reason\">\n\t\t\t\t{{ displayReason }}\n\t\t\t</p>\n\t\t</div>\n\t\t<div v-if=\"resolvedActions.length > 0\" class=\"cn-banner-widget__actions\">\n\t\t\t<component\n\t\t\t\t:is=\"action.href ? 'a' : 'button'\"\n\t\t\t\tv-for=\"action in resolvedActions\"\n\t\t\t\t:key=\"action.key\"\n\t\t\t\tclass=\"cn-banner-widget__action\"\n\t\t\t\t:class=\"action.primary ? 'cn-banner-widget__action--primary' : 'cn-banner-widget__action--secondary'\"\n\t\t\t\t:href=\"action.href || null\"\n\t\t\t\t:type=\"action.href ? null : 'button'\"\n\t\t\t\tdata-testid=\"cn-banner-widget-action\"\n\t\t\t\t@click=\"onActionClick($event, action)\">\n\t\t\t\t{{ action.label }}\n\t\t\t</component>\n\t\t</div>\n\t</section>\n\t<div v-else-if=\"visible\" class=\"cn-banner-widget\">\n\t\t<NcNoteCard :type=\"resolvedVariant\" class=\"cn-banner-widget__card\">\n\t\t\t<component\n\t\t\t\t:is=\"clickable ? 'a' : 'span'\"\n\t\t\t\tclass=\"cn-banner-widget__text\"\n\t\t\t\t:class=\"{ 'cn-banner-widget__text--clickable': clickable }\"\n\t\t\t\t:href=\"clickable ? routeHref : null\"\n\t\t\t\tdata-testid=\"cn-banner-widget-text\"\n\t\t\t\t@click=\"onClick\">\n\t\t\t\t{{ displayText }}\n\t\t\t</component>\n\t\t</NcNoteCard>\n\t</div>\n</template>\n\n<script>\nimport { NcNoteCard } from '@nextcloud/vue'\nimport { followLinkClick, resolveHref } from '../../utils/linkNavigation.js'\nimport { safeHref } from '../../utils/safeHref.js'\nimport { nextUid } from '../../utils/uid.js'\nimport { compareVisibleWhen, readVisibleWhenValue } from '../../utils/visibleWhen.js'\n\n/** Variants understood by NcNoteCard. */\nconst VARIANTS = ['info', 'warning', 'error', 'success']\n\n/** Layouts: the note card (default) or the attention card. */\nconst LAYOUTS = ['banner', 'attention']\n\n/** An attention card offers a primary and a secondary action, no more. */\nconst MAX_ACTIONS = 2\n\n/**\n * CnBannerWidget — declarative notice banner for dashboards and v2 pages\n * (`banner` widget type, Wave 1 of nextcloud-vue#91).\n *\n * Renders a themed NcNoteCard with a variant (`info | warning | error`),\n * a text, an optional click-through route, and an optional `visibleWhen`\n * condition: a simple `{ field, op, value }` predicate evaluated against\n * an endpoint response or an OpenRegister source (the doriath\n * migration-banner case — \"show while `pending > 0`\").\n *\n * Config arrives either as flat props (v2 grid spreads `props`) or as a\n * stored `content` blob (CnDashboardPage's registry branch) — explicit\n * flat props win on collision.\n *\n * ```json\n * {\n * \"widgetKey\": \"banner\",\n * \"props\": {\n * \"variant\": \"warning\",\n * \"text\": \"Migrations pending — open the migration overview.\",\n * \"visibleWhen\": { \"endpoint\": \"/apps/doriath/api/migrations/status\", \"field\": \"pending\", \"op\": \"gt\", \"value\": 0 },\n * \"route\": \"migrations\"\n * }\n * }\n * ```\n *\n * Fail-safe: with a `visibleWhen`, the banner stays HIDDEN until the\n * condition evaluates true — a failed fetch never breaks (or spams) a\n * dashboard.\n *\n * `layout: \"attention\"` turns the banner into an attention card: a card with\n * a coloured edge by severity, a `kicker` label, a `title`, a `reason` line\n * and up to two `actions` (the first is the primary one unless an action says\n * `primary` itself). `visibleWhen` works the same way. Without `layout` the\n * note card above renders, as it always did.\n *\n * ```json\n * {\n * \"widgetKey\": \"banner\",\n * \"props\": {\n * \"layout\": \"attention\",\n * \"variant\": \"error\",\n * \"kicker\": \"First today\",\n * \"title\": \"Parking permits city centre\",\n * \"reason\": \"The deadline ends today.\",\n * \"actions\": [\n * { \"label\": \"Open case\", \"route\": { \"name\": \"CaseDetail\", \"params\": { \"id\": \"2026-0061\" } } },\n * { \"label\": \"Suspend deadline\", \"route\": \"CaseSuspend\" }\n * ]\n * }\n * }\n * ```\n */\nexport default {\n\tname: 'CnBannerWidget',\n\n\tcomponents: { NcNoteCard },\n\n\tprops: {\n\t\t/**\n\t\t * Banner severity variant: `info | warning | error` (plus `success`\n\t\t * for completeness — NcNoteCard's set). Empty falls back to the\n\t\t * `content` blob, then `info`.\n\t\t */\n\t\tvariant: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t\tvalidator: (v) => v === '' || VARIANTS.includes(v),\n\t\t},\n\n\t\t/** Pre-translated banner text. Empty falls back to the `content` blob. */\n\t\ttext: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Optional visibility condition. Shape:\n\t\t * `{ endpoint?, source?, field?, op?, value }` — exactly one of\n\t\t * `endpoint` (a same-origin URL returning JSON) or `source`\n\t\t * (`{ register, schema, filter? }`, an OpenRegister object query whose\n\t\t * `filter` supports the shared @-token grammar). `field` is a\n\t\t * dot-path into the response (for a `source`, into the first result;\n\t\t * omit it — or use `@total` — to compare the collection total).\n\t\t * `op` is `eq | neq | gt | gte | lt | lte` (default `eq`); `value` is\n\t\t * the literal right-hand side. `null` (the default) shows the banner\n\t\t * unconditionally.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tvisibleWhen: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Optional click-through route: a vue-router route NAME (string) or\n\t\t * a full location object. When set, the banner text is a link to that\n\t\t * route. `null` renders static text.\n\t\t *\n\t\t * @type {string|object|null}\n\t\t */\n\t\troute: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * How the banner renders: `banner` (the note card, default) or\n\t\t * `attention` (a card with a severity edge, kicker, title, reason and\n\t\t * up to two actions). Empty falls back to the `content` blob, then\n\t\t * `banner`.\n\t\t *\n\t\t * @type {''|'banner'|'attention'}\n\t\t */\n\t\tlayout: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t\tvalidator: (v) => v === '' || LAYOUTS.includes(v),\n\t\t},\n\n\t\t/** Attention card: the small label above the title (\"First today\"). */\n\t\tkicker: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Attention card: the title. Falls back to `text` when empty. */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Attention card: the line under the title that says why this needs attention. `{value}` is replaced like in `text`. */\n\t\treason: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Attention card: at most two actions. Each is `{ label, route?,\n\t\t * href?, primary?, id? }`: `route` is a route name or location,\n\t\t * `href` an URL. An action with neither renders a button that emits\n\t\t * `action`. The first action is the primary one unless one sets\n\t\t * `primary: true`.\n\t\t *\n\t\t * @type {Array<{label: string, route?: (string|object), href?: string, primary?: boolean, id?: string}>|null}\n\t\t */\n\t\tactions: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Stored content blob (CnDashboardPage registry branch) carrying the\n\t\t * same keys as the flat props: `{ variant, text, visibleWhen, route,\n\t\t * layout, kicker, title, reason, actions }`. Explicit flat props win\n\t\t * on collision.\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 * Pre-evaluated `visibleWhen` outcome `{ met, value }`, injected by a\n\t\t * host that already ran the predicate (CnDashboardPage evaluates it\n\t\t * to know whether to collapse the banner's grid cell). When present,\n\t\t * the banner renders from this verdict instead of fetching again —\n\t\t * no duplicate request and no hidden-until-self-evaluated flash —\n\t\t * and `value` feeds the `{value}` text placeholder. `null` (the\n\t\t * default) keeps the banner self-evaluating.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tconditionOutcome: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\temits: [\n\t\t/**\n\t\t * Emitted when an attention-card action without a `route` or `href`\n\t\t * is clicked, so the host can run it.\n\t\t *\n\t\t * @event action\n\t\t * @type {{id: (string|undefined), label: string, index: number}}\n\t\t */\n\t\t'action',\n\t],\n\n\tdata() {\n\t\treturn {\n\t\t\t/** Id that ties the attention card's region to its title. */\n\t\t\theadingId: `cn-banner-title-${nextUid()}`,\n\t\t\t/** Evaluated visibleWhen outcome (null = not yet evaluated). */\n\t\t\tconditionMet: null,\n\t\t\t/**\n\t\t\t * The raw field value the predicate read (from the injected\n\t\t\t * outcome or the banner's own evaluation) — interpolated into\n\t\t\t * the text wherever it says `{value}`.\n\t\t\t */\n\t\t\tconditionValue: null,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** The effective variant (prop → content → 'info'). */\n\t\tresolvedVariant() {\n\t\t\tconst v = this.variant || (this.content && this.content.variant) || 'info'\n\t\t\treturn VARIANTS.includes(v) ? v : 'info'\n\t\t},\n\n\t\t/** The effective banner text (prop → content → ''). */\n\t\tresolvedText() {\n\t\t\treturn this.text || (this.content && this.content.text) || ''\n\t\t},\n\n\t\t/** The effective visibleWhen condition (prop → content → null). */\n\t\tresolvedVisibleWhen() {\n\t\t\treturn this.visibleWhen || (this.content && this.content.visibleWhen) || null\n\t\t},\n\n\t\t/** The effective injected outcome (prop → content → null). */\n\t\tresolvedConditionOutcome() {\n\t\t\treturn this.conditionOutcome || (this.content && this.content.conditionOutcome) || null\n\t\t},\n\n\t\t/**\n\t\t * The rendered text: `resolvedText` with `{value}` replaced by the\n\t\t * predicate's field value once one is known. Without a known value\n\t\t * the text renders as written — a `{value}` placeholder only makes\n\t\t * sense on a conditional banner, which stays hidden until its\n\t\t * predicate (and therefore its value) resolves.\n\t\t *\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card\n\t\t */\n\t\tdisplayText() {\n\t\t\treturn this.fillValue(this.resolvedText)\n\t\t},\n\n\t\t/** The effective click-through route (prop → content → null). */\n\t\tresolvedRoute() {\n\t\t\treturn this.route || (this.content && this.content.route) || null\n\t\t},\n\n\t\t/**\n\t\t * Whether the attention card renders instead of the note card.\n\t\t *\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card\n\t\t */\n\t\tisAttention() {\n\t\t\treturn (this.layout || (this.content && this.content.layout) || 'banner') === 'attention'\n\t\t},\n\n\t\t/**\n\t\t * The attention card's kicker (prop, else content).\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card\n\t\t */\n\t\tresolvedKicker() {\n\t\t\tconst kicker = this.kicker || (this.content && this.content.kicker) || ''\n\t\t\treturn kicker\n\t\t},\n\n\t\t/**\n\t\t * The attention card's title: `title`, else `text`, with `{value}` filled in.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card\n\t\t */\n\t\tdisplayTitle() {\n\t\t\tconst title = this.title || (this.content && this.content.title) || ''\n\t\t\treturn title ? this.fillValue(title) : this.displayText\n\t\t},\n\n\t\t/**\n\t\t * The attention card's reason line. When the title already used\n\t\t * `text`, the text is not repeated here.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card\n\t\t */\n\t\tdisplayReason() {\n\t\t\tconst reason = this.reason || (this.content && this.content.reason) || ''\n\t\t\tif (reason) {\n\t\t\t\treturn this.fillValue(reason)\n\t\t\t}\n\t\t\tconst title = this.title || (this.content && this.content.title) || ''\n\t\t\treturn title ? this.displayText : ''\n\t\t},\n\n\t\t/**\n\t\t * The attention card's actions: the first two that have a label, each\n\t\t * with its href and whether it is the primary one.\n\t\t *\n\t\t * @return {Array<{key: string, id: (string|undefined), label: string, href: string, target: (object|null), primary: boolean, index: number}>}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card\n\t\t */\n\t\tresolvedActions() {\n\t\t\tconst raw = this.actions || (this.content && this.content.actions) || []\n\t\t\tif (!Array.isArray(raw)) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\tconst usable = raw\n\t\t\t\t.filter((action) => action && typeof action === 'object' && typeof action.label === 'string' && action.label !== '')\n\t\t\t\t.slice(0, MAX_ACTIONS)\n\t\t\tconst declaredPrimary = usable.findIndex((action) => action.primary === true)\n\t\t\tconst primaryIndex = declaredPrimary === -1 ? 0 : declaredPrimary\n\t\t\treturn usable.map((action, index) => {\n\t\t\t\tlet target = null\n\t\t\t\tlet href = ''\n\t\t\t\tif (action.route) {\n\t\t\t\t\ttarget = typeof action.route === 'string' ? { name: action.route } : action.route\n\t\t\t\t\thref = resolveHref(target, this.$router)\n\t\t\t\t} else if (typeof action.href === 'string' && action.href !== '') {\n\t\t\t\t\tconst safe = safeHref(action.href)\n\t\t\t\t\thref = safe === '#' ? '' : safe\n\t\t\t\t}\n\t\t\t\treturn {\n\t\t\t\t\tkey: `${index}-${action.id || action.label}`,\n\t\t\t\t\tid: action.id,\n\t\t\t\t\tlabel: action.label,\n\t\t\t\t\thref,\n\t\t\t\t\ttarget,\n\t\t\t\t\tprimary: index === primaryIndex,\n\t\t\t\t\tindex,\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Whether the banner renders: no condition = always; else the evaluated outcome.\n\t\t *\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card\n\t\t */\n\t\tvisible() {\n\t\t\tconst title = this.title || (this.content && this.content.title) || ''\n\t\t\tif (this.resolvedText === '' && !(this.isAttention && title !== '')) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif (!this.resolvedVisibleWhen) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn this.conditionMet === true\n\t\t},\n\n\t\t/** Whether the banner navigates on click (route set + router present). */\n\t\tclickable() {\n\t\t\treturn !!this.resolvedRoute && !!this.$router\n\t\t},\n\n\t\t/** The click-through route as a router location (a string is a route name). */\n\t\trouteLocation() {\n\t\t\tconst route = this.resolvedRoute\n\t\t\treturn typeof route === 'string' ? { name: route } : route\n\t\t},\n\n\t\t/** The href of the click-through link. */\n\t\trouteHref() {\n\t\t\treturn resolveHref(this.routeLocation, this.$router)\n\t\t},\n\t},\n\n\twatch: {\n\t\tresolvedVisibleWhen: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.evaluateCondition()\n\t\t\t},\n\t\t},\n\n\t\tresolvedConditionOutcome() {\n\t\t\tthis.evaluateCondition()\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Replace `{value}` in a text by the value the predicate read, once\n\t\t * one is known.\n\t\t *\n\t\t * @param {string} text The text.\n\t\t * @return {string} The text with the value filled in.\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card\n\t\t */\n\t\tfillValue(text) {\n\t\t\tif (this.conditionValue === null || !text.includes('{value}')) {\n\t\t\t\treturn text\n\t\t\t}\n\t\t\treturn text.replaceAll('{value}', String(this.conditionValue))\n\t\t},\n\n\t\t/**\n\t\t * Run an attention-card action: route an in-app link, leave an\n\t\t * external link to the browser, and emit `action` for a button.\n\t\t *\n\t\t * @param {MouseEvent} event The click.\n\t\t * @param {object} action The resolved action.\n\t\t * @return {void}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card\n\t\t */\n\t\tonActionClick(event, action) {\n\t\t\tif (action.target) {\n\t\t\t\tfollowLinkClick(event, action.target, this.$router)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!action.href) {\n\t\t\t\tthis.$emit('action', { id: action.id, label: action.label, index: action.index })\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Resolve the `visibleWhen` verdict. A host-injected\n\t\t * `conditionOutcome` wins outright — the host already ran the\n\t\t * predicate, so re-fetching would double the request and flash the\n\t\t * banner hidden until its own copy resolved. Otherwise evaluate\n\t\t * through the shared util primitives (endpoint / OpenRegister-source\n\t\t * modes — the Wave-1 banner shape is the canonical one, extracted to\n\t\t * `utils/visibleWhen.js` in Wave 3 so manifest actions reuse it),\n\t\t * keeping the read VALUE for the `{value}` text placeholder.\n\t\t * Fail-safe: any fetch/shape error leaves the banner hidden.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync evaluateCondition() {\n\t\t\tconst outcome = this.resolvedConditionOutcome\n\t\t\tif (outcome) {\n\t\t\t\tthis.conditionMet = outcome.met === true\n\t\t\t\tthis.conditionValue = outcome.value !== undefined ? outcome.value : null\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst cond = this.resolvedVisibleWhen\n\t\t\tif (!cond) {\n\t\t\t\tthis.conditionMet = null\n\t\t\t\tthis.conditionValue = null\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst value = await readVisibleWhenValue(cond)\n\t\t\t\tthis.conditionValue = value\n\t\t\t\tthis.conditionMet = compareVisibleWhen(value, cond.op || 'eq', cond.value)\n\t\t\t} catch {\n\t\t\t\tthis.conditionMet = false\n\t\t\t\tthis.conditionValue = null\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Route a plain click on the link; a modified click opens the href\n\t\t * in the browser.\n\t\t *\n\t\t * @param {MouseEvent} event The click event.\n\t\t * @return {void}\n\t\t */\n\t\tonClick(event) {\n\t\t\tif (!this.clickable) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfollowLinkClick(event, this.routeLocation, this.$router)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-banner-widget {\n\twidth: 100%;\n}\n\n.cn-banner-widget__card {\n\tmargin: 0;\n}\n\n.cn-banner-widget__text--clickable {\n\tcursor: pointer;\n\ttext-decoration: underline;\n\tcolor: inherit;\n}\n\n/* Attention card. The severity edge is an inset shadow, so it takes no width. */\n.cn-banner-widget--attention {\n\t--cn-banner-accent: var(--color-primary-element);\n\t--cn-banner-accent-text: var(--color-primary-element);\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 16px 24px;\n\tbox-sizing: border-box;\n\tpadding: 20px 24px 20px 28px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-container, var(--border-radius-large, 12px));\n\tbackground: var(--color-main-background);\n\tbox-shadow: inset 4px 0 0 0 var(--cn-banner-accent);\n}\n\n.cn-banner-widget--attention.cn-banner-widget--warning {\n\t--cn-banner-accent: var(--color-element-warning, var(--color-warning));\n\t--cn-banner-accent-text: var(--color-text-warning, var(--color-warning-text, var(--color-main-text)));\n}\n\n.cn-banner-widget--attention.cn-banner-widget--error {\n\t--cn-banner-accent: var(--color-element-error, var(--color-error));\n\t--cn-banner-accent-text: var(--color-text-error, var(--color-error-text));\n}\n\n.cn-banner-widget--attention.cn-banner-widget--success {\n\t--cn-banner-accent: var(--color-element-success, var(--color-success));\n\t--cn-banner-accent-text: var(--color-text-success, var(--color-success-text));\n}\n\n.cn-banner-widget__body {\n\tdisplay: flex;\n\tflex: 1 1 320px;\n\tflex-direction: column;\n\tgap: 6px;\n\tmin-width: 0;\n}\n\n.cn-banner-widget__kicker {\n\tmargin: 0;\n\tfont-size: 0.85em;\n\tfont-weight: 700;\n\tletter-spacing: 0.06em;\n\ttext-transform: uppercase;\n\tcolor: var(--cn-banner-accent-text);\n}\n\n.cn-banner-widget__title {\n\tmargin: 0;\n\tfont-size: 1.3em;\n\tfont-weight: 700;\n\tline-height: 1.3;\n\tcolor: var(--color-main-text);\n}\n\n.cn-banner-widget__reason {\n\tmargin: 0;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-banner-widget__actions {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 10px;\n}\n\n.cn-banner-widget__action {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tjustify-content: center;\n\tbox-sizing: border-box;\n\tmin-height: var(--default-clickable-area, 40px);\n\tmargin: 0;\n\tpadding: 0 16px;\n\tborder: 1px solid var(--color-border-maxcontrast);\n\tborder-radius: var(--border-radius-element, var(--border-radius-large, 8px));\n\tfont: inherit;\n\tfont-weight: 600;\n\ttext-decoration: none;\n\tcursor: pointer;\n}\n\n.cn-banner-widget__action--secondary {\n\tbackground: var(--color-main-background);\n\tcolor: var(--color-main-text);\n}\n\n.cn-banner-widget__action--secondary:hover {\n\tbackground: var(--color-background-hover);\n}\n\n.cn-banner-widget__action--primary {\n\tborder-color: var(--color-primary-element);\n\tbackground: var(--color-primary-element);\n\tcolor: var(--color-primary-element-text);\n}\n\n.cn-banner-widget__action--primary:hover {\n\tborder-color: var(--color-primary-element-hover);\n\tbackground: var(--color-primary-element-hover);\n}\n\n.cn-banner-widget__action:focus-visible {\n\toutline: 2px solid var(--color-main-text);\n\toutline-offset: 2px;\n}\n</style>\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"CnBannerWidget.vue2.js","sources":["../../../../src/components/CnBannerWidget/CnBannerWidget.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<section\n\t\tv-if=\"visible && isAttention\"\n\t\tclass=\"cn-banner-widget cn-banner-widget--attention\"\n\t\t:class=\"'cn-banner-widget--' + resolvedVariant\"\n\t\t:aria-labelledby=\"headingId\"\n\t\tdata-testid=\"cn-banner-widget-attention\">\n\t\t<div class=\"cn-banner-widget__body\">\n\t\t\t<p v-if=\"resolvedKicker\" class=\"cn-banner-widget__kicker\">\n\t\t\t\t{{ resolvedKicker }}\n\t\t\t</p>\n\t\t\t<h3 :id=\"headingId\" class=\"cn-banner-widget__title\">\n\t\t\t\t{{ displayTitle }}\n\t\t\t</h3>\n\t\t\t<p v-if=\"displayReason\" class=\"cn-banner-widget__reason\">\n\t\t\t\t{{ displayReason }}\n\t\t\t</p>\n\t\t</div>\n\t\t<div v-if=\"resolvedActions.length > 0\" class=\"cn-banner-widget__actions\">\n\t\t\t<component\n\t\t\t\t:is=\"action.href ? 'a' : 'button'\"\n\t\t\t\tv-for=\"action in resolvedActions\"\n\t\t\t\t:key=\"action.key\"\n\t\t\t\tclass=\"cn-banner-widget__action\"\n\t\t\t\t:class=\"action.primary ? 'cn-banner-widget__action--primary' : 'cn-banner-widget__action--secondary'\"\n\t\t\t\t:href=\"action.href || null\"\n\t\t\t\t:type=\"action.href ? null : 'button'\"\n\t\t\t\tdata-testid=\"cn-banner-widget-action\"\n\t\t\t\t@click=\"onActionClick($event, action)\">\n\t\t\t\t{{ action.label }}\n\t\t\t</component>\n\t\t</div>\n\t</section>\n\t<!-- The count could not be read. One quiet line, not the card and not an\n\t alarm: hidden would read as \"nothing to report\", which is not known. -->\n\t<p\n\t\tv-else-if=\"checkFailed\"\n\t\tclass=\"cn-banner-widget cn-banner-widget--unchecked\"\n\t\t:title=\"failureTooltip\"\n\t\tdata-testid=\"cn-banner-widget-unchecked\">\n\t\t<span class=\"cn-banner-widget__unchecked-text\">{{ uncheckedText }}</span>\n\t\t<span class=\"cn-banner-widget__unchecked-reason\">{{ failureTooltip }}</span>\n\t</p>\n\t<div v-else-if=\"visible\" class=\"cn-banner-widget\">\n\t\t<NcNoteCard :type=\"resolvedVariant\" class=\"cn-banner-widget__card\">\n\t\t\t<component\n\t\t\t\t:is=\"clickable ? 'a' : 'span'\"\n\t\t\t\tclass=\"cn-banner-widget__text\"\n\t\t\t\t:class=\"{ 'cn-banner-widget__text--clickable': clickable }\"\n\t\t\t\t:href=\"clickable ? routeHref : null\"\n\t\t\t\tdata-testid=\"cn-banner-widget-text\"\n\t\t\t\t@click=\"onClick\">\n\t\t\t\t{{ displayText }}\n\t\t\t</component>\n\t\t</NcNoteCard>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcNoteCard } from '@nextcloud/vue'\nimport { followLinkClick, resolveHref } from '../../utils/linkNavigation.js'\nimport { safeHref } from '../../utils/safeHref.js'\nimport { nextUid } from '../../utils/uid.js'\nimport { compareVisibleWhen, readVisibleWhenValue } from '../../utils/visibleWhen.js'\n\n/** Variants understood by NcNoteCard. */\nconst VARIANTS = ['info', 'warning', 'error', 'success']\n\n/** Layouts: the note card (default) or the attention card. */\nconst LAYOUTS = ['banner', 'attention']\n\n/** An attention card offers a primary and a secondary action, no more. */\nconst MAX_ACTIONS = 2\n\n/**\n * CnBannerWidget — declarative notice banner for dashboards and v2 pages\n * (`banner` widget type, Wave 1 of nextcloud-vue#91).\n *\n * Renders a themed NcNoteCard with a variant (`info | warning | error`),\n * a text, an optional click-through route, and an optional `visibleWhen`\n * condition: a simple `{ field, op, value }` predicate evaluated against\n * an endpoint response or an OpenRegister source (the doriath\n * migration-banner case — \"show while `pending > 0`\").\n *\n * Config arrives either as flat props (v2 grid spreads `props`) or as a\n * stored `content` blob (CnDashboardPage's registry branch) — explicit\n * flat props win on collision.\n *\n * ```json\n * {\n * \"widgetKey\": \"banner\",\n * \"props\": {\n * \"variant\": \"warning\",\n * \"text\": \"Migrations pending — open the migration overview.\",\n * \"visibleWhen\": { \"endpoint\": \"/apps/doriath/api/migrations/status\", \"field\": \"pending\", \"op\": \"gt\", \"value\": 0 },\n * \"route\": \"migrations\"\n * }\n * }\n * ```\n *\n * Fail-safe: with a `visibleWhen`, the banner stays HIDDEN until the\n * condition evaluates true — a failed fetch never breaks (or spams) a\n * dashboard.\n *\n * An attention card (`layout: \"attention\"`) whose `visibleWhen` request\n * FAILS renders one quiet line instead, \"Could not check\", with the reason as\n * a tooltip. A hidden card reads as \"nothing needs attention\", and a failed\n * count does not know that. A request that succeeds with a value that does\n * not meet the condition still renders nothing.\n *\n * `layout: \"attention\"` turns the banner into an attention card: a card with\n * a coloured edge by severity, a `kicker` label, a `title`, a `reason` line\n * and up to two `actions` (the first is the primary one unless an action says\n * `primary` itself). `visibleWhen` works the same way. Without `layout` the\n * note card above renders, as it always did.\n *\n * ```json\n * {\n * \"widgetKey\": \"banner\",\n * \"props\": {\n * \"layout\": \"attention\",\n * \"variant\": \"error\",\n * \"kicker\": \"First today\",\n * \"title\": \"Parking permits city centre\",\n * \"reason\": \"The deadline ends today.\",\n * \"actions\": [\n * { \"label\": \"Open case\", \"route\": { \"name\": \"CaseDetail\", \"params\": { \"id\": \"2026-0061\" } } },\n * { \"label\": \"Suspend deadline\", \"route\": \"CaseSuspend\" }\n * ]\n * }\n * }\n * ```\n */\nexport default {\n\tname: 'CnBannerWidget',\n\n\tcomponents: { NcNoteCard },\n\n\tprops: {\n\t\t/**\n\t\t * Banner severity variant: `info | warning | error` (plus `success`\n\t\t * for completeness — NcNoteCard's set). Empty falls back to the\n\t\t * `content` blob, then `info`.\n\t\t */\n\t\tvariant: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t\tvalidator: (v) => v === '' || VARIANTS.includes(v),\n\t\t},\n\n\t\t/** Pre-translated banner text. Empty falls back to the `content` blob. */\n\t\ttext: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Optional visibility condition. Shape:\n\t\t * `{ endpoint?, source?, field?, op?, value }` — exactly one of\n\t\t * `endpoint` (a same-origin URL returning JSON) or `source`\n\t\t * (`{ register, schema, filter? }`, an OpenRegister object query whose\n\t\t * `filter` supports the shared @-token grammar). `field` is a\n\t\t * dot-path into the response (for a `source`, into the first result;\n\t\t * omit it — or use `@total` — to compare the collection total).\n\t\t * `op` is `eq | neq | gt | gte | lt | lte` (default `eq`); `value` is\n\t\t * the literal right-hand side. `null` (the default) shows the banner\n\t\t * unconditionally.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tvisibleWhen: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Optional click-through route: a vue-router route NAME (string) or\n\t\t * a full location object. When set, the banner text is a link to that\n\t\t * route. `null` renders static text.\n\t\t *\n\t\t * @type {string|object|null}\n\t\t */\n\t\troute: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * How the banner renders: `banner` (the note card, default) or\n\t\t * `attention` (a card with a severity edge, kicker, title, reason and\n\t\t * up to two actions). Empty falls back to the `content` blob, then\n\t\t * `banner`.\n\t\t *\n\t\t * @type {''|'banner'|'attention'}\n\t\t */\n\t\tlayout: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t\tvalidator: (v) => v === '' || LAYOUTS.includes(v),\n\t\t},\n\n\t\t/** Attention card: the small label above the title (\"First today\"). */\n\t\tkicker: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Attention card: the title. Falls back to `text` when empty. */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Attention card: the line under the title that says why this needs attention. `{value}` is replaced like in `text`. */\n\t\treason: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Attention card: at most two actions. Each is `{ label, route?,\n\t\t * href?, primary?, id? }`: `route` is a route name or location,\n\t\t * `href` an URL. An action with neither renders a button that emits\n\t\t * `action`. The first action is the primary one unless one sets\n\t\t * `primary: true`.\n\t\t *\n\t\t * @type {Array<{label: string, route?: (string|object), href?: string, primary?: boolean, id?: string}>|null}\n\t\t */\n\t\tactions: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Stored content blob (CnDashboardPage registry branch) carrying the\n\t\t * same keys as the flat props: `{ variant, text, visibleWhen, route,\n\t\t * layout, kicker, title, reason, actions }`. Explicit flat props win\n\t\t * on collision.\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 * Pre-evaluated `visibleWhen` outcome `{ met, value }`, injected by a\n\t\t * host that already ran the predicate (CnDashboardPage evaluates it\n\t\t * to know whether to collapse the banner's grid cell). When present,\n\t\t * the banner renders from this verdict instead of fetching again —\n\t\t * no duplicate request and no hidden-until-self-evaluated flash —\n\t\t * and `value` feeds the `{value}` text placeholder. `null` (the\n\t\t * default) keeps the banner self-evaluating. A host whose request\n\t\t * failed passes `{ met: false, value: null, failed: true, reason }`,\n\t\t * and an attention card then says it could not check.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tconditionOutcome: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\temits: [\n\t\t/**\n\t\t * Emitted when an attention-card action without a `route` or `href`\n\t\t * is clicked, so the host can run it.\n\t\t *\n\t\t * @event action\n\t\t * @type {{id: (string|undefined), label: string, index: number}}\n\t\t */\n\t\t'action',\n\t],\n\n\t/**\n\t * The banner's own state: the evaluated condition, the value it read,\n\t * and whether the request for it failed.\n\t *\n\t * @return {object} The state.\n\t * @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-an-attention-card-says-when-it-could-not-check\n\t */\n\tdata() {\n\t\treturn {\n\t\t\t/** Id that ties the attention card's region to its title. */\n\t\t\theadingId: `cn-banner-title-${nextUid()}`,\n\t\t\t/** Evaluated visibleWhen outcome (null = not yet evaluated). */\n\t\t\tconditionMet: null,\n\t\t\t/**\n\t\t\t * The raw field value the predicate read (from the injected\n\t\t\t * outcome or the banner's own evaluation) — interpolated into\n\t\t\t * the text wherever it says `{value}`.\n\t\t\t */\n\t\t\tconditionValue: null,\n\t\t\t/** Whether the visibleWhen request itself failed (not: answered \"no\"). */\n\t\t\tconditionFailed: false,\n\t\t\t/** Why it failed, as the request reported it. */\n\t\t\tconditionFailureReason: '',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** The effective variant (prop → content → 'info'). */\n\t\tresolvedVariant() {\n\t\t\tconst v = this.variant || (this.content && this.content.variant) || 'info'\n\t\t\treturn VARIANTS.includes(v) ? v : 'info'\n\t\t},\n\n\t\t/** The effective banner text (prop → content → ''). */\n\t\tresolvedText() {\n\t\t\treturn this.text || (this.content && this.content.text) || ''\n\t\t},\n\n\t\t/** The effective visibleWhen condition (prop → content → null). */\n\t\tresolvedVisibleWhen() {\n\t\t\treturn this.visibleWhen || (this.content && this.content.visibleWhen) || null\n\t\t},\n\n\t\t/** The effective injected outcome (prop → content → null). */\n\t\tresolvedConditionOutcome() {\n\t\t\treturn this.conditionOutcome || (this.content && this.content.conditionOutcome) || null\n\t\t},\n\n\t\t/**\n\t\t * The rendered text: `resolvedText` with `{value}` replaced by the\n\t\t * predicate's field value once one is known. Without a known value\n\t\t * the text renders as written — a `{value}` placeholder only makes\n\t\t * sense on a conditional banner, which stays hidden until its\n\t\t * predicate (and therefore its value) resolves.\n\t\t *\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card\n\t\t */\n\t\tdisplayText() {\n\t\t\treturn this.fillValue(this.resolvedText)\n\t\t},\n\n\t\t/** The effective click-through route (prop → content → null). */\n\t\tresolvedRoute() {\n\t\t\treturn this.route || (this.content && this.content.route) || null\n\t\t},\n\n\t\t/**\n\t\t * Whether the attention card renders instead of the note card.\n\t\t *\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card\n\t\t */\n\t\tisAttention() {\n\t\t\treturn (this.layout || (this.content && this.content.layout) || 'banner') === 'attention'\n\t\t},\n\n\t\t/**\n\t\t * The attention card's kicker (prop, else content).\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card\n\t\t */\n\t\tresolvedKicker() {\n\t\t\tconst kicker = this.kicker || (this.content && this.content.kicker) || ''\n\t\t\treturn kicker\n\t\t},\n\n\t\t/**\n\t\t * The attention card's title: `title`, else `text`, with `{value}` filled in.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card\n\t\t */\n\t\tdisplayTitle() {\n\t\t\tconst title = this.title || (this.content && this.content.title) || ''\n\t\t\treturn title ? this.fillValue(title) : this.displayText\n\t\t},\n\n\t\t/**\n\t\t * The attention card's reason line. When the title already used\n\t\t * `text`, the text is not repeated here.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card\n\t\t */\n\t\tdisplayReason() {\n\t\t\tconst reason = this.reason || (this.content && this.content.reason) || ''\n\t\t\tif (reason) {\n\t\t\t\treturn this.fillValue(reason)\n\t\t\t}\n\t\t\tconst title = this.title || (this.content && this.content.title) || ''\n\t\t\t// A card that sets `text` to the same words as its `title` says\n\t\t\t// them once.\n\t\t\treturn title && this.displayText !== this.displayTitle ? this.displayText : ''\n\t\t},\n\n\t\t/**\n\t\t * The attention card's actions: the first two that have a label, each\n\t\t * with its href and whether it is the primary one.\n\t\t *\n\t\t * @return {Array<{key: string, id: (string|undefined), label: string, href: string, target: (object|null), primary: boolean, index: number}>}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card\n\t\t */\n\t\tresolvedActions() {\n\t\t\tconst raw = this.actions || (this.content && this.content.actions) || []\n\t\t\tif (!Array.isArray(raw)) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\tconst usable = raw\n\t\t\t\t.filter((action) => action && typeof action === 'object' && typeof action.label === 'string' && action.label !== '')\n\t\t\t\t.slice(0, MAX_ACTIONS)\n\t\t\tconst declaredPrimary = usable.findIndex((action) => action.primary === true)\n\t\t\tconst primaryIndex = declaredPrimary === -1 ? 0 : declaredPrimary\n\t\t\treturn usable.map((action, index) => {\n\t\t\t\tlet target = null\n\t\t\t\tlet href = ''\n\t\t\t\tif (action.route) {\n\t\t\t\t\ttarget = typeof action.route === 'string' ? { name: action.route } : action.route\n\t\t\t\t\thref = resolveHref(target, this.$router)\n\t\t\t\t} else if (typeof action.href === 'string' && action.href !== '') {\n\t\t\t\t\tconst safe = safeHref(action.href)\n\t\t\t\t\thref = safe === '#' ? '' : safe\n\t\t\t\t}\n\t\t\t\treturn {\n\t\t\t\t\tkey: `${index}-${action.id || action.label}`,\n\t\t\t\t\tid: action.id,\n\t\t\t\t\tlabel: action.label,\n\t\t\t\t\thref,\n\t\t\t\t\ttarget,\n\t\t\t\t\tprimary: index === primaryIndex,\n\t\t\t\t\tindex,\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Whether the banner renders: no condition = always; else the evaluated outcome.\n\t\t *\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card\n\t\t */\n\t\tvisible() {\n\t\t\tconst title = this.title || (this.content && this.content.title) || ''\n\t\t\tif (this.resolvedText === '' && !(this.isAttention && title !== '')) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif (!this.resolvedVisibleWhen) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn this.conditionMet === true\n\t\t},\n\n\t\t/**\n\t\t * Whether to say the check failed: an attention card, with words to\n\t\t * show, whose `visibleWhen` request failed. A plain banner stays\n\t\t * hidden on failure, as it always did.\n\t\t *\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-an-attention-card-says-when-it-could-not-check\n\t\t */\n\t\tcheckFailed() {\n\t\t\tconst title = this.title || (this.content && this.content.title) || ''\n\t\t\treturn this.isAttention\n\t\t\t\t&& !!this.resolvedVisibleWhen\n\t\t\t\t&& this.conditionFailed\n\t\t\t\t&& (title !== '' || this.resolvedText !== '')\n\t\t},\n\n\t\t/**\n\t\t * The one line shown when the check failed. It names the card by its\n\t\t * title, unless the title needs the value that could not be read.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-an-attention-card-says-when-it-could-not-check\n\t\t */\n\t\tuncheckedText() {\n\t\t\tconst title = this.title || (this.content && this.content.title) || this.resolvedText\n\t\t\tif (!title || title.includes('{value}')) {\n\t\t\t\treturn t('nextcloud-vue', 'Could not check')\n\t\t\t}\n\t\t\t// `escape: false`: the result is rendered through `{{ }}`, which escapes.\n\t\t\treturn t('nextcloud-vue', 'Could not check: {subject}', { subject: title }, undefined, { escape: false })\n\t\t},\n\n\t\t/**\n\t\t * The reason the check failed, for the tooltip and for assistive\n\t\t * technology.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-an-attention-card-says-when-it-could-not-check\n\t\t */\n\t\tfailureTooltip() {\n\t\t\tif (!this.conditionFailureReason) {\n\t\t\t\treturn t('nextcloud-vue', 'The check failed.')\n\t\t\t}\n\t\t\treturn t('nextcloud-vue', 'The check failed: {reason}', { reason: this.conditionFailureReason }, undefined, { escape: false })\n\t\t},\n\n\t\t/** Whether the banner navigates on click (route set + router present). */\n\t\tclickable() {\n\t\t\treturn !!this.resolvedRoute && !!this.$router\n\t\t},\n\n\t\t/** The click-through route as a router location (a string is a route name). */\n\t\trouteLocation() {\n\t\t\tconst route = this.resolvedRoute\n\t\t\treturn typeof route === 'string' ? { name: route } : route\n\t\t},\n\n\t\t/** The href of the click-through link. */\n\t\trouteHref() {\n\t\t\treturn resolveHref(this.routeLocation, this.$router)\n\t\t},\n\t},\n\n\twatch: {\n\t\tresolvedVisibleWhen: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.evaluateCondition()\n\t\t\t},\n\t\t},\n\n\t\tresolvedConditionOutcome() {\n\t\t\tthis.evaluateCondition()\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Replace `{value}` in a text by the value the predicate read, once\n\t\t * one is known.\n\t\t *\n\t\t * @param {string} text The text.\n\t\t * @return {string} The text with the value filled in.\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card\n\t\t */\n\t\tfillValue(text) {\n\t\t\tif (this.conditionValue === null || !text.includes('{value}')) {\n\t\t\t\treturn text\n\t\t\t}\n\t\t\treturn text.replaceAll('{value}', String(this.conditionValue))\n\t\t},\n\n\t\t/**\n\t\t * Run an attention-card action: route an in-app link, leave an\n\t\t * external link to the browser, and emit `action` for a button.\n\t\t *\n\t\t * @param {MouseEvent} event The click.\n\t\t * @param {object} action The resolved action.\n\t\t * @return {void}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card\n\t\t */\n\t\tonActionClick(event, action) {\n\t\t\tif (action.target) {\n\t\t\t\tfollowLinkClick(event, action.target, this.$router)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!action.href) {\n\t\t\t\tthis.$emit('action', { id: action.id, label: action.label, index: action.index })\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Resolve the `visibleWhen` verdict. A host-injected\n\t\t * `conditionOutcome` wins outright — the host already ran the\n\t\t * predicate, so re-fetching would double the request and flash the\n\t\t * banner hidden until its own copy resolved. Otherwise evaluate\n\t\t * through the shared util primitives (endpoint / OpenRegister-source\n\t\t * modes — the Wave-1 banner shape is the canonical one, extracted to\n\t\t * `utils/visibleWhen.js` in Wave 3 so manifest actions reuse it),\n\t\t * keeping the read VALUE for the `{value}` text placeholder.\n\t\t * Fail-safe: any fetch/shape error leaves the banner hidden. The\n\t\t * failure is kept (`conditionFailed`, with its reason) so an\n\t\t * attention card can say it could not check.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-an-attention-card-says-when-it-could-not-check\n\t\t */\n\t\tasync evaluateCondition() {\n\t\t\tconst outcome = this.resolvedConditionOutcome\n\t\t\tif (outcome) {\n\t\t\t\tthis.conditionMet = outcome.met === true\n\t\t\t\tthis.conditionValue = outcome.value !== undefined ? outcome.value : null\n\t\t\t\tthis.conditionFailed = outcome.failed === true\n\t\t\t\tthis.conditionFailureReason = outcome.failed === true && typeof outcome.reason === 'string' ? outcome.reason : ''\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst cond = this.resolvedVisibleWhen\n\t\t\tif (!cond) {\n\t\t\t\tthis.conditionMet = null\n\t\t\t\tthis.conditionValue = null\n\t\t\t\tthis.conditionFailed = false\n\t\t\t\tthis.conditionFailureReason = ''\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst value = await readVisibleWhenValue(cond)\n\t\t\t\tthis.conditionValue = value\n\t\t\t\tthis.conditionMet = compareVisibleWhen(value, cond.op || 'eq', cond.value)\n\t\t\t\tthis.conditionFailed = false\n\t\t\t\tthis.conditionFailureReason = ''\n\t\t\t} catch (error) {\n\t\t\t\t// Still hidden: a banner that cannot tell whether it applies\n\t\t\t\t// should not claim attention. But not silently, because a\n\t\t\t\t// failed count and \"nothing to report\" look the same on screen.\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn('[CnBannerWidget] visibleWhen could not be evaluated, banner hidden:', error?.message || error)\n\t\t\t\tthis.conditionMet = false\n\t\t\t\tthis.conditionValue = null\n\t\t\t\tthis.conditionFailed = true\n\t\t\t\tthis.conditionFailureReason = typeof error?.message === 'string' ? error.message : String(error ?? '')\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Route a plain click on the link; a modified click opens the href\n\t\t * in the browser.\n\t\t *\n\t\t * @param {MouseEvent} event The click event.\n\t\t * @return {void}\n\t\t */\n\t\tonClick(event) {\n\t\t\tif (!this.clickable) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfollowLinkClick(event, this.routeLocation, this.$router)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-banner-widget {\n\twidth: 100%;\n}\n\n.cn-banner-widget__card {\n\tmargin: 0;\n}\n\n.cn-banner-widget__text--clickable {\n\tcursor: pointer;\n\ttext-decoration: underline;\n\tcolor: inherit;\n}\n\n/* The check failed. Muted text, no fill, no severity colour: it is a note\n that something is not known, not a warning. */\n.cn-banner-widget--unchecked {\n\tbox-sizing: border-box;\n\tmargin: 0;\n\tpadding: 8px 12px;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n\tcursor: help;\n}\n\n/* The reason, for assistive technology; sighted readers get the tooltip. */\n.cn-banner-widget__unchecked-reason {\n\tposition: absolute;\n\twidth: 1px;\n\theight: 1px;\n\tmargin: -1px;\n\tpadding: 0;\n\toverflow: hidden;\n\tclip-path: inset(50%);\n\twhite-space: nowrap;\n\tborder: 0;\n}\n\n/* Attention card. The severity edge is an inset shadow, so it takes no width. */\n.cn-banner-widget--attention {\n\t--cn-banner-accent: var(--color-primary-element);\n\t--cn-banner-accent-text: var(--color-primary-element);\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 16px 24px;\n\tbox-sizing: border-box;\n\tpadding: 20px 24px 20px 28px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-container, var(--border-radius-large, 12px));\n\tbackground: var(--color-main-background);\n\tbox-shadow: inset 4px 0 0 0 var(--cn-banner-accent);\n}\n\n.cn-banner-widget--attention.cn-banner-widget--warning {\n\t--cn-banner-accent: var(--color-element-warning, var(--color-warning));\n\t--cn-banner-accent-text: var(--color-text-warning, var(--color-warning-text, var(--color-main-text)));\n}\n\n.cn-banner-widget--attention.cn-banner-widget--error {\n\t--cn-banner-accent: var(--color-element-error, var(--color-error));\n\t--cn-banner-accent-text: var(--color-text-error, var(--color-error-text));\n}\n\n.cn-banner-widget--attention.cn-banner-widget--success {\n\t--cn-banner-accent: var(--color-element-success, var(--color-success));\n\t--cn-banner-accent-text: var(--color-text-success, var(--color-success-text));\n}\n\n.cn-banner-widget__body {\n\tdisplay: flex;\n\tflex: 1 1 320px;\n\tflex-direction: column;\n\tgap: 6px;\n\tmin-width: 0;\n}\n\n.cn-banner-widget__kicker {\n\tmargin: 0;\n\tfont-size: 0.85em;\n\tfont-weight: 700;\n\tletter-spacing: 0.06em;\n\ttext-transform: uppercase;\n\tcolor: var(--cn-banner-accent-text);\n}\n\n.cn-banner-widget__title {\n\tmargin: 0;\n\tfont-size: 1.3em;\n\tfont-weight: 700;\n\tline-height: 1.3;\n\tcolor: var(--color-main-text);\n}\n\n.cn-banner-widget__reason {\n\tmargin: 0;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-banner-widget__actions {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 10px;\n}\n\n.cn-banner-widget__action {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tjustify-content: center;\n\tbox-sizing: border-box;\n\tmin-height: var(--default-clickable-area, 40px);\n\tmargin: 0;\n\tpadding: 0 16px;\n\tborder: 1px solid var(--color-border-maxcontrast);\n\tborder-radius: var(--border-radius-element, var(--border-radius-large, 8px));\n\tfont: inherit;\n\tfont-weight: 600;\n\ttext-decoration: none;\n\tcursor: pointer;\n}\n\n.cn-banner-widget__action--secondary {\n\tbackground: var(--color-main-background);\n\tcolor: var(--color-main-text);\n}\n\n.cn-banner-widget__action--secondary:hover {\n\tbackground: var(--color-background-hover);\n}\n\n.cn-banner-widget__action--primary {\n\tborder-color: var(--color-primary-element);\n\tbackground: var(--color-primary-element);\n\tcolor: var(--color-primary-element-text);\n}\n\n.cn-banner-widget__action--primary:hover {\n\tborder-color: var(--color-primary-element-hover);\n\tbackground: var(--color-primary-element-hover);\n}\n\n.cn-banner-widget__action:focus-visible {\n\toutline: 2px solid var(--color-main-text);\n\toutline-offset: 2px;\n}\n</style>\n"],"names":["t"],"mappings":"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"}
@@ -15,8 +15,11 @@ const _hoisted_6 = {
15
15
  key: 0,
16
16
  class: "cn-banner-widget__actions"
17
17
  };
18
- const _hoisted_7 = {
19
- key: 1,
18
+ const _hoisted_7 = ["title"];
19
+ const _hoisted_8 = { class: "cn-banner-widget__unchecked-text" };
20
+ const _hoisted_9 = { class: "cn-banner-widget__unchecked-reason" };
21
+ const _hoisted_10 = {
22
+ key: 2,
20
23
  class: "cn-banner-widget"
21
24
  };
22
25
 
@@ -62,29 +65,39 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
62
65
  ]))
63
66
  : createCommentVNode("v-if", true)
64
67
  ], 10 /* CLASS, PROPS */, _hoisted_1))
65
- : ($options.visible)
66
- ? (openBlock(), createElementBlock("div", _hoisted_7, [
67
- createVNode(_component_NcNoteCard, {
68
- type: $options.resolvedVariant,
69
- class: "cn-banner-widget__card"
70
- }, {
71
- default: withCtx(() => [
72
- (openBlock(), createBlock(resolveDynamicComponent($options.clickable ? 'a' : 'span'), {
73
- class: normalizeClass(["cn-banner-widget__text", { 'cn-banner-widget__text--clickable': $options.clickable }]),
74
- href: $options.clickable ? $options.routeHref : null,
75
- "data-testid": "cn-banner-widget-text",
76
- onClick: $options.onClick
77
- }, {
78
- default: withCtx(() => [
79
- createTextVNode(toDisplayString($options.displayText), 1 /* TEXT */)
80
- ]),
81
- _: 1 /* STABLE */
82
- }, 8 /* PROPS */, ["class", "href", "onClick"]))
83
- ]),
84
- _: 1 /* STABLE */
85
- }, 8 /* PROPS */, ["type"])
86
- ]))
87
- : createCommentVNode("v-if", true)
68
+ : ($options.checkFailed)
69
+ ? (openBlock(), createElementBlock("p", {
70
+ key: 1,
71
+ class: "cn-banner-widget cn-banner-widget--unchecked",
72
+ title: $options.failureTooltip,
73
+ "data-testid": "cn-banner-widget-unchecked"
74
+ }, [
75
+ createElementVNode("span", _hoisted_8, toDisplayString($options.uncheckedText), 1 /* TEXT */),
76
+ createElementVNode("span", _hoisted_9, toDisplayString($options.failureTooltip), 1 /* TEXT */)
77
+ ], 8 /* PROPS */, _hoisted_7))
78
+ : ($options.visible)
79
+ ? (openBlock(), createElementBlock("div", _hoisted_10, [
80
+ createVNode(_component_NcNoteCard, {
81
+ type: $options.resolvedVariant,
82
+ class: "cn-banner-widget__card"
83
+ }, {
84
+ default: withCtx(() => [
85
+ (openBlock(), createBlock(resolveDynamicComponent($options.clickable ? 'a' : 'span'), {
86
+ class: normalizeClass(["cn-banner-widget__text", { 'cn-banner-widget__text--clickable': $options.clickable }]),
87
+ href: $options.clickable ? $options.routeHref : null,
88
+ "data-testid": "cn-banner-widget-text",
89
+ onClick: $options.onClick
90
+ }, {
91
+ default: withCtx(() => [
92
+ createTextVNode(toDisplayString($options.displayText), 1 /* TEXT */)
93
+ ]),
94
+ _: 1 /* STABLE */
95
+ }, 8 /* PROPS */, ["class", "href", "onClick"]))
96
+ ]),
97
+ _: 1 /* STABLE */
98
+ }, 8 /* PROPS */, ["type"])
99
+ ]))
100
+ : createCommentVNode("v-if", true)
88
101
  }
89
102
 
90
103
  export { render };
@@ -1 +1 @@
1
- {"version":3,"file":"CnBannerWidget.vue3.js","sources":["../../../../src/components/CnBannerWidget/CnBannerWidget.vue?vue&type=template&id=3531692f&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<section\n\t\tv-if=\"visible && isAttention\"\n\t\tclass=\"cn-banner-widget cn-banner-widget--attention\"\n\t\t:class=\"'cn-banner-widget--' + resolvedVariant\"\n\t\t:aria-labelledby=\"headingId\"\n\t\tdata-testid=\"cn-banner-widget-attention\">\n\t\t<div class=\"cn-banner-widget__body\">\n\t\t\t<p v-if=\"resolvedKicker\" class=\"cn-banner-widget__kicker\">\n\t\t\t\t{{ resolvedKicker }}\n\t\t\t</p>\n\t\t\t<h3 :id=\"headingId\" class=\"cn-banner-widget__title\">\n\t\t\t\t{{ displayTitle }}\n\t\t\t</h3>\n\t\t\t<p v-if=\"displayReason\" class=\"cn-banner-widget__reason\">\n\t\t\t\t{{ displayReason }}\n\t\t\t</p>\n\t\t</div>\n\t\t<div v-if=\"resolvedActions.length > 0\" class=\"cn-banner-widget__actions\">\n\t\t\t<component\n\t\t\t\t:is=\"action.href ? 'a' : 'button'\"\n\t\t\t\tv-for=\"action in resolvedActions\"\n\t\t\t\t:key=\"action.key\"\n\t\t\t\tclass=\"cn-banner-widget__action\"\n\t\t\t\t:class=\"action.primary ? 'cn-banner-widget__action--primary' : 'cn-banner-widget__action--secondary'\"\n\t\t\t\t:href=\"action.href || null\"\n\t\t\t\t:type=\"action.href ? null : 'button'\"\n\t\t\t\tdata-testid=\"cn-banner-widget-action\"\n\t\t\t\t@click=\"onActionClick($event, action)\">\n\t\t\t\t{{ action.label }}\n\t\t\t</component>\n\t\t</div>\n\t</section>\n\t<div v-else-if=\"visible\" class=\"cn-banner-widget\">\n\t\t<NcNoteCard :type=\"resolvedVariant\" class=\"cn-banner-widget__card\">\n\t\t\t<component\n\t\t\t\t:is=\"clickable ? 'a' : 'span'\"\n\t\t\t\tclass=\"cn-banner-widget__text\"\n\t\t\t\t:class=\"{ 'cn-banner-widget__text--clickable': clickable }\"\n\t\t\t\t:href=\"clickable ? routeHref : null\"\n\t\t\t\tdata-testid=\"cn-banner-widget-text\"\n\t\t\t\t@click=\"onClick\">\n\t\t\t\t{{ displayText }}\n\t\t\t</component>\n\t\t</NcNoteCard>\n\t</div>\n</template>\n\n<script>\nimport { NcNoteCard } from '@nextcloud/vue'\nimport { followLinkClick, resolveHref } from '../../utils/linkNavigation.js'\nimport { safeHref } from '../../utils/safeHref.js'\nimport { nextUid } from '../../utils/uid.js'\nimport { compareVisibleWhen, readVisibleWhenValue } from '../../utils/visibleWhen.js'\n\n/** Variants understood by NcNoteCard. */\nconst VARIANTS = ['info', 'warning', 'error', 'success']\n\n/** Layouts: the note card (default) or the attention card. */\nconst LAYOUTS = ['banner', 'attention']\n\n/** An attention card offers a primary and a secondary action, no more. */\nconst MAX_ACTIONS = 2\n\n/**\n * CnBannerWidget — declarative notice banner for dashboards and v2 pages\n * (`banner` widget type, Wave 1 of nextcloud-vue#91).\n *\n * Renders a themed NcNoteCard with a variant (`info | warning | error`),\n * a text, an optional click-through route, and an optional `visibleWhen`\n * condition: a simple `{ field, op, value }` predicate evaluated against\n * an endpoint response or an OpenRegister source (the doriath\n * migration-banner case — \"show while `pending > 0`\").\n *\n * Config arrives either as flat props (v2 grid spreads `props`) or as a\n * stored `content` blob (CnDashboardPage's registry branch) — explicit\n * flat props win on collision.\n *\n * ```json\n * {\n * \"widgetKey\": \"banner\",\n * \"props\": {\n * \"variant\": \"warning\",\n * \"text\": \"Migrations pending — open the migration overview.\",\n * \"visibleWhen\": { \"endpoint\": \"/apps/doriath/api/migrations/status\", \"field\": \"pending\", \"op\": \"gt\", \"value\": 0 },\n * \"route\": \"migrations\"\n * }\n * }\n * ```\n *\n * Fail-safe: with a `visibleWhen`, the banner stays HIDDEN until the\n * condition evaluates true — a failed fetch never breaks (or spams) a\n * dashboard.\n *\n * `layout: \"attention\"` turns the banner into an attention card: a card with\n * a coloured edge by severity, a `kicker` label, a `title`, a `reason` line\n * and up to two `actions` (the first is the primary one unless an action says\n * `primary` itself). `visibleWhen` works the same way. Without `layout` the\n * note card above renders, as it always did.\n *\n * ```json\n * {\n * \"widgetKey\": \"banner\",\n * \"props\": {\n * \"layout\": \"attention\",\n * \"variant\": \"error\",\n * \"kicker\": \"First today\",\n * \"title\": \"Parking permits city centre\",\n * \"reason\": \"The deadline ends today.\",\n * \"actions\": [\n * { \"label\": \"Open case\", \"route\": { \"name\": \"CaseDetail\", \"params\": { \"id\": \"2026-0061\" } } },\n * { \"label\": \"Suspend deadline\", \"route\": \"CaseSuspend\" }\n * ]\n * }\n * }\n * ```\n */\nexport default {\n\tname: 'CnBannerWidget',\n\n\tcomponents: { NcNoteCard },\n\n\tprops: {\n\t\t/**\n\t\t * Banner severity variant: `info | warning | error` (plus `success`\n\t\t * for completeness — NcNoteCard's set). Empty falls back to the\n\t\t * `content` blob, then `info`.\n\t\t */\n\t\tvariant: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t\tvalidator: (v) => v === '' || VARIANTS.includes(v),\n\t\t},\n\n\t\t/** Pre-translated banner text. Empty falls back to the `content` blob. */\n\t\ttext: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Optional visibility condition. Shape:\n\t\t * `{ endpoint?, source?, field?, op?, value }` — exactly one of\n\t\t * `endpoint` (a same-origin URL returning JSON) or `source`\n\t\t * (`{ register, schema, filter? }`, an OpenRegister object query whose\n\t\t * `filter` supports the shared @-token grammar). `field` is a\n\t\t * dot-path into the response (for a `source`, into the first result;\n\t\t * omit it — or use `@total` — to compare the collection total).\n\t\t * `op` is `eq | neq | gt | gte | lt | lte` (default `eq`); `value` is\n\t\t * the literal right-hand side. `null` (the default) shows the banner\n\t\t * unconditionally.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tvisibleWhen: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Optional click-through route: a vue-router route NAME (string) or\n\t\t * a full location object. When set, the banner text is a link to that\n\t\t * route. `null` renders static text.\n\t\t *\n\t\t * @type {string|object|null}\n\t\t */\n\t\troute: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * How the banner renders: `banner` (the note card, default) or\n\t\t * `attention` (a card with a severity edge, kicker, title, reason and\n\t\t * up to two actions). Empty falls back to the `content` blob, then\n\t\t * `banner`.\n\t\t *\n\t\t * @type {''|'banner'|'attention'}\n\t\t */\n\t\tlayout: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t\tvalidator: (v) => v === '' || LAYOUTS.includes(v),\n\t\t},\n\n\t\t/** Attention card: the small label above the title (\"First today\"). */\n\t\tkicker: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Attention card: the title. Falls back to `text` when empty. */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Attention card: the line under the title that says why this needs attention. `{value}` is replaced like in `text`. */\n\t\treason: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Attention card: at most two actions. Each is `{ label, route?,\n\t\t * href?, primary?, id? }`: `route` is a route name or location,\n\t\t * `href` an URL. An action with neither renders a button that emits\n\t\t * `action`. The first action is the primary one unless one sets\n\t\t * `primary: true`.\n\t\t *\n\t\t * @type {Array<{label: string, route?: (string|object), href?: string, primary?: boolean, id?: string}>|null}\n\t\t */\n\t\tactions: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Stored content blob (CnDashboardPage registry branch) carrying the\n\t\t * same keys as the flat props: `{ variant, text, visibleWhen, route,\n\t\t * layout, kicker, title, reason, actions }`. Explicit flat props win\n\t\t * on collision.\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 * Pre-evaluated `visibleWhen` outcome `{ met, value }`, injected by a\n\t\t * host that already ran the predicate (CnDashboardPage evaluates it\n\t\t * to know whether to collapse the banner's grid cell). When present,\n\t\t * the banner renders from this verdict instead of fetching again —\n\t\t * no duplicate request and no hidden-until-self-evaluated flash —\n\t\t * and `value` feeds the `{value}` text placeholder. `null` (the\n\t\t * default) keeps the banner self-evaluating.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tconditionOutcome: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\temits: [\n\t\t/**\n\t\t * Emitted when an attention-card action without a `route` or `href`\n\t\t * is clicked, so the host can run it.\n\t\t *\n\t\t * @event action\n\t\t * @type {{id: (string|undefined), label: string, index: number}}\n\t\t */\n\t\t'action',\n\t],\n\n\tdata() {\n\t\treturn {\n\t\t\t/** Id that ties the attention card's region to its title. */\n\t\t\theadingId: `cn-banner-title-${nextUid()}`,\n\t\t\t/** Evaluated visibleWhen outcome (null = not yet evaluated). */\n\t\t\tconditionMet: null,\n\t\t\t/**\n\t\t\t * The raw field value the predicate read (from the injected\n\t\t\t * outcome or the banner's own evaluation) — interpolated into\n\t\t\t * the text wherever it says `{value}`.\n\t\t\t */\n\t\t\tconditionValue: null,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** The effective variant (prop → content → 'info'). */\n\t\tresolvedVariant() {\n\t\t\tconst v = this.variant || (this.content && this.content.variant) || 'info'\n\t\t\treturn VARIANTS.includes(v) ? v : 'info'\n\t\t},\n\n\t\t/** The effective banner text (prop → content → ''). */\n\t\tresolvedText() {\n\t\t\treturn this.text || (this.content && this.content.text) || ''\n\t\t},\n\n\t\t/** The effective visibleWhen condition (prop → content → null). */\n\t\tresolvedVisibleWhen() {\n\t\t\treturn this.visibleWhen || (this.content && this.content.visibleWhen) || null\n\t\t},\n\n\t\t/** The effective injected outcome (prop → content → null). */\n\t\tresolvedConditionOutcome() {\n\t\t\treturn this.conditionOutcome || (this.content && this.content.conditionOutcome) || null\n\t\t},\n\n\t\t/**\n\t\t * The rendered text: `resolvedText` with `{value}` replaced by the\n\t\t * predicate's field value once one is known. Without a known value\n\t\t * the text renders as written — a `{value}` placeholder only makes\n\t\t * sense on a conditional banner, which stays hidden until its\n\t\t * predicate (and therefore its value) resolves.\n\t\t *\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card\n\t\t */\n\t\tdisplayText() {\n\t\t\treturn this.fillValue(this.resolvedText)\n\t\t},\n\n\t\t/** The effective click-through route (prop → content → null). */\n\t\tresolvedRoute() {\n\t\t\treturn this.route || (this.content && this.content.route) || null\n\t\t},\n\n\t\t/**\n\t\t * Whether the attention card renders instead of the note card.\n\t\t *\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card\n\t\t */\n\t\tisAttention() {\n\t\t\treturn (this.layout || (this.content && this.content.layout) || 'banner') === 'attention'\n\t\t},\n\n\t\t/**\n\t\t * The attention card's kicker (prop, else content).\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card\n\t\t */\n\t\tresolvedKicker() {\n\t\t\tconst kicker = this.kicker || (this.content && this.content.kicker) || ''\n\t\t\treturn kicker\n\t\t},\n\n\t\t/**\n\t\t * The attention card's title: `title`, else `text`, with `{value}` filled in.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card\n\t\t */\n\t\tdisplayTitle() {\n\t\t\tconst title = this.title || (this.content && this.content.title) || ''\n\t\t\treturn title ? this.fillValue(title) : this.displayText\n\t\t},\n\n\t\t/**\n\t\t * The attention card's reason line. When the title already used\n\t\t * `text`, the text is not repeated here.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card\n\t\t */\n\t\tdisplayReason() {\n\t\t\tconst reason = this.reason || (this.content && this.content.reason) || ''\n\t\t\tif (reason) {\n\t\t\t\treturn this.fillValue(reason)\n\t\t\t}\n\t\t\tconst title = this.title || (this.content && this.content.title) || ''\n\t\t\treturn title ? this.displayText : ''\n\t\t},\n\n\t\t/**\n\t\t * The attention card's actions: the first two that have a label, each\n\t\t * with its href and whether it is the primary one.\n\t\t *\n\t\t * @return {Array<{key: string, id: (string|undefined), label: string, href: string, target: (object|null), primary: boolean, index: number}>}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card\n\t\t */\n\t\tresolvedActions() {\n\t\t\tconst raw = this.actions || (this.content && this.content.actions) || []\n\t\t\tif (!Array.isArray(raw)) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\tconst usable = raw\n\t\t\t\t.filter((action) => action && typeof action === 'object' && typeof action.label === 'string' && action.label !== '')\n\t\t\t\t.slice(0, MAX_ACTIONS)\n\t\t\tconst declaredPrimary = usable.findIndex((action) => action.primary === true)\n\t\t\tconst primaryIndex = declaredPrimary === -1 ? 0 : declaredPrimary\n\t\t\treturn usable.map((action, index) => {\n\t\t\t\tlet target = null\n\t\t\t\tlet href = ''\n\t\t\t\tif (action.route) {\n\t\t\t\t\ttarget = typeof action.route === 'string' ? { name: action.route } : action.route\n\t\t\t\t\thref = resolveHref(target, this.$router)\n\t\t\t\t} else if (typeof action.href === 'string' && action.href !== '') {\n\t\t\t\t\tconst safe = safeHref(action.href)\n\t\t\t\t\thref = safe === '#' ? '' : safe\n\t\t\t\t}\n\t\t\t\treturn {\n\t\t\t\t\tkey: `${index}-${action.id || action.label}`,\n\t\t\t\t\tid: action.id,\n\t\t\t\t\tlabel: action.label,\n\t\t\t\t\thref,\n\t\t\t\t\ttarget,\n\t\t\t\t\tprimary: index === primaryIndex,\n\t\t\t\t\tindex,\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Whether the banner renders: no condition = always; else the evaluated outcome.\n\t\t *\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card\n\t\t */\n\t\tvisible() {\n\t\t\tconst title = this.title || (this.content && this.content.title) || ''\n\t\t\tif (this.resolvedText === '' && !(this.isAttention && title !== '')) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif (!this.resolvedVisibleWhen) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn this.conditionMet === true\n\t\t},\n\n\t\t/** Whether the banner navigates on click (route set + router present). */\n\t\tclickable() {\n\t\t\treturn !!this.resolvedRoute && !!this.$router\n\t\t},\n\n\t\t/** The click-through route as a router location (a string is a route name). */\n\t\trouteLocation() {\n\t\t\tconst route = this.resolvedRoute\n\t\t\treturn typeof route === 'string' ? { name: route } : route\n\t\t},\n\n\t\t/** The href of the click-through link. */\n\t\trouteHref() {\n\t\t\treturn resolveHref(this.routeLocation, this.$router)\n\t\t},\n\t},\n\n\twatch: {\n\t\tresolvedVisibleWhen: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.evaluateCondition()\n\t\t\t},\n\t\t},\n\n\t\tresolvedConditionOutcome() {\n\t\t\tthis.evaluateCondition()\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Replace `{value}` in a text by the value the predicate read, once\n\t\t * one is known.\n\t\t *\n\t\t * @param {string} text The text.\n\t\t * @return {string} The text with the value filled in.\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card\n\t\t */\n\t\tfillValue(text) {\n\t\t\tif (this.conditionValue === null || !text.includes('{value}')) {\n\t\t\t\treturn text\n\t\t\t}\n\t\t\treturn text.replaceAll('{value}', String(this.conditionValue))\n\t\t},\n\n\t\t/**\n\t\t * Run an attention-card action: route an in-app link, leave an\n\t\t * external link to the browser, and emit `action` for a button.\n\t\t *\n\t\t * @param {MouseEvent} event The click.\n\t\t * @param {object} action The resolved action.\n\t\t * @return {void}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card\n\t\t */\n\t\tonActionClick(event, action) {\n\t\t\tif (action.target) {\n\t\t\t\tfollowLinkClick(event, action.target, this.$router)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!action.href) {\n\t\t\t\tthis.$emit('action', { id: action.id, label: action.label, index: action.index })\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Resolve the `visibleWhen` verdict. A host-injected\n\t\t * `conditionOutcome` wins outright — the host already ran the\n\t\t * predicate, so re-fetching would double the request and flash the\n\t\t * banner hidden until its own copy resolved. Otherwise evaluate\n\t\t * through the shared util primitives (endpoint / OpenRegister-source\n\t\t * modes — the Wave-1 banner shape is the canonical one, extracted to\n\t\t * `utils/visibleWhen.js` in Wave 3 so manifest actions reuse it),\n\t\t * keeping the read VALUE for the `{value}` text placeholder.\n\t\t * Fail-safe: any fetch/shape error leaves the banner hidden.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync evaluateCondition() {\n\t\t\tconst outcome = this.resolvedConditionOutcome\n\t\t\tif (outcome) {\n\t\t\t\tthis.conditionMet = outcome.met === true\n\t\t\t\tthis.conditionValue = outcome.value !== undefined ? outcome.value : null\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst cond = this.resolvedVisibleWhen\n\t\t\tif (!cond) {\n\t\t\t\tthis.conditionMet = null\n\t\t\t\tthis.conditionValue = null\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst value = await readVisibleWhenValue(cond)\n\t\t\t\tthis.conditionValue = value\n\t\t\t\tthis.conditionMet = compareVisibleWhen(value, cond.op || 'eq', cond.value)\n\t\t\t} catch {\n\t\t\t\tthis.conditionMet = false\n\t\t\t\tthis.conditionValue = null\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Route a plain click on the link; a modified click opens the href\n\t\t * in the browser.\n\t\t *\n\t\t * @param {MouseEvent} event The click event.\n\t\t * @return {void}\n\t\t */\n\t\tonClick(event) {\n\t\t\tif (!this.clickable) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfollowLinkClick(event, this.routeLocation, this.$router)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-banner-widget {\n\twidth: 100%;\n}\n\n.cn-banner-widget__card {\n\tmargin: 0;\n}\n\n.cn-banner-widget__text--clickable {\n\tcursor: pointer;\n\ttext-decoration: underline;\n\tcolor: inherit;\n}\n\n/* Attention card. The severity edge is an inset shadow, so it takes no width. */\n.cn-banner-widget--attention {\n\t--cn-banner-accent: var(--color-primary-element);\n\t--cn-banner-accent-text: var(--color-primary-element);\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 16px 24px;\n\tbox-sizing: border-box;\n\tpadding: 20px 24px 20px 28px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-container, var(--border-radius-large, 12px));\n\tbackground: var(--color-main-background);\n\tbox-shadow: inset 4px 0 0 0 var(--cn-banner-accent);\n}\n\n.cn-banner-widget--attention.cn-banner-widget--warning {\n\t--cn-banner-accent: var(--color-element-warning, var(--color-warning));\n\t--cn-banner-accent-text: var(--color-text-warning, var(--color-warning-text, var(--color-main-text)));\n}\n\n.cn-banner-widget--attention.cn-banner-widget--error {\n\t--cn-banner-accent: var(--color-element-error, var(--color-error));\n\t--cn-banner-accent-text: var(--color-text-error, var(--color-error-text));\n}\n\n.cn-banner-widget--attention.cn-banner-widget--success {\n\t--cn-banner-accent: var(--color-element-success, var(--color-success));\n\t--cn-banner-accent-text: var(--color-text-success, var(--color-success-text));\n}\n\n.cn-banner-widget__body {\n\tdisplay: flex;\n\tflex: 1 1 320px;\n\tflex-direction: column;\n\tgap: 6px;\n\tmin-width: 0;\n}\n\n.cn-banner-widget__kicker {\n\tmargin: 0;\n\tfont-size: 0.85em;\n\tfont-weight: 700;\n\tletter-spacing: 0.06em;\n\ttext-transform: uppercase;\n\tcolor: var(--cn-banner-accent-text);\n}\n\n.cn-banner-widget__title {\n\tmargin: 0;\n\tfont-size: 1.3em;\n\tfont-weight: 700;\n\tline-height: 1.3;\n\tcolor: var(--color-main-text);\n}\n\n.cn-banner-widget__reason {\n\tmargin: 0;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-banner-widget__actions {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 10px;\n}\n\n.cn-banner-widget__action {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tjustify-content: center;\n\tbox-sizing: border-box;\n\tmin-height: var(--default-clickable-area, 40px);\n\tmargin: 0;\n\tpadding: 0 16px;\n\tborder: 1px solid var(--color-border-maxcontrast);\n\tborder-radius: var(--border-radius-element, var(--border-radius-large, 8px));\n\tfont: inherit;\n\tfont-weight: 600;\n\ttext-decoration: none;\n\tcursor: pointer;\n}\n\n.cn-banner-widget__action--secondary {\n\tbackground: var(--color-main-background);\n\tcolor: var(--color-main-text);\n}\n\n.cn-banner-widget__action--secondary:hover {\n\tbackground: var(--color-background-hover);\n}\n\n.cn-banner-widget__action--primary {\n\tborder-color: var(--color-primary-element);\n\tbackground: var(--color-primary-element);\n\tcolor: var(--color-primary-element-text);\n}\n\n.cn-banner-widget__action--primary:hover {\n\tborder-color: var(--color-primary-element-hover);\n\tbackground: var(--color-primary-element-hover);\n}\n\n.cn-banner-widget__action:focus-visible {\n\toutline: 2px solid var(--color-main-text);\n\toutline-offset: 2px;\n}\n</style>\n"],"names":["_createElementBlock","_normalizeClass","_createElementVNode","_toDisplayString","_openBlock","_Fragment","_renderList","_resolveDynamicComponent","_createTextVNode","_createVNode","_createBlock"],"mappings":";;;AAiBO,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,wBAAQ,GAAA;;;AACd,EAAA,KAAA,EAAA,0BAAA;;;;;AAMK,EAAA,KAAA,EAAA,0BAAA;;;;AAIuB,EAAA,KAAA,EAAA,2BAAA;;;;AAeT,EAAA,KAAK,EAAC,kBAAmB;;;;;;AA/B3C,EAAA,OAAA,CAAA,QAAA,CAAA,OAAO,IAAI,QAAW,CAAA,WAAA;oBAD5BA,kBA+BS,CAAA,SAAA,EAAA;;aA7BJ,EAAAC,cAAA,CAAA,CAAC,8CAA8C,EAAA,oBAAA,GAC/B,QAAA,CAAA,eAAA,CAAA,CAAA;AACpB,QAAA,iBAAG,EAAA,KAAA,CAAA,SAAA;qBACO,EAAC,4BAA4B;;AACtC,QAAAC,kBAAA,CAUI,OAVJ,UAUI,EAAA;WATD,QAAA,CAAA,cAAA;4BAAJF,kBAEI,CAAA,GAAA,EAFJ,UAEI,EAAAG,eAAA,CADA,QAAc,CAAA,cAAA,CAAA,EAAA,CAAA,YAAA;;UAElBD,kBAEK,CAAA,IAAA,EAAA;AAFF,YAAA,EAAA,EAAA,KAAA,CAAA,SAAA;AAAA,YAAA,KAAA,EAAA,yBAAA;6BACC,QAAA,CAAA,YAAA,CAAA,EAAA,CAAA,oBAAA,UAAA,CAAA;WAEK,QAAA,CAAA,aAAA;4BAATF,kBAEI,CAAA,GAAA,EAFJ,UAEI,EAAAG,eAAA,CADA,QAAY,CAAA,aAAA,CAAA,EAAA,CAAA,YAAA;;;AAGN,QAAA,CAAA,QAAA,CAAA,eAAe,CAAC,MAAM,GAAA,CAAA;AAA/B,aAAAC,SAAA,EAAA,EAAAJ,kBAAA,CAaI,OAbJ,UAaI,EAAA;kDADI,CAAAK,QAAA,EAAA,IAAA,EAAAC,UAAA,CATS,QAAe,CAAA,eAAA,EAAA,CAAzB,MAAM,KAAA;gDASL,CAAAC,uBAAA,CAVH,MAAM,CAAC,IAAI,GAAA,GAAA,GAAA,QAAA,CAAA,EAAA;kBAEf,GAAG,EAAE,MAAM,CAAC,GAAG;AAChB,kBAAA,KAAK,EAACN,cAAA,CAAA,CAAA,0BAA0B,EACxB,MAAM,QAAG,GAAA,mCAAA,GAAA,qCAAA,CAAA,CAAA;kBAChB,IAAI,EAAE,MAAG,CAAA,IAAA,IAAA,IAAA;kBACT,IAAE,EAAA,MAAA,CAAA,IAAA,GAAA,IAAA,GAAA,QAAA;AACH,kBAAA,aAAM,EAAA,yBAAA;AACJ,kBAAA,OAAI,EAAE,MAAA,KAAA,QAAA,CAAA,aAAY,CAAE,MAAM,EAAE,MAAM,CAAA,CAAA;;mCACpC,MAAkB;AAAf,oBAAAO,eAAA,CAAAL,eAAA,CAAA,MAAK,CAAE,KAAK,CAAA,EAAA,CAAA,YAAA;;;;;;;;OAIF,QAAO,CAAA,OAAA;;UACpBM;AAAW,YAAA,IAAI,EAAE,QAAe,CAAA,eAAA;AAAE,YAAA,KAAC,EAAA,wBAAA;;6BACnC;AAAA,eAAAL,SAAA,EAAA,EAAAM,WAAA,yBACK,QAAS,CAAA,SAAA,GAAA,GAAA,GAAA,MAAA,CAAA,EAAA;gBACd,KAAK,EAAAT,cAAA,CAAA,CAAC,wBAAU,EAAA,EAAA,mCAAA,EACN,QAAA,CAAA,SAAA,EAAA,CAAA,CAAA;gBACT,IAAI,EAAE,QAAG,CAAA,SAAA,GAAA,QAAA,CAAA,SAAA,GAAA,IAAA;AACV,gBAAA,aAAE,EAAA,uBAAA;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"CnBannerWidget.vue3.js","sources":["../../../../src/components/CnBannerWidget/CnBannerWidget.vue?vue&type=template&id=3531692f&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<section\n\t\tv-if=\"visible && isAttention\"\n\t\tclass=\"cn-banner-widget cn-banner-widget--attention\"\n\t\t:class=\"'cn-banner-widget--' + resolvedVariant\"\n\t\t:aria-labelledby=\"headingId\"\n\t\tdata-testid=\"cn-banner-widget-attention\">\n\t\t<div class=\"cn-banner-widget__body\">\n\t\t\t<p v-if=\"resolvedKicker\" class=\"cn-banner-widget__kicker\">\n\t\t\t\t{{ resolvedKicker }}\n\t\t\t</p>\n\t\t\t<h3 :id=\"headingId\" class=\"cn-banner-widget__title\">\n\t\t\t\t{{ displayTitle }}\n\t\t\t</h3>\n\t\t\t<p v-if=\"displayReason\" class=\"cn-banner-widget__reason\">\n\t\t\t\t{{ displayReason }}\n\t\t\t</p>\n\t\t</div>\n\t\t<div v-if=\"resolvedActions.length > 0\" class=\"cn-banner-widget__actions\">\n\t\t\t<component\n\t\t\t\t:is=\"action.href ? 'a' : 'button'\"\n\t\t\t\tv-for=\"action in resolvedActions\"\n\t\t\t\t:key=\"action.key\"\n\t\t\t\tclass=\"cn-banner-widget__action\"\n\t\t\t\t:class=\"action.primary ? 'cn-banner-widget__action--primary' : 'cn-banner-widget__action--secondary'\"\n\t\t\t\t:href=\"action.href || null\"\n\t\t\t\t:type=\"action.href ? null : 'button'\"\n\t\t\t\tdata-testid=\"cn-banner-widget-action\"\n\t\t\t\t@click=\"onActionClick($event, action)\">\n\t\t\t\t{{ action.label }}\n\t\t\t</component>\n\t\t</div>\n\t</section>\n\t<!-- The count could not be read. One quiet line, not the card and not an\n\t alarm: hidden would read as \"nothing to report\", which is not known. -->\n\t<p\n\t\tv-else-if=\"checkFailed\"\n\t\tclass=\"cn-banner-widget cn-banner-widget--unchecked\"\n\t\t:title=\"failureTooltip\"\n\t\tdata-testid=\"cn-banner-widget-unchecked\">\n\t\t<span class=\"cn-banner-widget__unchecked-text\">{{ uncheckedText }}</span>\n\t\t<span class=\"cn-banner-widget__unchecked-reason\">{{ failureTooltip }}</span>\n\t</p>\n\t<div v-else-if=\"visible\" class=\"cn-banner-widget\">\n\t\t<NcNoteCard :type=\"resolvedVariant\" class=\"cn-banner-widget__card\">\n\t\t\t<component\n\t\t\t\t:is=\"clickable ? 'a' : 'span'\"\n\t\t\t\tclass=\"cn-banner-widget__text\"\n\t\t\t\t:class=\"{ 'cn-banner-widget__text--clickable': clickable }\"\n\t\t\t\t:href=\"clickable ? routeHref : null\"\n\t\t\t\tdata-testid=\"cn-banner-widget-text\"\n\t\t\t\t@click=\"onClick\">\n\t\t\t\t{{ displayText }}\n\t\t\t</component>\n\t\t</NcNoteCard>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcNoteCard } from '@nextcloud/vue'\nimport { followLinkClick, resolveHref } from '../../utils/linkNavigation.js'\nimport { safeHref } from '../../utils/safeHref.js'\nimport { nextUid } from '../../utils/uid.js'\nimport { compareVisibleWhen, readVisibleWhenValue } from '../../utils/visibleWhen.js'\n\n/** Variants understood by NcNoteCard. */\nconst VARIANTS = ['info', 'warning', 'error', 'success']\n\n/** Layouts: the note card (default) or the attention card. */\nconst LAYOUTS = ['banner', 'attention']\n\n/** An attention card offers a primary and a secondary action, no more. */\nconst MAX_ACTIONS = 2\n\n/**\n * CnBannerWidget — declarative notice banner for dashboards and v2 pages\n * (`banner` widget type, Wave 1 of nextcloud-vue#91).\n *\n * Renders a themed NcNoteCard with a variant (`info | warning | error`),\n * a text, an optional click-through route, and an optional `visibleWhen`\n * condition: a simple `{ field, op, value }` predicate evaluated against\n * an endpoint response or an OpenRegister source (the doriath\n * migration-banner case — \"show while `pending > 0`\").\n *\n * Config arrives either as flat props (v2 grid spreads `props`) or as a\n * stored `content` blob (CnDashboardPage's registry branch) — explicit\n * flat props win on collision.\n *\n * ```json\n * {\n * \"widgetKey\": \"banner\",\n * \"props\": {\n * \"variant\": \"warning\",\n * \"text\": \"Migrations pending — open the migration overview.\",\n * \"visibleWhen\": { \"endpoint\": \"/apps/doriath/api/migrations/status\", \"field\": \"pending\", \"op\": \"gt\", \"value\": 0 },\n * \"route\": \"migrations\"\n * }\n * }\n * ```\n *\n * Fail-safe: with a `visibleWhen`, the banner stays HIDDEN until the\n * condition evaluates true — a failed fetch never breaks (or spams) a\n * dashboard.\n *\n * An attention card (`layout: \"attention\"`) whose `visibleWhen` request\n * FAILS renders one quiet line instead, \"Could not check\", with the reason as\n * a tooltip. A hidden card reads as \"nothing needs attention\", and a failed\n * count does not know that. A request that succeeds with a value that does\n * not meet the condition still renders nothing.\n *\n * `layout: \"attention\"` turns the banner into an attention card: a card with\n * a coloured edge by severity, a `kicker` label, a `title`, a `reason` line\n * and up to two `actions` (the first is the primary one unless an action says\n * `primary` itself). `visibleWhen` works the same way. Without `layout` the\n * note card above renders, as it always did.\n *\n * ```json\n * {\n * \"widgetKey\": \"banner\",\n * \"props\": {\n * \"layout\": \"attention\",\n * \"variant\": \"error\",\n * \"kicker\": \"First today\",\n * \"title\": \"Parking permits city centre\",\n * \"reason\": \"The deadline ends today.\",\n * \"actions\": [\n * { \"label\": \"Open case\", \"route\": { \"name\": \"CaseDetail\", \"params\": { \"id\": \"2026-0061\" } } },\n * { \"label\": \"Suspend deadline\", \"route\": \"CaseSuspend\" }\n * ]\n * }\n * }\n * ```\n */\nexport default {\n\tname: 'CnBannerWidget',\n\n\tcomponents: { NcNoteCard },\n\n\tprops: {\n\t\t/**\n\t\t * Banner severity variant: `info | warning | error` (plus `success`\n\t\t * for completeness — NcNoteCard's set). Empty falls back to the\n\t\t * `content` blob, then `info`.\n\t\t */\n\t\tvariant: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t\tvalidator: (v) => v === '' || VARIANTS.includes(v),\n\t\t},\n\n\t\t/** Pre-translated banner text. Empty falls back to the `content` blob. */\n\t\ttext: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Optional visibility condition. Shape:\n\t\t * `{ endpoint?, source?, field?, op?, value }` — exactly one of\n\t\t * `endpoint` (a same-origin URL returning JSON) or `source`\n\t\t * (`{ register, schema, filter? }`, an OpenRegister object query whose\n\t\t * `filter` supports the shared @-token grammar). `field` is a\n\t\t * dot-path into the response (for a `source`, into the first result;\n\t\t * omit it — or use `@total` — to compare the collection total).\n\t\t * `op` is `eq | neq | gt | gte | lt | lte` (default `eq`); `value` is\n\t\t * the literal right-hand side. `null` (the default) shows the banner\n\t\t * unconditionally.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tvisibleWhen: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Optional click-through route: a vue-router route NAME (string) or\n\t\t * a full location object. When set, the banner text is a link to that\n\t\t * route. `null` renders static text.\n\t\t *\n\t\t * @type {string|object|null}\n\t\t */\n\t\troute: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * How the banner renders: `banner` (the note card, default) or\n\t\t * `attention` (a card with a severity edge, kicker, title, reason and\n\t\t * up to two actions). Empty falls back to the `content` blob, then\n\t\t * `banner`.\n\t\t *\n\t\t * @type {''|'banner'|'attention'}\n\t\t */\n\t\tlayout: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t\tvalidator: (v) => v === '' || LAYOUTS.includes(v),\n\t\t},\n\n\t\t/** Attention card: the small label above the title (\"First today\"). */\n\t\tkicker: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Attention card: the title. Falls back to `text` when empty. */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Attention card: the line under the title that says why this needs attention. `{value}` is replaced like in `text`. */\n\t\treason: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Attention card: at most two actions. Each is `{ label, route?,\n\t\t * href?, primary?, id? }`: `route` is a route name or location,\n\t\t * `href` an URL. An action with neither renders a button that emits\n\t\t * `action`. The first action is the primary one unless one sets\n\t\t * `primary: true`.\n\t\t *\n\t\t * @type {Array<{label: string, route?: (string|object), href?: string, primary?: boolean, id?: string}>|null}\n\t\t */\n\t\tactions: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Stored content blob (CnDashboardPage registry branch) carrying the\n\t\t * same keys as the flat props: `{ variant, text, visibleWhen, route,\n\t\t * layout, kicker, title, reason, actions }`. Explicit flat props win\n\t\t * on collision.\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 * Pre-evaluated `visibleWhen` outcome `{ met, value }`, injected by a\n\t\t * host that already ran the predicate (CnDashboardPage evaluates it\n\t\t * to know whether to collapse the banner's grid cell). When present,\n\t\t * the banner renders from this verdict instead of fetching again —\n\t\t * no duplicate request and no hidden-until-self-evaluated flash —\n\t\t * and `value` feeds the `{value}` text placeholder. `null` (the\n\t\t * default) keeps the banner self-evaluating. A host whose request\n\t\t * failed passes `{ met: false, value: null, failed: true, reason }`,\n\t\t * and an attention card then says it could not check.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tconditionOutcome: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\temits: [\n\t\t/**\n\t\t * Emitted when an attention-card action without a `route` or `href`\n\t\t * is clicked, so the host can run it.\n\t\t *\n\t\t * @event action\n\t\t * @type {{id: (string|undefined), label: string, index: number}}\n\t\t */\n\t\t'action',\n\t],\n\n\t/**\n\t * The banner's own state: the evaluated condition, the value it read,\n\t * and whether the request for it failed.\n\t *\n\t * @return {object} The state.\n\t * @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-an-attention-card-says-when-it-could-not-check\n\t */\n\tdata() {\n\t\treturn {\n\t\t\t/** Id that ties the attention card's region to its title. */\n\t\t\theadingId: `cn-banner-title-${nextUid()}`,\n\t\t\t/** Evaluated visibleWhen outcome (null = not yet evaluated). */\n\t\t\tconditionMet: null,\n\t\t\t/**\n\t\t\t * The raw field value the predicate read (from the injected\n\t\t\t * outcome or the banner's own evaluation) — interpolated into\n\t\t\t * the text wherever it says `{value}`.\n\t\t\t */\n\t\t\tconditionValue: null,\n\t\t\t/** Whether the visibleWhen request itself failed (not: answered \"no\"). */\n\t\t\tconditionFailed: false,\n\t\t\t/** Why it failed, as the request reported it. */\n\t\t\tconditionFailureReason: '',\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** The effective variant (prop → content → 'info'). */\n\t\tresolvedVariant() {\n\t\t\tconst v = this.variant || (this.content && this.content.variant) || 'info'\n\t\t\treturn VARIANTS.includes(v) ? v : 'info'\n\t\t},\n\n\t\t/** The effective banner text (prop → content → ''). */\n\t\tresolvedText() {\n\t\t\treturn this.text || (this.content && this.content.text) || ''\n\t\t},\n\n\t\t/** The effective visibleWhen condition (prop → content → null). */\n\t\tresolvedVisibleWhen() {\n\t\t\treturn this.visibleWhen || (this.content && this.content.visibleWhen) || null\n\t\t},\n\n\t\t/** The effective injected outcome (prop → content → null). */\n\t\tresolvedConditionOutcome() {\n\t\t\treturn this.conditionOutcome || (this.content && this.content.conditionOutcome) || null\n\t\t},\n\n\t\t/**\n\t\t * The rendered text: `resolvedText` with `{value}` replaced by the\n\t\t * predicate's field value once one is known. Without a known value\n\t\t * the text renders as written — a `{value}` placeholder only makes\n\t\t * sense on a conditional banner, which stays hidden until its\n\t\t * predicate (and therefore its value) resolves.\n\t\t *\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card\n\t\t */\n\t\tdisplayText() {\n\t\t\treturn this.fillValue(this.resolvedText)\n\t\t},\n\n\t\t/** The effective click-through route (prop → content → null). */\n\t\tresolvedRoute() {\n\t\t\treturn this.route || (this.content && this.content.route) || null\n\t\t},\n\n\t\t/**\n\t\t * Whether the attention card renders instead of the note card.\n\t\t *\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card\n\t\t */\n\t\tisAttention() {\n\t\t\treturn (this.layout || (this.content && this.content.layout) || 'banner') === 'attention'\n\t\t},\n\n\t\t/**\n\t\t * The attention card's kicker (prop, else content).\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card\n\t\t */\n\t\tresolvedKicker() {\n\t\t\tconst kicker = this.kicker || (this.content && this.content.kicker) || ''\n\t\t\treturn kicker\n\t\t},\n\n\t\t/**\n\t\t * The attention card's title: `title`, else `text`, with `{value}` filled in.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card\n\t\t */\n\t\tdisplayTitle() {\n\t\t\tconst title = this.title || (this.content && this.content.title) || ''\n\t\t\treturn title ? this.fillValue(title) : this.displayText\n\t\t},\n\n\t\t/**\n\t\t * The attention card's reason line. When the title already used\n\t\t * `text`, the text is not repeated here.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card\n\t\t */\n\t\tdisplayReason() {\n\t\t\tconst reason = this.reason || (this.content && this.content.reason) || ''\n\t\t\tif (reason) {\n\t\t\t\treturn this.fillValue(reason)\n\t\t\t}\n\t\t\tconst title = this.title || (this.content && this.content.title) || ''\n\t\t\t// A card that sets `text` to the same words as its `title` says\n\t\t\t// them once.\n\t\t\treturn title && this.displayText !== this.displayTitle ? this.displayText : ''\n\t\t},\n\n\t\t/**\n\t\t * The attention card's actions: the first two that have a label, each\n\t\t * with its href and whether it is the primary one.\n\t\t *\n\t\t * @return {Array<{key: string, id: (string|undefined), label: string, href: string, target: (object|null), primary: boolean, index: number}>}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card\n\t\t */\n\t\tresolvedActions() {\n\t\t\tconst raw = this.actions || (this.content && this.content.actions) || []\n\t\t\tif (!Array.isArray(raw)) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\tconst usable = raw\n\t\t\t\t.filter((action) => action && typeof action === 'object' && typeof action.label === 'string' && action.label !== '')\n\t\t\t\t.slice(0, MAX_ACTIONS)\n\t\t\tconst declaredPrimary = usable.findIndex((action) => action.primary === true)\n\t\t\tconst primaryIndex = declaredPrimary === -1 ? 0 : declaredPrimary\n\t\t\treturn usable.map((action, index) => {\n\t\t\t\tlet target = null\n\t\t\t\tlet href = ''\n\t\t\t\tif (action.route) {\n\t\t\t\t\ttarget = typeof action.route === 'string' ? { name: action.route } : action.route\n\t\t\t\t\thref = resolveHref(target, this.$router)\n\t\t\t\t} else if (typeof action.href === 'string' && action.href !== '') {\n\t\t\t\t\tconst safe = safeHref(action.href)\n\t\t\t\t\thref = safe === '#' ? '' : safe\n\t\t\t\t}\n\t\t\t\treturn {\n\t\t\t\t\tkey: `${index}-${action.id || action.label}`,\n\t\t\t\t\tid: action.id,\n\t\t\t\t\tlabel: action.label,\n\t\t\t\t\thref,\n\t\t\t\t\ttarget,\n\t\t\t\t\tprimary: index === primaryIndex,\n\t\t\t\t\tindex,\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Whether the banner renders: no condition = always; else the evaluated outcome.\n\t\t *\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card\n\t\t */\n\t\tvisible() {\n\t\t\tconst title = this.title || (this.content && this.content.title) || ''\n\t\t\tif (this.resolvedText === '' && !(this.isAttention && title !== '')) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif (!this.resolvedVisibleWhen) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn this.conditionMet === true\n\t\t},\n\n\t\t/**\n\t\t * Whether to say the check failed: an attention card, with words to\n\t\t * show, whose `visibleWhen` request failed. A plain banner stays\n\t\t * hidden on failure, as it always did.\n\t\t *\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-an-attention-card-says-when-it-could-not-check\n\t\t */\n\t\tcheckFailed() {\n\t\t\tconst title = this.title || (this.content && this.content.title) || ''\n\t\t\treturn this.isAttention\n\t\t\t\t&& !!this.resolvedVisibleWhen\n\t\t\t\t&& this.conditionFailed\n\t\t\t\t&& (title !== '' || this.resolvedText !== '')\n\t\t},\n\n\t\t/**\n\t\t * The one line shown when the check failed. It names the card by its\n\t\t * title, unless the title needs the value that could not be read.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-an-attention-card-says-when-it-could-not-check\n\t\t */\n\t\tuncheckedText() {\n\t\t\tconst title = this.title || (this.content && this.content.title) || this.resolvedText\n\t\t\tif (!title || title.includes('{value}')) {\n\t\t\t\treturn t('nextcloud-vue', 'Could not check')\n\t\t\t}\n\t\t\t// `escape: false`: the result is rendered through `{{ }}`, which escapes.\n\t\t\treturn t('nextcloud-vue', 'Could not check: {subject}', { subject: title }, undefined, { escape: false })\n\t\t},\n\n\t\t/**\n\t\t * The reason the check failed, for the tooltip and for assistive\n\t\t * technology.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-an-attention-card-says-when-it-could-not-check\n\t\t */\n\t\tfailureTooltip() {\n\t\t\tif (!this.conditionFailureReason) {\n\t\t\t\treturn t('nextcloud-vue', 'The check failed.')\n\t\t\t}\n\t\t\treturn t('nextcloud-vue', 'The check failed: {reason}', { reason: this.conditionFailureReason }, undefined, { escape: false })\n\t\t},\n\n\t\t/** Whether the banner navigates on click (route set + router present). */\n\t\tclickable() {\n\t\t\treturn !!this.resolvedRoute && !!this.$router\n\t\t},\n\n\t\t/** The click-through route as a router location (a string is a route name). */\n\t\trouteLocation() {\n\t\t\tconst route = this.resolvedRoute\n\t\t\treturn typeof route === 'string' ? { name: route } : route\n\t\t},\n\n\t\t/** The href of the click-through link. */\n\t\trouteHref() {\n\t\t\treturn resolveHref(this.routeLocation, this.$router)\n\t\t},\n\t},\n\n\twatch: {\n\t\tresolvedVisibleWhen: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.evaluateCondition()\n\t\t\t},\n\t\t},\n\n\t\tresolvedConditionOutcome() {\n\t\t\tthis.evaluateCondition()\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Replace `{value}` in a text by the value the predicate read, once\n\t\t * one is known.\n\t\t *\n\t\t * @param {string} text The text.\n\t\t * @return {string} The text with the value filled in.\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card\n\t\t */\n\t\tfillValue(text) {\n\t\t\tif (this.conditionValue === null || !text.includes('{value}')) {\n\t\t\t\treturn text\n\t\t\t}\n\t\t\treturn text.replaceAll('{value}', String(this.conditionValue))\n\t\t},\n\n\t\t/**\n\t\t * Run an attention-card action: route an in-app link, leave an\n\t\t * external link to the browser, and emit `action` for a button.\n\t\t *\n\t\t * @param {MouseEvent} event The click.\n\t\t * @param {object} action The resolved action.\n\t\t * @return {void}\n\t\t * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card\n\t\t */\n\t\tonActionClick(event, action) {\n\t\t\tif (action.target) {\n\t\t\t\tfollowLinkClick(event, action.target, this.$router)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!action.href) {\n\t\t\t\tthis.$emit('action', { id: action.id, label: action.label, index: action.index })\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Resolve the `visibleWhen` verdict. A host-injected\n\t\t * `conditionOutcome` wins outright — the host already ran the\n\t\t * predicate, so re-fetching would double the request and flash the\n\t\t * banner hidden until its own copy resolved. Otherwise evaluate\n\t\t * through the shared util primitives (endpoint / OpenRegister-source\n\t\t * modes — the Wave-1 banner shape is the canonical one, extracted to\n\t\t * `utils/visibleWhen.js` in Wave 3 so manifest actions reuse it),\n\t\t * keeping the read VALUE for the `{value}` text placeholder.\n\t\t * Fail-safe: any fetch/shape error leaves the banner hidden. The\n\t\t * failure is kept (`conditionFailed`, with its reason) so an\n\t\t * attention card can say it could not check.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-an-attention-card-says-when-it-could-not-check\n\t\t */\n\t\tasync evaluateCondition() {\n\t\t\tconst outcome = this.resolvedConditionOutcome\n\t\t\tif (outcome) {\n\t\t\t\tthis.conditionMet = outcome.met === true\n\t\t\t\tthis.conditionValue = outcome.value !== undefined ? outcome.value : null\n\t\t\t\tthis.conditionFailed = outcome.failed === true\n\t\t\t\tthis.conditionFailureReason = outcome.failed === true && typeof outcome.reason === 'string' ? outcome.reason : ''\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst cond = this.resolvedVisibleWhen\n\t\t\tif (!cond) {\n\t\t\t\tthis.conditionMet = null\n\t\t\t\tthis.conditionValue = null\n\t\t\t\tthis.conditionFailed = false\n\t\t\t\tthis.conditionFailureReason = ''\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst value = await readVisibleWhenValue(cond)\n\t\t\t\tthis.conditionValue = value\n\t\t\t\tthis.conditionMet = compareVisibleWhen(value, cond.op || 'eq', cond.value)\n\t\t\t\tthis.conditionFailed = false\n\t\t\t\tthis.conditionFailureReason = ''\n\t\t\t} catch (error) {\n\t\t\t\t// Still hidden: a banner that cannot tell whether it applies\n\t\t\t\t// should not claim attention. But not silently, because a\n\t\t\t\t// failed count and \"nothing to report\" look the same on screen.\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn('[CnBannerWidget] visibleWhen could not be evaluated, banner hidden:', error?.message || error)\n\t\t\t\tthis.conditionMet = false\n\t\t\t\tthis.conditionValue = null\n\t\t\t\tthis.conditionFailed = true\n\t\t\t\tthis.conditionFailureReason = typeof error?.message === 'string' ? error.message : String(error ?? '')\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Route a plain click on the link; a modified click opens the href\n\t\t * in the browser.\n\t\t *\n\t\t * @param {MouseEvent} event The click event.\n\t\t * @return {void}\n\t\t */\n\t\tonClick(event) {\n\t\t\tif (!this.clickable) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfollowLinkClick(event, this.routeLocation, this.$router)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-banner-widget {\n\twidth: 100%;\n}\n\n.cn-banner-widget__card {\n\tmargin: 0;\n}\n\n.cn-banner-widget__text--clickable {\n\tcursor: pointer;\n\ttext-decoration: underline;\n\tcolor: inherit;\n}\n\n/* The check failed. Muted text, no fill, no severity colour: it is a note\n that something is not known, not a warning. */\n.cn-banner-widget--unchecked {\n\tbox-sizing: border-box;\n\tmargin: 0;\n\tpadding: 8px 12px;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.9em;\n\tcursor: help;\n}\n\n/* The reason, for assistive technology; sighted readers get the tooltip. */\n.cn-banner-widget__unchecked-reason {\n\tposition: absolute;\n\twidth: 1px;\n\theight: 1px;\n\tmargin: -1px;\n\tpadding: 0;\n\toverflow: hidden;\n\tclip-path: inset(50%);\n\twhite-space: nowrap;\n\tborder: 0;\n}\n\n/* Attention card. The severity edge is an inset shadow, so it takes no width. */\n.cn-banner-widget--attention {\n\t--cn-banner-accent: var(--color-primary-element);\n\t--cn-banner-accent-text: var(--color-primary-element);\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 16px 24px;\n\tbox-sizing: border-box;\n\tpadding: 20px 24px 20px 28px;\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--border-radius-container, var(--border-radius-large, 12px));\n\tbackground: var(--color-main-background);\n\tbox-shadow: inset 4px 0 0 0 var(--cn-banner-accent);\n}\n\n.cn-banner-widget--attention.cn-banner-widget--warning {\n\t--cn-banner-accent: var(--color-element-warning, var(--color-warning));\n\t--cn-banner-accent-text: var(--color-text-warning, var(--color-warning-text, var(--color-main-text)));\n}\n\n.cn-banner-widget--attention.cn-banner-widget--error {\n\t--cn-banner-accent: var(--color-element-error, var(--color-error));\n\t--cn-banner-accent-text: var(--color-text-error, var(--color-error-text));\n}\n\n.cn-banner-widget--attention.cn-banner-widget--success {\n\t--cn-banner-accent: var(--color-element-success, var(--color-success));\n\t--cn-banner-accent-text: var(--color-text-success, var(--color-success-text));\n}\n\n.cn-banner-widget__body {\n\tdisplay: flex;\n\tflex: 1 1 320px;\n\tflex-direction: column;\n\tgap: 6px;\n\tmin-width: 0;\n}\n\n.cn-banner-widget__kicker {\n\tmargin: 0;\n\tfont-size: 0.85em;\n\tfont-weight: 700;\n\tletter-spacing: 0.06em;\n\ttext-transform: uppercase;\n\tcolor: var(--cn-banner-accent-text);\n}\n\n.cn-banner-widget__title {\n\tmargin: 0;\n\tfont-size: 1.3em;\n\tfont-weight: 700;\n\tline-height: 1.3;\n\tcolor: var(--color-main-text);\n}\n\n.cn-banner-widget__reason {\n\tmargin: 0;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-banner-widget__actions {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 10px;\n}\n\n.cn-banner-widget__action {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tjustify-content: center;\n\tbox-sizing: border-box;\n\tmin-height: var(--default-clickable-area, 40px);\n\tmargin: 0;\n\tpadding: 0 16px;\n\tborder: 1px solid var(--color-border-maxcontrast);\n\tborder-radius: var(--border-radius-element, var(--border-radius-large, 8px));\n\tfont: inherit;\n\tfont-weight: 600;\n\ttext-decoration: none;\n\tcursor: pointer;\n}\n\n.cn-banner-widget__action--secondary {\n\tbackground: var(--color-main-background);\n\tcolor: var(--color-main-text);\n}\n\n.cn-banner-widget__action--secondary:hover {\n\tbackground: var(--color-background-hover);\n}\n\n.cn-banner-widget__action--primary {\n\tborder-color: var(--color-primary-element);\n\tbackground: var(--color-primary-element);\n\tcolor: var(--color-primary-element-text);\n}\n\n.cn-banner-widget__action--primary:hover {\n\tborder-color: var(--color-primary-element-hover);\n\tbackground: var(--color-primary-element-hover);\n}\n\n.cn-banner-widget__action:focus-visible {\n\toutline: 2px solid var(--color-main-text);\n\toutline-offset: 2px;\n}\n</style>\n"],"names":["_createElementBlock","_normalizeClass","_createElementVNode","_toDisplayString","_openBlock","_Fragment","_renderList","_resolveDynamicComponent","_createTextVNode","_createVNode","_createBlock"],"mappings":"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"}
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * CnBrandStripe draws the organisation's brand stripe: up to three coloured
3
- * bands next to each other, for the top of a page, a card or a sidebar.
3
+ * bands next to each other, or the organisation's own motif, for the top of a
4
+ * page, a card or a sidebar.
4
5
  *
5
6
  * The theme decides what it looks like, through the library's own custom
6
7
  * properties: `--cn-brand-stripe-color-1/2/3` (the colours, start to end),
@@ -9,6 +10,12 @@
9
10
  * tokens onto these; the library never reads a theme's tokens. Without them the
10
11
  * stripe is one band in the Nextcloud primary colour, 4px high.
11
12
  *
13
+ * A theme whose motif is not three bands (hanging twigs, a canal and its bank,
14
+ * a slanted cut) names it in `--cn-brand-stripe-image`, any background-image
15
+ * value; the stripe then draws that image in the same box instead of the
16
+ * bands. `--cn-brand-stripe-image-inverse` is the same motif for a dark band;
17
+ * the `inverse` variant draws it, else the one image, else the bands.
18
+ *
12
19
  * The stripe is decoration: it carries no meaning and is hidden from
13
20
  * assistive technology.
14
21
  *
@@ -32,6 +39,19 @@ var script = {
32
39
  default: 'horizontal',
33
40
  validator: (v) => ['horizontal', 'vertical'].includes(v),
34
41
  },
42
+
43
+ /**
44
+ * The ground the stripe sits on: `default` (a light surface) or
45
+ * `inverse` (a dark band such as a footer), which draws the theme's
46
+ * `--cn-brand-stripe-image-inverse` when it names one.
47
+ *
48
+ * @type {'default'|'inverse'}
49
+ */
50
+ variant: {
51
+ type: String,
52
+ default: 'default',
53
+ validator: (v) => ['default', 'inverse'].includes(v),
54
+ },
35
55
  },
36
56
  };
37
57
 
@@ -1 +1 @@
1
- {"version":3,"file":"CnBrandStripe.vue2.js","sources":["../../../../src/components/CnBrandStripe/CnBrandStripe.vue"],"sourcesContent":["<!--\n SPDX-FileCopyrightText: 2026 Conduction B.V.\n SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<div\n\t\tclass=\"cn-brand-stripe\"\n\t\t:class=\"{ 'cn-brand-stripe--vertical': orientation === 'vertical' }\"\n\t\taria-hidden=\"true\"\n\t\tdata-testid=\"cn-brand-stripe\">\n\t\t<span class=\"cn-brand-stripe__band cn-brand-stripe__band--1\" />\n\t\t<span class=\"cn-brand-stripe__band cn-brand-stripe__band--2\" />\n\t\t<span class=\"cn-brand-stripe__band cn-brand-stripe__band--3\" />\n\t</div>\n</template>\n\n<script>\n/**\n * CnBrandStripe draws the organisation's brand stripe: up to three coloured\n * bands next to each other, for the top of a page, a card or a sidebar.\n *\n * The theme decides what it looks like, through the library's own custom\n * properties: `--cn-brand-stripe-color-1/2/3` (the colours, start to end),\n * `--cn-brand-stripe-ratio-1/2/3` (unitless shares, such as 6, 3 and 1)\n * and `--cn-brand-stripe-height` (such as 5px). A theming app maps its own\n * tokens onto these; the library never reads a theme's tokens. Without them the\n * stripe is one band in the Nextcloud primary colour, 4px high.\n *\n * The stripe is decoration: it carries no meaning and is hidden from\n * assistive technology.\n *\n * ```vue\n * <CnBrandStripe />\n * ```\n */\nexport default {\n\tname: 'CnBrandStripe',\n\n\tprops: {\n\t\t/**\n\t\t * The direction the stripe runs in: `horizontal` (the default, a bar\n\t\t * across the top) or `vertical` (a bar down the side; the height\n\t\t * token then sets its width).\n\t\t *\n\t\t * @type {'horizontal'|'vertical'}\n\t\t */\n\t\torientation: {\n\t\t\ttype: String,\n\t\t\tdefault: 'horizontal',\n\t\t\tvalidator: (v) => ['horizontal', 'vertical'].includes(v),\n\t\t},\n\t},\n}\n</script>\n\n<!--\n The stripe reads the library's own `--cn-brand-stripe-*` properties, each\n with a Nextcloud fallback. A theming app sets them from its own tokens.\n-->\n<style scoped>\n.cn-brand-stripe {\n\tdisplay: flex;\n\tflex: none;\n\twidth: 100%;\n\theight: var(--cn-brand-stripe-height, 4px);\n}\n\n.cn-brand-stripe--vertical {\n\tflex-direction: column;\n\twidth: var(--cn-brand-stripe-height, 4px);\n\theight: 100%;\n}\n\n.cn-brand-stripe__band {\n\tflex-basis: 0;\n\tflex-shrink: 1;\n}\n\n.cn-brand-stripe__band--1 {\n\tflex-grow: var(--cn-brand-stripe-ratio-1, 1);\n\tbackground-color: var(--cn-brand-stripe-color-1, var(--color-primary-element));\n}\n\n.cn-brand-stripe__band--2 {\n\tflex-grow: var(--cn-brand-stripe-ratio-2, 1);\n\tbackground-color: var(--cn-brand-stripe-color-2, var(--color-primary-element));\n}\n\n.cn-brand-stripe__band--3 {\n\tflex-grow: var(--cn-brand-stripe-ratio-3, 1);\n\tbackground-color: var(--cn-brand-stripe-color-3, var(--color-primary-element));\n}\n\n@media print {\n\t.cn-brand-stripe {\n\t\tprint-color-adjust: exact;\n\t\t-webkit-print-color-adjust: exact;\n\t}\n}\n</style>\n"],"names":[],"mappings":"AAiBA;AACC;;AAEA;;AAEA;;AAEA;AACA;;AAEA;;AAEA;AACA;AACA;AACA;AACA;;AAED,aAAe;AACd,CAAA,IAAI,EAAE,eAAe;;AAErB,CAAA,KAAK,EAAE;AACN;;;;AAIC;AACA;;EAED,WAAW,EAAE;GACZ,IAAI,EAAE,MAAM;AACZ,GAAA,OAAO,EAAE,YAAY;GACrB,SAAS,EAAE,CAAC,CAAC,KAAK,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;GACxD;EACD;AACF;;;;"}
1
+ {"version":3,"file":"CnBrandStripe.vue2.js","sources":["../../../../src/components/CnBrandStripe/CnBrandStripe.vue"],"sourcesContent":["<!--\n SPDX-FileCopyrightText: 2026 Conduction B.V.\n SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<div\n\t\tclass=\"cn-brand-stripe\"\n\t\t:class=\"{\n\t\t\t'cn-brand-stripe--vertical': orientation === 'vertical',\n\t\t\t'cn-brand-stripe--inverse': variant === 'inverse',\n\t\t}\"\n\t\taria-hidden=\"true\"\n\t\tdata-testid=\"cn-brand-stripe\">\n\t\t<span class=\"cn-brand-stripe__band cn-brand-stripe__band--1\" />\n\t\t<span class=\"cn-brand-stripe__band cn-brand-stripe__band--2\" />\n\t\t<span class=\"cn-brand-stripe__band cn-brand-stripe__band--3\" />\n\t</div>\n</template>\n\n<script>\n/**\n * CnBrandStripe draws the organisation's brand stripe: up to three coloured\n * bands next to each other, or the organisation's own motif, for the top of a\n * page, a card or a sidebar.\n *\n * The theme decides what it looks like, through the library's own custom\n * properties: `--cn-brand-stripe-color-1/2/3` (the colours, start to end),\n * `--cn-brand-stripe-ratio-1/2/3` (unitless shares, such as 6, 3 and 1)\n * and `--cn-brand-stripe-height` (such as 5px). A theming app maps its own\n * tokens onto these; the library never reads a theme's tokens. Without them the\n * stripe is one band in the Nextcloud primary colour, 4px high.\n *\n * A theme whose motif is not three bands (hanging twigs, a canal and its bank,\n * a slanted cut) names it in `--cn-brand-stripe-image`, any background-image\n * value; the stripe then draws that image in the same box instead of the\n * bands. `--cn-brand-stripe-image-inverse` is the same motif for a dark band;\n * the `inverse` variant draws it, else the one image, else the bands.\n *\n * The stripe is decoration: it carries no meaning and is hidden from\n * assistive technology.\n *\n * ```vue\n * <CnBrandStripe />\n * ```\n */\nexport default {\n\tname: 'CnBrandStripe',\n\n\tprops: {\n\t\t/**\n\t\t * The direction the stripe runs in: `horizontal` (the default, a bar\n\t\t * across the top) or `vertical` (a bar down the side; the height\n\t\t * token then sets its width).\n\t\t *\n\t\t * @type {'horizontal'|'vertical'}\n\t\t */\n\t\torientation: {\n\t\t\ttype: String,\n\t\t\tdefault: 'horizontal',\n\t\t\tvalidator: (v) => ['horizontal', 'vertical'].includes(v),\n\t\t},\n\n\t\t/**\n\t\t * The ground the stripe sits on: `default` (a light surface) or\n\t\t * `inverse` (a dark band such as a footer), which draws the theme's\n\t\t * `--cn-brand-stripe-image-inverse` when it names one.\n\t\t *\n\t\t * @type {'default'|'inverse'}\n\t\t */\n\t\tvariant: {\n\t\t\ttype: String,\n\t\t\tdefault: 'default',\n\t\t\tvalidator: (v) => ['default', 'inverse'].includes(v),\n\t\t},\n\t},\n}\n</script>\n\n<!--\n The stripe reads the library's own `--cn-brand-stripe-*` properties, each\n with a Nextcloud fallback. A theming app sets them from its own tokens.\n\n THE ROOT PAINTS, NOT THE BANDS. The three bands are one background on the\n root, a gradient with hard stops computed from the ratios, so a theme's\n motif image can take its place with one `var()` fallback. A band painted on\n its own element could not be switched off by the presence of an image: CSS\n cannot ask whether a custom property is set. The band elements stay, empty,\n so markup and tests that count them keep working.\n-->\n<style scoped>\n.cn-brand-stripe {\n\t--cn-brand-stripe-first: var(--cn-brand-stripe-ratio-1, 1);\n\t--cn-brand-stripe-second: var(--cn-brand-stripe-ratio-2, 1);\n\t--cn-brand-stripe-total: calc(\n\t\tvar(--cn-brand-stripe-first) + var(--cn-brand-stripe-second) + var(--cn-brand-stripe-ratio-3, 1)\n\t);\n\t--cn-brand-stripe-stop-1: calc(var(--cn-brand-stripe-first) / var(--cn-brand-stripe-total) * 100%);\n\t--cn-brand-stripe-stop-2: calc(\n\t\t(var(--cn-brand-stripe-first) + var(--cn-brand-stripe-second)) / var(--cn-brand-stripe-total) * 100%\n\t);\n\t--cn-brand-stripe-direction: to right;\n\t--cn-brand-stripe-bands: linear-gradient(\n\t\tvar(--cn-brand-stripe-direction),\n\t\tvar(--cn-brand-stripe-color-1, var(--color-primary-element)) 0 var(--cn-brand-stripe-stop-1),\n\t\tvar(--cn-brand-stripe-color-2, var(--color-primary-element)) var(--cn-brand-stripe-stop-1) var(--cn-brand-stripe-stop-2),\n\t\tvar(--cn-brand-stripe-color-3, var(--color-primary-element)) var(--cn-brand-stripe-stop-2) 100%\n\t);\n\tdisplay: flex;\n\tflex: none;\n\twidth: 100%;\n\theight: var(--cn-brand-stripe-height, 4px);\n\tbackground-image: var(--cn-brand-stripe-image, var(--cn-brand-stripe-bands));\n\tbackground-repeat: repeat-x;\n\tbackground-size: auto 100%;\n}\n\n.cn-brand-stripe--inverse {\n\tbackground-image: var(--cn-brand-stripe-image-inverse, var(--cn-brand-stripe-image, var(--cn-brand-stripe-bands)));\n}\n\n.cn-brand-stripe--vertical {\n\t--cn-brand-stripe-direction: to bottom;\n\tflex-direction: column;\n\twidth: var(--cn-brand-stripe-height, 4px);\n\theight: 100%;\n\tbackground-repeat: repeat-y;\n\tbackground-size: 100% auto;\n}\n\n.cn-brand-stripe__band {\n\tflex-basis: 0;\n\tflex-shrink: 1;\n}\n\n.cn-brand-stripe__band--1 {\n\tflex-grow: var(--cn-brand-stripe-ratio-1, 1);\n}\n\n.cn-brand-stripe__band--2 {\n\tflex-grow: var(--cn-brand-stripe-ratio-2, 1);\n}\n\n.cn-brand-stripe__band--3 {\n\tflex-grow: var(--cn-brand-stripe-ratio-3, 1);\n}\n\n@media print {\n\t.cn-brand-stripe {\n\t\tprint-color-adjust: exact;\n\t\t-webkit-print-color-adjust: exact;\n\t}\n}\n</style>\n"],"names":[],"mappings":"AAoBA;AACC;;AAEA;AACA;;AAEA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;;AAEA;;AAEA;AACA;AACA;AACA;AACA;;AAED,aAAe;AACd,CAAA,IAAI,EAAE,eAAe;;AAErB,CAAA,KAAK,EAAE;AACN;;;;AAIC;AACA;;EAED,WAAW,EAAE;GACZ,IAAI,EAAE,MAAM;AACZ,GAAA,OAAO,EAAE,YAAY;GACrB,SAAS,EAAE,CAAC,CAAC,KAAK,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;GACxD;;AAED;;;;AAIC;;;AAGD,EAAA,OAAO,EAAE;GACR,IAAI,EAAE,MAAM;AACZ,GAAA,OAAO,EAAE,SAAS;GAClB,SAAS,EAAE,CAAC,CAAC,KAAK,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;GACpD;EACD;AACF;;;;"}
@@ -2,7 +2,10 @@ import { openBlock, createElementBlock, normalizeClass, createElementVNode } fro
2
2
 
3
3
  function render(_ctx, _cache, $props, $setup, $data, $options) {
4
4
  return (openBlock(), createElementBlock("div", {
5
- class: normalizeClass(["cn-brand-stripe", { 'cn-brand-stripe--vertical': $props.orientation === 'vertical' }]),
5
+ class: normalizeClass(["cn-brand-stripe", {
6
+ 'cn-brand-stripe--vertical': $props.orientation === 'vertical',
7
+ 'cn-brand-stripe--inverse': $props.variant === 'inverse',
8
+ }]),
6
9
  "aria-hidden": "true",
7
10
  "data-testid": "cn-brand-stripe"
8
11
  }, [...(_cache[0] || (_cache[0] = [
@@ -1 +1 @@
1
- {"version":3,"file":"CnBrandStripe.vue3.js","sources":["../../../../src/components/CnBrandStripe/CnBrandStripe.vue?vue&type=template&id=6fdfa763&lang.js"],"sourcesContent":["<!--\n SPDX-FileCopyrightText: 2026 Conduction B.V.\n SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<div\n\t\tclass=\"cn-brand-stripe\"\n\t\t:class=\"{ 'cn-brand-stripe--vertical': orientation === 'vertical' }\"\n\t\taria-hidden=\"true\"\n\t\tdata-testid=\"cn-brand-stripe\">\n\t\t<span class=\"cn-brand-stripe__band cn-brand-stripe__band--1\" />\n\t\t<span class=\"cn-brand-stripe__band cn-brand-stripe__band--2\" />\n\t\t<span class=\"cn-brand-stripe__band cn-brand-stripe__band--3\" />\n\t</div>\n</template>\n\n<script>\n/**\n * CnBrandStripe draws the organisation's brand stripe: up to three coloured\n * bands next to each other, for the top of a page, a card or a sidebar.\n *\n * The theme decides what it looks like, through the library's own custom\n * properties: `--cn-brand-stripe-color-1/2/3` (the colours, start to end),\n * `--cn-brand-stripe-ratio-1/2/3` (unitless shares, such as 6, 3 and 1)\n * and `--cn-brand-stripe-height` (such as 5px). A theming app maps its own\n * tokens onto these; the library never reads a theme's tokens. Without them the\n * stripe is one band in the Nextcloud primary colour, 4px high.\n *\n * The stripe is decoration: it carries no meaning and is hidden from\n * assistive technology.\n *\n * ```vue\n * <CnBrandStripe />\n * ```\n */\nexport default {\n\tname: 'CnBrandStripe',\n\n\tprops: {\n\t\t/**\n\t\t * The direction the stripe runs in: `horizontal` (the default, a bar\n\t\t * across the top) or `vertical` (a bar down the side; the height\n\t\t * token then sets its width).\n\t\t *\n\t\t * @type {'horizontal'|'vertical'}\n\t\t */\n\t\torientation: {\n\t\t\ttype: String,\n\t\t\tdefault: 'horizontal',\n\t\t\tvalidator: (v) => ['horizontal', 'vertical'].includes(v),\n\t\t},\n\t},\n}\n</script>\n\n<!--\n The stripe reads the library's own `--cn-brand-stripe-*` properties, each\n with a Nextcloud fallback. A theming app sets them from its own tokens.\n-->\n<style scoped>\n.cn-brand-stripe {\n\tdisplay: flex;\n\tflex: none;\n\twidth: 100%;\n\theight: var(--cn-brand-stripe-height, 4px);\n}\n\n.cn-brand-stripe--vertical {\n\tflex-direction: column;\n\twidth: var(--cn-brand-stripe-height, 4px);\n\theight: 100%;\n}\n\n.cn-brand-stripe__band {\n\tflex-basis: 0;\n\tflex-shrink: 1;\n}\n\n.cn-brand-stripe__band--1 {\n\tflex-grow: var(--cn-brand-stripe-ratio-1, 1);\n\tbackground-color: var(--cn-brand-stripe-color-1, var(--color-primary-element));\n}\n\n.cn-brand-stripe__band--2 {\n\tflex-grow: var(--cn-brand-stripe-ratio-2, 1);\n\tbackground-color: var(--cn-brand-stripe-color-2, var(--color-primary-element));\n}\n\n.cn-brand-stripe__band--3 {\n\tflex-grow: var(--cn-brand-stripe-ratio-3, 1);\n\tbackground-color: var(--cn-brand-stripe-color-3, var(--color-primary-element));\n}\n\n@media print {\n\t.cn-brand-stripe {\n\t\tprint-color-adjust: exact;\n\t\t-webkit-print-color-adjust: exact;\n\t}\n}\n</style>\n"],"names":["_normalizeClass"],"mappings":";;;;IAUE,KAAM,EAAAA,cAAA,CAAA,CAAA,iBAAiB,EAAA,EAAA,2BAAA,EACgB,MAAW,CAAA,WAAA,KAAA,UAAA,EAAA,CAAA,CAAA;AAClD,IAAA,aAAW,EAAC,MAAM;AAClB,IAAA,aAAI,EAAA,iBAAA;;;;;;;;;;"}
1
+ {"version":3,"file":"CnBrandStripe.vue3.js","sources":["../../../../src/components/CnBrandStripe/CnBrandStripe.vue?vue&type=template&id=6fdfa763&lang.js"],"sourcesContent":["<!--\n SPDX-FileCopyrightText: 2026 Conduction B.V.\n SPDX-License-Identifier: EUPL-1.2\n-->\n<template>\n\t<div\n\t\tclass=\"cn-brand-stripe\"\n\t\t:class=\"{\n\t\t\t'cn-brand-stripe--vertical': orientation === 'vertical',\n\t\t\t'cn-brand-stripe--inverse': variant === 'inverse',\n\t\t}\"\n\t\taria-hidden=\"true\"\n\t\tdata-testid=\"cn-brand-stripe\">\n\t\t<span class=\"cn-brand-stripe__band cn-brand-stripe__band--1\" />\n\t\t<span class=\"cn-brand-stripe__band cn-brand-stripe__band--2\" />\n\t\t<span class=\"cn-brand-stripe__band cn-brand-stripe__band--3\" />\n\t</div>\n</template>\n\n<script>\n/**\n * CnBrandStripe draws the organisation's brand stripe: up to three coloured\n * bands next to each other, or the organisation's own motif, for the top of a\n * page, a card or a sidebar.\n *\n * The theme decides what it looks like, through the library's own custom\n * properties: `--cn-brand-stripe-color-1/2/3` (the colours, start to end),\n * `--cn-brand-stripe-ratio-1/2/3` (unitless shares, such as 6, 3 and 1)\n * and `--cn-brand-stripe-height` (such as 5px). A theming app maps its own\n * tokens onto these; the library never reads a theme's tokens. Without them the\n * stripe is one band in the Nextcloud primary colour, 4px high.\n *\n * A theme whose motif is not three bands (hanging twigs, a canal and its bank,\n * a slanted cut) names it in `--cn-brand-stripe-image`, any background-image\n * value; the stripe then draws that image in the same box instead of the\n * bands. `--cn-brand-stripe-image-inverse` is the same motif for a dark band;\n * the `inverse` variant draws it, else the one image, else the bands.\n *\n * The stripe is decoration: it carries no meaning and is hidden from\n * assistive technology.\n *\n * ```vue\n * <CnBrandStripe />\n * ```\n */\nexport default {\n\tname: 'CnBrandStripe',\n\n\tprops: {\n\t\t/**\n\t\t * The direction the stripe runs in: `horizontal` (the default, a bar\n\t\t * across the top) or `vertical` (a bar down the side; the height\n\t\t * token then sets its width).\n\t\t *\n\t\t * @type {'horizontal'|'vertical'}\n\t\t */\n\t\torientation: {\n\t\t\ttype: String,\n\t\t\tdefault: 'horizontal',\n\t\t\tvalidator: (v) => ['horizontal', 'vertical'].includes(v),\n\t\t},\n\n\t\t/**\n\t\t * The ground the stripe sits on: `default` (a light surface) or\n\t\t * `inverse` (a dark band such as a footer), which draws the theme's\n\t\t * `--cn-brand-stripe-image-inverse` when it names one.\n\t\t *\n\t\t * @type {'default'|'inverse'}\n\t\t */\n\t\tvariant: {\n\t\t\ttype: String,\n\t\t\tdefault: 'default',\n\t\t\tvalidator: (v) => ['default', 'inverse'].includes(v),\n\t\t},\n\t},\n}\n</script>\n\n<!--\n The stripe reads the library's own `--cn-brand-stripe-*` properties, each\n with a Nextcloud fallback. A theming app sets them from its own tokens.\n\n THE ROOT PAINTS, NOT THE BANDS. The three bands are one background on the\n root, a gradient with hard stops computed from the ratios, so a theme's\n motif image can take its place with one `var()` fallback. A band painted on\n its own element could not be switched off by the presence of an image: CSS\n cannot ask whether a custom property is set. The band elements stay, empty,\n so markup and tests that count them keep working.\n-->\n<style scoped>\n.cn-brand-stripe {\n\t--cn-brand-stripe-first: var(--cn-brand-stripe-ratio-1, 1);\n\t--cn-brand-stripe-second: var(--cn-brand-stripe-ratio-2, 1);\n\t--cn-brand-stripe-total: calc(\n\t\tvar(--cn-brand-stripe-first) + var(--cn-brand-stripe-second) + var(--cn-brand-stripe-ratio-3, 1)\n\t);\n\t--cn-brand-stripe-stop-1: calc(var(--cn-brand-stripe-first) / var(--cn-brand-stripe-total) * 100%);\n\t--cn-brand-stripe-stop-2: calc(\n\t\t(var(--cn-brand-stripe-first) + var(--cn-brand-stripe-second)) / var(--cn-brand-stripe-total) * 100%\n\t);\n\t--cn-brand-stripe-direction: to right;\n\t--cn-brand-stripe-bands: linear-gradient(\n\t\tvar(--cn-brand-stripe-direction),\n\t\tvar(--cn-brand-stripe-color-1, var(--color-primary-element)) 0 var(--cn-brand-stripe-stop-1),\n\t\tvar(--cn-brand-stripe-color-2, var(--color-primary-element)) var(--cn-brand-stripe-stop-1) var(--cn-brand-stripe-stop-2),\n\t\tvar(--cn-brand-stripe-color-3, var(--color-primary-element)) var(--cn-brand-stripe-stop-2) 100%\n\t);\n\tdisplay: flex;\n\tflex: none;\n\twidth: 100%;\n\theight: var(--cn-brand-stripe-height, 4px);\n\tbackground-image: var(--cn-brand-stripe-image, var(--cn-brand-stripe-bands));\n\tbackground-repeat: repeat-x;\n\tbackground-size: auto 100%;\n}\n\n.cn-brand-stripe--inverse {\n\tbackground-image: var(--cn-brand-stripe-image-inverse, var(--cn-brand-stripe-image, var(--cn-brand-stripe-bands)));\n}\n\n.cn-brand-stripe--vertical {\n\t--cn-brand-stripe-direction: to bottom;\n\tflex-direction: column;\n\twidth: var(--cn-brand-stripe-height, 4px);\n\theight: 100%;\n\tbackground-repeat: repeat-y;\n\tbackground-size: 100% auto;\n}\n\n.cn-brand-stripe__band {\n\tflex-basis: 0;\n\tflex-shrink: 1;\n}\n\n.cn-brand-stripe__band--1 {\n\tflex-grow: var(--cn-brand-stripe-ratio-1, 1);\n}\n\n.cn-brand-stripe__band--2 {\n\tflex-grow: var(--cn-brand-stripe-ratio-2, 1);\n}\n\n.cn-brand-stripe__band--3 {\n\tflex-grow: var(--cn-brand-stripe-ratio-3, 1);\n}\n\n@media print {\n\t.cn-brand-stripe {\n\t\tprint-color-adjust: exact;\n\t\t-webkit-print-color-adjust: exact;\n\t}\n}\n</style>\n"],"names":[],"mappings":";;;;AAUE,IAAA,KAAC,kBAAA,iBAAA,EAAA;gCACgB,MAAA,CAAA,WAAA,KAAA,UAAA;+BAAA,MAAA,CAAA,OAAA,KAAA,SAAA;;AAIjB,IAAA,aAAW,EAAC,MAAM;AAClB,IAAA,aAAI,EAAA,iBAAA;;;;;;;;;;"}
@@ -2063,17 +2063,30 @@ var script = {
2063
2063
  * the registry branch forwards to CnBannerWidget), or in the legacy
2064
2064
  * manifest `props` blob (the fallback getStatsBlockProps and
2065
2065
  * getChartProps already honour for their own widget types). `text`
2066
- * is only meaningful for banners.
2066
+ * is only meaningful for banners: it is what the banner would show,
2067
+ * which for an attention card is its `title` when it has no `text`.
2067
2068
  *
2068
2069
  * @param {object} def Widget definition.
2069
- * @return {{ text: string, visibleWhen: (object|null) }}
2070
+ * `layout` is returned so the page can tell an attention card from a
2071
+ * plain banner: only the former keeps its cell when its check fails.
2072
+ *
2073
+ * @spec openspec/changes/link-cards-page/specs/link-cards-page/spec.md#requirement-an-attention-card-with-a-title-keeps-its-cell
2074
+ * @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-an-attention-card-says-when-it-could-not-check
2075
+ * @return {{ text: string, visibleWhen: (object|null), layout: string }}
2070
2076
  */
2071
2077
  widgetDisplayConfig(def) {
2072
2078
  const content = (def && typeof def.content === 'object' && def.content) || {};
2073
2079
  const props = (def && typeof def.props === 'object' && def.props) || {};
2080
+ // An attention card (`layout: "attention"`) is headed by `title`,
2081
+ // and its title only FALLS BACK to `text`. Reading `text` alone
2082
+ // called a card with a title and a reason empty, and gave its cell
2083
+ // up before its `visibleWhen` was even looked at.
2084
+ const layout = content.layout || props.layout || '';
2085
+ const title = layout === 'attention' ? (content.title || props.title || '') : '';
2074
2086
  return {
2075
- text: content.text || props.text || '',
2087
+ text: content.text || props.text || title || '',
2076
2088
  visibleWhen: (def && def.visibleWhen) || content.visibleWhen || props.visibleWhen || null,
2089
+ layout,
2077
2090
  }
2078
2091
  },
2079
2092
 
@@ -2083,15 +2096,22 @@ var script = {
2083
2096
  * text at all — and must therefore surrender its grid cell in live
2084
2097
  * mode instead of leaving an empty card behind.
2085
2098
  *
2099
+ * One exception: an attention card whose `visibleWhen` request FAILED
2100
+ * keeps its cell, so the card can say it could not check. A collapsed
2101
+ * cell reads as "nothing needs attention", which a failed count does
2102
+ * not know. A count that was read and does not meet the condition
2103
+ * still collapses.
2104
+ *
2086
2105
  * @param {object} item Layout item.
2087
2106
  * @return {boolean} true when the cell must collapse.
2107
+ * @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-an-attention-card-says-when-it-could-not-check
2088
2108
  */
2089
2109
  isCollapsedWidget(item) {
2090
2110
  const def = this.getWidgetDef(item.widgetId);
2091
2111
  if (!def || !def.type) {
2092
2112
  return false
2093
2113
  }
2094
- const { text, visibleWhen } = this.widgetDisplayConfig(def);
2114
+ const { text, visibleWhen, layout } = this.widgetDisplayConfig(def);
2095
2115
  if (this.isBannerDef(def) && text === '') {
2096
2116
  return true
2097
2117
  }
@@ -2099,6 +2119,9 @@ var script = {
2099
2119
  return false
2100
2120
  }
2101
2121
  const outcome = this.widgetConditionOutcome[item.widgetId];
2122
+ if (outcome && outcome.failed === true && layout === 'attention' && this.isBannerDef(def)) {
2123
+ return false
2124
+ }
2102
2125
  return !outcome || outcome.met !== true
2103
2126
  },
2104
2127
 
@@ -2109,13 +2132,17 @@ var script = {
2109
2132
  * Runs on created() and again whenever `widgets` changes (the
2110
2133
  * in-app editor mutates defs in place). Fail-safe like the banner
2111
2134
  * itself: any fetch/shape error counts as "not met", so a broken
2112
- * predicate collapses the cell rather than breaking the page.
2135
+ * predicate collapses the cell rather than breaking the page. The
2136
+ * outcome of a failed read carries `failed: true` and the `reason`,
2137
+ * which `isCollapsedWidget` and CnBannerWidget use to show an
2138
+ * attention card's "Could not check" line instead of nothing.
2113
2139
  *
2114
2140
  * Flips `widgetConditionsSettled` when the initial round is done —
2115
2141
  * synchronously when there is nothing to evaluate, so a dashboard
2116
2142
  * without conditional widgets never waits.
2117
2143
  *
2118
2144
  * @return {Promise<void>}
2145
+ * @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-an-attention-card-says-when-it-could-not-check
2119
2146
  */
2120
2147
  async evaluateWidgetConditions() {
2121
2148
  const conditional = (this.widgets || []).filter((def) => def && def.id && def.type
@@ -2137,12 +2164,17 @@ var script = {
2137
2164
  }
2138
2165
  await Promise.all(conditional.map(async (def) => {
2139
2166
  const cond = this.widgetDisplayConfig(def).visibleWhen;
2140
- let outcome = { met: false, value: null };
2167
+ let outcome;
2141
2168
  try {
2142
2169
  const value = await readVisibleWhenValue(cond);
2143
2170
  outcome = { met: compareVisibleWhen(value, cond.op || 'eq', cond.value), value };
2144
- } catch {
2145
- // fail-safe: hidden
2171
+ } catch (error) {
2172
+ // Fail-safe: not met. But the failure is kept, and said
2173
+ // out loud: a failed count and "nothing to report" look
2174
+ // the same on screen.
2175
+ outcome = { met: false, value: null, failed: true, reason: typeof error?.message === 'string' ? error.message : String(error ?? '') };
2176
+ // eslint-disable-next-line no-console
2177
+ console.warn(`[CnDashboardPage] visibleWhen of widget "${def.id}" could not be evaluated:`, error?.message || error);
2146
2178
  }
2147
2179
  // A newer run owns the map now — a stale verdict (possibly for
2148
2180
  // a def that no longer exists) must not overwrite its writes.