@conduction/nextcloud-vue 2.145.0 → 2.147.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/esm/components/CnAiCompanion/CnAiChatPanel.vue2.js.map +1 -1
  2. package/dist/esm/components/CnAiCompanion/CnAiChatPanel.vue3.js.map +1 -1
  3. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue.js +1 -1
  4. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue2.js.map +1 -1
  5. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue3.js.map +1 -1
  6. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +12 -2
  7. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  8. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +42 -30
  9. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  10. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +22 -0
  11. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  12. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +2 -1
  13. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  14. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue.js +1 -1
  15. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue2.js.map +1 -1
  16. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue3.js.map +1 -1
  17. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  18. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +112 -15
  19. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  20. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +40 -16
  21. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  22. package/dist/esm/components/CnNotesCard/CnNotesCard.vue.js +1 -1
  23. package/dist/esm/components/CnNotesCard/CnNotesCard.vue2.js.map +1 -1
  24. package/dist/esm/components/CnNotesCard/CnNotesCard.vue3.js +1 -0
  25. package/dist/esm/components/CnNotesCard/CnNotesCard.vue3.js.map +1 -1
  26. package/dist/esm/components/CnNotificationMatrix/CnNotificationMatrix.vue.js +1 -1
  27. package/dist/esm/components/CnNotificationMatrix/CnNotificationMatrix.vue2.js +21 -2
  28. package/dist/esm/components/CnNotificationMatrix/CnNotificationMatrix.vue2.js.map +1 -1
  29. package/dist/esm/components/CnNotificationMatrix/CnNotificationMatrix.vue3.js.map +1 -1
  30. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue.js +1 -1
  31. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue2.js +2 -2
  32. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue2.js.map +1 -1
  33. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue3.js.map +1 -1
  34. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  35. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  36. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  37. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue.js +1 -1
  38. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue2.js.map +1 -1
  39. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue3.js.map +1 -1
  40. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  41. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  42. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  43. package/dist/esm/library-style.js +1 -1
  44. package/dist/esm/nextcloud-vue.css +634 -301
  45. package/dist/esm/utils/detailObjectContext.js +7 -2
  46. package/dist/esm/utils/detailObjectContext.js.map +1 -1
  47. package/dist/esm/utils/objectParties.js +30 -1
  48. package/dist/esm/utils/objectParties.js.map +1 -1
  49. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  50. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  51. package/dist/esm/utils/visibleWhen.js +6 -0
  52. package/dist/esm/utils/visibleWhen.js.map +1 -1
  53. package/dist/nextcloud-vue.cjs.js +362 -138
  54. package/dist/nextcloud-vue.cjs.js.map +2 -2
  55. package/dist/nextcloud-vue.css +634 -301
  56. package/package.json +1 -1
  57. package/src/components/CnAiCompanion/CnAiChatPanel.vue +3 -0
  58. package/src/components/CnChecklistRunWidget/CnChecklistRunWidget.vue +4 -2
  59. package/src/components/CnDataTable/CnDataTable.vue +19 -2
  60. package/src/components/CnDetailPage/CnDetailPage.md +1 -0
  61. package/src/components/CnDetailPage/CnDetailPage.vue +23 -0
  62. package/src/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue +1 -0
  63. package/src/components/CnFactsWidget/CnFactsWidget.vue +168 -16
  64. package/src/components/CnNotesCard/CnNotesCard.vue +7 -2
  65. package/src/components/CnNotificationMatrix/CnNotificationMatrix.vue +21 -2
  66. package/src/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue +2 -1
  67. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +1 -0
  68. package/src/components/CnRetentionWidget/CnRetentionWidget.vue +2 -0
  69. package/src/components/CnTabsWidget/CnTabsWidget.vue +6 -1
  70. package/src/css/dialog.css +7 -0
  71. package/src/css/look-board-detail.css +152 -8
  72. package/src/css/look-board-index.css +78 -0
  73. package/src/css/look-board.css +33 -3
  74. package/src/css/table.css +9 -0
  75. package/src/schemas/app-manifest-v2.schema.json +2 -2
  76. package/src/utils/detailObjectContext.js +7 -2
  77. package/src/utils/objectParties.js +29 -0
  78. package/src/utils/validateManifestV2.compiled.js +2 -2
  79. package/src/utils/visibleWhen.js +6 -0
@@ -1 +1 @@
1
- {"version":3,"file":"CnDetailPage.vue3.js","sources":["../../../../src/components/CnDetailPage/CnDetailPage.vue?vue&type=template&id=7e3e0b7c&lang.js"],"sourcesContent":["<!--\n CnDetailPage — Generic detail/overview page.\n\n The detail page equivalent of CnDashboardPage. Assembles a complete entity detail\n view from card-based sections, matching the dashboard visual style (rounded cards\n with headers). Uses a fixed declarative layout (no drag-and-drop).\n\n Features:\n - Header with back button, title, subtitle, and action buttons\n - Card-based content area (via default slot with CnDetailCard components)\n - Optional 12-column CSS grid layout mode (via layout + widgets props)\n - Optional right sidebar (CnObjectSidebar) for files, notes, tags, tasks, audit trail\n - Loading and error states\n - Edit mode toggle\n\n A simpler alternative to CnIndexPage for detail, stats, and overview pages.\n No multi-object table, no CRUD dialogs — just a clean layout with:\n - Header (title, description, icon, action buttons)\n - Loading / error / empty states\n - Statistics table section\n - Content sections via slots\n-->\n<template>\n\t<div\n\t\tclass=\"cn-detail-page\"\n\t\t:class=\"[{ 'cn-detail-page--with-side': showsSideColumn, 'cn-detail-page--card-buttons-regular': cardButtons === 'regular' }, boardClass]\"\n\t\tdata-testid=\"cn-detail-page\"\n\t\t:style=\"{ maxWidth: maxWidth }\">\n\t\t<!-- Skip link to the page's primary action. Off screen until it takes\n\t\t focus, so it costs a sighted mouse user nothing and saves a\n\t\t keyboard user every widget on the page. Targets the action the\n\t\t page DECLARES rather than a guessed first button: a skip link\n\t\t that lands somewhere arbitrary is worse than none, because it is\n\t\t the one link a keyboard user is told to trust. -->\n\t\t<a\n\t\t\tv-if=\"effectivePrimaryLabel && !objectNotFound\"\n\t\t\t:href=\"`#${primaryActionAnchorId}`\"\n\t\t\tclass=\"cn-detail-page__skip-link\"\n\t\t\tdata-testid=\"cn-detail-page-skip-link\"\n\t\t\t@click=\"onSkipToPrimaryAction\">\n\t\t\t{{ skipLinkLabel }}\n\t\t</a>\n\n\t\t<!-- Breadcrumb line (manifest `config.breadcrumb`): the list the record\n\t\t belongs to, then the record itself as the current crumb. -->\n\t\t<CnBreadcrumbs\n\t\t\tv-if=\"breadcrumbCrumbs.length > 0 && !objectNotFound && !contentWithheld && !isBoard\"\n\t\t\tclass=\"cn-detail-page__breadcrumbs\"\n\t\t\t:crumbs=\"breadcrumbCrumbs\"\n\t\t\t:rootText=\"breadcrumbCrumbs[0].icon === undefined\"\n\t\t\t:separator=\"breadcrumbSeparator\"\n\t\t\tdata-testid=\"cn-detail-page-breadcrumbs\" />\n\t\t<!-- Header -->\n\t\t<div\n\t\t\tv-if=\"!objectNotFound && !contentWithheld\"\n\t\t\tclass=\"cn-detail-page__header\"\n\t\t\t:class=\"{ 'cn-detail-page__header--card': headerCard && !isBoard, 'cn-detail-page__header--with-widget': headerWidgetDef !== null, 'cn-detail-page__header--board': isBoard }\"\n\t\t\tdata-testid=\"cn-detail-page-header\">\n\t\t\t<!-- Header (left block) — overridable via #header slot. Default\n\t\t\t renders the icon + title + description. The right-hand\n\t\t\t #actions slot remains separate so headerComponent and\n\t\t\t actionsComponent can be replaced independently. -->\n\t\t\t<!--\n\t\t\t\t@slot header\n\t\t\t\t@description Replace the entire left header block (icon + title + description).\n\t\t\t\t@binding {string} title Page title.\n\t\t\t\t@binding {string} description Subtitle / description.\n\t\t\t\t@binding {string} icon MDI icon name resolved by CnIcon.\n\t\t\t\t@binding {number} icon-size Icon size in pixels.\n\t\t\t-->\n\t\t\t<slot\n\t\t\t\tname=\"header\"\n\t\t\t\t:title=\"title\"\n\t\t\t\t:description=\"description\"\n\t\t\t\t:icon=\"icon\"\n\t\t\t\t:iconSize=\"iconSize\">\n\t\t\t\t<div class=\"cn-detail-page__header-left\">\n\t\t\t\t\t<!--\n\t\t\t\t\t\t@slot icon\n\t\t\t\t\t\t@description Replace only the header icon (keeping the lib's title +\n\t\t\t\t\t\tdescription rendering). Default content: a `<CnIcon>` resolved from\n\t\t\t\t\t\tthe `icon` prop.\n\t\t\t\t\t-->\n\t\t\t\t\t<slot name=\"icon\">\n\t\t\t\t\t\t<CnIcon\n\t\t\t\t\t\t\tv-if=\"icon && !isBoard\"\n\t\t\t\t\t\t\t:name=\"icon\"\n\t\t\t\t\t\t\t:size=\"iconSize\"\n\t\t\t\t\t\t\tclass=\"cn-detail-page__icon\" />\n\t\t\t\t\t</slot>\n\t\t\t\t\t<div class=\"cn-detail-page__header-text\">\n\t\t\t\t\t\t<!-- Type and status pills (manifest `config.typePill` /\n\t\t\t\t\t\t `config.statusPill`). What kind of record this is and\n\t\t\t\t\t\t where it stands, readable before the title. Nothing\n\t\t\t\t\t\t renders for a page that declares neither. -->\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tv-if=\"headerPills.length > 0 && !isBoard\"\n\t\t\t\t\t\t\tclass=\"cn-detail-page__pills\"\n\t\t\t\t\t\t\tdata-testid=\"cn-detail-page-pills\">\n\t\t\t\t\t\t\t<CnStatusBadge\n\t\t\t\t\t\t\t\tv-for=\"pill in headerPills\"\n\t\t\t\t\t\t\t\t:key=\"pill.key\"\n\t\t\t\t\t\t\t\t:label=\"pill.label\"\n\t\t\t\t\t\t\t\t:colorKey=\"pill.colorKey\"\n\t\t\t\t\t\t\t\t:colorMap=\"pill.colorMap\"\n\t\t\t\t\t\t\t\t:variant=\"pill.variant\"\n\t\t\t\t\t\t\t\t:data-testid=\"`cn-detail-page-pill-${pill.key}`\">\n\t\t\t\t\t\t\t\t<template v-if=\"pill.icon\" #icon>\n\t\t\t\t\t\t\t\t\t<CnIcon :name=\"pill.icon\" :size=\"14\" class=\"cn-detail-page__pill-icon\" />\n\t\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t</CnStatusBadge>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<!-- Type name as a small eyebrow above the record name, so\n\t\t\t\t\t\t the header reads \"the record\" not \"the kind of record\"\n\t\t\t\t\t\t (ADR-062). Only shown once the object resolves to a\n\t\t\t\t\t\t display name that differs from the type label. -->\n\t\t\t\t\t\t<p\n\t\t\t\t\t\t\tv-if=\"typeEyebrow && showTypeEyebrow && !isBoard\"\n\t\t\t\t\t\t\tclass=\"cn-detail-page__type-eyebrow\"\n\t\t\t\t\t\t\tdata-testid=\"cn-detail-page-type-eyebrow\">\n\t\t\t\t\t\t\t{{ typeEyebrow }}\n\t\t\t\t\t\t</p>\n\t\t\t\t\t\t<div v-if=\"displayTitle || showFollowToggle\" class=\"cn-detail-page__title-row\">\n\t\t\t\t\t\t\t<component\n\t\t\t\t\t\t\t\t:is=\"isBoard ? 'h1' : 'h2'\"\n\t\t\t\t\t\t\t\tv-if=\"displayTitle\"\n\t\t\t\t\t\t\t\tclass=\"cn-detail-page__title\"\n\t\t\t\t\t\t\t\t:title=\"displayTitle\">\n\t\t\t\t\t\t\t\t{{ displayTitle }}\n\t\t\t\t\t\t\t</component>\n\t\t\t\t\t\t\t<!-- The one Follow control beside the title (manifest `config.follow`):\n\t\t\t\t\t\t\t shown when the object carries `@self.watching`. There is no star:\n\t\t\t\t\t\t\t a favourite is a follow with notifications off. The board look\n\t\t\t\t\t\t\t draws it on a row of its own under the breadcrumb. -->\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tv-if=\"showFollowToggle && !isBoard\"\n\t\t\t\t\t\t\t\tclass=\"cn-detail-page__interactions\"\n\t\t\t\t\t\t\t\tdata-testid=\"cn-detail-page-interactions\">\n\t\t\t\t\t\t\t\t<CnFollowToggle v-bind=\"followToggleProps\" />\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<!--\n\t\t\t\t\t\t\t@slot translation-badge\n\t\t\t\t\t\t\t@description Replace the default `CnTranslatedBadge` rendered when the\n\t\t\t\t\t\t\tresolved object's `_translationMeta.translatedFrom` is set. Receives\n\t\t\t\t\t\t\t`:object=\"resolvedObject\"`. Default renders `<CnTranslatedBadge :object=\"resolvedObject\" />`\n\t\t\t\t\t\t\t(which auto-hides when there's nothing to surface). See\n\t\t\t\t\t\t\t`openspec/changes/cn-detail-translation-aware-surfacing`.\n\t\t\t\t\t\t-->\n\t\t\t\t\t\t<slot name=\"translation-badge\" :object=\"resolvedObject\">\n\t\t\t\t\t\t\t<CnTranslatedBadge v-if=\"resolvedObject\" :object=\"resolvedObject\" />\n\t\t\t\t\t\t</slot>\n\t\t\t\t\t\t<p v-if=\"description\" class=\"cn-detail-page__description\">\n\t\t\t\t\t\t\t{{ resolvedDescription }}\n\t\t\t\t\t\t</p>\n\t\t\t\t\t\t<!-- Identifying field chips (manifest `config.headerFields`):\n\t\t\t\t\t\t number, status, assignee, deadline. Absent without the key. -->\n\t\t\t\t\t\t<CnDetailHeaderChips\n\t\t\t\t\t\t\tv-if=\"headerFields.length > 0 && !isBoard\"\n\t\t\t\t\t\t\t:fields=\"headerFields\"\n\t\t\t\t\t\t\t:object=\"resolvedObject\"\n\t\t\t\t\t\t\t:schema=\"currentSchema\"\n\t\t\t\t\t\t\t:register=\"register\" />\n\t\t\t\t\t\t<!-- Declarative cross-schema summary chips (manifest\n\t\t\t\t\t\t `config.summaryAggregates`). Count/sum/avg over a\n\t\t\t\t\t\t related schema scoped to this object via @objectId. -->\n\t\t\t\t\t\t<CnSummaryAggregates\n\t\t\t\t\t\t\tv-if=\"summaryAggregates && summaryAggregates.length > 0\"\n\t\t\t\t\t\t\t:aggregates=\"summaryAggregates\" />\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</slot>\n\t\t\t<div class=\"cn-detail-page__header-actions\">\n\t\t\t\t<!-- The page's view switch (`views`), unless a greeting header\n\t\t\t\t widget on the page draws it. Its options control the view\n\t\t\t\t region in the body. -->\n\t\t\t\t<CnSegmentedControl\n\t\t\t\t\tv-if=\"showsPageViewSwitch\"\n\t\t\t\t\tclass=\"cn-page-view-switch\"\n\t\t\t\t\t:options=\"viewSwitchOptions\"\n\t\t\t\t\t:modelValue=\"activeViewId\"\n\t\t\t\t\t:ariaLabel=\"viewSwitchLabel\"\n\t\t\t\t\t:controls=\"viewRegionId\"\n\t\t\t\t\tdata-testid=\"cn-detail-page-view-switch\"\n\t\t\t\t\t@update:modelValue=\"selectView\" />\n\t\t\t\t<!-- With `userLayout`, the user arranges the chosen view for\n\t\t\t\t themselves: Arrange view makes its grid draggable, Done\n\t\t\t\t stores the arrangement, Reset view returns the view to the\n\t\t\t\t layout the app ships. -->\n\t\t\t\t<NcButton\n\t\t\t\t\tv-if=\"userLayout && viewUserLayoutOn && !viewIsEmpty\"\n\t\t\t\t\t:variant=\"arrangingView ? 'primary' : 'secondary'\"\n\t\t\t\t\tdata-testid=\"cn-detail-page-arrange-view\"\n\t\t\t\t\t@click=\"toggleArrangeView\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<Check v-if=\"arrangingView\" :size=\"20\" />\n\t\t\t\t\t\t<Pencil v-else :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t{{ arrangingView ? t('nextcloud-vue', 'Done') : t('nextcloud-vue', 'Arrange view') }}\n\t\t\t\t</NcButton>\n\t\t\t\t<NcButton\n\t\t\t\t\tv-if=\"viewUserLayoutOn && arrangingView\"\n\t\t\t\t\tvariant=\"tertiary\"\n\t\t\t\t\tdata-testid=\"cn-detail-page-reset-view\"\n\t\t\t\t\t@click=\"resetViewUserLayout()\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<Restore :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t{{ t('nextcloud-vue', 'Reset view') }}\n\t\t\t\t</NcButton>\n\t\t\t\t<!-- Next and previous inside the list this record was opened\n\t\t\t\t from. Rendered only when the address names that list: a\n\t\t\t\t record reached by a bare link offers neither, rather than\n\t\t\t\t stepping through an order nobody chose. Both ends say so\n\t\t\t\t instead of wrapping. -->\n\t\t\t\t<div\n\t\t\t\t\tv-if=\"listNavigation && listNavigation.available\"\n\t\t\t\t\tclass=\"cn-detail-page__list-nav\"\n\t\t\t\t\tdata-testid=\"cn-detail-page-list-nav\">\n\t\t\t\t\t<!-- With previousTo/nextTo the control is a link; a disabled\n\t\t\t\t\t end drops `to` so it renders as a disabled button. -->\n\t\t\t\t\t<NcButton\n\t\t\t\t\t\tvariant=\"tertiary\"\n\t\t\t\t\t\t:disabled=\"listNavigation.isFirst\"\n\t\t\t\t\t\t:to=\"listNavigation.isFirst ? undefined : (previousTo || undefined)\"\n\t\t\t\t\t\t:aria-label=\"previousRecordLabel\"\n\t\t\t\t\t\t:title=\"previousRecordLabel\"\n\t\t\t\t\t\tdata-testid=\"cn-detail-page-previous\"\n\t\t\t\t\t\t@click=\"onListStep('previous-record', previousTo, $event)\">\n\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t<ChevronLeft :size=\"20\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t</NcButton>\n\t\t\t\t\t<span class=\"cn-detail-page__list-nav-position\" data-testid=\"cn-detail-page-list-position\">\n\t\t\t\t\t\t{{ listPositionLabel }}\n\t\t\t\t\t</span>\n\t\t\t\t\t<NcButton\n\t\t\t\t\t\tvariant=\"tertiary\"\n\t\t\t\t\t\t:disabled=\"listNavigation.isLast\"\n\t\t\t\t\t\t:to=\"listNavigation.isLast ? undefined : (nextTo || undefined)\"\n\t\t\t\t\t\t:aria-label=\"nextRecordLabel\"\n\t\t\t\t\t\t:title=\"nextRecordLabel\"\n\t\t\t\t\t\tdata-testid=\"cn-detail-page-next\"\n\t\t\t\t\t\t@click=\"onListStep('next-record', nextTo, $event)\">\n\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t<ChevronRight :size=\"20\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t</NcButton>\n\t\t\t\t</div>\n\t\t\t\t<!-- The action the page declares as its primary one, and the\n\t\t\t\t target the skip link above lands on. -->\n\t\t\t\t<!-- With a \"what now\" card on the page the button lives in that\n\t\t\t\t card, next to the checklist that explains it, and not here\n\t\t\t\t as well: one primary button per page. -->\n\t\t\t\t<NcButton\n\t\t\t\t\tv-if=\"effectivePrimaryLabel && !showsNextStepCard && stagePrimaryOnTab\"\n\t\t\t\t\t:id=\"primaryActionAnchorId\"\n\t\t\t\t\tref=\"primaryActionButton\"\n\t\t\t\t\tvariant=\"primary\"\n\t\t\t\t\t:href=\"primaryActionHref || undefined\"\n\t\t\t\t\t:target=\"primaryActionHref ? primaryActionTarget : undefined\"\n\t\t\t\t\t:disabled=\"primaryActionDisabled\"\n\t\t\t\t\tdata-testid=\"cn-detail-page-primary-action\"\n\t\t\t\t\t@click=\"onPrimaryActionClick\">\n\t\t\t\t\t{{ effectivePrimaryLabel }}\n\t\t\t\t</NcButton>\n\t\t\t\t<!-- The record source's own verbs (a task's claim, hand back,\n\t\t\t\t complete), offered from the record's state. The ones a source\n\t\t\t\t marks `menu` join the Actions menu instead. -->\n\t\t\t\t<NcButton\n\t\t\t\t\tv-for=\"verb in sourceVerbButtons\"\n\t\t\t\t\t:key=\"`verb-${verb.id}`\"\n\t\t\t\t\tvariant=\"secondary\"\n\t\t\t\t\t:disabled=\"sourceVerbBusy\"\n\t\t\t\t\t:data-testid=\"`cn-detail-page-verb-${verb.id}`\"\n\t\t\t\t\t@click=\"runSourceVerb(verb)\">\n\t\t\t\t\t<template v-if=\"verb.icon\" #icon>\n\t\t\t\t\t\t<CnIcon v-if=\"typeof verb.icon === 'string'\" :name=\"verb.icon\" :size=\"20\" />\n\t\t\t\t\t\t<component :is=\"verb.icon\" v-else :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t{{ verb.label }}\n\t\t\t\t</NcButton>\n\t\t\t\t<!-- Quick actions (manifest `config.quickActions`): the few\n\t\t\t\t actions a handler reaches for on every record, always\n\t\t\t\t visible. At most three, so the row cannot grow back into\n\t\t\t\t the button wall the Actions menu replaced. -->\n\t\t\t\t<NcButton\n\t\t\t\t\tv-for=\"entry in quickActionEntries\"\n\t\t\t\t\t:key=\"`quick-${entry.id}`\"\n\t\t\t\t\tvariant=\"secondary\"\n\t\t\t\t\t:href=\"entry.href || undefined\"\n\t\t\t\t\t:target=\"entry.href ? (entry.linkTarget || undefined) : undefined\"\n\t\t\t\t\t:disabled=\"entry.disabled\"\n\t\t\t\t\t:data-testid=\"`cn-detail-page-quick-${entry.id}`\"\n\t\t\t\t\t@click=\"onPinnedActionClick(entry)\">\n\t\t\t\t\t<template v-if=\"entry.iconName\" #icon>\n\t\t\t\t\t\t<CnIcon :name=\"entry.iconName\" :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t{{ entry.label }}\n\t\t\t\t</NcButton>\n\t\t\t\t<!-- Declarative lifecycle transitions (manifest\n\t\t\t\t `config.lifecycleActions`). Status-gated; driven by the\n\t\t\t\t object's x-openregister-lifecycle. They land in the Actions\n\t\t\t\t menu below, since a record with several transitions filled\n\t\t\t\t the header with buttons. -->\n\t\t\t\t<CnLifecycleActions\n\t\t\t\t\tv-if=\"showsLifecycleActions\"\n\t\t\t\t\t:objectId=\"objectId\"\n\t\t\t\t\t:object=\"currentObject\"\n\t\t\t\t\t:config=\"lifecycleActions\"\n\t\t\t\t\t:register=\"register\"\n\t\t\t\t\t:schema=\"currentSchema\"\n\t\t\t\t\tdisplay=\"menu\"\n\t\t\t\t\t@entries=\"lifecycleMenuEntries = $event\"\n\t\t\t\t\t@transitioned=\"onTransitioned\"\n\t\t\t\t\t@reload=\"onLifecycleReload\" />\n\t\t\t\t<!-- Declarative header actions (#91 Wave 3): a manifest\n\t\t\t\t `config.headerActions[]` dispatched with visibleWhen gating —\n\t\t\t\t the object context this page provides drives `@objectId` /\n\t\t\t\t `@object.<field>` tokens + local predicates (the shillinq\n\t\t\t\t PaymentRunDetailActions contract).\n\n\t\t\t\t They render as entries in the Actions menu below, not as a\n\t\t\t\t row of buttons, which is what the manifest schema has always\n\t\t\t\t said they are and what CnIndexPage already does with the same\n\t\t\t\t key. A dossiq case declares twelve, and twelve buttons pushed\n\t\t\t\t the record's own title down to a truncated stub. This\n\t\t\t\t instance draws nothing itself; it owns the dialogs the\n\t\t\t\t actions open and feeds the menu through `entries`. -->\n\t\t\t\t<CnActionButtons\n\t\t\t\t\tv-if=\"effectiveHeaderActions.length > 0\"\n\t\t\t\t\t:actions=\"effectiveHeaderActions\"\n\t\t\t\t\tdisplay=\"menu\"\n\t\t\t\t\tdata-testid=\"cn-detail-page-header-actions\"\n\t\t\t\t\t@entries=\"menuHeaderActions = $event\"\n\t\t\t\t\t@created=\"onLifecycleReload\" />\n\t\t\t\t<!--\n\t\t\t\t\t@slot actions\n\t\t\t\t\t@description Right-hand action surface in the page header (typically NcActions\n\t\t\t\t\tor buttons). Renders alongside (not inside) the `header` slot. Receives the\n\t\t\t\t\tresolved object context so an actionsComponent (resolved by CnPageRenderer)\n\t\t\t\t\tcan act on the current record without re-fetching.\n\t\t\t\t\t@binding {object} object The resolved record.\n\t\t\t\t\t@binding {string} objectId The resolved object id.\n\t\t\t\t\t@binding {object} schema The resolved schema.\n\t\t\t\t\t@binding {string} objectType The resolved object type.\n\t\t\t\t\t@binding {object} store The effective object store.\n\t\t\t\t\t@binding {Function} openEditForm Opens this page's own record edit form. Lets an actions component put Edit inside ITS action cluster instead of leaving the page's button beside it — the page renders no button of its own once `showEditAction` is false, but this still opens the same dialog.\n\t\t\t\t\t@binding {boolean} canEditRecord Whether the page would render its own Edit button (`showEditAction` plus a record and schema to edit).\n\t\t\t\t-->\n\t\t\t\t<slot\n\t\t\t\t\tname=\"actions\"\n\t\t\t\t\t:object=\"resolvedObject\"\n\t\t\t\t\t:objectId=\"objectId\"\n\t\t\t\t\t:schema=\"currentSchema\"\n\t\t\t\t\t:objectType=\"resolvedObjectType\"\n\t\t\t\t\t:store=\"effectiveObjectStore\"\n\t\t\t\t\t:openEditForm=\"openEditForm\"\n\t\t\t\t\t:canEditRecord=\"canEditRecord\" />\n\t\t\t\t<!-- Record edit (ADR-062): the detail page's own way to change the\n\t\t\t\t record it is showing. Without it a `type:\"detail\"` page is\n\t\t\t\t read-only and the ONLY edit surface is the modal launched\n\t\t\t\t from the index table — which is why that modal could not\n\t\t\t\t simply be removed. Opens the same schema-driven CnFormDialog\n\t\t\t\t the index used, scoped to this record. -->\n\t\t\t\t<!-- Standalone unless `inlineActions` is set, in which case Edit\n\t\t\t\t joins the header actions above and lands in the Actions menu\n\t\t\t\t with them. Kept standalone by default so every existing\n\t\t\t\t consumer's header is byte-identical. -->\n\t\t\t\t<NcButton\n\t\t\t\t\tv-if=\"canEditRecord && !foldsEditIntoActions\"\n\t\t\t\t\tvariant=\"secondary\"\n\t\t\t\t\tdata-testid=\"cn-detail-page-edit\"\n\t\t\t\t\t:aria-label=\"editActionLabel\"\n\t\t\t\t\t@click=\"openEditForm\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<Pencil :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t{{ editActionLabel }}\n\t\t\t\t</NcButton>\n\t\t\t\t<!-- NB: no sidebar-toggle here — NcObjectSidebar/NcAppSidebar\n\t\t\t\t renders its own open toggle (`.app-sidebar__toggle`) when\n\t\t\t\t closed and an X (`.app-sidebar__close`) when open, so a custom\n\t\t\t\t button would duplicate it. -->\n\t\t\t\t<!-- In-app edit button (ADR-041): icon-only, self-wires from CnAppRoot. -->\n\t\t\t\t<CnBuildiqEditButton />\n\t\t\t\t<CnActionsMenu\n\t\t\t\t\t:showRefresh=\"menuShowsRefresh\"\n\t\t\t\t\t:refreshing=\"effectiveRefreshing\"\n\t\t\t\t\t:showRequestFeature=\"showRequestFeature && menuShowsHelpLinks\"\n\t\t\t\t\t:showReportBug=\"showReportBug && menuShowsHelpLinks\"\n\t\t\t\t\t:showDocumentation=\"showDocumentation && menuShowsHelpLinks\"\n\t\t\t\t\t:actionsMenuLabel=\"boardMenuLabel\"\n\t\t\t\t\t:variant=\"isBoard ? 'secondary' : undefined\"\n\t\t\t\t\t:trailingChevron=\"isBoard\"\n\t\t\t\t\t:placement=\"isBoard ? 'bottom-end' : 'bottom'\"\n\t\t\t\t\t:hasPrimaryItems=\"headerMenuGroups.length > 0 || showMarkUnread || showOpenSidebar\"\n\t\t\t\t\t:documentationUrl=\"documentationUrl\"\n\t\t\t\t\t:docsAnchor=\"resolvedPageId\"\n\t\t\t\t\t:documentationLabel=\"documentationLabel || undefined\"\n\t\t\t\t\t:widgetId=\"resolvedPageId\"\n\t\t\t\t\t:title=\"title\"\n\t\t\t\t\t:surface=\"`detail:${resolvedPageId}`\"\n\t\t\t\t\trefreshChannel=\"cn:page:refresh\"\n\t\t\t\t\ttestidBase=\"cn-detail-page\"\n\t\t\t\t\t@refresh=\"onHeaderRefresh\"\n\t\t\t\t\t@requestFeature=\"onHeaderRequestFeature\">\n\t\t\t\t\t<template v-if=\"headerMenuGroups.length || showMarkUnread || showOpenSidebar\" #primary-items>\n\t\t\t\t\t\t<!-- One block per group. A page that groups nothing has\n\t\t\t\t\t\t a single captionless group, and renders exactly the\n\t\t\t\t\t\t flat list it always did. -->\n\t\t\t\t\t\t<template v-for=\"(group, groupIndex) in headerMenuGroups\">\n\t\t\t\t\t\t\t<NcActionSeparator\n\t\t\t\t\t\t\t\tv-if=\"groupIndex > 0\"\n\t\t\t\t\t\t\t\t:key=\"`${group.key}-separator`\" />\n\t\t\t\t\t\t\t<NcActionCaption\n\t\t\t\t\t\t\t\tv-if=\"group.label\"\n\t\t\t\t\t\t\t\t:key=\"`${group.key}-caption`\"\n\t\t\t\t\t\t\t\t:name=\"group.label\"\n\t\t\t\t\t\t\t\t:data-testid=\"`cn-detail-page-action-group-${group.key}`\" />\n\t\t\t\t\t\t\t<template v-for=\"entry in group.entries\">\n\t\t\t\t\t\t\t\t<!-- An action that goes to a URL is a LINK. The browser\n\t\t\t\t\t\t\t\t then supplies middle-click, \"open in new tab\" and the\n\t\t\t\t\t\t\t\t semantics assistive tech announces, none of which a\n\t\t\t\t\t\t\t\t click handler can. -->\n\t\t\t\t\t\t\t\t<NcActionLink\n\t\t\t\t\t\t\t\t\tv-if=\"entry.href\"\n\t\t\t\t\t\t\t\t\t:key=\"`${entry.id}-link`\"\n\t\t\t\t\t\t\t\t\t:href=\"entry.href\"\n\t\t\t\t\t\t\t\t\t:target=\"entry.linkTarget || undefined\"\n\t\t\t\t\t\t\t\t\t:data-testid=\"entry.testid\"\n\t\t\t\t\t\t\t\t\t:data-cn-admin-tag=\"adminTagFor(group)\">\n\t\t\t\t\t\t\t\t\t<template v-if=\"entry.iconName || entry.iconClass\" #icon>\n\t\t\t\t\t\t\t\t\t\t<CnIcon v-if=\"entry.iconName\" :name=\"entry.iconName\" :size=\"20\" />\n\t\t\t\t\t\t\t\t\t\t<span v-else :class=\"entry.iconClass\" />\n\t\t\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t\t\t{{ entry.label }}\n\t\t\t\t\t\t\t\t</NcActionLink>\n\t\t\t\t\t\t\t\t<NcActionButton\n\t\t\t\t\t\t\t\t\tv-else\n\t\t\t\t\t\t\t\t\t:key=\"entry.id\"\n\t\t\t\t\t\t\t\t\t:data-testid=\"entry.testid\"\n\t\t\t\t\t\t\t\t\t:disabled=\"entry.disabled\"\n\t\t\t\t\t\t\t\t\t:aria-pressed=\"entry.pressed === null ? null : String(entry.pressed)\"\n\t\t\t\t\t\t\t\t\t:closeAfterClick=\"true\"\n\t\t\t\t\t\t\t\t\t:data-cn-admin-tag=\"adminTagFor(group)\"\n\t\t\t\t\t\t\t\t\t@click=\"entry.run()\">\n\t\t\t\t\t\t\t\t\t<template v-if=\"entry.iconName || entry.iconClass\" #icon>\n\t\t\t\t\t\t\t\t\t\t<CnIcon v-if=\"entry.iconName\" :name=\"entry.iconName\" :size=\"20\" />\n\t\t\t\t\t\t\t\t\t\t<span v-else :class=\"entry.iconClass\" />\n\t\t\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t\t\t{{ entry.label }}\n\t\t\t\t\t\t\t\t</NcActionButton>\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t</template>\n\t\t\t\t\t\t<!-- Mark as unread (record-unread-markers): offered when the\n\t\t\t\t\t\t object carries @self.unread. -->\n\t\t\t\t\t\t<NcActionButton\n\t\t\t\t\t\t\tv-if=\"showMarkUnread\"\n\t\t\t\t\t\t\tkey=\"mark-unread\"\n\t\t\t\t\t\t\tdata-testid=\"cn-detail-page-mark-unread\"\n\t\t\t\t\t\t\t:closeAfterClick=\"true\"\n\t\t\t\t\t\t\t@click=\"onMarkUnread\">\n\t\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t\t<EyeOffOutline :size=\"20\" />\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t{{ markUnreadLabel }}\n\t\t\t\t\t\t</NcActionButton>\n\t\t\t\t\t\t<!-- Board look: the sidebar's own open toggle is hidden (it sat\n\t\t\t\t\t\t over the header's last button, where the board draws none), so\n\t\t\t\t\t\t the menu opens it. -->\n\t\t\t\t\t\t<NcActionButton\n\t\t\t\t\t\t\tv-if=\"showOpenSidebar\"\n\t\t\t\t\t\t\tkey=\"open-sidebar\"\n\t\t\t\t\t\t\tdata-testid=\"cn-detail-page-open-sidebar\"\n\t\t\t\t\t\t\t:closeAfterClick=\"true\"\n\t\t\t\t\t\t\t@click=\"openSidebar\">\n\t\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t\t<DockRight :size=\"20\" />\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t{{ openSidebarLabel }}\n\t\t\t\t\t\t</NcActionButton>\n\t\t\t\t\t\t<!-- Divides the page's own actions from the help links\n\t\t\t\t\t\t below. With the help links switched off there is\n\t\t\t\t\t\t nothing below to divide from. -->\n\t\t\t\t\t\t<NcActionSeparator v-if=\"menuShowsHelpLinks\" />\n\t\t\t\t\t</template>\n\t\t\t\t</CnActionsMenu>\n\t\t\t</div>\n\t\t\t<!-- Row 2 of the board look's header (screens-detail-page-parity): the\n\t\t\t pills, the breadcrumb, a middle dot, the meta line and the Follow\n\t\t\t control, on the\n\t\t\t ground under the title. The breadcrumb does not render above the\n\t\t\t header in this look, so this is its only place. -->\n\t\t\t<div\n\t\t\t\tv-if=\"isBoard && showsHeaderRow2\"\n\t\t\t\tclass=\"cn-detail-page__header-row2\"\n\t\t\t\tdata-testid=\"cn-detail-page-header-row2\">\n\t\t\t\t<div\n\t\t\t\t\tv-if=\"headerPills.length > 0\"\n\t\t\t\t\tclass=\"cn-detail-page__pills\"\n\t\t\t\t\tdata-testid=\"cn-detail-page-pills\">\n\t\t\t\t\t<CnStatusBadge\n\t\t\t\t\t\tv-for=\"pill in headerPills\"\n\t\t\t\t\t\t:key=\"pill.key\"\n\t\t\t\t\t\t:label=\"pill.label\"\n\t\t\t\t\t\t:colorKey=\"pill.colorKey\"\n\t\t\t\t\t\t:colorMap=\"pill.colorMap\"\n\t\t\t\t\t\t:variant=\"pill.variant\"\n\t\t\t\t\t\t:data-testid=\"`cn-detail-page-pill-${pill.key}`\">\n\t\t\t\t\t\t<template v-if=\"pill.icon\" #icon>\n\t\t\t\t\t\t\t<CnIcon :name=\"pill.icon\" :size=\"14\" class=\"cn-detail-page__pill-icon\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t</CnStatusBadge>\n\t\t\t\t</div>\n\t\t\t\t<CnBreadcrumbs\n\t\t\t\t\tv-if=\"breadcrumbCrumbs.length > 0\"\n\t\t\t\t\tclass=\"cn-detail-page__breadcrumbs cn-detail-page__breadcrumbs--row2\"\n\t\t\t\t\t:crumbs=\"breadcrumbCrumbs\"\n\t\t\t\t\t:rootText=\"breadcrumbCrumbs[0].icon === undefined\"\n\t\t\t\t\t:separator=\"breadcrumbSeparator || '/'\"\n\t\t\t\t\tdata-testid=\"cn-detail-page-breadcrumbs\" />\n\t\t\t\t<span\n\t\t\t\t\tv-if=\"breadcrumbCrumbs.length > 0 && hasHeaderMeta\"\n\t\t\t\t\tclass=\"cn-detail-page__header-dot\"\n\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\tdata-testid=\"cn-detail-page-header-dot\">&middot;</span>\n\t\t\t\t<span\n\t\t\t\t\tv-if=\"headerMetaText\"\n\t\t\t\t\tclass=\"cn-detail-page__header-meta\"\n\t\t\t\t\tdata-testid=\"cn-detail-page-header-meta\">{{ headerMetaText }}</span>\n\t\t\t\t<CnDetailHeaderChips\n\t\t\t\t\tv-if=\"headerFields.length > 0\"\n\t\t\t\t\t:fields=\"headerFields\"\n\t\t\t\t\t:object=\"resolvedObject\"\n\t\t\t\t\t:schema=\"currentSchema\"\n\t\t\t\t\t:register=\"register\" />\n\t\t\t</div>\n\t\t\t<!-- Row 3 of the board look's header: the Follow control under the\n\t\t\t breadcrumb, clear of the title row and its actions (DqZaak\n\t\t\t \"Volgen\"). -->\n\t\t\t<div\n\t\t\t\tv-if=\"isBoard && showFollowToggle\"\n\t\t\t\tclass=\"cn-detail-page__header-row3\"\n\t\t\t\tdata-testid=\"cn-detail-page-header-row3\">\n\t\t\t\t<span\n\t\t\t\t\tv-if=\"presenceSentence\"\n\t\t\t\t\tclass=\"cn-detail-page__presence\"\n\t\t\t\t\tdata-testid=\"cn-detail-page-presence\">\n\t\t\t\t\t<CnPresenceAvatars :present=\"presentOthers\" :max=\"3\" :size=\"24\" />\n\t\t\t\t\t<span>{{ presenceSentence }}</span>\n\t\t\t\t\t<span aria-hidden=\"true\">·</span>\n\t\t\t\t</span>\n\t\t\t\t<span class=\"cn-detail-page__interactions\" data-testid=\"cn-detail-page-interactions\">\n\t\t\t\t\t<CnFollowToggle v-bind=\"followToggleProps\" />\n\t\t\t\t</span>\n\t\t\t</div>\n\t\t\t<!-- A widget inside the header (manifest `config.headerWidget`), on\n\t\t\t a row of its own under the title and the actions: the stages\n\t\t\t bars of the board's case card. Drawn without a card of its own,\n\t\t\t and taken out of the body grid. Nothing renders without the\n\t\t\t key. -->\n\t\t\t<div\n\t\t\t\tv-if=\"headerWidgetDef !== null\"\n\t\t\t\tclass=\"cn-detail-page__header-widget\"\n\t\t\t\tdata-testid=\"cn-detail-page-header-widget\">\n\t\t\t\t<!--\n\t\t\t\t\t@slot `widget-${headerWidgetDef.id}`\n\t\t\t\t\t@description The same per-widget slot the body grid and the side\n\t\t\t\t\tcolumn offer, for the widget placed in the header\n\t\t\t\t\t(`headerWidget`). Same name and same bindings.\n\t\t\t\t-->\n\t\t\t\t<slot\n\t\t\t\t\t:name=\"`widget-${headerWidgetDef.id}`\"\n\t\t\t\t\t:item=\"{ id: headerWidgetDef.id, widgetId: headerWidgetDef.id }\"\n\t\t\t\t\t:widget=\"titled(headerWidgetDef)\"\n\t\t\t\t\t:objectId=\"objectId\"\n\t\t\t\t\t:object=\"resolvedObject\"\n\t\t\t\t\t:objectData=\"resolvedObject\"\n\t\t\t\t\t:objectType=\"resolvedObjectType\"\n\t\t\t\t\t:register=\"register\"\n\t\t\t\t\t:schema=\"schema\">\n\t\t\t\t\t<CnDetailWidgetHost\n\t\t\t\t\t\t:widget=\"titled(headerWidgetDef)\"\n\t\t\t\t\t\tchrome=\"bare\"\n\t\t\t\t\t\t:objectId=\"objectId\"\n\t\t\t\t\t\t:object=\"currentObject\"\n\t\t\t\t\t\t:objectType=\"resolvedObjectType\"\n\t\t\t\t\t\t:schemaObject=\"currentSchema\"\n\t\t\t\t\t\t:register=\"register\"\n\t\t\t\t\t\t:schema=\"schema\"\n\t\t\t\t\t\t:store=\"effectiveObjectStore\"\n\t\t\t\t\t\t:surface=\"surface\"\n\t\t\t\t\t\t:integrationContext=\"effectiveIntegrationContext\"\n\t\t\t\t\t\t:hideEmpty=\"hideEmpty\"\n\t\t\t\t\t\t:cnRegistry=\"cnRegistry\"\n\t\t\t\t\t\t:availableWidgets=\"titledWidgets\"\n\t\t\t\t\t\t@geoSaved=\"onGeoSaved\"\n\t\t\t\t\t\t@openIntegration=\"onAutoBodyOpenIntegration\"\n\t\t\t\t\t\t@selectObject=\"onRelatedObjectSelect\" />\n\t\t\t\t</slot>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- The locked card, under the title, for ANY active lock — not only a\n\t\t remote one. It used to be suppressed when the current user held the\n\t\t lock, which meant the one person who could do something about a\n\t\t stale lock was also the only person the UI never told about it.\n\t\t The card carries the tone: an error for somebody else's lock, a\n\t\t neutral notice plus Unlock for your own. -->\n\t\t<CnLockedBanner\n\t\t\tv-if=\"lockState && lockState.locked.value\"\n\t\t\t:lockedBy=\"lockState.lockedBy.value\"\n\t\t\t:lockedByMe=\"lockState.lockedByMe.value\"\n\t\t\t:expiresAt=\"lockState.expiresAt.value\"\n\t\t\t:unlocking=\"releasingLock\"\n\t\t\t@unlock=\"onReleaseLock\" />\n\n\t\t<!-- The sentence a refused acquire left behind. Shown SEPARATELY from\n\t\t the banner above and not folded into it: a refused acquire does not\n\t\t refresh the cached object, so `@self.locked` is still whatever the\n\t\t last read said and the banner may not have appeared yet. This\n\t\t carries the server's own message, which names the holder. -->\n\t\t<p\n\t\t\tv-if=\"editLockRefusal\"\n\t\t\tclass=\"cn-detail-page__lock-refusal\"\n\t\t\trole=\"alert\"\n\t\t\tdata-testid=\"cn-detail-page-lock-refusal\">\n\t\t\t{{ editLockRefusal }}\n\t\t</p>\n\n\t\t<!-- The \"what now\" card (manifest `config.nextStep`): what this stage\n\t\t still needs, next to the button that takes the step. Above the\n\t\t body and the side column, at the full width of both. Hidden for a\n\t\t stage that declares no checklist. -->\n\t\t<CnNextStepCard\n\t\t\tv-if=\"showsNextStepCard\"\n\t\t\tclass=\"cn-detail-page__next-step\"\n\t\t\t:title=\"nextStepCard.title\"\n\t\t\t:titleTag=\"isBoard ? 'h2' : 'h3'\"\n\t\t\t:items=\"nextStepCard.items\"\n\t\t\t:after=\"nextStepCard.after\"\n\t\t\tdata-testid=\"cn-detail-page-next-step\">\n\t\t\t<template v-if=\"effectivePrimaryLabel\" #action>\n\t\t\t\t<NcButton\n\t\t\t\t\t:id=\"primaryActionAnchorId\"\n\t\t\t\t\tref=\"primaryActionButton\"\n\t\t\t\t\tvariant=\"primary\"\n\t\t\t\t\t:href=\"primaryActionHref || undefined\"\n\t\t\t\t\t:target=\"primaryActionHref ? primaryActionTarget : undefined\"\n\t\t\t\t\t:disabled=\"primaryActionDisabled\"\n\t\t\t\t\tdata-testid=\"cn-detail-page-primary-action\"\n\t\t\t\t\t@click=\"onPrimaryActionClick\">\n\t\t\t\t\t{{ effectivePrimaryLabel }}\n\t\t\t\t</NcButton>\n\t\t\t</template>\n\t\t</CnNextStepCard>\n\n\t\t<!-- The band (manifest `config.band`): a row of widgets across the whole\n\t\t page, above the body and the side column (PtPortal: the traffic\n\t\t figures and the start-page notice). Each entry takes `span`\n\t\t twelfths of the row and the row wraps. -->\n\t\t<div\n\t\t\tv-if=\"showsBand\"\n\t\t\tclass=\"cn-detail-page__band\"\n\t\t\tdata-testid=\"cn-detail-page-band\">\n\t\t\t<!-- The page filters (`config.pageFilters`), right-aligned above the\n\t\t\t band's widgets; each writes the page's workspace context. -->\n\t\t\t<div\n\t\t\t\tv-if=\"detailPageFilters.length > 0\"\n\t\t\t\tclass=\"cn-detail-page__page-filters\"\n\t\t\t\tdata-testid=\"cn-detail-page-page-filters\">\n\t\t\t\t<template v-for=\"pf in detailPageFilters\" :key=\"pf.key\">\n\t\t\t\t\t<div v-if=\"pf.control === 'segmented'\" class=\"cn-detail-page__page-filter\">\n\t\t\t\t\t\t<span v-if=\"pf.label\" class=\"cn-detail-page__page-filter-label\">{{ effectiveTranslate(pf.label) }}</span>\n\t\t\t\t\t\t<CnSegmentedControl\n\t\t\t\t\t\t\tsize=\"compact\"\n\t\t\t\t\t\t\tmode=\"toggle\"\n\t\t\t\t\t\t\t:options=\"pageFilterSegments(pf)\"\n\t\t\t\t\t\t\t:modelValue=\"workspaceContext[pf.key]\"\n\t\t\t\t\t\t\t:ariaLabel=\"pf.label ? effectiveTranslate(pf.label) : pf.key\"\n\t\t\t\t\t\t\t:data-testid=\"'cn-page-filter-' + pf.key\"\n\t\t\t\t\t\t\t@update:modelValue=\"onPageFilterChange(pf, $event)\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<label v-else class=\"cn-detail-page__page-filter\">\n\t\t\t\t\t\t<span v-if=\"pf.label\" class=\"cn-detail-page__page-filter-label\">{{ effectiveTranslate(pf.label) }}</span>\n\t\t\t\t\t\t<NcSelect\n\t\t\t\t\t\t\t:modelValue=\"selectedPageFilterOption(pf)\"\n\t\t\t\t\t\t\t:options=\"pageFilterSegments(pf)\"\n\t\t\t\t\t\t\t:clearable=\"pf.clearable === true\"\n\t\t\t\t\t\t\t:placeholder=\"pf.placeholder ? effectiveTranslate(pf.placeholder) : undefined\"\n\t\t\t\t\t\t\t:loading=\"pageFilterLoading[pf.key] === true\"\n\t\t\t\t\t\t\t:inputLabel=\"pf.label ? effectiveTranslate(pf.label) : pf.key\"\n\t\t\t\t\t\t\tlabel=\"label\"\n\t\t\t\t\t\t\t:data-testid=\"'cn-page-filter-' + pf.key\"\n\t\t\t\t\t\t\t@update:modelValue=\"onPageFilterChange(pf, $event)\" />\n\t\t\t\t\t</label>\n\t\t\t\t</template>\n\t\t\t</div>\n\t\t\t<div\n\t\t\t\tv-for=\"entry in bandWidgets\"\n\t\t\t\t:key=\"`band-${entry.widget.id}`\"\n\t\t\t\tclass=\"cn-detail-page__band-item\"\n\t\t\t\t:class=\"{ 'cn-detail-page__band-item--full': entry.span === 12 }\"\n\t\t\t\t:style=\"{ '--cn-band-span': entry.span }\"\n\t\t\t\t:data-testid=\"`cn-detail-page-band-${entry.widget.id}`\">\n\t\t\t\t<!--\n\t\t\t\t\t@slot `widget-${widget.id}`\n\t\t\t\t\t@description The same per-widget slot the body grid and the side\n\t\t\t\t\tcolumn offer, for a widget placed in the band.\n\t\t\t\t-->\n\t\t\t\t<slot\n\t\t\t\t\t:name=\"`widget-${entry.widget.id}`\"\n\t\t\t\t\t:item=\"{ id: entry.widget.id, widgetId: entry.widget.id }\"\n\t\t\t\t\t:widget=\"titled(entry.widget)\"\n\t\t\t\t\t:objectId=\"objectId\"\n\t\t\t\t\t:object=\"resolvedObject\"\n\t\t\t\t\t:objectData=\"resolvedObject\"\n\t\t\t\t\t:objectType=\"resolvedObjectType\"\n\t\t\t\t\t:register=\"register\"\n\t\t\t\t\t:schema=\"schema\">\n\t\t\t\t\t<CnDetailWidgetHost\n\t\t\t\t\t\t:widget=\"titled(entry.widget)\"\n\t\t\t\t\t\t:chrome=\"entry.chrome\"\n\t\t\t\t\t\t:showCardTitle=\"true\"\n\t\t\t\t\t\t:showActions=\"effectiveShowWidgetActions\"\n\t\t\t\t\t\t:objectId=\"objectId\"\n\t\t\t\t\t\t:object=\"currentObject\"\n\t\t\t\t\t\t:objectType=\"resolvedObjectType\"\n\t\t\t\t\t\t:schemaObject=\"currentSchema\"\n\t\t\t\t\t\t:register=\"register\"\n\t\t\t\t\t\t:schema=\"schema\"\n\t\t\t\t\t\t:store=\"effectiveObjectStore\"\n\t\t\t\t\t\t:surface=\"surface\"\n\t\t\t\t\t\t:integrationContext=\"effectiveIntegrationContext\"\n\t\t\t\t\t\t:hideEmpty=\"hideEmpty\"\n\t\t\t\t\t\t:cnRegistry=\"cnRegistry\"\n\t\t\t\t\t\t:availableWidgets=\"titledWidgets\" />\n\t\t\t\t</slot>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- Not-found state: the record behind the address does not exist. -->\n\t\t<div v-if=\"objectNotFound\" class=\"cn-detail-page__not-found\" data-testid=\"cn-detail-page-not-found\">\n\t\t\t<!--\n\t\t\t\t@slot not-found\n\t\t\t\t@description Replace the default not-found surface, shown instead of the\n\t\t\t\theader and body when the schema-driven fetch answers 404.\n\t\t\t\t@binding {object|string} target Router location of the back button.\n\t\t\t-->\n\t\t\t<slot name=\"not-found\" :target=\"notFoundTarget\">\n\t\t\t\t<NcEmptyContent :name=\"notFoundLabels.name\" :description=\"notFoundLabels.description\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<FileQuestionOutline :size=\"48\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t<template v-if=\"$router\" #action>\n\t\t\t\t\t\t<NcButton\n\t\t\t\t\t\t\tvariant=\"primary\"\n\t\t\t\t\t\t\tdata-testid=\"cn-detail-page-not-found-back\"\n\t\t\t\t\t\t\t:to=\"notFoundTarget\">\n\t\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t\t<ArrowLeft :size=\"20\" />\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t{{ notFoundButtonLabel }}\n\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t</template>\n\t\t\t\t</NcEmptyContent>\n\t\t\t</slot>\n\t\t</div>\n\n\t\t<!-- Access purpose (`accessPurpose`): nothing of the record shows until\n\t\t the reader has stated why they open it and the purpose is recorded. -->\n\t\t<div v-else-if=\"contentWithheld\" class=\"cn-detail-page__access-purpose\" data-testid=\"cn-detail-page-access-purpose\">\n\t\t\t<CnAccessPurposeDialog\n\t\t\t\tv-if=\"accessPurposeAsking\"\n\t\t\t\t:title=\"accessPurposeText('title')\"\n\t\t\t\t:description=\"accessPurposeText('description')\"\n\t\t\t\t:options=\"accessPurposeOptions\"\n\t\t\t\t:loading=\"accessPurposeBusy\"\n\t\t\t\t:error=\"accessPurposeError\"\n\t\t\t\t@submit=\"recordAccessPurpose\"\n\t\t\t\t@cancel=\"accessPurposeAsking = false\" />\n\t\t\t<NcEmptyContent\n\t\t\t\tv-else-if=\"accessPurposeRequired\"\n\t\t\t\t:name=\"t('nextcloud-vue', 'State a purpose to open this')\"\n\t\t\t\t:description=\"t('nextcloud-vue', 'This record holds special categories of personal data. It opens once your purpose is recorded.')\"\n\t\t\t\tdata-testid=\"cn-detail-page-access-purpose-refused\">\n\t\t\t\t<template #action>\n\t\t\t\t\t<NcButton variant=\"primary\" data-testid=\"cn-detail-page-access-purpose-ask\" @click=\"accessPurposeAsking = true\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'State a purpose') }}\n\t\t\t\t\t</NcButton>\n\t\t\t\t</template>\n\t\t\t</NcEmptyContent>\n\t\t\t<NcLoadingIcon v-else :size=\"32\" :name=\"loadingLabel\" />\n\t\t</div>\n\n\t\t<!-- Loading state -->\n\t\t<div v-else-if=\"showLoadingState\" class=\"cn-detail-page__loading\">\n\t\t\t<NcLoadingIcon :size=\"32\" />\n\t\t\t<span>{{ loadingLabel }}</span>\n\t\t</div>\n\n\t\t<!-- Error state -->\n\t\t<div v-else-if=\"error\" class=\"cn-detail-page__error\">\n\t\t\t<!--\n\t\t\t\t@slot error\n\t\t\t\t@description Replace the default `<NcEmptyContent>` error surface. Default\n\t\t\t\tcontent uses `errorMessage` + an MDI alert icon + an optional retry button.\n\t\t\t-->\n\t\t\t<slot name=\"error\">\n\t\t\t\t<NcEmptyContent :name=\"errorMessage\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<AlertCircleOutline :size=\"48\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t<template #action>\n\t\t\t\t\t\t<NcButton v-if=\"onRetry\" variant=\"primary\" @click=\"onRetry\">\n\t\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t\t<Refresh :size=\"20\" />\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t{{ retryLabel }}\n\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t\t<!--\n\t\t\t\t\t\t\t@slot error-actions\n\t\t\t\t\t\t\t@description Extra buttons rendered inside the default error\n\t\t\t\t\t\t\tsurface's action area, after the (optional) Retry button.\n\t\t\t\t\t\t-->\n\t\t\t\t\t\t<slot name=\"error-actions\" />\n\t\t\t\t\t</template>\n\t\t\t\t</NcEmptyContent>\n\t\t\t</slot>\n\t\t</div>\n\n\t\t<!-- Empty state -->\n\t\t<div v-else-if=\"empty\" class=\"cn-detail-page__empty\">\n\t\t\t<!--\n\t\t\t\t@slot empty\n\t\t\t\t@description Replace the default empty-state `<NcEmptyContent>` surface.\n\t\t\t-->\n\t\t\t<slot name=\"empty\">\n\t\t\t\t<NcEmptyContent :name=\"emptyLabel\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<InformationOutline :size=\"48\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t<template #action>\n\t\t\t\t\t\t<!--\n\t\t\t\t\t\t\t@slot empty-actions\n\t\t\t\t\t\t\t@description Buttons rendered inside the default empty-state\n\t\t\t\t\t\t\taction area.\n\t\t\t\t\t\t-->\n\t\t\t\t\t\t<slot name=\"empty-actions\" />\n\t\t\t\t\t</template>\n\t\t\t\t</NcEmptyContent>\n\t\t\t</slot>\n\t\t</div>\n\n\t\t<!-- Main content -->\n\t\t<div v-else class=\"cn-detail-page__body\">\n\t\t\t<div v-if=\"$slots['before-body'] || $slots['before-body']\" class=\"cn-detail-page__before-body\">\n\t\t\t\t<!--\n\t\t\t\t\t@slot before-body\n\t\t\t\t\t@description Content rendered at the top of the body, above the grid\n\t\t\t\t\tlayout / stats / schema-driven auto-body (Data + Related). Use it to\n\t\t\t\t\tsurface page-level widgets (e.g. a KPI/insights dashboard) in the body\n\t\t\t\t\tbelow the header+actions line without suppressing the auto Data/Related\n\t\t\t\t\tsections (which a `default` slot override would).\n\t\t\t\t\t@binding {object} object The resolved record.\n\t\t\t\t\t@binding {object} schema The resolved schema.\n\t\t\t\t\t@binding {string} objectType The resolved object type.\n\t\t\t\t\t@binding {string} objectId The resolved object id.\n\t\t\t\t\t@binding {object} store The effective object store.\n\t\t\t\t-->\n\t\t\t\t<slot\n\t\t\t\t\tname=\"before-body\"\n\t\t\t\t\t:object=\"resolvedObject\"\n\t\t\t\t\t:schema=\"currentSchema\"\n\t\t\t\t\t:objectType=\"resolvedObjectType\"\n\t\t\t\t\t:objectId=\"objectId\"\n\t\t\t\t\t:store=\"effectiveObjectStore\" />\n\t\t\t</div>\n\n\t\t\t<!-- Declarative in-body sections, `placement: \"before-body\"` —\n\t\t\t above the grid / stats / auto-body. -->\n\t\t\t<CnBodySections\n\t\t\t\tv-if=\"hasBodyWidgets\"\n\t\t\t\t:sections=\"bodyWidgets\"\n\t\t\t\t:context=\"sectionContext\"\n\t\t\t\tplacement=\"before-body\"\n\t\t\t\tdata-testid=\"cn-detail-body-sections-before-body\" />\n\n\t\t\t<!-- Adjustable widget grid (GridStack). The detail body is, at its\n\t\t\t core, a real drag/resize grid: by default it is seeded with the\n\t\t\t schema-driven Data + Related widgets (see `bodyGridLayout`), but in\n\t\t\t Buildiq edit mode widgets can be moved, resized, configured and\n\t\t\t added. Explicit `layout` + `widgets` props (manifest grid pages)\n\t\t\t feed the same engine, so hand-authored grid pages also become\n\t\t\t draggable. -->\n\t\t\t<!-- :key remounts the grid when in-app edit mode flips (ADR-041):\n\t\t\t the manifest is raw (non-reactive) until useManifestEditor\n\t\t\t observes it in place, so a grid mounted pre-edit never\n\t\t\t subscribed to the layout/widgets arrays — in-place pushes\n\t\t\t (Add widget) keep the same array identity and can't re-render\n\t\t\t it. One remount per edit flip re-subscribes it. -->\n\t\t\t<!-- One grid per section: the body grid, then the chosen view\n\t\t\t (`views`) in a region the view switch controls. A view grid is\n\t\t\t keyed on its view, so switching mounts the other view's grid. -->\n\t\t\t<CnDashboardGrid\n\t\t\t\tv-for=\"section in bodyGridSections\"\n\t\t\t\t:key=\"`cn-detail-grid-${section.key}-${section.editable ? 'editing' : 'live'}`\"\n\t\t\t\tv-bind=\"section.attrs\"\n\t\t\t\t:layout=\"section.layout\"\n\t\t\t\t:editable=\"section.editable\"\n\t\t\t\t:columns=\"12\"\n\t\t\t\t:columnOpts=\"bodyColumnOpts\"\n\t\t\t\tclass=\"cn-detail-page__grid\"\n\t\t\t\t@layoutChange=\"section.view ? onViewLayoutChange($event) : onBodyLayoutChange($event)\">\n\t\t\t\t<template #widget=\"{ item }\">\n\t\t\t\t\t<div\n\t\t\t\t\t\tclass=\"cn-detail-page__grid-item\"\n\t\t\t\t\t\t:class=\"{ 'cn-detail-page__grid-item--fit': !!item.sizeToContent }\"\n\t\t\t\t\t\t:aria-labelledby=\"showGridTitle(item) ? `widget-title-${item.id}` : undefined\">\n\t\t\t\t\t\t<!-- In-app edit overlay (ADR-041): a configure cog appears on\n\t\t\t\t\t\t EVERY grid widget while the page is in Buildiq edit mode,\n\t\t\t\t\t\t as it does on CnDashboardPage. It was gated on the type\n\t\t\t\t\t\t having a registered config form, which left a `custom`\n\t\t\t\t\t\t widget and an `integration` leaf with no cog at all: they\n\t\t\t\t\t\t could be dragged and resized but never configured or\n\t\t\t\t\t\t removed, because the modal's own Delete affordance is the\n\t\t\t\t\t\t only way off the page. The modal already handles a type\n\t\t\t\t\t\t without a form (title, icon, style, delete), so the gate\n\t\t\t\t\t\t bought nothing. -->\n\t\t\t\t\t\t<div v-if=\"showsCog(item)\" class=\"cn-detail-page__widget-edit\">\n\t\t\t\t\t\t\t<NcButton variant=\"tertiary\" :aria-label=\"t('nextcloud-vue', 'Configure widget')\" @click=\"configureWidget(item)\">\n\t\t\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t\t\t<Cog :size=\"18\" />\n\t\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<!-- Section heading ONLY for consumer-supplied bare-content slots.\n\t\t\t\t\t\t Built-in widgets (data / related / integration / catalog)\n\t\t\t\t\t\t render their own titled card header, so a grid <h3> here would\n\t\t\t\t\t\t duplicate the title above the widget. -->\n\t\t\t\t\t\t<component :is=\"isBoard ? 'h2' : 'h3'\"\n\t\t\t\t\t\t\tv-if=\"showGridTitle(item)\"\n\t\t\t\t\t\t\t:id=\"`widget-title-${item.id}`\"\n\t\t\t\t\t\t\tclass=\"cn-detail-page__widget-title\">\n\t\t\t\t\t\t\t{{ gridTitle(item) }}\n\t\t\t\t\t\t</component>\n\t\t\t\t\t\t<!--\n\t\t\t\t\t\t\t@slot `widget-${item.widgetId}`\n\t\t\t\t\t\t\t@description Per-widget slot whose name is `widget-<widgetId>`. Use\n\t\t\t\t\t\t\tit to inject custom widget content into a grid slot. Default\n\t\t\t\t\t\t\tfallback renders a registry widget: `type: 'data'` renders the\n\t\t\t\t\t\t\tschema-driven data widget; `type: 'related'` the related-objects\n\t\t\t\t\t\t\twidget; `type: 'integration'` resolves from the integration\n\t\t\t\t\t\t\tregistry; any other content-driven catalog type (stat / chart /\n\t\t\t\t\t\t\tdelta / gauge / object-list / …) renders its registered renderer.\n\n\t\t\t\t\t\t\tThe page's object context is bound alongside the widget\n\t\t\t\t\t\t\tdescriptor because a `type: \"custom\"` widget is not a template —\n\t\t\t\t\t\t\tCnPageRenderer mounts the mapped component as\n\t\t\t\t\t\t\t`<component v-bind=\"slotProps\" />`, so whatever THIS slot binds\n\t\t\t\t\t\t\tIS that widget's entire prop set. Binding only `item`/`widget`\n\t\t\t\t\t\t\ttherefore left every custom widget that declares `objectId`\n\t\t\t\t\t\t\tsitting on its own `default: ''` — and an empty id does not\n\t\t\t\t\t\t\terror, it widens the widget's OpenRegister filter to the whole\n\t\t\t\t\t\t\tcollection (decidiq#968: a voting-round panel filtering on\n\t\t\t\t\t\t\t`relations.motion: ''` listed every round in the instance, so a\n\t\t\t\t\t\t\tvote could be cast against another motion's round). Widgets that\n\t\t\t\t\t\t\tdeclare none of these props are unaffected — this is purely\n\t\t\t\t\t\t\tadditive.\n\n\t\t\t\t\t\t\t`objectId` is bound from the PROP, never read back off the loaded\n\t\t\t\t\t\t\trecord: `currentObject` / `resolvedObject` are themselves keyed by\n\t\t\t\t\t\t\t`objectId`, and the record is null for the first frames of a\n\t\t\t\t\t\t\tdetail page — the same \"the record has not arrived yet\" race\n\t\t\t\t\t\t\t`editFormAwaitingRecord` guards (#850). The route param reaches\n\t\t\t\t\t\t\tthe prop through CnPageRenderer, which maps `params.id` →\n\t\t\t\t\t\t\t`objectId`, so the id is there on the first render while `object`\n\t\t\t\t\t\t\tis not.\n\t\t\t\t\t\t\t@binding {object} item Layout item descriptor.\n\t\t\t\t\t\t\t@binding {object} widget Resolved widget definition.\n\t\t\t\t\t\t\t@binding {string|number} objectId This record's id. Unlike `object`\n\t\t\t\t\t\t\tit is present on the very first render, because it comes from the route.\n\t\t\t\t\t\t\t@binding {object} object The loaded record, or null while it is\n\t\t\t\t\t\t\tstill being fetched.\n\t\t\t\t\t\t\t@binding {object} objectData Alias of `object`, spelled the way widget\n\t\t\t\t\t\t\tcomponents declare the prop (`objectData`).\n\t\t\t\t\t\t\t@binding {string} objectType Resolved object type slug.\n\t\t\t\t\t\t\t@binding {string} register Register slug of this page.\n\t\t\t\t\t\t\t@binding {string} schema Schema slug of this page.\n\t\t\t\t\t\t-->\n\t\t\t\t\t\t<slot\n\t\t\t\t\t\t\t:name=\"`widget-${item.widgetId}`\"\n\t\t\t\t\t\t\t:item=\"item\"\n\t\t\t\t\t\t\t:widget=\"titled(findWidget(item))\"\n\t\t\t\t\t\t\t:objectId=\"objectId\"\n\t\t\t\t\t\t\t:object=\"resolvedObject\"\n\t\t\t\t\t\t\t:objectData=\"resolvedObject\"\n\t\t\t\t\t\t\t:objectType=\"resolvedObjectType\"\n\t\t\t\t\t\t\t:register=\"register\"\n\t\t\t\t\t\t\t:schema=\"schema\">\n\t\t\t\t\t\t\t<CnDetailWidgetHost\n\t\t\t\t\t\t\t\t:widget=\"titled(findWidget(item))\"\n\t\t\t\t\t\t\t\tchrome=\"card\"\n\t\t\t\t\t\t\t\tinGridCell\n\t\t\t\t\t\t\t\t:showCardTitle=\"showCardTitle(item)\"\n\t\t\t\t\t\t\t\t:showActions=\"effectiveShowWidgetActions\"\n\t\t\t\t\t\t\t\t:objectId=\"objectId\"\n\t\t\t\t\t\t\t\t:object=\"currentObject\"\n\t\t\t\t\t\t\t\t:objectType=\"resolvedObjectType\"\n\t\t\t\t\t\t\t\t:schemaObject=\"currentSchema\"\n\t\t\t\t\t\t\t\t:register=\"register\"\n\t\t\t\t\t\t\t\t:schema=\"schema\"\n\t\t\t\t\t\t\t\t:store=\"effectiveObjectStore\"\n\t\t\t\t\t\t\t\t:surface=\"surface\"\n\t\t\t\t\t\t\t\t:integrationContext=\"effectiveIntegrationContext\"\n\t\t\t\t\t\t\t\t:hideEmpty=\"hideEmpty\"\n\t\t\t\t\t\t\t\t:cnRegistry=\"cnRegistry\"\n\t\t\t\t\t\t\t\t:availableWidgets=\"titledBodyGridWidgets\"\n\t\t\t\t\t\t\t\t@geoSaved=\"onGeoSaved\"\n\t\t\t\t\t\t\t\t@openIntegration=\"onAutoBodyOpenIntegration\"\n\t\t\t\t\t\t\t\t@selectObject=\"onRelatedObjectSelect\" />\n\t\t\t\t\t\t</slot>\n\t\t\t\t\t</div>\n\t\t\t\t</template>\n\t\t\t</CnDashboardGrid>\n\n\t\t\t<!-- The chosen view has nothing to draw: say so in the region,\n\t\t\t never leave a blank area under the switch. -->\n\t\t\t<div\n\t\t\t\tv-if=\"viewIsEmpty\"\n\t\t\t\t:id=\"viewRegionId\"\n\t\t\t\trole=\"region\"\n\t\t\t\t:aria-label=\"activeView ? activeView.label : null\"\n\t\t\t\tclass=\"cn-page-view-region cn-page-view-region--empty\"\n\t\t\t\tdata-testid=\"cn-detail-page-view-empty\">\n\t\t\t\t<NcEmptyContent :description=\"viewEmptyText\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<ViewDashboardOutline :size=\"48\" />\n\t\t\t\t\t</template>\n\t\t\t\t</NcEmptyContent>\n\t\t\t</div>\n\n\t\t\t<!-- Statistics table -->\n\t\t\t<div v-if=\"hasStats\" class=\"cn-detail-page__stats\">\n\t\t\t\t<!--\n\t\t\t\t\t@slot stats-header\n\t\t\t\t\t@description Replace the heading above the stats table. Default\n\t\t\t\t\tcontent: an `<h3>` showing `statsTitle`.\n\t\t\t\t-->\n\t\t\t\t<slot name=\"stats-header\">\n\t\t\t\t\t<component :is=\"isBoard ? 'h2' : 'h3'\" v-if=\"statsTitle\" class=\"cn-detail-page__section-title\">\n\t\t\t\t\t\t{{ resolvedStatsTitle }}\n\t\t\t\t\t</component>\n\t\t\t\t</slot>\n\t\t\t\t<table class=\"cn-detail-page__stats-table\">\n\t\t\t\t\t<thead v-if=\"statsColumns.length > 0\">\n\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t<th v-for=\"col in statsColumns\"\n\t\t\t\t\t\t\t\t:key=\"col.key\"\n\t\t\t\t\t\t\t\tscope=\"col\"\n\t\t\t\t\t\t\t\t:class=\"col.align ? 'cn-detail-page__stats-cell--' + col.align : ''\">\n\t\t\t\t\t\t\t\t{{ col.label }}\n\t\t\t\t\t\t\t</th>\n\t\t\t\t\t\t</tr>\n\t\t\t\t\t</thead>\n\t\t\t\t\t<tbody>\n\t\t\t\t\t\t<!--\n\t\t\t\t\t\t\t@slot stats-rows\n\t\t\t\t\t\t\t@description Replace the default rendering of the `statsRows`\n\t\t\t\t\t\t\tprop. Use when rows need custom formatting per column.\n\t\t\t\t\t\t-->\n\t\t\t\t\t\t<slot name=\"stats-rows\">\n\t\t\t\t\t\t\t<tr v-for=\"(row, index) in statsRows\" :key=\"index\" :class=\"{ 'cn-detail-page__stats-row--sub': row.indent }\">\n\t\t\t\t\t\t\t\t<td v-for=\"col in statsColumns\" :key=\"col.key\" :class=\"[row.indent ? 'cn-detail-page__stats-cell--indented' : '', col.align ? 'cn-detail-page__stats-cell--' + col.align : '']\">\n\t\t\t\t\t\t\t\t\t{{ row[col.key] !== undefined ? row[col.key] : '-' }}\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t</slot>\n\t\t\t\t\t</tbody>\n\t\t\t\t</table>\n\t\t\t</div>\n\n\t\t\t<!-- Default vertical stacking mode. The schema-driven auto-body (Data +\n\t\t\t Related) is now rendered by the adjustable grid above (seeded by\n\t\t\t `bodyGridLayout`), so this branch only carries hand-authored\n\t\t\t default-slot content when no widget grid is active. -->\n\t\t\t<div v-else class=\"cn-detail-page__content\">\n\t\t\t\t<!-- Default content -->\n\t\t\t\t<div v-if=\"!hasBodyGrid\" class=\"cn-detail-page__content\">\n\t\t\t\t\t<!--\n\t\t\t\t\t\t@slot default\n\t\t\t\t\t\t@description Main body content rendered when no grid layout, no\n\t\t\t\t\t\tstats table, and no schema-driven auto-body apply. Use it to ship\n\t\t\t\t\t\ta hand-authored detail page.\n\t\t\t\t\t-->\n\t\t\t\t\t<slot />\n\t\t\t\t</div>\n\n\t\t\t\t<!-- Sections slot — additional content below stats -->\n\t\t\t\t<div v-if=\"$slots.sections\" class=\"cn-detail-page__sections\">\n\t\t\t\t\t<!--\n\t\t\t\t\t\t@slot sections\n\t\t\t\t\t\t@description Additional vertically-stacked content rendered below\n\t\t\t\t\t\tthe default body / stats table.\n\t\t\t\t\t-->\n\t\t\t\t\t<slot name=\"sections\" />\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t<!-- Declarative in-body sections, `placement: \"after-data\"` —\n\t\t\t below the data/auto-body, above the related-object lists. -->\n\t\t\t<CnBodySections\n\t\t\t\tv-if=\"hasBodyWidgets\"\n\t\t\t\t:sections=\"bodyWidgets\"\n\t\t\t\t:context=\"sectionContext\"\n\t\t\t\tplacement=\"after-data\"\n\t\t\t\tdata-testid=\"cn-detail-body-sections-after-data\" />\n\n\t\t\t<!-- Declarative related-object list sections (manifest\n\t\t\t `config.relatedCollections`). Rendered below the detail body;\n\t\t\t each section's filter is scoped to this object via @objectId.\n\t\t\t An optional relation-link button opens CnRelationLinkModal. -->\n\t\t\t<div\n\t\t\t\tv-if=\"(relatedCollections && relatedCollections.length > 0) || (relationLinks && relationLinks.length > 0)\"\n\t\t\t\tclass=\"cn-detail-page__related-collections\">\n\t\t\t\t<div v-if=\"relationLinks && relationLinks.length > 0\" class=\"cn-detail-page__relation-links\">\n\t\t\t\t\t<NcButton\n\t\t\t\t\t\tv-for=\"(link, li) in relationLinks\"\n\t\t\t\t\t\t:key=\"li\"\n\t\t\t\t\t\tclass=\"cn-detail-page__relation-link-button\"\n\t\t\t\t\t\t:data-testid=\"`cn-detail-relation-link-${li}`\"\n\t\t\t\t\t\t@click=\"openRelationLink(link)\">\n\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t<Plus :size=\"18\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t\t{{ link.label || t('nextcloud-vue', 'Link related object') }}\n\t\t\t\t\t</NcButton>\n\t\t\t\t</div>\n\t\t\t\t<CnRelatedCollections\n\t\t\t\t\tv-if=\"relatedCollections && relatedCollections.length > 0\"\n\t\t\t\t\t:collections=\"relatedCollections\"\n\t\t\t\t\t@rowClick=\"onRelatedRowClick\" />\n\t\t\t</div>\n\n\t\t\t<!-- Declarative in-body sections, `placement: \"after-related\"` —\n\t\t\t below the related-object lists. -->\n\t\t\t<CnBodySections\n\t\t\t\tv-if=\"hasBodyWidgets\"\n\t\t\t\t:sections=\"bodyWidgets\"\n\t\t\t\t:context=\"sectionContext\"\n\t\t\t\tplacement=\"after-related\"\n\t\t\t\tdata-testid=\"cn-detail-body-sections-after-related\" />\n\n\t\t\t<!-- Declarative in-body sections, `placement: \"end\"` (the default\n\t\t\t placement) — the last body content, above the footer. -->\n\t\t\t<CnBodySections\n\t\t\t\tv-if=\"hasBodyWidgets\"\n\t\t\t\t:sections=\"endPlacementSections\"\n\t\t\t\t:context=\"sectionContext\"\n\t\t\t\t:placement=\"null\"\n\t\t\t\tdata-testid=\"cn-detail-body-sections-end\" />\n\n\t\t\t<!-- Footer -->\n\t\t\t<div v-if=\"$slots.footer\" class=\"cn-detail-page__footer\">\n\t\t\t\t<!--\n\t\t\t\t\t@slot footer\n\t\t\t\t\t@description Footer surface rendered below the detail-page body. Use\n\t\t\t\t\tit for save/cancel button rows or status text.\n\t\t\t\t-->\n\t\t\t\t<slot name=\"footer\" />\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- Side column (manifest `config.sideColumn`): the record's facts as\n\t\t a column of cards beside the body. A sibling of the body, after it\n\t\t in the document, so it reads after the body and stacks under it\n\t\t when the page is too narrow for both. -->\n\t\t<aside\n\t\t\tv-if=\"showsSideColumn\"\n\t\t\tclass=\"cn-detail-page__side\"\n\t\t\t:aria-label=\"sideColumnLabel\"\n\t\t\tdata-testid=\"cn-detail-page-side\">\n\t\t\t<!-- Where a body widget puts its own cards about what is selected in\n\t\t\t it (`cnDetailSideColumn`); while it does, the generic cards\n\t\t\t below stand aside. -->\n\t\t\t<div :id=\"sideTakeoverId\"\n\t\t\t\tclass=\"cn-detail-page__side-takeover\"\n\t\t\t\tdata-testid=\"cn-detail-page-side-takeover\" />\n\t\t\t<div\n\t\t\t\tv-for=\"widget in sideClaims.length > 0 ? [] : sideColumnWidgets\"\n\t\t\t\t:key=\"`side-${widget.id}`\"\n\t\t\t\tclass=\"cn-detail-page__side-item\"\n\t\t\t\t:class=\"{ 'cn-detail-page__side-item--notice': isBoard && widget.type === 'banner', 'cn-detail-page__side-item--history': isBoard && widget.id === sideHistoryId, 'cn-detail-page__side-item--with-identifier': isBoard && widget.id === sideHistoryId && identifierLine !== '' }\"\n\t\t\t\t:data-testid=\"`cn-detail-page-side-${widget.id}`\">\n\t\t\t\t<!--\n\t\t\t\t\t@slot `widget-${widget.id}`\n\t\t\t\t\t@description The same per-widget slot the body grid offers,\n\t\t\t\t\tfor a widget placed in the side column. Same name and same\n\t\t\t\t\tbindings, so a custom widget placed here needs nothing it\n\t\t\t\t\twould not need in the grid. Default content renders the\n\t\t\t\t\twidget through `CnDetailWidgetHost` as a card.\n\t\t\t\t\t@binding {object} item `{ id, widgetId }`, for parity with the grid slot.\n\t\t\t\t\t@binding {object} widget The widget definition.\n\t\t\t\t\t@binding {string|number} objectId This record's id.\n\t\t\t\t\t@binding {object} object The loaded record, or null while it is still being fetched.\n\t\t\t\t\t@binding {object} objectData Alias of `object`.\n\t\t\t\t\t@binding {string} objectType Resolved object type slug.\n\t\t\t\t\t@binding {string} register Register slug of this page.\n\t\t\t\t\t@binding {string} schema Schema slug of this page.\n\t\t\t\t-->\n\t\t\t\t<slot\n\t\t\t\t\t:name=\"`widget-${widget.id}`\"\n\t\t\t\t\t:item=\"{ id: widget.id, widgetId: widget.id }\"\n\t\t\t\t\t:widget=\"titled(widget)\"\n\t\t\t\t\t:objectId=\"objectId\"\n\t\t\t\t\t:object=\"resolvedObject\"\n\t\t\t\t\t:objectData=\"resolvedObject\"\n\t\t\t\t\t:objectType=\"resolvedObjectType\"\n\t\t\t\t\t:register=\"register\"\n\t\t\t\t\t:schema=\"schema\">\n\t\t\t\t\t<component\n\t\t\t\t\t\t:is=\"widget.follows ? 'CnFollowingWidgetHost' : 'CnDetailWidgetHost'\"\n\t\t\t\t\t\t:widget=\"titled(widget)\"\n\t\t\t\t\t\tchrome=\"card\"\n\t\t\t\t\t\t:showCardTitle=\"true\"\n\t\t\t\t\t\t:showActions=\"effectiveShowWidgetActions\"\n\t\t\t\t\t\t:objectId=\"objectId\"\n\t\t\t\t\t\t:object=\"currentObject\"\n\t\t\t\t\t\t:objectType=\"resolvedObjectType\"\n\t\t\t\t\t\t:schemaObject=\"currentSchema\"\n\t\t\t\t\t\t:register=\"register\"\n\t\t\t\t\t\t:schema=\"schema\"\n\t\t\t\t\t\t:store=\"effectiveObjectStore\"\n\t\t\t\t\t\t:surface=\"surface\"\n\t\t\t\t\t\t:integrationContext=\"effectiveIntegrationContext\"\n\t\t\t\t\t\t:hideEmpty=\"hideEmpty\"\n\t\t\t\t\t\t:cnRegistry=\"cnRegistry\"\n\t\t\t\t\t\t:availableWidgets=\"titledWidgets\"\n\t\t\t\t\t\t@geoSaved=\"onGeoSaved\"\n\t\t\t\t\t\t@openIntegration=\"onAutoBodyOpenIntegration\"\n\t\t\t\t\t\t@selectObject=\"onRelatedObjectSelect\" />\n\t\t\t\t</slot>\n\t\t\t\t<!-- The identifier line that ends the History card in the board\n\t\t\t\t look (`config.identifierField`). -->\n\t\t\t\t<p\n\t\t\t\t\tv-if=\"isBoard && widget.id === sideHistoryId && identifierLine !== ''\"\n\t\t\t\t\tclass=\"cn-detail-page__side-identifier\"\n\t\t\t\t\tdata-testid=\"cn-detail-page-side-identifier\">\n\t\t\t\t\t{{ identifierLine }}\n\t\t\t\t</p>\n\t\t\t</div>\n\t\t</aside>\n\n\t\t<!-- Per-widget style/config editor (ADR-041). Opened by the per-widget\n\t\t configure cog while in Buildiq edit mode; the modal's own Delete\n\t\t button removes the widget from the grid. -->\n\t\t<CnWidgetStyleEditorModal\n\t\t\tv-if=\"showWidgetConfig\"\n\t\t\t:show=\"showWidgetConfig\"\n\t\t\t:widget=\"titled(configWidget)\"\n\t\t\t:deletable=\"true\"\n\t\t\t@save=\"onWidgetConfigSave\"\n\t\t\t@delete=\"onWidgetConfigDelete\"\n\t\t\t@close=\"showWidgetConfig = false\" />\n\n\t\t<!-- @slot form-dialog Replace the built-in create/edit form dialog (use CnFormDialog or CnAdvancedFormDialog). Deliberately the SAME slot name and the same scope as CnIndexPage's `form-dialog`, so one replacement component serves both pages and an app author learns one contract, not two. Covers both of this page's built-in dialogs — the create archetype's empty form and the record edit form — which are mutually exclusive, exactly as CnIndexPage's single slot covers create and edit. -->\n\t\t<!-- @binding {boolean} show Whether the form dialog is currently visible. -->\n\t\t<!-- @binding {?object} item The record being edited; in create mode the route-derived prefill, or null when there is nothing to prefill. -->\n\t\t<!-- @binding {object} schema The effective JSON schema driving the form. Transform this — rather than replacing the save path — to splice a runtime vocabulary into an enum. -->\n\t\t<!-- @binding {Function} confirm Persists the form data through the page's own save path and emits `created`/`edited`. Call this instead of saving in the replacement dialog, so a create or edit made there behaves exactly like one made in the built-in dialog. RESOLVES to a `{ success: true, data }` / `{ error }` result, which a replacement rendering its own CnFormDialog must hand back to that dialog's `setResult()`: CnFormDialog sets `loading` on submit and only `setResult` clears it, and `no-close` is bound to `loading`, so a replacement that ignores the result locks its own modal open on both success and failure. -->\n\t\t<!-- @binding {Function} close Closes the form dialog — in create mode this is the \"dismissed without saving\" path, which navigates back so the user is not stranded on an id-less page. -->\n\t\t<!--\n\t\t `show` and `confirm` are bound as PROPS, not left as listeners on\n\t\t the default children. A manifest-declared replacement is mounted by\n\t\t CnPageRenderer as `<component :is=… v-bind=\"slotProps\" />`, which\n\t\t binds props only — so a listener is unreachable from a manifest\n\t\t even in principle, and a replacement dialog could render and close\n\t\t but never save (openconnector#1150, fixed the same way on\n\t\t CnIndexPage).\n\n\t\t The two default children below are UNCHANGED and still wire their\n\t\t own `@confirm` / `@close` directly, so a page that passes no slot\n\t\t renders and behaves exactly as it did before this slot existed.\n\t\t-->\n\t\t<slot\n\t\t\tname=\"form-dialog\"\n\t\t\t:show=\"formDialogVisible\"\n\t\t\t:item=\"formDialogItem\"\n\t\t\t:schema=\"currentSchema\"\n\t\t\t:confirm=\"onFormDialogConfirm\"\n\t\t\t:close=\"onFormDialogClose\">\n\t\t\t<!-- Create archetype (ADR-062): a schema-bound `type:\"detail\"` page with\n\t\t\t no object id renders an empty create form for its schema instead of a\n\t\t\t blank page. Save POSTs the object and navigates to its detail route\n\t\t\t (or back); prefill comes from the route query. -->\n\t\t\t<CnFormDialog\n\t\t\t\tv-if=\"isCreateMode && currentSchema\"\n\t\t\t\tref=\"createFormDialog\"\n\t\t\t\t:schema=\"currentSchema\"\n\t\t\t\t:item=\"createPrefill\"\n\t\t\t\t:register=\"register\"\n\t\t\t\t:dialogTitle=\"title || undefined\"\n\t\t\t\t:excludeFields=\"excludeFields\"\n\t\t\t\t:includeFields=\"includeFields\"\n\t\t\t\t:fieldOverrides=\"fieldOverrides\"\n\t\t\t\t:size=\"formSize\"\n\t\t\t\t:columns=\"formColumns\"\n\t\t\t\t@confirm=\"onCreateFormConfirm\"\n\t\t\t\t@close=\"onCreateFormClose\" />\n\n\t\t\t<!-- Record edit form. Same component and same schema the index table's\n\t\t\t modal used; the difference is that it is reached from the record,\n\t\t\t with the record's own page around it.\n\n\t\t\t Waits for the RECORD as well as the schema. `currentObject` is a\n\t\t\t store read that is null until this page's own fetch lands, and a\n\t\t\t dialog opened before then shows an empty form for a record that\n\t\t\t has values — which the user can type into, and whose Save would\n\t\t\t PUT those blanks over the record. -->\n\t\t\t<CnFormDialog\n\t\t\t\tv-if=\"editFormOpen && currentSchema && !editFormAwaitingRecord\"\n\t\t\t\tref=\"editFormDialog\"\n\t\t\t\t:schema=\"currentSchema\"\n\t\t\t\t:item=\"currentObject\"\n\t\t\t\t:register=\"register\"\n\t\t\t\t:dialogTitle=\"editFormTitle\"\n\t\t\t\t:subtitle=\"editFormSubtitle\"\n\t\t\t\t:excludeFields=\"excludeFields\"\n\t\t\t\t:includeFields=\"includeFields\"\n\t\t\t\t:fieldOverrides=\"fieldOverrides\"\n\t\t\t\t:size=\"formSize\"\n\t\t\t\t:columns=\"formColumns\"\n\t\t\t\t@confirm=\"onEditFormConfirm\"\n\t\t\t\t@close=\"closeEditForm\" />\n\t\t</slot>\n\n\t\t<!-- A record-source verb that asks for values first (the tasks source's\n\t\t Edit): its own form, saved through the source. -->\n\t\t<CnFormDialog\n\t\t\tv-if=\"sourceVerbForm\"\n\t\t\tref=\"sourceVerbFormDialog\"\n\t\t\t:schema=\"sourceVerbForm.form.schema\"\n\t\t\t:item=\"sourceVerbForm.form.item || {}\"\n\t\t\t:dialogTitle=\"sourceVerbForm.label\"\n\t\t\t:fieldOverrides=\"sourceVerbForm.form.fieldOverrides || {}\"\n\t\t\t@confirm=\"onSourceVerbFormConfirm\"\n\t\t\t@close=\"sourceVerbForm = null\" />\n\n\t\t<!-- Relation-link modal (manifest `config.relationLinks`): async-search a\n\t\t target schema and patch a FK on the current object. -->\n\t\t<CnRelationLinkModal\n\t\t\tv-if=\"activeRelationLink\"\n\t\t\t:title=\"activeRelationLink.title || undefined\"\n\t\t\t:selectLabel=\"activeRelationLink.selectLabel || undefined\"\n\t\t\t:register=\"activeRelationLink.register\"\n\t\t\t:schema=\"activeRelationLink.schema\"\n\t\t\t:labelField=\"activeRelationLink.labelField || 'name'\"\n\t\t\t:allowCreate=\"activeRelationLink.allowCreate === true\"\n\t\t\t:currentType=\"resolvedObjectType\"\n\t\t\t:currentObject=\"currentObject || {}\"\n\t\t\t:fkField=\"activeRelationLink.fkField\"\n\t\t\t@linked=\"onRelationLinked\"\n\t\t\t@close=\"activeRelationLink = null\" />\n\t</div>\n</template>\n\n<script>\nimport { getCurrentUser } from '@nextcloud/auth'\nimport { subscribe, unsubscribe } from '@nextcloud/event-bus'\nimport { translatePlural as n, translate as t } from '@nextcloud/l10n'\nimport { NcActionButton, NcActionCaption, NcActionLink, NcActionSeparator, NcButton, NcEmptyContent, NcLoadingIcon, NcSelect } from '@nextcloud/vue'\nimport { computed, markRaw, provide, ref, watch } from 'vue'\nimport AlertCircleOutline from 'vue-material-design-icons/AlertCircleOutline.vue'\nimport ArrowLeft from 'vue-material-design-icons/ArrowLeft.vue'\nimport Check from 'vue-material-design-icons/Check.vue'\nimport ChevronLeft from 'vue-material-design-icons/ChevronLeft.vue'\nimport ChevronRight from 'vue-material-design-icons/ChevronRight.vue'\nimport Cog from 'vue-material-design-icons/Cog.vue'\nimport DockRight from 'vue-material-design-icons/DockRight.vue'\nimport EyeOffOutline from 'vue-material-design-icons/EyeOffOutline.vue'\nimport FileQuestionOutline from 'vue-material-design-icons/FileQuestionOutline.vue'\nimport InformationOutline from 'vue-material-design-icons/InformationOutline.vue'\nimport Pencil from 'vue-material-design-icons/Pencil.vue'\nimport Plus from 'vue-material-design-icons/Plus.vue'\nimport Refresh from 'vue-material-design-icons/Refresh.vue'\nimport Restore from 'vue-material-design-icons/Restore.vue'\nimport ViewDashboardOutline from 'vue-material-design-icons/ViewDashboardOutline.vue'\nimport CnAccessPurposeDialog from '../../dialogs/CnAccessPurposeDialog.vue'\nimport CnRelationLinkModal from '../../dialogs/CnRelationLinkModal.vue'\nimport CnWidgetStyleEditorModal from '../../dialogs/CnWidgetStyleEditorModal.vue'\nimport CnActionsMenu from '../CnActionsMenu/CnActionsMenu.vue'\nimport CnBodySections from '../CnBodySections/CnBodySections.vue'\nimport CnBuildiqEditButton from '../CnBuildiqEditButton/CnBuildiqEditButton.vue'\nimport CnDashboardGrid from '../CnDashboardGrid/CnDashboardGrid.vue'\nimport CnDetailWidgetHost from '../CnDetailWidgetHost/CnDetailWidgetHost.vue'\nimport CnFollowingWidgetHost from '../CnFollowingWidgetHost/CnFollowingWidgetHost.vue'\nimport CnFollowToggle from '../CnFollowToggle/CnFollowToggle.vue'\nimport CnFormDialog from '../CnFormDialog/CnFormDialog.vue'\nimport CnLifecycleActions from '../CnLifecycleActions/CnLifecycleActions.vue'\nimport CnLockedBanner from '../CnLockedBanner/CnLockedBanner.vue'\nimport CnNextStepCard from '../CnNextStepCard/CnNextStepCard.vue'\nimport CnPresenceAvatars from '../CnPresenceAvatars/CnPresenceAvatars.vue'\nimport CnRelatedCollections from '../CnRelatedCollections/CnRelatedCollections.vue'\nimport CnSegmentedControl from '../CnSegmentedControl/CnSegmentedControl.vue'\nimport CnSummaryAggregates from '../CnSummaryAggregates/CnSummaryAggregates.vue'\nimport CnTranslatedBadge from '../CnTranslatedBadge/CnTranslatedBadge.vue'\nimport CnDetailHeaderChips from './CnDetailHeaderChips.vue'\nimport { resolveIndexSource, resolveRecordSource } from '../../composables/indexSources.js'\nimport { useIntegrationRegistry } from '../../composables/useIntegrationRegistry.js'\nimport { normalizeLook } from '../../composables/useLook.js'\nimport { useObjectLock } from '../../composables/useObjectLock.js'\nimport { useObjectPresence } from '../../composables/useObjectPresence.js'\nimport { useObjectSubscription } from '../../composables/useObjectSubscription.js'\nimport { createRefLabelResolver } from '../../composables/useRefLabels.js'\nimport { gridLayout } from '../../mixins/gridLayout.js'\nimport { pageFilters } from '../../mixins/pageFilters.js'\nimport { pageViews } from '../../mixins/pageViews.js'\nimport { useObjectStore } from '../../store/index.js'\nimport { headerActionAsLink, resolveManifestHref, routeHref } from '../../utils/actionLink.js'\nimport { isAppInstalled } from '../../utils/appInstalled.js'\nimport { compactLayoutRows } from '../../utils/dashboardPlacement.js'\nimport { defaultDetailGrid } from '../../utils/defaultDetailGrid.js'\nimport {\n\tgroupMenuEntries,\n\tMAX_QUICK_ACTIONS,\n\tnormalisePinnedAction,\n\tonTab,\n\tpillCountSource,\n\tpillLook,\n\tresolveCountPill,\n\tresolveNextStep,\n\tresolvePill,\n\tstageEntry,\n\tstageOf,\n} from '../../utils/detailActionModel.js'\nimport { detailWidgetVisible, fetchVisibilityRows, visibilityContextRefs, widgetConditionData } from '../../utils/detailWidgetVisibility.js'\nimport { reportBindingProblems } from '../../utils/diagnostics.js'\nimport { fetchListTotal } from '../../utils/fetchFilterCounts.js'\nimport { fillTemplate, firstFilled } from '../../utils/fillTemplate.js'\nimport { cnGridCellStyle, hasGridRow } from '../../utils/grid.js'\nimport { isActivityWidget, resolveHeaderMeta } from '../../utils/headerMeta.js'\nimport { prefixUrl } from '../../utils/headers.js'\nimport { HEADING_LEVEL } from '../../utils/headingLevel.js'\nimport { interpolateUrlTokens } from '../../utils/interpolateUrlTokens.js'\nimport { followLinkClick } from '../../utils/linkNavigation.js'\nimport { patchStoredSelf } from '../../utils/patchStoredSelf.js'\nimport { setReadState } from '../../utils/recordInteractions.js'\nimport { saveEdited } from '../../utils/saveEdited.js'\nimport { slotRenders } from '../../utils/slotContent.js'\nimport { userDisplayName } from '../../utils/userDisplayName.js'\nimport { evaluateVisibleWhen, isLocallyDecidableVisibleWhen, readVisibleWhenPath } from '../../utils/visibleWhen.js'\nimport {\n\tisCardWidgetDef,\n\tisContentOnlyWidgetDef,\n\tisDataWidgetDef,\n\tisGeoWidgetDef,\n\tisRelatedWidgetDef,\n\tresolveRegistryRenderer,\n\twidgetContentOf,\n\twidgetTitleOf,\n} from '../../utils/widgetDispatch.js'\nimport { createWidgetSelection, WIDGET_SELECTION } from '../../utils/widgetSelection.js'\nimport { withTitlesWhen, withTitleWhen } from '../../utils/widgetTitleWhen.js'\nimport { CnActionButtons } from '../CnActionButtons/index.js'\nimport { CnBreadcrumbs } from '../CnBreadcrumbs/index.js'\nimport { CnIcon } from '../CnIcon/index.js'\nimport { CnStatusBadge } from '../CnStatusBadge/index.js'\nimport { getWidgetTypeEntry } from '../CnWidgetGrid/dashboardWidgetRegistry.js'\n\nimport '../CnWidgetGrid/registerDashboardWidgets.js'\n\n/**\n * Responsive column table for a detail body, measured against the GRID element.\n *\n * ONE threshold, and deliberately no steps between. `moveScale` rescales the\n * authored geometry into the new count, so an intermediate width turns a\n * 12-column record into fractional positions nobody placed: a `gridWidth: 3`\n * tile becomes 1 of 4, a `gridWidth: 9` panel becomes 3 of 4, and widgets land\n * left, centre or right depending purely on how the division rounded. That reads\n * as a broken page rather than a narrow one. A record is authored at 12 columns\n * or it is a single stacked column; the sizes in between are not designs.\n *\n * 1000px is where the 12-column geometry stops working rather than merely\n * tightening: it puts a `gridWidth: 3` side panel — Timeline, Flow runs — under\n * 250px, which is below its own title.\n *\n * `breakpointForWindow` is FALSE, unlike `getDashboardColumnOpts()`: a page in a\n * split pane sits in a narrow container on a wide window, so measuring the window\n * is measuring the wrong box and the grid would never reflow.\n *\n * `columnMax` is set EXPLICITLY. Above the top breakpoint GridStack assigns\n * `newColumn = columnOpts.columnMax`, and `column(undefined)` silently no-ops, so\n * a grid that once narrowed would never widen again.\n */\nconst DEFAULT_DETAIL_COLUMN_OPTS = Object.freeze({\n\tcolumnMax: 12,\n\tbreakpoints: [\n\t\t{ w: 1000, c: 1 },\n\t],\n\tlayout: 'moveScale',\n\tbreakpointForWindow: false,\n})\n\n/**\n * Where the default table stacks when a side column sits beside the body grid.\n *\n * The side column takes about 320px, so the grid it leaves is far narrower than\n * the page: 790px at a 1100px window. Stacking that at 1000px made a row of\n * small tiles (a KPI row of 5 or 6 stat tiles) impossible beside a side column.\n * 1000px exists to keep a `gridWidth: 3` side PANEL inside the grid readable; a\n * page with a real side column has no such panel, and 12 columns at 600px is\n * still 50px a column, enough for a tile. Below 600px it stacks as before.\n */\nconst SIDE_COLUMN_STACK_WIDTH = 600\n\n/**\n * Whether `opts` is the untouched default table, so a page that passed its own\n * `columnOpts` keeps exactly what it asked for.\n *\n * @param {object|null} opts The `columnOpts` value.\n * @return {boolean}\n */\nfunction isDefaultColumnOpts(opts) {\n\tconst breakpoints = opts && opts.breakpoints\n\treturn !!opts\n\t\t&& opts.columnMax === DEFAULT_DETAIL_COLUMN_OPTS.columnMax\n\t\t&& Array.isArray(breakpoints)\n\t\t&& breakpoints.length === 1\n\t\t&& breakpoints[0].w === DEFAULT_DETAIL_COLUMN_OPTS.breakpoints[0].w\n\t\t&& breakpoints[0].c === 1\n}\n\n/**\n * Event-bus channel a page-level refresh is announced on. The page's own\n * Actions menu emits it, the manifest `refresh` / `api-call` actions bump it,\n * and an app's own dialog emits it after a successful write.\n */\nconst PAGE_REFRESH_CHANNEL = 'cn:page:refresh'\n\n/** Surfaces understood by the pluggable integration registry (AD-19). */\nconst INTEGRATION_SURFACES = ['user-dashboard', 'app-dashboard', 'detail-page', 'single-entity']\n\n/** The session storage key that lists the records whose access purpose is recorded. */\nconst ACCESS_PURPOSE_SESSION_KEY = 'cn-access-purposes'\n\n/** Counter for unique side column slot ids across instances. */\nlet sideTakeoverCounter = 0\n\n/**\n * The records whose access purpose this browser session recorded. Empty when\n * session storage cannot be read (a private window, blocked site data).\n *\n * @return {Array<string>} The record keys.\n */\nfunction readGrantedPurposes() {\n\ttry {\n\t\tconst raw = window.sessionStorage.getItem(ACCESS_PURPOSE_SESSION_KEY)\n\t\tconst list = raw ? JSON.parse(raw) : []\n\t\treturn Array.isArray(list) ? list.filter((key) => typeof key === 'string') : []\n\t} catch {\n\t\treturn []\n\t}\n}\n\n/**\n * CnDetailPage — Generic detail/overview page.\n *\n * Supports two layout modes:\n * 1. **Default (vertical stacking):** Content provided via default slot, cards stack vertically.\n * 2. **Grid layout:** When `layout` and `widgets` props are provided, content renders in a\n * 12-column CSS grid with `#widget-{widgetId}` scoped slots. Same API as CnDashboardPage.\n *\n * Basic usage (vertical stacking)\n *\n * A simpler alternative to CnIndexPage for pages that display detail info,\n * statistics, charts, or card grids — without multi-object tables or CRUD\n * dialogs. Provides a consistent layout with header, loading/error/empty\n * states, a statistics table, and flexible content slots.\n *\n * Basic usage with stats table and content\n * ```vue\n * <CnDetailPage\n * title=\"Register Overview\"\n * description=\"Statistics and schema details\"\n * icon=\"DatabaseOutline\"\n * :stats-title=\"'Register Statistics'\"\n * :stats-columns=\"[\n * { key: 'type', label: 'Type' },\n * { key: 'total', label: 'Total' },\n * { key: 'size', label: 'Size' },\n * ]\"\n * :stats-rows=\"[\n * { type: 'Objects', total: 150, size: '2.4 MB' },\n * { type: 'Files', total: 42, size: '1.1 MB' },\n * ]\"\n * :loading=\"isLoading\">\n * <ChartGrid :data=\"chartData\" />\n * <SchemaCards :schemas=\"schemas\" />\n * </CnDetailPage>\n * ```\n *\n * Grid layout mode\n * ```vue\n * <CnDetailPage\n * title=\"Character Detail\"\n * :layout=\"[\n * { id: 1, widgetId: 'info', gridX: 0, gridY: 0, gridWidth: 8 },\n * { id: 2, widgetId: 'stats', gridX: 8, gridY: 0, gridWidth: 4 },\n * ]\"\n * :widgets=\"[\n * { id: 'info', title: 'Character Info' },\n * { id: 'stats', title: 'Statistics' },\n * ]\">\n * <template #widget-info=\"{ item, widget }\">\n * <CharacterInfoCard :character=\"character\" />\n * </template>\n * <template #widget-stats=\"{ item, widget }\">\n * <StatsCard :stats=\"character.stats\" />\n * </template>\n * ```\n *\n * With header actions and error handling\n * ```vue\n * <CnDetailPage\n * title=\"Schema Details\"\n * :error=\"hasError\"\n * error-message=\"Failed to load schema\"\n * :on-retry=\"loadSchema\">\n * <template #actions>\n * <NcButton @click=\"editSchema\">Edit</NcButton>\n * </template>\n * <DetailContent :schema=\"schema\" />\n * </CnDetailPage>\n * ```\n */\n/**\n * A header pill's label with the record's fields filled in (\"Version\n * {version} published\" reads \"Version 4 published\"). A field the record does\n * not have is left out rather than shown as a token.\n *\n * @param {string} label The translated label.\n * @param {object|null} object The record.\n * @return {string} The label to show.\n * @spec openspec/changes/detail-pill-label-tokens/specs/detail-page/spec.md#requirement-a-header-pill-label-can-name-the-records-fields\n */\nfunction pillLabelFilled(label, object) {\n\tif (typeof label !== 'string' || !label.includes('{')) {\n\t\treturn label\n\t}\n\tconst filled = fillTemplate(label, object)\n\tif (filled !== '') {\n\t\treturn filled\n\t}\n\treturn label.replace(/\\{[^{}]*\\}/g, '').replace(/\\s+/g, ' ').trim()\n}\n\nexport default {\n\tname: 'CnDetailPage',\n\n\tcomponents: {\n\t\tChevronLeft,\n\t\tCnAccessPurposeDialog,\n\t\tChevronRight,\n\t\tNcActionButton,\n\t\tNcActionCaption,\n\t\tNcActionLink,\n\t\tNcActionSeparator,\n\t\tNcButton,\n\t\tNcEmptyContent,\n\t\tNcLoadingIcon,\n\t\tCnIcon,\n\t\tAlertCircleOutline,\n\t\tArrowLeft,\n\t\tFileQuestionOutline,\n\t\tInformationOutline,\n\t\tRefresh,\n\t\tCnActionsMenu,\n\t\tCnBuildiqEditButton,\n\t\tCnLockedBanner,\n\t\tCnNextStepCard,\n\t\tCnDetailWidgetHost,\n\t\tCnFollowingWidgetHost,\n\t\tCnFormDialog,\n\t\tCnDashboardGrid,\n\t\tCnSegmentedControl,\n\t\tNcSelect,\n\t\tViewDashboardOutline,\n\t\tCnLifecycleActions,\n\t\tCnActionButtons,\n\t\tCnBreadcrumbs,\n\t\tCnStatusBadge,\n\t\tCnSummaryAggregates,\n\t\tCnDetailHeaderChips,\n\t\tCnFollowToggle,\n\t\tCnPresenceAvatars,\n\t\tDockRight,\n\t\tEyeOffOutline,\n\t\tCnRelatedCollections,\n\t\tCnBodySections,\n\t\tCnTranslatedBadge,\n\t\tCnWidgetStyleEditorModal,\n\t\tCnRelationLinkModal,\n\t\tCheck,\n\t\tCog,\n\t\tPlus,\n\t\tPencil,\n\t\tRestore,\n\t},\n\n\tmixins: [gridLayout, pageFilters, pageViews],\n\n\tinject: {\n\t\t/**\n\t\t * Consumer widget registry provided by CnAppRoot — the SAME injection\n\t\t * CnWidgetGrid reads. A consuming app registers its own components\n\t\t * here (hrmq registers `stat`, `chart`, `actions`,\n\t\t * `lifecycle-actions`, …), and per REQ-MVR-005 a consumer entry\n\t\t * OVERRIDES a built-in of the same name.\n\t\t *\n\t\t * Without this, a detail page could only render library widgets: an\n\t\t * app's own widget resolved on the CnWidgetGrid path and vanished on\n\t\t * this one, which is half of why the two paths disagreed.\n\t\t */\n\t\tcnRegistry: { default: () => ({}) },\n\t\tobjectSidebarState: { default: null },\n\t\t/**\n\t\t * Reactive AI context holder provided by CnAppRoot. This page\n\t\t * component writes pageKind, objectUuid, registerSlug, schemaSlug\n\t\t * in created() and watches for changes. Resets on beforeUnmount().\n\t\t */\n\t\tcnAiContext: { default: null },\n\t\t/**\n\t\t * Buildiq in-app edit state (ADR-041), a ref provided by CnAppRoot /\n\t\t * the edit button. When truthy, configurable grid widgets show a\n\t\t * configure cog. Defaults to null (no edit affordance) for standalone use.\n\t\t */\n\t\tcnEditingBody: { default: null },\n\t\t/**\n\t\t * Host translate function provided by CnAppRoot as\n\t\t * `cnTranslate: this.translate` (bound to the host app's id). The\n\t\t * manifest-authored page/section/widget titles are run through it.\n\t\t * Defaults to an identity function so an untranslated key renders\n\t\t * as itself. Record data (the resolved object's display name) is\n\t\t * NEVER translated — only manifest chrome is.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t\t/** The app's look, provided by CnAppRoot or CnPageRenderer (`nextcloud` or `board`). */\n\t\tcnLook: { default: 'nextcloud' },\n\t},\n\n\t/**\n\t * Offer the board look's tab list name to the tabs widget below this page\n\t * (`config.tabsLabel`, default \"<type> parts\"), and the records\n\t * `visibilityContext` names. Getter objects, so the widget reads them live.\n\t *\n\t * @return {object} The provided values.\n\t * @spec openspec/changes/screens-detail-page-parity/specs/detail-page-board-look/spec.md#requirement-folder-tabs-take-the-board-strip\n\t */\n\tprovide() {\n\t\tconst isBoard = computed(() => this.isBoard)\n\t\tconst tabsLabel = computed(() => this.resolvedTabsLabel)\n\t\tconst visibilityRows = computed(() => (this.visibilityRowsPending ? null : this.visibilityRows))\n\t\tconst canCheckItems = computed(() => this.recordCanCheckItems)\n\t\tconst sideAvailable = computed(() => this.showsSideColumn)\n\t\tconst canEditRecord = computed(() => this.canEditRecord)\n\t\tconst lastChange = computed(() => this.lastChange)\n\t\treturn {\n\t\t\t// The page's own record edit form, for a widget header button whose\n\t\t\t// action is `edit-record` (DqZaaktype \"Termijnen\" Bewerken).\n\t\t\tcnRecordEdit: {\n\t\t\t\tget available() {\n\t\t\t\t\treturn canEditRecord.value\n\t\t\t\t},\n\n\t\t\t\topen: () => this.openEditForm(),\n\t\t\t},\n\n\t\t\t// The record's newest create or update entry, for a widget that names\n\t\t\t// who changed it last (`recordLastChange`); null until read.\n\t\t\tcnRecordLastChange: lastChange,\n\n\t\t\t// Widget titles sit one level under the record title: h1 in the\n\t\t\t// board look, h2 in the Nextcloud look.\n\t\t\t[HEADING_LEVEL]: () => (this.isBoard ? 2 : 3),\n\t\t\t// The rows a list on this page shows and the one selected, for a side\n\t\t\t// card that follows them (side-card-follows-row).\n\t\t\t[WIDGET_SELECTION]: this.widgetSelection,\n\t\t\t// The side column, for a widget in the body that describes what is\n\t\t\t// selected in it (a files list's cards about the selected file).\n\t\t\t// `available` says whether the page draws a side column; `target`\n\t\t\t// is the selector to teleport the cards to; `claim(owner, on)`\n\t\t\t// puts the generic side cards aside while an owner shows its own.\n\t\t\tcnDetailSideColumn: {\n\t\t\t\tget available() {\n\t\t\t\t\treturn sideAvailable.value\n\t\t\t\t},\n\n\t\t\t\ttarget: `#${this.sideTakeoverId}`,\n\t\t\t\tclaim: (owner, on) => this.claimSideColumn(owner, on),\n\t\t\t},\n\n\t\t\t// The page's own look reaches the tabs, the cards and the activity\n\t\t\t// below it, also when it came from the `look` prop and not the app.\n\t\t\tcnLook: computed(() => (isBoard.value ? 'board' : 'nextcloud')),\n\t\t\tcnDetailTabsLabel: {\n\t\t\t\tget value() {\n\t\t\t\t\treturn tabsLabel.value\n\t\t\t\t},\n\t\t\t},\n\n\t\t\t// The record source's checklist writes, for a `checklist` widget.\n\t\t\tcnRecordChecklist: {\n\t\t\t\tget canCheck() {\n\t\t\t\t\treturn canCheckItems.value\n\t\t\t\t},\n\n\t\t\t\tcheck: (itemId, checked) => this.checkSourceItem(itemId, checked),\n\t\t\t},\n\n\t\t\t// The open body tab, reported by a `tabs` widget, so a side entry\n\t\t\t// with `tabs` shows only beside those tabs.\n\t\t\tcnDetailReportTab: (id) => {\n\t\t\t\tthis.activeBodyTab = typeof id === 'string' ? id : ''\n\t\t\t},\n\n\t\t\t// The records `visibilityContext` names, keyed `@context.<name>`, so a tab's\n\t\t\t// `visibleWhen` reads the case type as a grid widget's does. Null\n\t\t\t// while they are on their way.\n\t\t\tcnDetailVisibilityRows: {\n\t\t\t\tget value() {\n\t\t\t\t\treturn visibilityRows.value\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t},\n\n\tprops: {\n\t\t/** Page title */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * How tall the labelled buttons in the page's card title rows stand in\n\t\t * the board look: `compact` (34px, setting and case-type pages) or\n\t\t * `regular` (the 40px control height, the case page's tab cards:\n\t\t * DqZaakContact \"Live gesprek\"). Manifest `config.cardButtons`.\n\t\t */\n\t\tcardButtons: {\n\t\t\ttype: String,\n\t\t\tdefault: 'compact',\n\t\t\tvalidator: (value) => ['compact', 'regular'].includes(value),\n\t\t},\n\n\t\t/**\n\t\t * NcDialog size for this page's create and edit form dialogs.\n\t\t *\n\t\t * CnFormDialog has taken a `size` since it shipped; this page never\n\t\t * passed one, so both its forms were stuck at `normal` however many\n\t\t * properties the schema declared.\n\t\t */\n\t\tformSize: {\n\t\t\ttype: String,\n\t\t\tdefault: 'normal',\n\t\t},\n\n\t\t/**\n\t\t * How many columns the create and edit forms flow their fields into.\n\t\t *\n\t\t * Pair `2` with `formSize: 'large'`, or the two columns are merely two\n\t\t * narrow ones. CnFormDialog collapses back to one column below 700px\n\t\t * on its own, so this is safe on a narrow viewport.\n\t\t */\n\t\tformColumns: {\n\t\t\ttype: Number,\n\t\t\tdefault: 1,\n\t\t\tvalidator: (value) => value === 1 || value === 2,\n\t\t},\n\n\t\t/**\n\t\t * Fields the create and edit forms ask for, in this order. Null asks for\n\t\t * every property the schema declares.\n\t\t *\n\t\t * Same three keys as CnIndexPage, so one manifest describes the record's\n\t\t * form wherever it opens rather than the list page and the detail page\n\t\t * each having their own.\n\t\t */\n\t\tincludeFields: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Fields the create and edit forms never ask for. */\n\t\texcludeFields: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Per-field widget / label overrides for the create and edit forms. */\n\t\tfieldOverrides: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** Page description (shown below title) */\n\t\tdescription: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Optional MDI icon name (rendered via CnIcon) */\n\t\ticon: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Icon size in pixels */\n\t\ticonSize: {\n\t\t\ttype: Number,\n\t\t\tdefault: 28,\n\t\t},\n\n\t\t/** Whether the page is in a loading state */\n\t\tloading: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Message shown during loading */\n\t\tloadingLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Loading…'),\n\t\t},\n\n\t\t/**\n\t\t * Sidebar configuration. Accepts EITHER form:\n\t\t *\n\t\t * - **Boolean (legacy, deprecated):** `true` activates the\n\t\t * external sidebar, `false` deactivates. The first time this\n\t\t * form is observed per component instance a one-shot\n\t\t * `console.warn` is logged pointing at the migration path.\n\t\t * Continues to work in v1.x for back-compat.\n\t\t *\n\t\t * - **Object (preferred):** mirrors `CnIndexPage.sidebar` plus\n\t\t * the detail-specific fields previously on `sidebarProps`:\n\t\t * ```ts\n\t\t * {\n\t\t * show?: boolean, // default true; false suppresses sidebar\n\t\t * enabled?: boolean, // default true; false bypasses external sidebar\n\t\t * register?: string,\n\t\t * schema?: string,\n\t\t * hiddenTabs?: string[],\n\t\t * title?: string,\n\t\t * subtitle?: string,\n\t\t * tabs?: Array<TabDef>, // see manifest-abstract-sidebar\n\t\t * }\n\t\t * ```\n\t\t * When BOTH `sidebar` (Object) and `sidebarProps` are set\n\t\t * with overlapping fields, the Object form wins and a\n\t\t * `console.warn` lists the conflicting fields once per\n\t\t * component instance.\n\t\t *\n\t\t * @type {boolean | object}\n\t\t */\n\t\tsidebar: {\n\t\t\ttype: [Boolean, Object],\n\t\t\t// Object default (not Boolean `false`) so apps that omit the\n\t\t\t// prop don't trip the Boolean-form deprecation warning. Equivalent\n\t\t\t// to `false`: resolves to a suppressed/inactive sidebar.\n\t\t\tdefault: () => ({ enabled: false }),\n\t\t},\n\n\t\t/**\n\t\t * Initial open state of the object sidebar when it first activates.\n\t\t * Defaults to closed so the detail content fills the page; the user\n\t\t * opens it on demand via the header sidebar-toggle (and the sidebar's\n\t\t * own X closes it). After the first activation the open state is owned\n\t\t * by the shared `objectSidebarState` channel (toggle / close), so this\n\t\t * prop only seeds the initial value.\n\t\t */\n\t\tsidebarOpen: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** The registered object type slug for the sidebar */\n\t\tobjectType: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The object ID to display in the sidebar */\n\t\tobjectId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Whether a schema-bound page without an object id shows an empty\n\t\t * CnFormDialog for its schema instead of a blank page.\n\t\t *\n\t\t * Set from the manifest as `config.createForm`:\n\t\t * - `'auto'` (default) — heuristic: only when the page is schema-bound,\n\t\t * has no object id, and supplies no body of its own (no default slot,\n\t\t * no grid layout). Existing behaviour.\n\t\t * - `'never'` — never render it. Use when the body owns data entry, e.g.\n\t\t * a page whose CnObjectDataWidget (or a registry component) already\n\t\t * provides the form; this is what stops two form dialogs stacking.\n\t\t * - `'always'` — render it even when the page has a body, for a page\n\t\t * that deliberately pairs custom content with a create form.\n\t\t *\n\t\t * The dialog itself is always the generic, schema-driven CnFormDialog, so\n\t\t * whichever way it is summoned it follows the same OpenRegister/schema\n\t\t * form rules (required, readOnly, enum/$ref, `visibleWhen`).\n\t\t *\n\t\t * @type {'auto'|'never'|'always'}\n\t\t */\n\t\tcreateForm: {\n\t\t\ttype: String,\n\t\t\tdefault: 'auto',\n\t\t\tvalidator: (value) => ['auto', 'never', 'always'].includes(value),\n\t\t},\n\n\t\t/** Subtitle shown in the sidebar header */\n\t\tsubtitle: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Additional sidebar configuration (register, schema, hiddenTabs, title, subtitle) */\n\t\tsidebarProps: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Rendering surface forwarded to integration widgets in the\n\t\t * grid layout (widget defs with `type === 'integration'`).\n\t\t * Drives the AD-19 surface fallback. Defaults to\n\t\t * `'detail-page'` — the surface this page represents.\n\t\t *\n\t\t * @type {'user-dashboard'|'app-dashboard'|'detail-page'|'single-entity'}\n\t\t */\n\t\tsurface: {\n\t\t\ttype: String,\n\t\t\tdefault: 'detail-page',\n\t\t\tvalidator: (value) => INTEGRATION_SURFACES.includes(value),\n\t\t},\n\n\t\t/**\n\t\t * Object context forwarded to integration widgets:\n\t\t * `{ register, schema, objectId }`. When omitted it is derived\n\t\t * from `sidebarProps.register` / `sidebarProps.schema` (or\n\t\t * `objectType`) and `objectId`, so `CnFilesCard` etc. can\n\t\t * fetch the right object's sub-resources without extra wiring.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tintegrationContext: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Whether the page is in an error state */\n\t\terror: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Error message shown in error state */\n\t\terrorMessage: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'An error occurred'),\n\t\t},\n\n\t\t/** Callback for retry button in error state. If null, no retry button is shown. */\n\t\tonRetry: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Label for the retry button */\n\t\tretryLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Retry'),\n\t\t},\n\n\t\t/**\n\t\t * Router location the not-found state's button opens, shown when the\n\t\t * schema-driven fetch answers 404. Usually the record's list; null\n\t\t * sends the user to the app's root (`/`). CnPageRenderer fills it with\n\t\t * the index page the record was opened from.\n\t\t */\n\t\tnotFoundRoute: {\n\t\t\ttype: [Object, String],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Name of the `notFoundRoute` page, used in the button's \"Back to …\" label. */\n\t\tnotFoundRouteLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Whether the page has no data to show */\n\t\tempty: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Message shown when page is empty */\n\t\temptyLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'No data available'),\n\t\t},\n\n\t\t/** Title shown above the statistics table */\n\t\tstatsTitle: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Column definitions for the statistics table.\n\t\t * Each column: `{ key: string, label: string, align?: 'left'|'center'|'right' }`\n\t\t *\n\t\t * @type {Array<{ key: string, label: string, align: string }>}\n\t\t */\n\t\tstatsColumns: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Row data for the statistics table. Each row is an object keyed by\n\t\t * column keys. Set `indent: true` on a row for sub-row styling.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\tstatsRows: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Maximum width of the page content */\n\t\tmaxWidth: {\n\t\t\ttype: String,\n\t\t\tdefault: '1800px',\n\t\t},\n\n\t\t/**\n\t\t * Responsive `columnOpts` for the body grid, measured against the GRID's own\n\t\t * width rather than the window's — so a page rendered in a narrow container\n\t\t * (a split pane beside a list) reflows even though the viewport is wide.\n\t\t * Defaults to a table that stacks to one column at or below 1000px. Pass\n\t\t * `null` for a fixed 12-column grid at every size.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tcolumnOpts: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({ ...DEFAULT_DETAIL_COLUMN_OPTS }),\n\t\t},\n\n\t\t/**\n\t\t * Whether to auto-subscribe to live updates for this object.\n\t\t * Defaults to true. The page calls\n\t\t * `store.subscribe(type, objectId)` on mount and unsubscribes on\n\t\t * unmount, against the explicit `objectStore` prop when passed,\n\t\t * else (schema-driven / manifest mode: `register` + `schema` set)\n\t\t * against the library's default `useObjectStore()`. Set `false`\n\t\t * (manifest: `config.subscribe: false`) for read-only / archive\n\t\t * views.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tsubscribe: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Whether the schema-driven auto-body renders the related-objects widget\n\t\t * beneath the data widget. Defaults to true (back-compat). Set false on a\n\t\t * page that surfaces relations elsewhere (e.g. in the sidebar) to drop the\n\t\t * \"Related\" section from the body.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tshowRelatedObjects: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Optional explicit Pinia store instance to subscribe / lock\n\t\t * against; always wins when passed. When omitted, a page in\n\t\t * schema-driven mode (`register` + `schema` — the manifest\n\t\t * renderer path) falls back to the library's default\n\t\t * `useObjectStore()` so auto-subscribe still engages; the\n\t\t * resolution is defensive so consumer apps that haven't\n\t\t * activated Pinia yet (e.g. tests) don't crash.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tobjectStore: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Name of a registered entity source that reads ONE record (manifest\n\t\t * `config.entitySource`), for a record that is not a register/schema\n\t\t * object. The same registry and the same name as `CnIndexPage`'s\n\t\t * `entitySource`: `organisations` shows the OpenRegister organisation\n\t\t * whose uuid is `objectId`. The record feeds the header, pills, facts,\n\t\t * widgets and the `@objectId` / `@object.*` tokens exactly as a fetched\n\t\t * object does. Wins over `register` + `schema`. Empty (the default)\n\t\t * leaves the page on its object store.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tentitySource: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * App routes that replace a record source's own route for a verb, by\n\t\t * verb id (manifest `config.verbEndpoints`), such as\n\t\t * `{ \"complete\": \"/apps/dossiq/api/case-tasks/{id}/complete\" }` for an\n\t\t * app that checks something of its own before the engine completes the\n\t\t * task. `{id}` is the record id. Only read with `entitySource`.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tverbEndpoints: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * OpenRegister register slug. Pair with `schema` to opt into the\n\t\t * schema-driven mode: the page fuses the two into an internal\n\t\t * `${register}-${schema}` object-type slug, registers it on the\n\t\t * store, fetches the object identified by `objectId`, and auto-\n\t\t * renders `CnObjectDataWidget` + `CnObjectMetadataWidget` when no\n\t\t * default-slot content is supplied. Compatible with the existing\n\t\t * `objectType` prop — `objectType` wins on collision, so legacy\n\t\t * direct mounts are unaffected.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tregister: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * OpenRegister schema slug. See `register` for the schema-driven\n\t\t * contract.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tschema: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Deprecated and ignored: the star is gone, a favourite is a follow with\n\t\t * notifications off (OpenRegister `merge-follow-and-favourites`). Kept so a\n\t\t * manifest that still sets `config.favourite` stays valid; use `follow`.\n\t\t *\n\t\t * @deprecated Use `follow`.\n\t\t * @type {boolean|null}\n\t\t */\n\t\tfavourite: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The board look's follow row (manifest `config.followRow`):\n\t\t * `presence: true` says who else has the record open now, with their\n\t\t * initials, before the Follow control (DqZaak \"Anouk Bakker kijkt nu\n\t\t * ook naar deze zaak\"); `count: false` draws no follower count.\n\t\t *\n\t\t * @type {{presence?: boolean, count?: boolean}|null}\n\t\t * @spec openspec/changes/follow-row-presence/specs/board-follow-row/spec.md#requirement-the-follow-row-can-say-who-else-is-looking\n\t\t */\n\t\tfollowRow: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Follow toggle beside the title (manifest `config.follow`). Automatic\n\t\t * (null): shown when the object carries `@self.watching`. `false`\n\t\t * removes it, and the read then no longer asks for `@self.can`.\n\t\t *\n\t\t * @type {boolean|null}\n\t\t */\n\t\tfollow: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** False when the register sends no change notifications to followers; the Follow tooltip then says so. */\n\t\tfollowNotifies: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Send `PUT .../read-state` once per page load after the object has\n\t\t * rendered, when it carries `@self.unread`. `false` (manifest\n\t\t * `config.markRead: false`) keeps the page from marking anything read.\n\t\t */\n\t\tmarkRead: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** After Mark as unread, go back one step in the router history. Default: stay, and emit `marked-unread`. */\n\t\tmarkUnreadNavigatesBack: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Extra `_extend[]` values for the object read (for markers OpenRegister\n\t\t * returns only on request). `@self.can` is added whenever the Follow\n\t\t * toggle can render.\n\t\t *\n\t\t * @type {string[]}\n\t\t */\n\t\textend: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Hide valueless fields on the auto-rendered data widget, instead of\n\t\t * showing them with an em dash. See `CnObjectDataWidget`'s `hide-empty`.\n\t\t *\n\t\t * Set this from the manifest (`config.hideEmpty`) for a discriminated\n\t\t * supertype — one schema holding several variants, where each object only\n\t\t * carries the fields its variant uses. A widget that declares its own\n\t\t * `content.hideEmpty` still wins over this page-level default.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\thideEmpty: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Tab definitions forwarded to the host App's `CnObjectSidebar`\n\t\t * via the injected `objectSidebarState`. Each entry follows the\n\t\t * `CnObjectSidebar` tab shape (see that component for the exact\n\t\t * fields). When empty (default) the sidebar falls back to its\n\t\t * own default tab set. The actual `<CnObjectSidebar>` is rendered\n\t\t * at `NcContent` level by `CnAppRoot` (ADR-017 — external sidebar\n\t\t * pattern); this page only publishes the tabs.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\tsidebarTabs: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Documentation link surfaced in the page-header Actions menu. When\n\t\t * set, the menu renders a \"Documentation\" entry that opens this URL\n\t\t * in a new tab. The Refresh and Request-a-feature entries render by\n\t\t * default regardless of this value.\n\t\t */\n\t\tdocumentationUrl: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Label for the Documentation entry in the header menu. */\n\t\tdocumentationLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Stable id for the page-header Actions menu — forwarded as the\n\t\t * `widgetId` on the `@refresh` / `cn:page:refresh` payloads and as\n\t\t * the `surface: \"detail:<pageId>\"` on the feature-request modal.\n\t\t * Falls back to a slugified title when empty.\n\t\t */\n\t\tpageId: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Whether the user arranges each view (`views`) for themselves. On,\n\t\t * the header offers Arrange view next to the view switch; the\n\t\t * arrangement is stored per view and per user (the server, mirrored\n\t\t * in the browser) and a reset returns the view to its manifest\n\t\t * layout. Off (the default), a view renders as the manifest says.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tuserLayout: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Whether the Refresh entry renders in the page-header menu.\n\t\t * Tri-state:\n\t\t * - `true` / `false` — force it on or off.\n\t\t * - `null` (the default) — **auto**: show Refresh only when it will\n\t\t * do something, i.e. a consumer attached an `@refresh` listener OR\n\t\t * the page is in schema-driven mode (`register` + `schema` +\n\t\t * `objectId`) and can self-fetch. Legacy `objectType`-mode detail\n\t\t * pages that never wire `@refresh` therefore show no dead button.\n\t\t *\n\t\t * @type {boolean|null}\n\t\t */\n\t\tshowRefresh: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Show an Edit button in the header that opens the record's schema\n\t\t * form. Needs `register` + `schema` + `objectId` to have something to\n\t\t * edit; without them the button stays hidden however this is set.\n\t\t *\n\t\t * `CnPageRenderer` turns this on for every schema-bound `type:\"detail\"`\n\t\t * page, which is the same set of pages whose index tables stop offering\n\t\t * an edit modal — the two are one rule, and shipping either half alone\n\t\t * would leave records with two edit surfaces or none.\n\t\t */\n\t\tshowEditAction: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Label for the header Edit button, run through the host translate. Defaults to a translated \"Edit\". */\n\t\teditLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Title of the record edit form, run through the host translate. Empty uses the Edit button's label. */\n\t\teditTitle: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** One line under the edit form's title, run through the host translate. Empty shows none. */\n\t\teditSubtitle: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Whether a header refresh is in flight. Disables the Refresh entry\n\t\t * and swaps its icon for a spinner. Wire this to the same flag the\n\t\t * host toggles around its `@refresh` handler.\n\t\t */\n\t\trefreshing: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Whether the Request-a-feature entry renders in the page-header menu. */\n\t\tshowRequestFeature: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Whether the \"Report a bug\" entry renders in the page-header menu.\n\t\t * On by default — the trio Request a feature / Report a bug /\n\t\t * Documentation is the contract for every Conduction surface.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tshowReportBug: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Whether the Documentation entry renders in the page-header menu. On\n\t\t * by default; the shared menu resolves the target itself, so leaving\n\t\t * it on costs the host nothing.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tshowDocumentation: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Declarative lifecycle/transition actions (manifest `config.lifecycleActions`).\n\t\t * When set, renders status-gated transition buttons in the page header driven\n\t\t * by the object's `x-openregister-lifecycle`. Shape:\n\t\t * `{ field?: 'status', transitions?: [{ from, to, action, label, confirm?, variant? }], autoFetch?: boolean }`.\n\t\t * With just `{ field: 'status' }` the allowed transitions are fetched live from\n\t\t * OpenRegister's `/available-actions` endpoint (server-authoritative). An\n\t\t * explicit `transitions` array is filtered client-side by the object's current\n\t\t * state. Omit (default `null`) to keep the current behaviour (no transition\n\t\t * buttons). See `CnLifecycleActions`.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tlifecycleActions: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Declarative header actions (manifest `config.headerActions`, #91\n\t\t * Wave 3) rendered as items in the header's Actions menu via\n\t\t * CnActionButtons — `api-call` (POST/PUT + toast + refresh),\n\t\t * `open-form`, `toggle`, `navigate` / `open-modal`, each with an\n\t\t * optional `visibleWhen` predicate. Distinct from `lifecycleActions`\n\t\t * (state-machine transitions): these are free-form record actions\n\t\t * (approve, send, archive). The page's object context drives\n\t\t * `@objectId` / `@object.<field>` token + local-predicate resolution.\n\t\t * Empty (the default) renders nothing.\n\t\t *\n\t\t * An entry with `href` is a link, never dispatched: an absolute\n\t\t * `http(s)`, `mailto:`, `tel:` or `//host/...` URL as written, or an instance path\n\t\t * (leading `/`) through `generateUrl()`, opening in place unless\n\t\t * `target` is `'_blank'`.\n\t\t *\n\t\t * The menu, not a row of buttons: that is what the manifest schema has\n\t\t * always said this key is, and what CnIndexPage already does with it.\n\t\t * The two surfaces had drifted, and on a dossiq case — twelve actions —\n\t\t * the button row squeezed the record's own title to a truncated stub.\n\t\t * Edit stays a button because it is the one action a handler reaches\n\t\t * for on nearly every visit.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\theaderActions: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Set to fold this page's own Edit button in with the header actions,\n\t\t * so it becomes an entry in the Actions menu instead of a button\n\t\t * standing beside it. `null` (the default) keeps Edit standalone, so\n\t\t * every existing consumer's header is unchanged.\n\t\t *\n\t\t * The number itself no longer caps anything HERE: this page draws its\n\t\t * header actions as Actions-menu entries (`display: \"menu\"`), where\n\t\t * there are no buttons to cap. It is still CnActionButtons' `inline`\n\t\t * count for a host that mounts that component in `buttons` mode — see\n\t\t * that component's `inline` prop.\n\t\t *\n\t\t * @type {number|null}\n\t\t */\n\t\tinlineActions: {\n\t\t\ttype: Number,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Declarative related-object list sections (manifest\n\t\t * `config.relatedCollections`). Each entry renders a titled\n\t\t * `CnObjectListWidget` below the detail body, filtered to this object via\n\t\t * `@objectId` / `@object.<field>` tokens. Shape per entry:\n\t\t * `{ title?, register, schema, filter?, columns?, sort?, limit?, rowRoute? }`.\n\t\t * Empty / omitted (default `[]`) renders nothing. See `CnRelatedCollections`.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\trelatedCollections: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Declarative cross-schema summary chips (manifest\n\t\t * `config.summaryAggregates`). Each entry runs ONE count/sum/avg over a\n\t\t * related schema scoped to this object and shows it as a stat chip in the\n\t\t * header. Shape per entry:\n\t\t * `{ label, register, schema, metric?, field?, filter?, format? }`.\n\t\t * Empty / omitted (default `[]`) renders nothing. See `CnSummaryAggregates`.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\tsummaryAggregates: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Declarative relation-link actions (manifest `config.relationLinks`).\n\t\t * Each entry renders a button that opens a search-and-link modal which\n\t\t * patches a foreign-key field on THIS object with the chosen object's id.\n\t\t * Shape per entry:\n\t\t * `{ label?, register, schema, fkField, labelField?, allowCreate?, title?, selectLabel? }`.\n\t\t * Empty / omitted (default `[]`) renders nothing.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\trelationLinks: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Declarative IN-BODY sections (manifest `config.bodyWidgets`). Each\n\t\t * entry renders a REGISTERED host-app component as a titled section in\n\t\t * the page BODY (not the sidebar), with the object/page context injected.\n\t\t * This is the primitive that lets a rich bespoke detail page (BRP panels,\n\t\t * activity timelines, comms history, relationship graphs, bookings, …)\n\t\t * become declarative while keeping its sections in the body. Shape per\n\t\t * entry:\n\t\t * `{ id?, component, title?, props?, placement?, colSpan? }`.\n\t\t * - `component` — registry name resolved from the app's v2 `registry`\n\t\t * (any `kind` exposing a `.component`, e.g. `kind:\"section\"` or\n\t\t * `kind:\"widget\"`) or the legacy `customComponents` map. NO sidebar\n\t\t * tab is required (unlike integration widgets).\n\t\t * - `props` — values are token-resolved: `@objectId` → this object's id,\n\t\t * `@object.<field>` → a field off it, `@workspace.<key>` → page\n\t\t * context; unset `@…?` optional / unresolved tokens are dropped.\n\t\t * - `placement` — `before-body` | `after-data` | `after-related` | `end`\n\t\t * (default `end`). Controls where in the body the section lands.\n\t\t * - `colSpan` — 1–12 grid span when sections share a placement.\n\t\t * The loaded object + objectId are also `provide`d on `cnSectionContext`\n\t\t * so a host component can inject them instead of taking explicit props.\n\t\t * Empty / omitted (default `[]`) renders nothing. See `CnBodySections`.\n\t\t *\n\t\t * @type {Array<{id?: string, component: string, title?: string, props?: object, placement?: string, colSpan?: number}>}\n\t\t */\n\t\tbodyWidgets: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Vue-router route NAME to navigate to after a create-form save (the\n\t\t * create archetype: a `type:\"detail\"` page whose route carries no `:id`).\n\t\t * The created object's id is passed as the `id` route param. When empty\n\t\t * (the default) the page navigates back in history after a successful\n\t\t * create instead. See `isCreateMode`.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tcreateRoute: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Page-level app config map exposed to declarative widget / section\n\t\t * config via the `@config.<key>` token (e.g. the reporting `currency`\n\t\t * the setup wizard captures). Provided to descendants on `cnAppConfig`,\n\t\t * so a stat widget's `format: { style: 'currency', currency:\n\t\t * '@config.currency' }` formats with the configured value (falling back\n\t\t * to the literal default when unset). A manifest renderer typically seeds\n\t\t * this from `loadState(appId, 'config', {})`. Empty (default) leaves every\n\t\t * `@config.<key>` token to fall back to its literal default.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tappConfig: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Next and previous within the list this record was opened from, as\n\t\t * returned by `useListNavigation`. Omit it and neither control\n\t\t * renders, which is what a record reached by a bare link must do:\n\t\t * stepping through an order the reader never chose is worse than\n\t\t * offering no step at all.\n\t\t *\n\t\t * @type {{ available: boolean, isFirst: boolean, isLast: boolean, position: number, total: number }}\n\t\t */\n\t\tlistNavigation: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Router location of the previous record, as `useListNavigation`'s\n\t\t * `previousRoute`. When set, the previous control is a real link\n\t\t * (middle-click, open in new tab) and `previous-record` is emitted\n\t\t * with `{ event, to }` after it navigated. Null keeps it a button.\n\t\t *\n\t\t * @type {object|string|null}\n\t\t */\n\t\tpreviousTo: {\n\t\t\ttype: [Object, String],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Router location of the next record, as `useListNavigation`'s\n\t\t * `nextRoute`. Same behaviour as `previousTo`.\n\t\t *\n\t\t * @type {object|string|null}\n\t\t */\n\t\tnextTo: {\n\t\t\ttype: [Object, String],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The page's primary action, as declared on its manifest page. Renders\n\t\t * as the header's primary button and is the target the skip link\n\t\t * lands on. Omit it and neither renders.\n\t\t *\n\t\t * @type {{ id?: string, label: string, icon?: string, route?: string, href?: string }}\n\t\t */\n\t\tprimaryAction: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Puts the active tab in the address as `?_tab=<id>`, so a link points\n\t\t * at a tab of this record rather than at the record. Off by default,\n\t\t * which leaves the address exactly as it is today.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\ttabInAddress: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t// ── The action model (detail-action-model-and-case-surfaces) ────\n\n\t\t/**\n\t\t * The primary action per stage (manifest `config.primaryActionByStage`):\n\t\t * a map from a stage value to an action. When the record's stage has\n\t\t * an entry, that action is the page's primary button, so the button\n\t\t * names the next step: \"Take on\" on a new case, \"Publish\" on a\n\t\t * decided one. An entry is an action object in the `headerActions`\n\t\t * shape, or the id of a declared header action. A stage without an\n\t\t * entry falls back to `primaryAction`.\n\t\t *\n\t\t * @type {Record<string, string|object>|null}\n\t\t */\n\t\tprimaryActionByStage: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Dot-path to the field that holds the record's stage. Read by\n\t\t * `primaryActionByStage` and `nextStep`.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tstageField: {\n\t\t\ttype: String,\n\t\t\tdefault: 'status',\n\t\t},\n\n\t\t/**\n\t\t * Always visible buttons in the header (manifest `config.quickActions`)\n\t\t * for the few actions a handler uses on every record. At most three\n\t\t * render; later entries are dropped. An entry is an action object in\n\t\t * the `headerActions` shape, or the id of a declared header action,\n\t\t * which then leaves the Actions menu. `{ id, tabs?, notTabs? }` names\n\t\t * a declared action that is a button only beside those body tabs, as\n\t\t * a side card follows them; elsewhere it stays in the Actions menu.\n\t\t *\n\t\t * @type {Array<string|object>}\n\t\t */\n\t\tquickActions: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * What the Actions menu itself shows (manifest `config.actionsMenu`):\n\t\t * `{ showRefresh?, showHelpLinks?, showMarkUnread?, showOpenSidebar?, label? }`.\n\t\t * `showRefresh: false` removes Refresh. `showHelpLinks: false` removes\n\t\t * Request a feature, Report a bug and Documentation, for an app that\n\t\t * offers them in its own help menu. `showMarkUnread: false` and\n\t\t * `showOpenSidebar: false` remove those two built-in entries, for a\n\t\t * menu the board draws without them (DqZaakMeer). `label` names the\n\t\t * menu, for example \"More\". `null` (the default) changes nothing.\n\t\t *\n\t\t * @type {{showRefresh?: boolean, showHelpLinks?: boolean, showMarkUnread?: boolean, showOpenSidebar?: boolean, label?: string}|null}\n\t\t */\n\t\tactionsMenu: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The \"what now\" card (manifest `config.nextStep`):\n\t\t * `{ field?, stagesFrom?, stages: { <stage>: { title?, checklist, after? } } }`.\n\t\t * Each checklist item is `{ label, doneField?, doneWhen?, hint? }`.\n\t\t * `stagesFrom` is a field path read like a `visibleWhen` field\n\t\t * (`@context.caseType.nextStep`): when it holds a stages map (or an\n\t\t * object with `stages`), that map replaces `stages` for this record, so\n\t\t * a case type keeps its own stage texts. Otherwise `stages` stays.\n\t\t * Rendered above the body for a stage that declares a checklist, with\n\t\t * the primary button inside it. `tabs` / `notTabs` tie the card to\n\t\t * body tabs as a side card. See `CnNextStepCard`.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tnextStep: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * A pill above the title saying what kind of record this is\n\t\t * (manifest `config.typePill`): `{ field, colorMap?, labels?, variant? }`.\n\t\t * Rendered through `CnStatusBadge`; `colorMap` is keyed on the field's\n\t\t * raw value, `labels` maps a raw value to the text to show. Instead of\n\t\t * `field`: `text`, a constant (\"Inwoner\"), or `countFrom`\n\t\t * (`{ register, schema, filter? }`) with `countLabel` (`{ one, other }`\n\t\t * with `{count}`), a count of a related list (\"1 lopende zaak\") that\n\t\t * shows nothing at 0. With `resolve: { register, schema, labelField?,\n\t\t * colorField?, iconField? }` the pill shows the referenced record's\n\t\t * label, takes its tone from that record's colour (an NL Design System\n\t\t * hue such as `red`, or a badge role) and shows its icon. `values`\n\t\t * draws a `field` pill only for those raw values (a priority pill for\n\t\t * `high` and `urgent` only).\n\t\t *\n\t\t * @type {{field?: string, text?: string, countFrom?: object, countLabel?: object, colorMap?: object, labels?: object, variant?: string, resolve?: object, values?: Array<string>}|null}\n\t\t */\n\t\ttypePill: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * A pill above the title saying where the record stands (manifest\n\t\t * `config.statusPill`). Same shape as `typePill`.\n\t\t *\n\t\t * @type {{field: string, colorMap?: object, labels?: object, variant?: string}|null}\n\t\t */\n\t\tstatusPill: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Fields shown as chips under the title (manifest `config.headerFields`):\n\t\t * each entry a property key, or `{ key, format, labelField, colorField,\n\t\t * warnWhenPast }`. `format` is `text` (default), `mono`, `badge`, `user`\n\t\t * or `date`. An empty value shows no chip; no row without chips.\n\t\t *\n\t\t * @type {Array<string|{key: string, format?: string, labelField?: string, colorField?: string, warnWhenPast?: boolean}>}\n\t\t */\n\t\theaderFields: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Whether the type eyebrow (the type label above the record name)\n\t\t * renders once the record resolves. `false` (manifest\n\t\t * `config.showTypeEyebrow: false`) drops it for a header that says\n\t\t * the type in a pill instead.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-detail-page-drops-its-eyebrow-and-shows-a-breadcrumb\n\t\t */\n\t\tshowTypeEyebrow: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * A breadcrumb line above the header (manifest `config.breadcrumb`):\n\t\t * `{ label, route?, params?, href? }` names the list the record\n\t\t * belongs to; the record's display name follows as the current\n\t\t * crumb. `label` goes through the host translate function and shows\n\t\t * as text; `icon` (an MDI name, e.g. `Home`) draws that icon instead,\n\t\t * with the label as its accessible name. `currentField` names a field\n\t\t * of the record (a dotted path, e.g. `identifier`) whose value is the\n\t\t * current crumb instead of the display name; an empty value falls back\n\t\t * to the display name. `separator` draws that text (e.g. `/`) between\n\t\t * the crumbs instead of the chevron. Null (the default) draws no\n\t\t * trail.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-detail-page-drops-its-eyebrow-and-shows-a-breadcrumb\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-declared-breadcrumb-label-shows-as-text\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-breadcrumb-can-name-the-record-by-a-field-and-use-a-text-separator\n\t\t * A list of such crumbs draws a deeper trail (\"Settings / Case types /\"\n\t\t * and then the record); `currentField` and `separator` may sit on any\n\t\t * of them.\n\t\t *\n\t\t * @spec openspec/changes/detail-breadcrumb-trail/specs/detail-page/spec.md#requirement-a-record-page-breadcrumb-can-name-more-than-one-level\n\t\t * @type {{label: string, route?: string, params?: object, href?: string, icon?: string, currentField?: string, separator?: string}|Array<object>|null}\n\t\t */\n\t\tbreadcrumb: {\n\t\t\ttype: [Object, Array],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Whether the record's widget cards (the body grid and the side\n\t\t * column) keep their overflow Actions menu when their definition does\n\t\t * not say (`showActions`). `false` (manifest\n\t\t * `config.showWidgetActions: false`) drops it, as on a dashboard; a\n\t\t * widget with `showActions: true` keeps its menu. A card that offers\n\t\t * an Add action keeps the menu that holds it. When the key is not set\n\t\t * it is true, and false under the board look.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-detail-page-can-drop-the-widget-actions-menu\n\t\t */\n\t\tshowWidgetActions: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * A field path whose value picks the tabs of the page's `tabs` widgets\n\t\t * and their order for this record (manifest `config.tabsFrom`), read\n\t\t * like a `visibleWhen` field: `@context.caseType.tabs` reads the list a\n\t\t * case type keeps. A non-empty list of tab ids (a tab's `id`, else its\n\t\t * `widgetId`, or the string entry itself) keeps those tabs in that\n\t\t * order and leaves the rest out. Any other value keeps the declared\n\t\t * tabs. Edit mode shows them all.\n\t\t *\n\t\t * @spec openspec/changes/detail-tabs-from/specs/detail-page/spec.md#requirement-a-record-can-pick-its-tabs-and-their-order\n\t\t */\n\t\ttabsFrom: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * A column of cards beside the body (manifest `config.sideColumn`),\n\t\t * for the record's facts: deadline, requester, handler, links. Each\n\t\t * entry is a widget definition (`{ id?, type, title?, content? }`) or\n\t\t * the id of a widget declared in `widgets`. The column stacks under\n\t\t * the body on a narrow page. Empty (the default) leaves the layout as\n\t\t * it is. An entry with `tabs` (tab ids of the body's `tabs` widget)\n\t\t * shows only while one of those tabs is open (DqZaaktypeVelden: the\n\t\t * option list beside Velden).\n\t\t *\n\t\t * @type {Array<string|object>}\n\t\t */\n\t\tsideColumn: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * A row of widgets across the whole page, above the body and the side\n\t\t * column (manifest `config.band`). Each entry is the id of a widget\n\t\t * declared in `widgets`, a widget definition, or `{ widgetId, span,\n\t\t * chrome }`; `span` is the entry's share of the row in twelfths\n\t\t * (default 12) and the row wraps; `chrome` is `card` or `bare` (a\n\t\t * banner defaults to bare, anything else to card, so a widget that\n\t\t * draws its own card, such as a KPI, asks for bare). Each entry\n\t\t * follows its `visibleWhen`. Empty (the default) draws no band.\n\t\t *\n\t\t * @type {Array<string|object>}\n\t\t * @spec openspec/changes/detail-band/specs/detail-page/spec.md#requirement-a-detail-page-can-draw-a-band-across-the-page-above-the-body-and-the-side-column\n\t\t */\n\t\tband: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Page-level filters (manifest `config.pageFilters`), the same a\n\t\t * dashboard takes: each writes its choice into the page's workspace\n\t\t * context, which every widget on the page reads through\n\t\t * `@page.<key>` / `@workspace.<key>`. Drawn in the band, above its\n\t\t * widgets (PtPortal \"Periode\").\n\t\t *\n\t\t * @type {Array<{key: string, label?: string, type?: string, control?: ('select'|'segmented'), options?: Array<{value: (string|number), label: string}>, optionsFrom?: object, clearable?: boolean, placeholder?: string, default?: (string|number)}>}\n\t\t * @spec openspec/changes/detail-page-filters/specs/detail-page/spec.md#requirement-a-detail-page-can-draw-page-filters-that-its-widgets-read\n\t\t */\n\t\tpageFilters: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * A field path whose value picks the side column and its order for\n\t\t * this record (manifest `config.sideColumnFrom`), read like a\n\t\t * `visibleWhen` field: `@context.caseType.sideCards` reads the list a\n\t\t * case type keeps. A non-empty list of ids keeps those `sideColumn`\n\t\t * entries, in that order, and leaves the rest out; each still follows\n\t\t * its own `visibleWhen`. Any other value keeps `sideColumn` as\n\t\t * declared, so a record whose list is empty changes nothing.\n\t\t *\n\t\t * @spec openspec/changes/detail-side-column-from/specs/detail-page/spec.md#requirement-a-record-can-pick-the-side-column-and-its-order\n\t\t */\n\t\tsideColumnFrom: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * A field path, read like a `visibleWhen` field, whose list of\n\t\t * declared header action ids picks this record's quick actions\n\t\t * (manifest `config.quickActionsFrom`, e.g.\n\t\t * `@context.caseType.quickActions`). Only ids of `headerActions` are\n\t\t * taken, so the record's data never defines an action. An empty or\n\t\t * missing list keeps `quickActions`.\n\t\t *\n\t\t * @spec openspec/changes/detail-quick-actions-from/specs/detail-page/spec.md#requirement-a-record-can-pick-its-quick-actions\n\t\t */\n\t\tquickActionsFrom: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Draw the header as a card (manifest `config.headerCard: true`): the\n\t\t * pills, the title and the actions on the page's surface colour inside\n\t\t * a bordered, rounded box, as the board's case header. Off by default,\n\t\t * which keeps the header on the page ground.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-detail-header-can-be-a-card-that-holds-a-widget\n\t\t */\n\t\theaderCard: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * The id of a widget declared in `widgets` to render inside the header,\n\t\t * on its own row under the title and the actions (manifest\n\t\t * `config.headerWidget`), e.g. the stages bars. It renders without a\n\t\t * card of its own and leaves the body grid. Empty (the default), or an\n\t\t * id that names no widget, renders nothing extra.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-detail-header-can-be-a-card-that-holds-a-widget\n\t\t */\n\t\theaderWidget: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Records the page reads beside the loaded one, so a `visibleWhen`\n\t\t * can ask about them (manifest `config.visibilityContext`). Each entry\n\t\t * is `{ <name>: { field, register, schema } }`: `field` is the\n\t\t * property of the loaded record holding the other record's id, and a\n\t\t * condition reads that record as `@context.<name>.<path>`. A case page that\n\t\t * declares `{ caseType: { field: \"caseType\", register: \"dossiq\",\n\t\t * schema: \"caseType\" } }` can show a widget for some case types only\n\t\t * with `{ field: \"@context.caseType.category\", op: \"in\", value: [...] }`.\n\t\t * A record that cannot be read is absent, so its condition fails safe.\n\t\t * Empty (the default) reads nothing extra.\n\t\t *\n\t\t * @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-page-can-load-records-a-condition-names\n\t\t */\n\t\tvisibilityContext: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Ask why the reader opens the record before the page shows it, and\n\t\t * record the answer (manifest `config.accessPurpose`). `{ when, options,\n\t\t * url, title?, description? }`: `when` is a condition in the\n\t\t * `visibleWhen` grammar on the record and its `visibilityContext`\n\t\t * records (DqSociaalDomein: the case type lists special categories of\n\t\t * personal data); `options` the purposes offered (`{ value, label }`,\n\t\t * \"Other\" with a written purpose is always added); `url` the app\n\t\t * endpoint that records the purpose (POST `{ purpose, note }`, `{objectId}`\n\t\t * filled in). The page shows nothing of the record until the endpoint\n\t\t * accepts, and does not ask again for the same record in the same\n\t\t * browser session. A refusal keeps the record closed. `null` (the\n\t\t * default) asks nothing.\n\t\t *\n\t\t * @spec openspec/changes/detail-access-purpose/specs/detail-access-purpose/spec.md#requirement-a-detail-page-can-ask-for-a-purpose-before-it-opens\n\t\t * @type {object|null}\n\t\t */\n\t\taccessPurpose: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Whether the current user administers this instance. Shows the\n\t\t * `adminOnly` header actions. `null` (the default) reads it from\n\t\t * `getCurrentUser()`. Visibility only, never an authorization\n\t\t * decision: the endpoint behind an action decides who may call it.\n\t\t *\n\t\t * @type {boolean|null}\n\t\t */\n\t\tisAdmin: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The look this page is drawn in (`nextcloud` or `board`). Empty (the\n\t\t * default) takes the app's look, which CnAppRoot or CnPageRenderer\n\t\t * provides, so a manifest's `look` reaches the page without a prop.\n\t\t *\n\t\t * @spec openspec/changes/screens-detail-page-parity/specs/detail-page-board-look/spec.md\n\t\t */\n\t\tlook: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * The meta line on row 2 of the board look's header (manifest\n\t\t * `config.headerMeta`): a field template such as `\"via {channel}\"`.\n\t\t * Every `{path}` is read from the record (an enum field as its label);\n\t\t * `{path|format}` formats it (`weekday`, `longdate`, `time`, `user`,\n\t\t * `currency:EUR`, ...). An empty token drops itself and its separator;\n\t\t * a line with no value at all is dropped. Only the board look draws it.\n\t\t * `{ count, one, other, zero? }` picks a form by the `headerMetaCounts`\n\t\t * total named `count` (\"1 lopende zaak\" / \"2 lopende zaken\"); each form\n\t\t * is translated on its own, and `zero` (when given) is used for none.\n\t\t *\n\t\t * @spec openspec/changes/screens-detail-page-parity/specs/detail-page-board-look/spec.md#requirement-the-detail-header-is-two-rows-on-the-ground\n\t\t * @spec openspec/changes/header-meta-plural/specs/detail-page/spec.md#requirement-a-header-line-can-name-a-count-in-the-singular-or-the-plural\n\t\t */\n\t\theaderMeta: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * The lists `headerMeta` counts (manifest `config.headerMetaCounts`), as\n\t\t * on a list page: `{ <name>: { register, schema, filter? } | { entitySource } }`,\n\t\t * each read as one total and filled in for `{count:<name>}` (\"een van\n\t\t * {count:organisations} organisaties op deze omgeving\", DqTenant). While\n\t\t * a count loads, or when it fails, the line is left out.\n\t\t *\n\t\t * @spec openspec/changes/detail-header-meta-counts/specs/detail-page/spec.md#requirement-a-record-page-header-line-can-count-a-list\n\t\t * @type {Record<string, object>|null}\n\t\t */\n\t\theaderMetaCounts: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Read the record's newest create or update entry from its audit\n\t\t * trail and offer it to `headerMeta` as `{_lastChange.userName}`,\n\t\t * `{_lastChange.user|user}` and `{_lastChange.created|date}`\n\t\t * (\"laatst gewijzigd 2 okt door Lars de Koning\", DqZaaktype). A record\n\t\t * does not say who changed it last; its trail does. A trail that\n\t\t * cannot be read leaves the tokens empty.\n\t\t *\n\t\t * @spec openspec/changes/header-meta-last-change/specs/detail-page/spec.md#requirement-the-header-meta-line-can-name-who-changed-the-record-last\n\t\t */\n\t\theaderMetaLastChange: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Read the record's newest create or update entry (as\n\t\t * `headerMetaLastChange` does) for the widgets on the page: a facts\n\t\t * card reads it as `_lastChange.userName` / `_lastChange.created`\n\t\t * (\"Laatst gewijzigd op 3 oktober door Pieter Jansen\", DqContact\n\t\t * \"Historie\"). `headerMetaLastChange` provides it as well.\n\t\t *\n\t\t * @spec openspec/changes/record-last-change-in-widgets/specs/record-last-change-in-widgets/spec.md#requirement-a-widget-can-read-who-changed-the-record-last\n\t\t */\n\t\trecordLastChange: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * The accessible name of the tab list under the board look (manifest\n\t\t * `config.tabsLabel`). Empty gives \"<type> parts\", for example \"Case\n\t\t * parts\", from the page title.\n\t\t *\n\t\t * @spec openspec/changes/screens-detail-page-parity/specs/detail-page-board-look/spec.md#requirement-folder-tabs-take-the-board-strip\n\t\t */\n\t\ttabsLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * The field holding the record's identifier (manifest\n\t\t * `config.identifierField`, for example `identifier`). Under the board\n\t\t * look the side column's History card ends with a line naming it.\n\t\t *\n\t\t * @spec openspec/changes/screens-detail-page-parity/specs/detail-page-board-look/spec.md#requirement-body-and-side-cards-take-the-board-anatomy\n\t\t */\n\t\tidentifierField: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * The record field the header names the record by (manifest\n\t\t * `config.titleField`, a dotted path such as `number`), as an invoice\n\t\t * page names the invoice number. An empty value falls back to the\n\t\t * display name. Empty (the default) changes nothing.\n\t\t *\n\t\t * @spec openspec/changes/detail-title-field/specs/detail-page/spec.md#requirement-a-detail-page-can-name-the-record-by-a-field\n\t\t */\n\t\ttitleField: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\temits: [\n\t\t'create-cancel',\n\t\t'created',\n\t\t'edited',\n\t\t'geo-saved',\n\t\t'layout-change',\n\t\t'marked-unread',\n\t\t'open-integration',\n\t\t'refresh',\n\t\t'related-object-click',\n\t\t'related-row-click',\n\t\t'relation-linked',\n\t\t'request-feature',\n\t\t'transitioned',\n\t\t'update:layout',\n\t\t'widget-config-change',\n\t\t/**\n\t\t * The reader asked for the next record of the list this one was\n\t\t * opened from. The host steps, because the host owns the router.\n\t\t * With `nextTo` set the link has navigated already and the payload\n\t\t * is `{ event, to }`.\n\t\t */\n\t\t'next-record',\n\t\t/** The reader asked for the previous record of that same list. Payload as `next-record`. */\n\t\t'previous-record',\n\t\t/** The page's declared primary action was pressed. Payload is the declaration. */\n\t\t'primary-action',\n\t\t/**\n\t\t * The active tab changed. Payload is the tab id. Emitted whether or\n\t\t * not `tabInAddress` is set, so a host may mirror it somewhere else.\n\t\t */\n\t\t'tab-change',\n\t],\n\n\tsetup(props) {\n\t\t// Pluggable integration registry — used to resolve `type:\n\t\t// 'integration'` widgets in the grid layout to their Vue\n\t\t// component (AD-19 surface fallback). Always wired; cheap when\n\t\t// no integration widgets are configured.\n\t\tconst { resolveWidget, getById } = useIntegrationRegistry()\n\t\tconst registryExposed = { resolveRegistryWidget: resolveWidget, getRegistryProvider: getById }\n\n\t\t// The host reads presence because only the host can act on it: the grid\n\t\t// reserves the row before the widget renders. See `layoutItemCanDraw`.\n\t\t// A getter, not a one-shot read: a page whose widgets arrive after mount\n\t\t// would otherwise never subscribe, and `layoutItemCanDraw` would drop the\n\t\t// presence row for good instead of until somebody arrives.\n\t\tconst presencePlaced = () => (props.widgets || []).some((w) => w && w.type === 'presence') || props.followRow?.presence === true\n\t\tconst { others: presentOthers } = useObjectPresence(\n\t\t\t() => props.register || '',\n\t\t\t() => props.schema || '',\n\t\t\t() => String(props.objectId || ''),\n\t\t\t{ enabled: presencePlaced },\n\t\t)\n\t\tregistryExposed.presentOthers = presentOthers\n\n\t\t// Object context for detail-page abstract widgets (ADR-041): a reactive\n\t\t// `{ objectId, object, register, schema }` holder kept current by the\n\t\t// Options watcher below. Provided so CnObjectListWidget / CnStatWidget\n\t\t// can resolve `@objectId` / `@object.<field>` filter tokens and scope\n\t\t// their query to the object on this page. Null fields until loaded.\n\t\tconst cnObjectContext = ref({ objectId: props.objectId || null, object: null, register: props.register || '', schema: props.schema || '' })\n\t\tprovide('cnObjectContext', cnObjectContext)\n\t\tregistryExposed.cnObjectContextRef = cnObjectContext\n\n\t\t// Page-level APP CONFIG exposed to declarative widget / section config via\n\t\t// the `@config.<key>` token (e.g. the reporting currency the setup wizard\n\t\t// captures). Provided so CnStatWidget / CnBodySections can format with the\n\t\t// configured value. Kept in sync with the `appConfig` prop.\n\t\tconst cnAppConfig = ref({ ...(props.appConfig || {}) })\n\t\tprovide('cnAppConfig', cnAppConfig)\n\t\tregistryExposed.cnAppConfigRef = cnAppConfig\n\t\twatch(\n\t\t\t() => props.appConfig,\n\t\t\t(next) => {\n\t\t\t\tcnAppConfig.value = { ...(next || {}) }\n\t\t\t},\n\t\t\t{ deep: true },\n\t\t)\n\n\t\t// Auto-subscribe + reactive lock state for the current object.\n\t\t// Both are no-ops when no store resolves (no Pinia active), when\n\t\t// subscribe is false (read-only / archive views), or when\n\t\t// objectType / objectId aren't yet known. Using composables in\n\t\t// setup() keeps the lifecycle bound to the component scope —\n\t\t// `tryOnScopeDispose` releases the subscription on unmount.\n\t\t//\n\t\t// Store resolution (manifest-live-updates): an explicit\n\t\t// `objectStore` prop always wins (existing behaviour, unchanged\n\t\t// for apps that pass their own store). In schema-driven mode —\n\t\t// `register` + `schema` set, the manifest renderer path — fall\n\t\t// back to the library's default store, the same one the\n\t\t// schema-driven fetch (`effectiveObjectStore`) uses, so\n\t\t// manifest-rendered detail pages auto-subscribe with zero\n\t\t// app-side wiring. Legacy `objectType`-only mounts without an\n\t\t// explicit store keep today's behaviour (no subscription).\n\t\tlet subscriptionStore = props.objectStore\n\t\tif (!subscriptionStore && props.subscribe && props.register && props.schema) {\n\t\t\ttry {\n\t\t\t\tsubscriptionStore = useObjectStore()\n\t\t\t} catch {\n\t\t\t\t// Pinia not active (stand-alone test mounts) — no live updates.\n\t\t\t\tsubscriptionStore = null\n\t\t\t}\n\t\t\tif (subscriptionStore) {\n\t\t\t\t// Register the EFFECTIVE type slug synchronously — the same\n\t\t\t\t// slug `resolveType()`/`resolvedObjectType` yields (an explicit\n\t\t\t\t// `objectType` prop wins; note the manifest renderer aliases\n\t\t\t\t// `config.schema` → `objectType`), with the same idempotence\n\t\t\t\t// guard as fetchObjectIfNeeded (which therefore skips its own\n\t\t\t\t// registration). This ensures the subscription's mount-time\n\t\t\t\t// subscribe() never races the mounted() fetch path into a\n\t\t\t\t// \"type not registered\" rejection.\n\t\t\t\tconst effectiveType = props.objectType || `${props.register}-${props.schema}`\n\t\t\t\tif (typeof subscriptionStore.registerObjectType === 'function'\n\t\t\t\t\t&& !subscriptionStore.objectTypeRegistry?.[effectiveType]) {\n\t\t\t\t\tsubscriptionStore.registerObjectType(\n\t\t\t\t\t\teffectiveType,\n\t\t\t\t\t\tprops.schema,\n\t\t\t\t\t\tprops.register,\n\t\t\t\t\t\t{ registerSlug: props.register, schemaSlug: props.schema },\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (!subscriptionStore || !props.subscribe) {\n\t\t\treturn { ...registryExposed, lockState: null }\n\t\t}\n\t\t// Resolve the effective object-type slug the same way `computed.resolvedObjectType`\n\t\t// does — the explicit `objectType` prop wins, else fuse `register` + `schema`.\n\t\t// The composables take getter functions so the resolution stays reactive when\n\t\t// `register` / `schema` / `objectType` change at runtime.\n\t\tconst resolveType = () => props.objectType || (props.register && props.schema ? `${props.register}-${props.schema}` : '')\n\t\tconst subscription = useObjectSubscription(\n\t\t\tsubscriptionStore,\n\t\t\tresolveType,\n\t\t\t() => props.objectId,\n\t\t\t{ enabled: () => Boolean(resolveType() && props.objectId) },\n\t\t)\n\t\t// Getters, read at call time: a register or schema that arrives after\n\t\t// mount (route params, a resolved sidebar) must reach the lock URL. They\n\t\t// used to be consts captured once at setup.\n\t\tconst sidebarReg = () => props.sidebarProps?.register || props.resolvedSidebar?.register || props.register || ''\n\t\tconst sidebarSchema = () => props.sidebarProps?.schema || props.resolvedSidebar?.schema || props.schema || ''\n\t\tconst lock = useObjectLock(\n\t\t\tsubscriptionStore,\n\t\t\tsidebarReg,\n\t\t\t() => resolveType() || sidebarSchema(),\n\t\t\t() => props.objectId,\n\t\t\t// The third argument is the OBJECT CACHE KEY (`<register>-<schema>`)\n\t\t\t// and the lock URL needs the schema SLUG. They were one string, and\n\t\t\t// the acquire went to `/api/objects/<register>/<register>-<schema>/`,\n\t\t\t// which no register answers.\n\t\t\t{ schemaSlug: () => props.schema || sidebarSchema() },\n\t\t)\n\t\t// The page's workspace context, as a dashboard provides it: page\n\t\t// filters write their choice here and a widget reads it through\n\t\t// `@page.<key>` / `@workspace.<key>`.\n\t\tconst workspaceContext = ref({})\n\t\tprovide('cnWorkspaceContext', workspaceContext)\n\t\treturn {\n\t\t\t...registryExposed,\n\t\t\tsubscriptionStatus: subscription.status,\n\t\t\tlockState: lock,\n\t\t\tworkspaceContext,\n\t\t}\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\t/** The `headerMetaCounts` totals, by name; a failed one is null. */\n\t\t\theaderMetaCountValues: {},\n\t\t\t/** The newest create or update entry of the record's audit trail (`headerMetaLastChange`). */\n\t\t\tlastChange: null,\n\t\t\t/** The rows a list on this page shows and the one selected (side-card-follows-row). */\n\t\t\twidgetSelection: markRaw(createWidgetSelection()),\n\t\t\t/** The id of the side column's slot for a body widget's own cards. */\n\t\t\tsideTakeoverId: `cn-detail-side-takeover-${++sideTakeoverCounter}`,\n\t\t\t/** The body widgets that show their own cards in the side column now. */\n\t\t\tsideClaims: [],\n\t\t\t/** The session keys of the records whose access purpose is recorded. */\n\t\t\taccessPurposeGranted: readGrantedPurposes(),\n\t\t\t/** Whether the purpose dialog is open; closing it without a purpose keeps the record closed. */\n\t\t\taccessPurposeAsking: true,\n\t\t\t/** Whether a purpose is being recorded. */\n\t\t\taccessPurposeBusy: false,\n\t\t\t/** Why the last purpose was refused. */\n\t\t\taccessPurposeError: '',\n\t\t\t/** The id of the open body tab a `tabs` widget reported, '' before one did. */\n\t\t\tactiveBodyTab: '',\n\t\t\t/**\n\t\t\t * The records `visibilityContext` names, keyed `@context.<name>`: the row,\n\t\t\t * `null` when it could not be read, absent while it is on its way.\n\t\t\t */\n\t\t\tvisibilityRows: {},\n\t\t\t/** The `visibilityRefsKey` the rows in `visibilityRows` were read for. */\n\t\t\tvisibilityRowsFor: '',\n\t\t\t/** Sequence of the last `visibilityContext` read, so a late answer for another record is dropped. */\n\t\t\tvisibilityRowsSeq: 0,\n\t\t\t/** The answers of endpoint and source `visibleWhen` conditions on grid widgets, by widget id. */\n\t\t\twidgetConditionAnswers: {},\n\t\t\t/** Sequence of the last remote condition round. */\n\t\t\twidgetConditionSeq: 0,\n\t\t\t/** The counts of the header's count pills (`countFrom`), by pill key (`type`, `status`). */\n\t\t\tpillCounts: {},\n\t\t\t/** Sequence of the last pill count round, so a late answer does not overwrite a newer one. */\n\t\t\tpillCountSeq: 0,\n\t\t\t/** The labels of the records pills with `resolve` point at, by pill key. */\n\t\t\tpillRefLabels: {},\n\t\t\t/** The colour and icon names read off the records pills with `resolve` point at, by pill key. */\n\t\t\tpillRefLooks: {},\n\t\t\t/** Sequence of the last pill label lookup, so a late answer for another record is dropped. */\n\t\t\tpillRefSeq: 0,\n\t\t\t/** `register/schema/id` the read-state call was last made for, so it is sent once per load. */\n\t\t\treadStateSentFor: '',\n\t\t\t/** Whether the user is arranging the chosen view (`userLayout`). */\n\t\t\tarrangingView: false,\n\t\t\t/** Whether the record edit form is open. */\n\t\t\teditFormOpen: false,\n\t\t\t/**\n\t\t\t * The sentence a refused lock left behind, shown in place of the\n\t\t\t * form. Empty whenever the form opened.\n\t\t\t */\n\t\t\teditLockRefusal: '',\n\t\t\t/**\n\t\t\t * Whether a lock release is in flight, so the locked card's Unlock\n\t\t\t * button can disable itself for the round trip. Without it a slow\n\t\t\t * release invites a second click, and the second `release()` lands\n\t\t\t * on a lock that is already gone.\n\t\t\t */\n\t\t\treleasingLock: false,\n\t\t\t/**\n\t\t\t * The manifest `headerActions[]` flattened into menu items by the\n\t\t\t * `display: \"menu\"` CnActionButtons instance, which keeps owning\n\t\t\t * their dialogs. Each carries its own pre-bound `run()`, so the\n\t\t\t * menu dispatches without reaching back into that component.\n\t\t\t */\n\t\t\tmenuHeaderActions: [],\n\t\t\t/** Whether a record-source verb is in flight, so a double click cannot run it twice. */\n\t\t\tsourceVerbBusy: false,\n\t\t\t/** The record-source verb whose form is open, or null. */\n\t\t\tsourceVerbForm: null,\n\t\t\t/** Lifecycle transitions CnLifecycleActions hands the Actions menu. */\n\t\t\tlifecycleMenuEntries: [],\n\t\t\t/** Whether the per-widget style/config editor modal is open. */\n\t\t\tshowWidgetConfig: false,\n\t\t\t/** The widgetId currently being configured via the cog. */\n\t\t\tconfigWidgetId: null,\n\t\t\t/**\n\t\t\t * Whether the object sidebar has been seeded with its initial open\n\t\t\t * state for the current activation. Gates the one-shot `sidebarOpen`\n\t\t\t * seed in `syncSidebarState` so user close/toggle is not clobbered.\n\t\t\t */\n\t\t\tsidebarSeeded: false,\n\t\t\t/** The relation-link descriptor whose modal is currently open (or null). */\n\t\t\tactiveRelationLink: null,\n\t\t\t/**\n\t\t\t * Materialized default body grid for the schema-driven auto-body (Data +\n\t\t\t * Related). Lazily built by `materializeAutoBody()` the first time the\n\t\t\t * object resolves so the detail body is a real, mutable GridStack grid\n\t\t\t * (drag / resize / configure) rather than a static stack. `null` until\n\t\t\t * materialized; reset when the schema-driven context changes.\n\t\t\t */\n\t\t\tautoBodyLayout: null,\n\t\t\t/** Widget definitions paired with `autoBodyLayout` (id ↔ widgetId). */\n\t\t\tautoBodyWidgets: null,\n\t\t\t/**\n\t\t\t * True when `materializeAutoBody()` dropped the Data widget only\n\t\t\t * because the schema had not arrived yet — the object won the race\n\t\t\t * against `fetchSchema`. The `currentSchema` watcher re-materializes\n\t\t\t * once on that signal, which is what stops a lost race from\n\t\t\t * permanently hiding every scalar field (shillinq#1073).\n\t\t\t *\n\t\t\t * A flag rather than an \"is the widget missing?\" test, so a body the\n\t\t\t * user has edited is never rewritten underneath them.\n\t\t\t */\n\t\t\tdataWidgetDeferred: false,\n\t\t\t/**\n\t\t\t * Drives the Actions-menu Refresh spinner during a schema-driven\n\t\t\t * self-fetch refresh, where the host has no promise to bind\n\t\t\t * `:refreshing` to. Mirrors CnIndexPage.internalRefreshing.\n\t\t\t */\n\t\t\tinternalRefreshing: false,\n\t\t\t/**\n\t\t\t * Whether a load has completed at least once. Gates the\n\t\t\t * full-page loading state to the FIRST load only: once content\n\t\t\t * has been shown, later loads (refresh, re-fetch) stay in place\n\t\t\t * and surface as the action-button spinner instead of blanking\n\t\t\t * the page. Set true the first time `loading` falls to false.\n\t\t\t */\n\t\t\thasLoadedOnce: false,\n\t\t\t/**\n\t\t\t * Whether the schema-driven object fetch is in flight. Starts true\n\t\t\t * so the immediate sidebar sync at creation already waits for it.\n\t\t\t */\n\t\t\tobjectFetchPending: Boolean(this.register && this.schema && this.objectId),\n\t\t\t/** Whether the schema-driven fetch answered 404 for `objectId`. */\n\t\t\tobjectNotFound: false,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t// ── The board look (screens-detail-page-parity) ─────────────────\n\n\t\t/**\n\t\t * Whether the page is drawn in the board look: its own `look` prop,\n\t\t * else the look the app provides.\n\t\t *\n\t\t * @return {boolean} True in the board look.\n\t\t * @spec openspec/changes/screens-detail-page-parity/specs/detail-page-board-look/spec.md\n\t\t */\n\t\tisBoard() {\n\t\t\treturn normalizeLook(this.look || this.cnLook) === 'board'\n\t\t},\n\n\t\t/**\n\t\t * The class that scopes the board rules to this page, so they apply\n\t\t * whether the look came from the app or from this page's own prop.\n\t\t *\n\t\t * @return {string} `cn-look-board` or ''.\n\t\t * @spec openspec/changes/screens-detail-page-parity/specs/detail-page-board-look/spec.md\n\t\t */\n\t\tboardClass() {\n\t\t\treturn this.isBoard ? 'cn-look-board' : ''\n\t\t},\n\n\t\t/**\n\t\t * Whether widget cards keep their overflow Actions menu: the manifest's\n\t\t * `showWidgetActions` when it says, else true, and false under the\n\t\t * board look (side cards have no Actions menu).\n\t\t *\n\t\t * @return {boolean} True when the menu shows.\n\t\t * @spec openspec/changes/screens-detail-page-parity/specs/detail-page-board-look/spec.md#requirement-body-and-side-cards-take-the-board-anatomy\n\t\t */\n\t\teffectiveShowWidgetActions() {\n\t\t\tif (typeof this.showWidgetActions === 'boolean') {\n\t\t\t\treturn this.showWidgetActions\n\t\t\t}\n\t\t\treturn !this.isBoard\n\t\t},\n\n\t\t/**\n\t\t * The header line's template: `headerMeta` itself, or for the plural\n\t\t * form `{ count, one, other, zero? }` the form its count asks for; null\n\t\t * while that count is missing.\n\t\t *\n\t\t * @return {string|null} The untranslated template, or null.\n\t\t * @spec openspec/changes/header-meta-plural/specs/detail-page/spec.md#requirement-a-header-line-can-name-a-count-in-the-singular-or-the-plural\n\t\t */\n\t\theaderMetaForm() {\n\t\t\tconst meta = this.headerMeta\n\t\t\tif (!meta || typeof meta !== 'object') {\n\t\t\t\treturn typeof meta === 'string' ? meta : ''\n\t\t\t}\n\t\t\tconst total = this.headerMetaCountValues[meta.count]\n\t\t\tif (typeof total !== 'number') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (total === 0 && typeof meta.zero === 'string') {\n\t\t\t\treturn meta.zero\n\t\t\t}\n\t\t\tconst form = total === 1 ? meta.one : meta.other\n\t\t\treturn typeof form === 'string' ? form : ''\n\t\t},\n\n\t\t/**\n\t\t * The meta line of row 2, translated and filled from the record.\n\t\t *\n\t\t * @return {string} The line, or ''.\n\t\t * @spec openspec/changes/screens-detail-page-parity/specs/detail-page-board-look/spec.md#requirement-the-detail-header-is-two-rows-on-the-ground\n\t\t */\n\t\theaderMetaText() {\n\t\t\tconst record = this.lastChange && this.resolvedObject ? { ...this.resolvedObject, _lastChange: this.lastChange } : this.resolvedObject\n\t\t\tlet missing = false\n\t\t\tconst form = this.headerMetaForm\n\t\t\tif (form === null) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst template = (form ? this.effectiveTranslate(form) : '').replace(/\\{count:([\\w-]+)\\}/g, (match, name) => {\n\t\t\t\tconst value = this.headerMetaCountValues[name]\n\t\t\t\tif (typeof value !== 'number') {\n\t\t\t\t\tmissing = true\n\t\t\t\t\treturn ''\n\t\t\t\t}\n\t\t\t\treturn String(value)\n\t\t\t})\n\t\t\treturn missing ? '' : resolveHeaderMeta(template, record, { schema: this.currentSchema, translate: this.effectiveTranslate })\n\t\t},\n\n\t\t/**\n\t\t * What the header line's counts depend on, as one key.\n\t\t *\n\t\t * @spec openspec/changes/detail-header-meta-counts/specs/detail-page/spec.md#requirement-a-record-page-header-line-can-count-a-list\n\t\t * @return {string}\n\t\t */\n\t\theaderMetaCountKey() {\n\t\t\tconst sources = this.headerMetaCounts && typeof this.headerMetaCounts === 'object' ? this.headerMetaCounts : {}\n\t\t\tconst valid = Object.entries(sources).filter(([, from]) => from && ((from.register && from.schema) || from.entitySource))\n\t\t\treturn valid.length > 0 ? JSON.stringify([valid, this.objectId]) : ''\n\t\t},\n\n\t\t/**\n\t\t * What the last-change read depends on.\n\t\t *\n\t\t * @spec openspec/changes/header-meta-last-change/specs/detail-page/spec.md#requirement-the-header-meta-line-can-name-who-changed-the-record-last\n\t\t * @return {string} The key, '' when nothing is to be read.\n\t\t */\n\t\tlastChangeKey() {\n\t\t\tif (!(this.headerMetaLastChange || this.recordLastChange) || !this.register || !this.schema || !this.objectId) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn `${this.register}/${this.schema}/${this.objectId}`\n\t\t},\n\n\t\t/**\n\t\t * Whether row 2 has a meta line or header chips to draw after the\n\t\t * breadcrumb.\n\t\t *\n\t\t * @return {boolean} True with meta or chips.\n\t\t * @spec openspec/changes/screens-detail-page-parity/specs/detail-page-board-look/spec.md#requirement-the-detail-header-is-two-rows-on-the-ground\n\t\t */\n\t\thasHeaderMeta() {\n\t\t\treturn this.headerMetaText !== '' || this.headerFields.length > 0\n\t\t},\n\n\t\t/**\n\t\t * Whether row 2 of the header has anything to draw.\n\t\t *\n\t\t * @return {boolean} True when a pill, the breadcrumb, meta or chips exist.\n\t\t * @spec openspec/changes/screens-detail-page-parity/specs/detail-page-board-look/spec.md#requirement-the-detail-header-is-two-rows-on-the-ground\n\t\t */\n\t\tshowsHeaderRow2() {\n\t\t\treturn this.headerPills.length > 0 || this.breadcrumbCrumbs.length > 0 || this.hasHeaderMeta\n\t\t},\n\n\t\t/**\n\t\t * The name of the tab list: `tabsLabel`, else \"<type> parts\" from the\n\t\t * page title, else \"Details\".\n\t\t *\n\t\t * @return {string} The accessible name.\n\t\t * @spec openspec/changes/screens-detail-page-parity/specs/detail-page-board-look/spec.md#requirement-folder-tabs-take-the-board-strip\n\t\t */\n\t\tresolvedTabsLabel() {\n\t\t\tif (this.tabsLabel) {\n\t\t\t\treturn this.effectiveTranslate(this.tabsLabel)\n\t\t\t}\n\t\t\tconst type = this.resolvedTitle\n\t\t\treturn type ? t('nextcloud-vue', '{type} parts', { type }) : t('nextcloud-vue', 'Details')\n\t\t},\n\n\t\t/**\n\t\t * The record's identifier, read from `identifierField`.\n\t\t *\n\t\t * @return {string} The identifier, or ''.\n\t\t * @spec openspec/changes/screens-detail-page-parity/specs/detail-page-board-look/spec.md#requirement-body-and-side-cards-take-the-board-anatomy\n\t\t */\n\t\tidentifierLine() {\n\t\t\tif (!this.identifierField) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst value = stageOf(this.resolvedObject, this.identifierField)\n\t\t\treturn value === '' ? '' : t('nextcloud-vue', 'Identifier: {identifier}', { identifier: value })\n\t\t},\n\n\t\t/**\n\t\t * The ids of the widgets a `tabs` widget on this page holds as tabs.\n\t\t *\n\t\t * @return {Set<string>} The widget ids.\n\t\t * @spec openspec/changes/screens-detail-page-parity/specs/detail-page-board-look/spec.md#requirement-history-is-the-last-tab\n\t\t */\n\t\ttabbedWidgetIds() {\n\t\t\tconst ids = new Set()\n\t\t\tconst declared = Array.isArray(this.widgets) ? this.widgets : []\n\t\t\tdeclared\n\t\t\t\t.filter((widget) => widget && widget.type === 'tabs')\n\t\t\t\t.forEach((widget) => {\n\t\t\t\t\tconst tabs = Array.isArray(widget.content?.tabs) ? widget.content.tabs : []\n\t\t\t\t\ttabs.forEach((tab) => {\n\t\t\t\t\t\tconst id = typeof tab === 'string' ? tab : tab?.widgetId\n\t\t\t\t\t\tif (id) {\n\t\t\t\t\t\t\tids.add(id)\n\t\t\t\t\t\t}\n\t\t\t\t\t})\n\t\t\t\t})\n\t\t\treturn ids\n\t\t},\n\n\t\t/**\n\t\t * The id of the side column's History card (the activity widget), the\n\t\t * one that ends with the identifier line.\n\t\t *\n\t\t * @return {string} The widget id, or ''.\n\t\t * @spec openspec/changes/screens-detail-page-parity/specs/detail-page-board-look/spec.md#requirement-body-and-side-cards-take-the-board-anatomy\n\t\t */\n\t\tsideHistoryId() {\n\t\t\tconst history = this.sideColumnWidgets.find((widget) => isActivityWidget(widget))\n\t\t\treturn history ? history.id : ''\n\t\t},\n\n\t\t// ── The record as a place (case-page-and-list-as-a-place) ───────\n\n\t\t/**\n\t\t * The primary action's label, run through the host's translate\n\t\t * function. Empty when the page declares no primary action, which is\n\t\t * what gates both the button and the skip link.\n\t\t *\n\t\t * @return {string} The label, or an empty string.\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tprimaryActionLabel() {\n\t\t\tconst label = this.primaryAction?.label\n\t\t\treturn typeof label === 'string' && label !== '' ? this.effectiveTranslate(label) : ''\n\t\t},\n\n\t\t/**\n\t\t * The id the skip link's anchor points at.\n\t\t *\n\t\t * Namespaced by the page so two detail pages in one document, which\n\t\t * the split view makes possible, cannot both answer to `#primary`.\n\t\t *\n\t\t * @return {string} The element id.\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tprimaryActionAnchorId() {\n\t\t\tconst scope = this.stagePrimaryEntry?.id || this.primaryAction?.id || this.pageId || this.objectId || 'record'\n\t\t\treturn `cn-primary-action-${String(scope).replace(/[^A-Za-z0-9_-]/g, '-')}`\n\t\t},\n\n\t\t/**\n\t\t * What the skip link says.\n\t\t *\n\t\t * It names the action rather than saying \"skip to main content\",\n\t\t * because a keyboard user who cannot see the page needs to know what\n\t\t * they are being offered before they take it.\n\t\t *\n\t\t * One library string with a placeholder, not \"Skip to\" from the host's\n\t\t * catalogue glued to the label: no app carried that fragment, so every\n\t\t * Dutch reader got \"Skip to In behandeling nemen\" (Zuiddrecht live\n\t\t * round 3), and a fragment cannot be put in Dutch word order anyway.\n\t\t *\n\t\t * @return {string} The link text.\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t * @spec openspec/changes/detail-page-strings-dutch/specs/detail-page-strings-dutch/spec.md#requirement-the-detail-page-speaks-the-readers-language\n\t\t */\n\t\tskipLinkLabel() {\n\t\t\treturn t('nextcloud-vue', 'Skip to {action}', { action: this.effectivePrimaryLabel })\n\t\t},\n\n\t\t// ── The action model (detail-action-model-and-case-surfaces) ────\n\n\t\t/**\n\t\t * The record's stage, as `primaryActionByStage` and `nextStep` key it.\n\t\t *\n\t\t * @return {string} The stage, or '' while the record has none.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-primary-action-follows-the-stage\n\t\t */\n\t\trecordStage() {\n\t\t\treturn stageOf(this.currentObject, this.stageField)\n\t\t},\n\n\t\t/**\n\t\t * The stage's primary action as an id, plus its definition when the\n\t\t * map carries one inline.\n\t\t *\n\t\t * @return {{id: string, inline: object|null}|null} The pinned action, or null when this stage declares none.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-primary-action-follows-the-stage\n\t\t */\n\t\tstagePrimary() {\n\t\t\treturn normalisePinnedAction(stageEntry(this.primaryActionByStage, this.recordStage), 'cn-stage-primary')\n\t\t},\n\n\t\t/**\n\t\t * The quick actions for this record: the declared header actions the\n\t\t * record lists through `quickActionsFrom`, in its order, else\n\t\t * `quickActions`. An entry is an id, or `{ id, ... }` without a label;\n\t\t * an id no header action declares is dropped.\n\t\t *\n\t\t * @return {Array<string|object>} The entries.\n\t\t * @spec openspec/changes/detail-quick-actions-from/specs/detail-page/spec.md#requirement-a-record-can-pick-its-quick-actions\n\t\t */\n\t\teffectiveQuickActions() {\n\t\t\tconst declared = Array.isArray(this.quickActions) ? this.quickActions : []\n\t\t\tif (!this.quickActionsFrom || !this.widgetVisibilityData) {\n\t\t\t\treturn declared\n\t\t\t}\n\t\t\tconst listed = readVisibleWhenPath(this.widgetVisibilityData, this.quickActionsFrom)\n\t\t\tif (!Array.isArray(listed)) {\n\t\t\t\treturn declared\n\t\t\t}\n\t\t\tconst known = new Set((Array.isArray(this.headerActions) ? this.headerActions : []).map((action) => action && action.id))\n\t\t\tconst idOf = (entry) => {\n\t\t\t\tif (typeof entry === 'string') {\n\t\t\t\t\treturn entry\n\t\t\t\t}\n\t\t\t\treturn entry && typeof entry === 'object' && !('label' in entry) && typeof entry.id === 'string' ? entry.id : ''\n\t\t\t}\n\t\t\tconst picked = listed.filter((entry) => known.has(idOf(entry)))\n\t\t\treturn picked.length > 0 ? picked : declared\n\t\t},\n\n\t\t/**\n\t\t * The quick actions as ids, plus definitions for the inline ones.\n\t\t * Capped at three, and never the action that is already the primary\n\t\t * button.\n\t\t *\n\t\t * @return {Array<{id: string, inline: object|null}>} The pinned actions.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-quick-actions\n\t\t */\n\t\tpinnedQuickActions() {\n\t\t\tconst primaryId = this.stagePrimary?.id\n\t\t\treturn this.effectiveQuickActions\n\t\t\t\t.map((entry, index) => normalisePinnedAction(entry, `cn-quick-${index}`))\n\t\t\t\t.filter((pinned) => pinned && pinned.id !== primaryId)\n\t\t\t\t.slice(0, MAX_QUICK_ACTIONS)\n\t\t},\n\n\t\t/**\n\t\t * Whether the stage's primary button belongs beside the open body tab:\n\t\t * an inline stage action with `tabs`/`notTabs` follows the tab like a\n\t\t * quick action (DqZaakAssistent: no \"Next step\" beside AI oversight).\n\t\t * It stays in the Actions menu either way.\n\t\t *\n\t\t * @return {boolean} true when the button may show.\n\t\t * @spec openspec/changes/stage-primary-follows-tab/specs/detail-page/spec.md#requirement-a-stage-action-can-follow-the-open-tab\n\t\t */\n\t\tstagePrimaryOnTab() {\n\t\t\treturn !this.stagePrimaryEntry || onTab(this.stagePrimary, this.activeBodyTab)\n\t\t},\n\n\t\t/**\n\t\t * The stage's primary action in dispatchable form: the menu entry\n\t\t * CnActionButtons built for it, with its `run()`. Null while the\n\t\t * action is hidden by its own `visibleWhen`, in which case the page\n\t\t * falls back to `primaryAction`.\n\t\t *\n\t\t * @return {object|null} The entry, or null.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-primary-action-follows-the-stage\n\t\t */\n\t\tstagePrimaryEntry() {\n\t\t\tconst id = this.stagePrimary?.id\n\t\t\treturn (id && this.menuHeaderActions.find((entry) => entry.id === id)) || null\n\t\t},\n\n\t\t/**\n\t\t * The quick actions in dispatchable form, in declaration order, less\n\t\t * the ones the open body tab leaves out.\n\t\t *\n\t\t * @return {Array<object>} The entries that are visible.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-quick-actions\n\t\t * @spec openspec/changes/detail-actions-per-tab/specs/detail-page/spec.md#requirement-quick-actions-and-the-next-step-card-can-follow-the-open-tab\n\t\t */\n\t\tquickActionEntries() {\n\t\t\treturn this.pinnedQuickActions\n\t\t\t\t.filter((pinned) => onTab(pinned, this.activeBodyTab))\n\t\t\t\t.map((pinned) => this.menuHeaderActions.find((entry) => entry.id === pinned.id))\n\t\t\t\t.filter(Boolean)\n\t\t},\n\n\t\t/**\n\t\t * What the primary button says: the stage's action when the stage\n\t\t * declares one, the page's `primaryAction` otherwise. Empty gates\n\t\t * both the button and the skip link.\n\t\t *\n\t\t * @return {string} The label, or an empty string.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-primary-action-follows-the-stage\n\t\t */\n\t\teffectivePrimaryLabel() {\n\t\t\treturn this.stagePrimaryEntry ? this.stagePrimaryEntry.label : this.primaryActionLabel\n\t\t},\n\n\t\t/**\n\t\t * The URL the primary button links to, when its action is a link.\n\t\t *\n\t\t * @return {string} The href, or '' for an action that is dispatched.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-primary-action-follows-the-stage\n\t\t */\n\t\tprimaryActionHref() {\n\t\t\tif (this.stagePrimaryEntry) {\n\t\t\t\treturn this.stagePrimaryEntry.href || ''\n\t\t\t}\n\t\t\treturn this.primaryActionLink?.href || ''\n\t\t},\n\n\t\t/**\n\t\t * The page's `primaryAction` as a link, when no stage action stands in\n\t\t * for it: `href` (its record tokens resolved) opens in a new tab, as the\n\t\t * schema says; `route` (with `query`) is an in-app link a plain click\n\t\t * routes to (PtPortal \"Nieuwe pagina\" opens the page create flow).\n\t\t *\n\t\t * @return {{href: string, to: object|null, target: string}|null} The link, or null.\n\t\t * @spec openspec/changes/detail-band/specs/detail-page/spec.md#requirement-a-detail-pages-own-primary-action-is-a-link\n\t\t */\n\t\tprimaryActionLink() {\n\t\t\tconst action = this.primaryAction\n\t\t\tif (!action || typeof action !== 'object') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (typeof action.href === 'string' && action.href !== '') {\n\t\t\t\tconst href = resolveManifestHref(interpolateUrlTokens(action.href, this.headerLinkTokenCtx))\n\t\t\t\treturn href ? { href, to: null, target: '_blank' } : null\n\t\t\t}\n\t\t\tif (typeof action.route === 'string' && action.route !== '') {\n\t\t\t\tconst to = { name: action.route, ...(action.query && typeof action.query === 'object' ? { query: action.query } : {}) }\n\t\t\t\tconst href = routeHref(to, this.$router)\n\t\t\t\treturn href ? { href, to, target: '_self' } : null\n\t\t\t}\n\t\t\treturn null\n\t\t},\n\n\t\t/**\n\t\t * The token context a header link resolves its URL against: this record.\n\t\t *\n\t\t * @return {{objectId: (string|number), object: object}} The context.\n\t\t * @spec openspec/changes/detail-band/specs/detail-page/spec.md#requirement-a-detail-header-link-builds-its-url-from-the-record\n\t\t */\n\t\theaderLinkTokenCtx() {\n\t\t\treturn { objectId: this.objectId, object: this.resolvedObject || {} }\n\t\t},\n\n\t\t/**\n\t\t * The link target that goes with `primaryActionHref`.\n\t\t *\n\t\t * @return {string|undefined} The target, or undefined.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-primary-action-follows-the-stage\n\t\t */\n\t\tprimaryActionTarget() {\n\t\t\tif (this.stagePrimaryEntry) {\n\t\t\t\treturn this.stagePrimaryEntry.linkTarget || undefined\n\t\t\t}\n\t\t\treturn this.primaryActionLink?.target || undefined\n\t\t},\n\n\t\t/**\n\t\t * Whether the primary button waits for its own request.\n\t\t *\n\t\t * @return {boolean} True while the stage's action is in flight.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-primary-action-follows-the-stage\n\t\t */\n\t\tprimaryActionDisabled() {\n\t\t\treturn Boolean(this.stagePrimaryEntry?.disabled)\n\t\t},\n\n\t\t/**\n\t\t * Whether the viewer administers this instance, for `adminOnly`\n\t\t * actions. The prop wins; otherwise Nextcloud says.\n\t\t *\n\t\t * @return {boolean} True for an instance admin.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-grouped-and-admin-only-menu-actions\n\t\t */\n\t\tviewerIsAdmin() {\n\t\t\tif (this.isAdmin !== null) {\n\t\t\t\treturn this.isAdmin\n\t\t\t}\n\t\t\ttry {\n\t\t\t\treturn getCurrentUser()?.isAdmin === true\n\t\t\t} catch {\n\t\t\t\treturn false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The declared actions by id, for the `group` and `adminOnly` a menu\n\t\t * entry does not carry itself.\n\t\t *\n\t\t * @return {Record<string, object>} The lookup.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-grouped-and-admin-only-menu-actions\n\t\t */\n\t\theaderActionsById() {\n\t\t\treturn Object.fromEntries(this.effectiveHeaderActions\n\t\t\t\t.filter((action) => action && action.id)\n\t\t\t\t.map((action) => [action.id, action]))\n\t\t},\n\n\t\t/**\n\t\t * The menu entries in the groups the menu draws: ungrouped first,\n\t\t * named groups in first appearance order, admin actions last and for\n\t\t * admins only. A page that groups nothing gets one captionless group.\n\t\t *\n\t\t * @return {Array<{key: string, label: string, entries: Array<object>}>} The groups.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-grouped-and-admin-only-menu-actions\n\t\t */\n\t\theaderMenuGroups() {\n\t\t\tconst groups = groupMenuEntries(this.headerMenuEntries, this.headerActionsById, {\n\t\t\t\tisAdmin: this.viewerIsAdmin,\n\t\t\t\tadminLabel: t('nextcloud-vue', 'Administration'),\n\t\t\t})\n\t\t\treturn groups.map((group) => ({ ...group, label: group.label ? this.effectiveTranslate(group.label) : '' }))\n\t\t},\n\n\t\t/**\n\t\t * Whether Refresh renders in the Actions menu.\n\t\t *\n\t\t * @return {boolean} The existing rule, unless `actionsMenu.showRefresh` is false.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-actions-menu-built-ins-are-optional\n\t\t */\n\t\tmenuShowsRefresh() {\n\t\t\treturn this.actionsMenu?.showRefresh === false ? false : this.effectiveHeaderShowRefresh\n\t\t},\n\n\t\t/**\n\t\t * Whether the help links render in the Actions menu.\n\t\t *\n\t\t * @return {boolean} True unless `actionsMenu.showHelpLinks` is false.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-actions-menu-built-ins-are-optional\n\t\t */\n\t\tmenuShowsHelpLinks() {\n\t\t\treturn this.actionsMenu?.showHelpLinks !== false\n\t\t},\n\n\t\t/**\n\t\t * The Actions menu's name.\n\t\t *\n\t\t * @return {string|undefined} `actionsMenu.label` translated, or undefined for the default \"Actions\".\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-actions-menu-built-ins-are-optional\n\t\t */\n\t\tactionsMenuName() {\n\t\t\tconst label = this.actionsMenu?.label\n\t\t\treturn typeof label === 'string' && label !== '' ? this.effectiveTranslate(label) : undefined\n\t\t},\n\n\t\t/**\n\t\t * The menu's name as the header draws it: under the board look always\n\t\t * a label (\"More\" unless the manifest names it), never icon-only.\n\t\t *\n\t\t * @return {string|undefined} The label, or undefined for the default \"Actions\".\n\t\t * @spec openspec/changes/screens-detail-page-parity/specs/detail-page-board-look/spec.md#requirement-the-detail-header-buttons-follow-one-order\n\t\t */\n\t\tboardMenuLabel() {\n\t\t\tif (this.actionsMenuName) {\n\t\t\t\treturn this.actionsMenuName\n\t\t\t}\n\t\t\treturn this.isBoard ? t('nextcloud-vue', 'More') : undefined\n\t\t},\n\n\t\t/**\n\t\t * Whether the page is showing its body: not the not-found, loading,\n\t\t * error or empty state. The \"what now\" card and the side column are\n\t\t * siblings of the body and follow it.\n\t\t *\n\t\t * @return {boolean} True when the body renders.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-next-step-card\n\t\t */\n\t\tshowsBody() {\n\t\t\treturn !this.objectNotFound && !this.contentWithheld && !this.showLoadingState && !this.error && !this.empty\n\t\t},\n\n\t\t/**\n\t\t * Whether the loaded record asks for a purpose before it opens\n\t\t * (`accessPurpose.when` holds for it). Undecided, and so withheld,\n\t\t * while the record or the records its condition reads are on their way.\n\t\t *\n\t\t * @spec openspec/changes/detail-access-purpose/specs/detail-access-purpose/spec.md#requirement-a-detail-page-can-ask-for-a-purpose-before-it-opens\n\t\t * @return {boolean}\n\t\t */\n\t\taccessPurposeRequired() {\n\t\t\tconst gate = this.accessPurpose\n\t\t\tif (!gate || typeof gate !== 'object' || !gate.when || !this.widgetVisibilityData || this.visibilityRowsPending) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn detailWidgetVisible({ visibleWhen: gate.when }, this.widgetVisibilityData)\n\t\t},\n\n\t\t/**\n\t\t * Whether the page holds the record back: it asks for a purpose and\n\t\t * none is recorded for this record in this session yet, or it cannot\n\t\t * tell yet whether it asks.\n\t\t *\n\t\t * @spec openspec/changes/detail-access-purpose/specs/detail-access-purpose/spec.md#requirement-a-detail-page-can-ask-for-a-purpose-before-it-opens\n\t\t * @return {boolean}\n\t\t */\n\t\tcontentWithheld() {\n\t\t\tconst gate = this.accessPurpose\n\t\t\tif (!gate || typeof gate !== 'object' || !gate.when || this.objectNotFound) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif (this.accessPurposeGranted.includes(this.accessPurposeKey)) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tconst undecided = !this.widgetVisibilityData || this.visibilityRowsPending\n\t\t\treturn undecided || this.accessPurposeRequired\n\t\t},\n\n\t\t/**\n\t\t * The session key of the purpose recorded for this record.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\taccessPurposeKey() {\n\t\t\treturn `cn-access-purpose:${this.resolvedObjectType || ''}:${this.objectId || ''}`\n\t\t},\n\n\t\t/**\n\t\t * The purposes offered, through the app's translate.\n\t\t *\n\t\t * @return {Array<{value: string, label: string}>}\n\t\t */\n\t\taccessPurposeOptions() {\n\t\t\tconst options = Array.isArray(this.accessPurpose?.options) ? this.accessPurpose.options : []\n\t\t\treturn options\n\t\t\t\t.filter((option) => option && typeof option.value === 'string' && option.value !== '')\n\t\t\t\t.map((option) => ({ value: option.value, label: this.cnTranslate(option.label || option.value) }))\n\t\t},\n\n\t\t/**\n\t\t * `nextStep` with its stages read from the record when `stagesFrom`\n\t\t * names a stages map there (a case type's own texts), else as declared.\n\t\t *\n\t\t * @spec openspec/changes/detail-next-step-from-record/specs/detail-page/spec.md#requirement-the-next-step-stages-can-come-from-the-record\n\t\t * @return {object|null}\n\t\t */\n\t\teffectiveNextStep() {\n\t\t\tconst config = this.nextStep\n\t\t\tif (!config || typeof config !== 'object' || typeof config.stagesFrom !== 'string' || config.stagesFrom === '' || !this.widgetVisibilityData) {\n\t\t\t\treturn config\n\t\t\t}\n\t\t\tconst found = readVisibleWhenPath(this.widgetVisibilityData, config.stagesFrom)\n\t\t\tconst stages = found && typeof found === 'object' && !Array.isArray(found)\n\t\t\t\t? (found.stages && typeof found.stages === 'object' && !Array.isArray(found.stages) ? found.stages : found)\n\t\t\t\t: null\n\t\t\treturn stages && Object.keys(stages).length > 0 ? { ...config, stages } : config\n\t\t},\n\n\t\t/**\n\t\t * The \"what now\" card for the record's stage, labels translated.\n\t\t *\n\t\t * @return {{stage: string, title: string, items: Array<object>, after: string}|null} The card, or null when the stage declares no checklist.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-next-step-card\n\t\t */\n\t\tnextStepCard() {\n\t\t\tconst card = resolveNextStep(this.effectiveNextStep, this.currentObject, this.stageField)\n\t\t\tif (!card) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst tr = (text) => (text ? this.effectiveTranslate(text) : '')\n\t\t\treturn {\n\t\t\t\t...card,\n\t\t\t\ttitle: firstFilled(card.title, this.currentObject, tr),\n\t\t\t\tafter: firstFilled(card.after, this.currentObject, tr),\n\t\t\t\titems: card.items.map((item) => ({ ...item, label: tr(item.label), hint: tr(item.hint) })),\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Whether the \"what now\" card renders.\n\t\t *\n\t\t * @return {boolean} True when the stage has a checklist, the body is showing and the open tab allows it.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-next-step-card\n\t\t * @spec openspec/changes/detail-actions-per-tab/specs/detail-page/spec.md#requirement-quick-actions-and-the-next-step-card-can-follow-the-open-tab\n\t\t */\n\t\tshowsNextStepCard() {\n\t\t\treturn Boolean(this.nextStepCard) && this.showsBody && onTab(this.nextStep, this.activeBodyTab)\n\t\t},\n\n\t\t/**\n\t\t * The pills above the title: type first, then status.\n\t\t *\n\t\t * @return {Array<{key: string, label: string, colorKey: string, colorMap: object|null, variant: string}>} The pills whose field has a value.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-header-pills\n\t\t */\n\t\theaderPills() {\n\t\t\treturn [['type', this.typePill], ['status', this.statusPill]]\n\t\t\t\t.map(([key, config]) => {\n\t\t\t\t\tconst pill = pillCountSource(config)\n\t\t\t\t\t\t? resolveCountPill(config, this.pillCounts[key], (text) => this.effectiveTranslate(text))\n\t\t\t\t\t\t: resolvePill(config, this.resolvedObject)\n\t\t\t\t\tif (!pill) {\n\t\t\t\t\t\treturn null\n\t\t\t\t\t}\n\t\t\t\t\t// `resolve`: the referenced record's label replaces the id, and\n\t\t\t\t\t// keys the colour too (\"Woo-verzoek\", not a uuid).\n\t\t\t\t\tconst resolved = this.pillRefLabels[key]\n\t\t\t\t\tconst look = pillLook(this.pillRefLooks[key])\n\t\t\t\t\tif (typeof resolved === 'string' && resolved !== '') {\n\t\t\t\t\t\treturn { ...pill, ...look, key, label: this.effectiveTranslate(resolved), colorKey: resolved }\n\t\t\t\t\t}\n\t\t\t\t\treturn { ...pill, ...look, key, label: pillLabelFilled(this.effectiveTranslate(pill.label), this.resolvedObject) }\n\t\t\t\t})\n\t\t\t\t.filter(Boolean)\n\t\t},\n\n\t\t/**\n\t\t * What the count pills depend on: the record id and the lists they\n\t\t * count. A change counts again.\n\t\t *\n\t\t * @return {string} The key.\n\t\t * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-a-header-pill-can-be-a-constant-or-a-count\n\t\t */\n\t\t/**\n\t\t * What the resolved pills depend on, as one key: a change looks again.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/detail-pill-resolve/specs/detail-page/spec.md#requirement-a-header-pill-can-show-a-referenced-record-s-label\n\t\t */\n\t\tpillResolveKey() {\n\t\t\treturn JSON.stringify(this.pillResolveTargets)\n\t\t},\n\n\t\t/**\n\t\t * The pills with `resolve: { register, schema, labelField? }` and the\n\t\t * id each one shows: `[key, register, schema, labelField, id]`. Empty\n\t\t * while the field is empty.\n\t\t *\n\t\t * @return {Array<Array<string>>}\n\t\t * @spec openspec/changes/detail-pill-resolve/specs/detail-page/spec.md#requirement-a-header-pill-can-show-a-referenced-record-s-label\n\t\t */\n\t\tpillResolveTargets() {\n\t\t\treturn [['type', this.typePill], ['status', this.statusPill]]\n\t\t\t\t.filter(([, config]) => config && config.resolve && config.resolve.register && config.resolve.schema && typeof config.field === 'string')\n\t\t\t\t.map(([key, config]) => {\n\t\t\t\t\tconst pill = resolvePill({ field: config.field }, this.resolvedObject)\n\t\t\t\t\tconst { register, schema, labelField, colorField, iconField } = config.resolve\n\t\t\t\t\treturn pill ? [key, register, schema, labelField || '', pill.colorKey, colorField || '', iconField || ''] : null\n\t\t\t\t})\n\t\t\t\t.filter(Boolean)\n\t\t},\n\n\t\tpillCountKey() {\n\t\t\treturn JSON.stringify([this.objectId, pillCountSource(this.typePill), pillCountSource(this.statusPill)])\n\t\t},\n\n\t\t/**\n\t\t * The widget the header holds (`headerWidget`): the definition in\n\t\t * `widgets` with that id, or null without the key or a match.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-detail-header-can-be-a-card-that-holds-a-widget\n\t\t * @return {object|null} The widget definition, or null.\n\t\t */\n\t\theaderWidgetDef() {\n\t\t\tif (!this.headerWidget) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst declared = Array.isArray(this.widgets) ? this.widgets : []\n\t\t\treturn declared.find((widget) => widget && widget.id === this.headerWidget) || null\n\t\t},\n\n\t\t/**\n\t\t * The side column's widget definitions. A string names a widget\n\t\t * declared in `widgets`; an object is a definition of its own. A\n\t\t * definition whose `visibleWhen` does not hold for the record (or the\n\t\t * records `visibilityContext` names) is left out, exactly as in the\n\t\t * body grid; edit mode shows them all.\n\t\t *\n\t\t * @return {Array<object>} The definitions that resolve, each with an `id`.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-side-column\n\t\t * @spec openspec/changes/detail-side-visible-when/specs/detail-page/spec.md#requirement-a-side-column-widget-shows-only-while-its-visiblewhen-holds\n\t\t */\n\t\tsideColumnWidgets() {\n\t\t\tconst resolved = this.pickedSideWidgets.filter((widget) => this.sideWidgetOnTab(widget) && this.sideWidgetVisible(widget))\n\t\t\tif (!this.isBoard) {\n\t\t\t\treturn resolved\n\t\t\t}\n\t\t\t// The board look: the notice first, the History card last.\n\t\t\tconst rank = (widget) => (widget.type === 'banner' ? 0 : isActivityWidget(widget) ? 2 : 1)\n\t\t\treturn resolved\n\t\t\t\t.map((widget, index) => ({ widget, index }))\n\t\t\t\t.sort((a, b) => (rank(a.widget) - rank(b.widget)) || (a.index - b.index))\n\t\t\t\t.map(({ widget }) => widget)\n\t\t},\n\n\t\t/**\n\t\t * The side column's definitions as the record picks them\n\t\t * (`sideColumnFrom`): the ids it lists, in its order, or every\n\t\t * declared entry when it lists none. Edit mode shows them all.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t * @spec openspec/changes/detail-side-column-from/specs/detail-page/spec.md#requirement-a-record-can-pick-the-side-column-and-its-order\n\t\t */\n\t\tpickedSideWidgets() {\n\t\t\tconst declared = this.declaredSideWidgets\n\t\t\tif (!this.sideColumnFrom || this.editingBody || !this.widgetVisibilityData) {\n\t\t\t\treturn declared\n\t\t\t}\n\t\t\tconst ids = readVisibleWhenPath(this.widgetVisibilityData, this.sideColumnFrom)\n\t\t\tif (!Array.isArray(ids) || ids.length === 0) {\n\t\t\t\treturn declared\n\t\t\t}\n\t\t\treturn ids\n\t\t\t\t.map((id) => declared.find((widget) => widget.id === id) || null)\n\t\t\t\t.filter(Boolean)\n\t\t},\n\n\t\t/**\n\t\t * The side column's entries resolved to definitions, before any\n\t\t * `visibleWhen` is read: a string names a widget declared in\n\t\t * `widgets`, an object is a definition of its own (given an id when it\n\t\t * has none).\n\t\t *\n\t\t * @return {Array<object>}\n\t\t * @spec openspec/changes/detail-side-visible-when/specs/detail-page/spec.md#requirement-a-side-column-widget-shows-only-while-its-visiblewhen-holds\n\t\t */\n\t\tdeclaredSideWidgets() {\n\t\t\tconst declared = Array.isArray(this.widgets) ? this.widgets : []\n\t\t\treturn (Array.isArray(this.sideColumn) ? this.sideColumn : [])\n\t\t\t\t.map((entry, index) => {\n\t\t\t\t\tif (typeof entry === 'string') {\n\t\t\t\t\t\treturn declared.find((widget) => widget && widget.id === entry) || null\n\t\t\t\t\t}\n\t\t\t\t\tif (!entry || typeof entry !== 'object' || Array.isArray(entry)) {\n\t\t\t\t\t\treturn null\n\t\t\t\t\t}\n\t\t\t\t\treturn entry.id ? entry : { ...entry, id: `cn-side-${index}` }\n\t\t\t\t})\n\t\t\t\t.filter(Boolean)\n\t\t},\n\n\t\t/**\n\t\t * The `columnOpts` the body grid gets: the default table stacks at 1000px\n\t\t * of grid width, or at 600px when a side column narrows the grid. An\n\t\t * explicit `columnOpts` prop is passed through untouched.\n\t\t *\n\t\t * @return {object|null} The column options.\n\t\t */\n\t\tbodyColumnOpts() {\n\t\t\tif (!this.showsSideColumn || !isDefaultColumnOpts(this.columnOpts)) {\n\t\t\t\treturn this.columnOpts\n\t\t\t}\n\t\t\treturn { ...this.columnOpts, breakpoints: [{ w: SIDE_COLUMN_STACK_WIDTH, c: 1 }] }\n\t\t},\n\n\t\t/**\n\t\t * Whether the side column renders.\n\t\t *\n\t\t * @return {boolean} True when it has widgets and the body is showing.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-side-column\n\t\t */\n\t\t/**\n\t\t * The band's widgets, resolved and visible, each with its span.\n\t\t *\n\t\t * @return {Array<{widget: object, span: number, chrome: string}>}\n\t\t * @spec openspec/changes/detail-band/specs/detail-page/spec.md#requirement-a-detail-page-can-draw-a-band-across-the-page-above-the-body-and-the-side-column\n\t\t */\n\t\tbandWidgets() {\n\t\t\tconst declared = Array.isArray(this.widgets) ? this.widgets : []\n\t\t\treturn (Array.isArray(this.band) ? this.band : [])\n\t\t\t\t.map((entry, index) => {\n\t\t\t\t\tlet widget = null\n\t\t\t\t\tlet span = 12\n\t\t\t\t\tif (typeof entry === 'string') {\n\t\t\t\t\t\twidget = declared.find((candidate) => candidate && candidate.id === entry) || null\n\t\t\t\t\t} else if (entry && typeof entry === 'object' && !Array.isArray(entry)) {\n\t\t\t\t\t\tspan = Number.isInteger(entry.span) ? Math.min(12, Math.max(1, entry.span)) : 12\n\t\t\t\t\t\tif (typeof entry.widgetId === 'string') {\n\t\t\t\t\t\t\twidget = declared.find((candidate) => candidate && candidate.id === entry.widgetId) || null\n\t\t\t\t\t\t} else if (typeof entry.type === 'string') {\n\t\t\t\t\t\t\twidget = entry.id ? entry : { ...entry, id: `cn-band-${index}` }\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif (!widget) {\n\t\t\t\t\t\treturn null\n\t\t\t\t\t}\n\t\t\t\t\tconst chrome = ['card', 'bare'].includes(entry?.chrome) ? entry.chrome : (widget.type === 'banner' ? 'bare' : 'card')\n\t\t\t\t\treturn { widget, span, chrome }\n\t\t\t\t})\n\t\t\t\t.filter((entry) => entry && this.sideWidgetVisible(entry.widget))\n\t\t},\n\n\t\t/**\n\t\t * Whether the band renders: it has a visible widget and the record shows.\n\t\t *\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/detail-band/specs/detail-page/spec.md#requirement-a-detail-page-can-draw-a-band-across-the-page-above-the-body-and-the-side-column\n\t\t */\n\t\tshowsBand() {\n\t\t\treturn (this.bandWidgets.length > 0 || this.detailPageFilters.length > 0) && this.showsBody\n\t\t},\n\n\t\t/**\n\t\t * The declared page filters that carry a key.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t * @spec openspec/changes/detail-page-filters/specs/detail-page/spec.md#requirement-a-detail-page-can-draw-page-filters-that-its-widgets-read\n\t\t */\n\t\tdetailPageFilters() {\n\t\t\treturn (Array.isArray(this.pageFilters) ? this.pageFilters : []).filter((pf) => pf && pf.key)\n\t\t},\n\n\t\tshowsSideColumn() {\n\t\t\treturn this.sideColumnWidgets.length > 0 && this.showsBody\n\t\t},\n\n\t\t/**\n\t\t * Accessible name of the side column.\n\t\t *\n\t\t * @return {string} The label.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-side-column\n\t\t */\n\t\tsideColumnLabel() {\n\t\t\treturn t('nextcloud-vue', 'Details')\n\t\t},\n\n\t\t/**\n\t\t * Where this record sits in the list it was opened from, as text.\n\t\t *\n\t\t * @return {string} For example \"4 of 40\".\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tlistPositionLabel() {\n\t\t\tconst nav = this.listNavigation\n\t\t\tif (!nav || !nav.total) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn `${nav.position} ${this.effectiveTranslate('of')} ${nav.total}`\n\t\t},\n\n\t\t/**\n\t\t * Accessible name for the previous control, which says what list it\n\t\t * steps through rather than only which direction it goes.\n\t\t *\n\t\t * @return {string} The label.\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tpreviousRecordLabel() {\n\t\t\treturn this.listNavigation?.isFirst\n\t\t\t\t? this.effectiveTranslate('This is the first record in the list')\n\t\t\t\t: this.effectiveTranslate('Previous record in the list')\n\t\t},\n\n\t\t/**\n\t\t * Accessible name for the next control.\n\t\t *\n\t\t * @return {string} The label.\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tnextRecordLabel() {\n\t\t\treturn this.listNavigation?.isLast\n\t\t\t\t? this.effectiveTranslate('This is the last record in the list')\n\t\t\t\t: this.effectiveTranslate('Next record in the list')\n\t\t},\n\n\t\t/**\n\t\t * Stable id for the page-header Actions menu. Prefers the explicit\n\t\t * `pageId` prop; falls back to a slugified `title` so the menu still\n\t\t * gets a usable id / surface.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\t/**\n\t\t * Whether the page is in Buildiq edit mode — unwraps the injected\n\t\t * `cnEditingBody` ref (or plain boolean). Drives the per-widget cog.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\teditingBody() {\n\t\t\tconst e = this.cnEditingBody\n\t\t\treturn Boolean(e && typeof e === 'object' && 'value' in e ? e.value : e)\n\t\t},\n\n\t\t/**\n\t\t * The widget definition currently being configured, shaped for\n\t\t * `CnWidgetStyleEditorModal` (carries `id`, `type`, `title`, `content`).\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tconfigWidget() {\n\t\t\tif (!this.configWidgetId) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst def = this.bodyGridWidgets.find((w) => w.id === this.configWidgetId)\n\t\t\treturn def || null\n\t\t},\n\n\t\tresolvedPageId() {\n\t\t\tif (this.pageId) {\n\t\t\t\treturn this.pageId\n\t\t\t}\n\t\t\treturn String(this.title || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')\n\t\t},\n\n\t\t/**\n\t\t * Whether to show the full-page loading state (spinner that replaces\n\t\t * the page content). Only on the FIRST load — when `loading` is true\n\t\t * and nothing has been shown yet. Once `hasLoadedOnce` is set, a\n\t\t * subsequent `loading` (refresh / re-fetch) keeps the existing\n\t\t * content in place; the action-button spinner signals the refresh\n\t\t * instead (see `effectiveRefreshing`).\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tshowLoadingState() {\n\t\t\treturn (this.loading && !this.hasLoadedOnce) || this.awaitingObject\n\t\t},\n\n\t\t/**\n\t\t * Whether the page is still waiting for its first answer on the record.\n\t\t * The body, lifecycle menu and sidebar stay unmounted meanwhile, because\n\t\t * each of them requests endpoints scoped to the id and a missing record\n\t\t * would turn every one of those into a 404 or 500.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tawaitingObject() {\n\t\t\tif (this.hasRecordSourceFetch) {\n\t\t\t\treturn this.objectFetchPending && !this.currentObject\n\t\t\t}\n\t\t\treturn this.hasSchemaDrivenFetch\n\t\t\t\t&& this.objectFetchPending\n\t\t\t\t&& typeof this.effectiveObjectStore?.fetchObject === 'function'\n\t\t\t\t&& !this.currentObject\n\t\t},\n\n\t\t/**\n\t\t * Where the not-found state's button goes: `notFoundRoute`, or the app's\n\t\t * root when the host names no list to return to.\n\t\t *\n\t\t * @return {object|string}\n\t\t */\n\t\tnotFoundTarget() {\n\t\t\treturn this.notFoundRoute || { path: '/' }\n\t\t},\n\n\t\t/** Heading and description of the not-found state. */\n\t\tnotFoundLabels() {\n\t\t\treturn {\n\t\t\t\tname: t('nextcloud-vue', 'Not found'),\n\t\t\t\tdescription: t('nextcloud-vue', 'This item does not exist or has been deleted.'),\n\t\t\t}\n\t\t},\n\n\t\t/** Label of the not-found state's button, unescaped because `{{ }}` escapes. */\n\t\tnotFoundButtonLabel() {\n\t\t\treturn this.notFoundRoute && this.notFoundRouteLabel\n\t\t\t\t? t('nextcloud-vue', 'Back to {page}', { page: this.notFoundRouteLabel }, undefined, { escape: false })\n\t\t\t\t: t('nextcloud-vue', 'Back to home')\n\t\t},\n\n\t\t/**\n\t\t * Refresh-spinner flag for the page-header Actions menu. True when:\n\t\t * an explicit `refreshing` prop is set; OR a schema-driven self-fetch\n\t\t * is in flight (`internalRefreshing`); OR a background load is running\n\t\t * after the first one completed (`loading && hasLoadedOnce`) — so\n\t\t * legacy `objectType` hosts get the spinner for free just by passing\n\t\t * `:loading`, without wiring `:refreshing` themselves.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\teffectiveRefreshing() {\n\t\t\treturn this.refreshing || this.internalRefreshing || (this.loading && this.hasLoadedOnce)\n\t\t},\n\n\t\t/**\n\t\t * Effective object-type slug, used for subscription, lock, store\n\t\t * registration, fetch, and sidebar state. Explicit `objectType`\n\t\t * prop wins (existing direct-mount call sites stay untouched);\n\t\t * otherwise `${register}-${schema}` fuses the schema-driven props\n\t\t * the manifest renderer passes. Returns `''` when neither path\n\t\t * yields a slug — the schema-driven gates downstream key off that.\n\t\t */\n\t\tresolvedObjectType() {\n\t\t\tif (this.objectType) {\n\t\t\t\treturn this.objectType\n\t\t\t}\n\t\t\tif (this.register && this.schema) {\n\t\t\t\treturn `${this.register}-${this.schema}`\n\t\t\t}\n\t\t\treturn ''\n\t\t},\n\n\t\t/**\n\t\t * True when this mount is wired for the manifest's schema-driven\n\t\t * contract: `register`, `schema`, and `objectId` are all set, so\n\t\t * the page should fetch the object + schema, register the type\n\t\t * on the store, and render the auto-body widgets when no slot\n\t\t * content is supplied. Legacy direct mounts (which pass an\n\t\t * explicit `objectType` instead) return `false` here and skip\n\t\t * the fetch path entirely.\n\t\t */\n\t\thasSchemaDrivenFetch() {\n\t\t\treturn !this.entitySource && Boolean(this.register && this.schema && this.objectId)\n\t\t},\n\n\t\t/**\n\t\t * The record source named by `entitySource`, or null. An unknown name,\n\t\t * or a source that cannot read one record, is null with a warning.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\trecordSource() {\n\t\t\tif (!this.entitySource) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\ttry {\n\t\t\t\treturn resolveRecordSource(this.entitySource)\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console -- the console is the only channel a host app can read the detail on\n\t\t\t\tconsole.warn('[CnDetailPage] the record source could not start; no record is shown.', err)\n\t\t\t\treturn null\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Whether the page reads its record through a record source.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\thasRecordSourceFetch() {\n\t\t\treturn Boolean(this.recordSource && this.objectId)\n\t\t},\n\n\t\t/**\n\t\t * Whether the page reads its own record, from the object store or a\n\t\t * record source, so Refresh and `cn:page:refresh` have something to do.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tselfFetchesRecord() {\n\t\t\treturn this.hasSchemaDrivenFetch || this.hasRecordSourceFetch\n\t\t},\n\n\t\t/**\n\t\t * Whether the header Edit button renders. Opted in via `showEditAction`,\n\t\t * but gated on there actually being a record and a schema to edit it\n\t\t * with: `isCreateMode` already owns the id-less page, and a button that\n\t\t * opens a form with no schema is a dead button.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\t/** Whether the lifecycle transitions are mounted. */\n\t\tshowsLifecycleActions() {\n\t\t\tif (this.objectNotFound || this.awaitingObject) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn Boolean(this.lifecycleActions && (this.objectId || this.currentObject))\n\t\t},\n\n\t\t/**\n\t\t * Everything the Actions menu lists above its built-in items: the\n\t\t * lifecycle transitions, then the declared header actions.\n\t\t *\n\t\t * @return {Array<object>} Menu-ready entries.\n\t\t */\n\t\theaderMenuEntries() {\n\t\t\tconst lifecycle = this.showsLifecycleActions ? this.lifecycleMenuEntries : []\n\t\t\t// An action drawn as a button, the stage's primary one or a quick\n\t\t\t// action, is not listed a second time in the menu.\n\t\t\tconst pinned = new Set([\n\t\t\t\t...(this.stagePrimaryEntry ? [this.stagePrimaryEntry.id] : []),\n\t\t\t\t...this.quickActionEntries.map((entry) => entry.id),\n\t\t\t])\n\t\t\tconst inMenu = pinned.size > 0\n\t\t\t\t? this.menuHeaderActions.filter((entry) => !pinned.has(entry.id))\n\t\t\t\t: this.menuHeaderActions\n\t\t\treturn [...this.sourceVerbMenuEntries, ...lifecycle, ...inMenu]\n\t\t},\n\n\t\t/**\n\t\t * The verbs the record source offers on the record in view, read off\n\t\t * the record's own state by the source. Empty without a source that\n\t\t * offers verbs, before the record is read, and when it was not found.\n\t\t *\n\t\t * @return {Array<object>} The verbs.\n\t\t * @spec openspec/changes/record-source-verbs/specs/record-source-verbs/spec.md#requirement-a-record-source-offers-header-verbs\n\t\t */\n\t\tsourceVerbs() {\n\t\t\tconst source = this.recordSource\n\t\t\tif (!source || typeof source.verbs !== 'function' || typeof source.runVerb !== 'function') {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\tif (this.objectNotFound || !this.currentObject) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\tconst verbs = source.verbs(this.currentObject)\n\t\t\treturn Array.isArray(verbs) ? verbs.filter((verb) => verb && typeof verb.id === 'string' && verb.id !== '') : []\n\t\t},\n\n\t\t/**\n\t\t * Whether the record source can tick the checklist of the record in\n\t\t * view: it writes items, and says this record can still be ticked.\n\t\t *\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/record-checklist-widget/specs/record-checklist-widget/spec.md#requirement-a-checklist-widget-ticks-items-through-the-record-source\n\t\t */\n\t\trecordCanCheckItems() {\n\t\t\tconst source = this.recordSource\n\t\t\tif (!source || typeof source.checkItem !== 'function' || !this.currentObject || this.objectNotFound) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn typeof source.canCheckItems !== 'function' || source.canCheckItems(this.currentObject) === true\n\t\t},\n\n\t\t/**\n\t\t * The source verbs drawn as header buttons.\n\t\t *\n\t\t * @return {Array<object>} The verbs.\n\t\t */\n\t\tsourceVerbButtons() {\n\t\t\treturn this.sourceVerbs.filter((verb) => verb.menu !== true)\n\t\t},\n\n\t\t/**\n\t\t * The source verbs listed in the Actions menu, as menu entries.\n\t\t *\n\t\t * @return {Array<object>} Menu-ready entries.\n\t\t */\n\t\tsourceVerbMenuEntries() {\n\t\t\treturn this.sourceVerbs\n\t\t\t\t.filter((verb) => verb.menu === true)\n\t\t\t\t.map((verb) => ({\n\t\t\t\t\tid: `cn-source-verb-${verb.id}`,\n\t\t\t\t\tlabel: verb.label,\n\t\t\t\t\ttestid: `cn-detail-page-verb-${verb.id}`,\n\t\t\t\t\ticonName: typeof verb.icon === 'string' ? verb.icon : '',\n\t\t\t\t\tdisabled: this.sourceVerbBusy,\n\t\t\t\t\tpressed: null,\n\t\t\t\t\trun: () => this.runSourceVerb(verb),\n\t\t\t\t}))\n\t\t},\n\n\t\t/**\n\t\t * Whether Edit joins the CnActionButtons cluster instead of standing\n\t\t * on its own. Opted into by `inlineActions`.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tfoldsEditIntoActions() {\n\t\t\t// Edit keeps its own labelled button in the board look.\n\t\t\treturn typeof this.inlineActions === 'number' && !this.isBoard\n\t\t},\n\n\t\t/**\n\t\t * The header actions actually handed to CnActionButtons: the declared\n\t\t * ones, plus this page's Edit appended when `inlineActions` folds it\n\t\t * in. Edit goes LAST so it reads after the app's own actions in the\n\t\t * Actions menu — the page's affordance yields to the app's.\n\t\t *\n\t\t * @spec openspec/changes/header-action-href/specs/header-action-href/spec.md#requirement-a-header-action-with-href-is-a-link-that-runs-and-emits-nothing\n\t\t * @return {Array<object>} The action descriptors.\n\t\t */\n\t\teffectiveHeaderActions() {\n\t\t\tconst authored = Array.isArray(this.headerActions) ? this.headerActions : []\n\t\t\t// Inline pinned actions (a stage's primary action, a quick action\n\t\t\t// written out in place) join the list CnActionButtons dispatches,\n\t\t\t// so they get the same dialogs, toasts and `visibleWhen` gating as\n\t\t\t// any header action. An id that is already declared is not added\n\t\t\t// twice. With nothing pinned inline and no `href` entry this is\n\t\t\t// `headerActions` itself.\n\t\t\tconst known = new Set(authored.map((action) => action && action.id))\n\t\t\tconst inline = [this.stagePrimary, ...this.pinnedQuickActions]\n\t\t\t\t.filter((pinned) => pinned && pinned.inline && !known.has(pinned.id))\n\t\t\t\t.map((pinned) => pinned.inline)\n\t\t\tconst combined = inline.length > 0 ? [...authored, ...inline] : authored\n\t\t\t// An entry (or child) with `href` becomes a link CnActionButtons renders as an anchor.\n\t\t\tconst linked = combined.map((action) => headerActionAsLink(action, this.headerLinkTokenCtx))\n\t\t\tconst declared = linked.some((action, index) => action !== combined[index]) ? linked : combined\n\t\t\tif (!(this.foldsEditIntoActions && this.canEditRecord)) {\n\t\t\t\treturn declared\n\t\t\t}\n\t\t\treturn [\n\t\t\t\t...declared,\n\t\t\t\t{\n\t\t\t\t\tid: 'cn-detail-page-edit',\n\t\t\t\t\tlabel: this.editActionLabel,\n\t\t\t\t\ticon: 'Pencil',\n\t\t\t\t\t// A function, not a dispatch type: `open-form` hardcodes\n\t\t\t\t\t// `item: null`, so it creates and cannot edit this record.\n\t\t\t\t\tonSelect: () => this.openEditForm(),\n\t\t\t\t},\n\t\t\t]\n\t\t},\n\n\t\tcanEditRecord() {\n\t\t\treturn this.showEditAction\n\t\t\t\t&& !this.isCreateMode\n\t\t\t\t&& this.hasSchemaDrivenFetch\n\t\t\t\t&& Boolean(this.currentSchema)\n\t\t\t\t&& !this.editLockedByOther\n\t\t},\n\n\t\t/**\n\t\t * Whether somebody else is editing this record right now.\n\t\t *\n\t\t * The banner above already names them, so the button is withdrawn\n\t\t * rather than shown and refused: an Edit that opens a form the write\n\t\t * will reject is a form somebody fills in twice.\n\t\t *\n\t\t * @return {boolean} True when another user holds the lock.\n\t\t */\n\t\teditLockedByOther() {\n\t\t\treturn Boolean(this.lockState\n\t\t\t\t&& this.lockState.locked.value\n\t\t\t\t&& !this.lockState.lockedByMe.value)\n\t\t},\n\n\t\t/**\n\t\t * Label for the header Edit button: the manifest's `editLabel` through\n\t\t * the host translate, else the library's translated \"Edit\".\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/detail-edit-label-translate/specs/detail-page/spec.md#requirement-the-edit-button-label-follows-the-reader-s-language\n\t\t */\n\t\teditActionLabel() {\n\t\t\treturn this.editLabel ? this.effectiveTranslate(this.editLabel) : t('nextcloud-vue', 'Edit')\n\t\t},\n\n\t\t/** @return {string} The edit form's title, translated; the Edit button's label when none is configured. */\n\t\teditFormTitle() {\n\t\t\treturn this.editTitle ? this.effectiveTranslate(this.editTitle) : this.editActionLabel\n\t\t},\n\n\t\t/** @return {string} The edit form's subtitle, translated; empty when none is configured. */\n\t\teditFormSubtitle() {\n\t\t\treturn this.editSubtitle ? this.effectiveTranslate(this.editSubtitle) : ''\n\t\t},\n\n\t\t/**\n\t\t * The create archetype: a schema-bound page (`register` + `schema`) whose\n\t\t * route carries NO object id, and which has no hand-authored body of its\n\t\t * own. Instead of rendering a blank page (procest `/tasks/new`), the page\n\t\t * shows an empty create form for the schema (ADR-062). Suppressed when a\n\t\t * default slot or an explicit grid `layout` supplies the body.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tisCreateMode() {\n\t\t\t// `createForm` (manifest `config.createForm`) is the explicit switch;\n\t\t\t// 'auto' keeps the original heuristic. A page still needs a schema to\n\t\t\t// build a form from, and one already showing an object is editing,\n\t\t\t// not creating — so those two conditions hold even for 'always'.\n\t\t\tif (this.createForm === 'never') {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tconst schemaBound = Boolean(this.register && this.schema) && !this.objectId\n\t\t\tif (this.createForm === 'always') {\n\t\t\t\treturn schemaBound\n\t\t\t}\n\t\t\treturn schemaBound && !this.hasDefaultSlotContent && !this.hasGridLayout\n\t\t},\n\n\t\t/**\n\t\t * Create-form prefill drawn from the route query, filtered to the\n\t\t * schema's own properties (e.g. `?caseId=x` seeds the `caseId` field so\n\t\t * a child created from a parent is pre-linked). Null when nothing\n\t\t * applies (true empty create).\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tcreatePrefill() {\n\t\t\tconst q = (this.$route && this.$route.query) || {}\n\t\t\tconst props = (this.currentSchema && this.currentSchema.properties) || {}\n\t\t\tconst out = {}\n\t\t\tfor (const [k, v] of Object.entries(q)) {\n\t\t\t\tif (Object.hasOwn(props, k)) {\n\t\t\t\t\tout[k] = v\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn Object.keys(out).length ? out : null\n\t\t},\n\n\t\t/**\n\t\t * `show` for the `#form-dialog` slot: whether the page's built-in form\n\t\t * dialog is on screen — the create archetype's empty form, or the\n\t\t * record edit form.\n\t\t *\n\t\t * Mirrors the `v-if` on the two default children exactly, so a\n\t\t * replacement opens and closes on the same conditions the built-in\n\t\t * dialog does. That includes the `editFormAwaitingRecord` guard (#850),\n\t\t * which is what stops an edit form opening over a record that has not\n\t\t * loaded yet — a replacement that reimplemented visibility from\n\t\t * `editFormOpen` alone would reintroduce that blank-form-overwrites-\n\t\t * the-record bug in every app that used it.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tformDialogVisible() {\n\t\t\tif (!this.currentSchema) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif (this.isCreateMode) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn this.editFormOpen && !this.editFormAwaitingRecord\n\t\t},\n\n\t\t/**\n\t\t * `item` for the `#form-dialog` slot: the record the built-in dialog is\n\t\t * bound to — the loaded object when editing, the route-derived prefill\n\t\t * when creating (null for a true empty create, matching CnIndexPage,\n\t\t * where a null `item` likewise means \"create\").\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tformDialogItem() {\n\t\t\treturn this.isCreateMode ? this.createPrefill : this.currentObject\n\t\t},\n\n\t\t/**\n\t\t * Effective Refresh visibility for the page header. An explicit\n\t\t * `showRefresh` prop wins; when unset (`null`), show Refresh only if\n\t\t * it will do something — a consumer attached an `@refresh` listener,\n\t\t * or the page can self-fetch (`hasSchemaDrivenFetch`).\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\teffectiveHeaderShowRefresh() {\n\t\t\tif (this.showRefresh !== null) {\n\t\t\t\treturn this.showRefresh\n\t\t\t}\n\t\t\t// `$.vnode.props`, not `$attrs`: `refresh` is a declared emit, and\n\t\t\t// Vue keeps declared emits out of `$attrs`.\n\t\t\treturn Boolean(this.$.vnode.props?.onRefresh) || this.selfFetchesRecord\n\t\t},\n\n\t\t/**\n\t\t * Pinia store instance used for the schema-driven fetch.\n\t\t * Mirrors `CnLogsPage.objectStore`: explicit `objectStore` prop\n\t\t * wins; otherwise falls back to the library's default\n\t\t * `useObjectStore()`. The composable is invoked lazily inside a\n\t\t * computed so test mounts that never activate Pinia don't crash.\n\t\t *\n\t\t * @return {object|null} The resolved store, or null when the page\n\t\t * is not in schema-driven mode (no need to touch Pinia at all).\n\t\t */\n\t\teffectiveObjectStore() {\n\t\t\tif (this.objectStore) {\n\t\t\t\treturn this.objectStore\n\t\t\t}\n\t\t\tif (!this.hasSchemaDrivenFetch && !this.isCreateMode) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\ttry {\n\t\t\t\treturn useObjectStore()\n\t\t\t} catch (err) {\n\t\t\t\t// Pinia not active in this consumer — fall back to no-op\n\t\t\t\t// and let the page render as if no store-driven content\n\t\t\t\t// were available. Real consumers (CnAppRoot-hosted apps)\n\t\t\t\t// always have Pinia active, so this branch only protects\n\t\t\t\t// stand-alone test mounts.\n\n\t\t\t\t// eslint-disable-next-line no-console -- a deliberate warning to the developer integrating this component\n\t\t\t\tconsole.warn('[CnDetailPage] useObjectStore() unavailable; schema-driven mode disabled.', err)\n\t\t\t\treturn null\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The fetched OR object for the schema-driven mode. Read straight\n\t\t * from the store's normalised `objects[type][id]` cache so any\n\t\t * other component fetching the same object (sidebar widgets,\n\t\t * locked-banner) shares state with no second request.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tcurrentObject() {\n\t\t\tif (this.recordSource) {\n\t\t\t\treturn this.objectId ? (this.recordSource.record(this.objectId) ?? null) : null\n\t\t\t}\n\t\t\tconst store = this.effectiveObjectStore\n\t\t\tif (!store) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst type = this.resolvedObjectType\n\t\t\tif (!type || !this.objectId) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn store.objects?.[type]?.[this.objectId] ?? null\n\t\t},\n\n\t\t/**\n\t\t * Whether the edit form is still waiting for the record it edits.\n\t\t *\n\t\t * True only while a record is actually RESOLVABLE and absent — a host\n\t\t * with no object store has nothing to wait for, and gating on it\n\t\t * there would make the Edit button dead.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\teditFormAwaitingRecord() {\n\t\t\tif (!this.effectiveObjectStore || !this.objectId) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn !this.currentObject\n\t\t},\n\n\t\t/**\n\t\t * The resolved OR object surfaced to the header's translation\n\t\t * badge slot (cn-detail-translation-aware-surfacing). Falls\n\t\t * back to `currentObject` from the schema-driven path AND, when\n\t\t * the schema-driven path is unavailable, attempts to read the\n\t\t * store via `objectType` + `objectId` directly (so legacy direct\n\t\t * mounts that don't pass `register` / `schema` still benefit).\n\t\t * Returns `null` when nothing can be resolved — the badge then\n\t\t * auto-hides.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tresolvedObject() {\n\t\t\t// Schema-driven path: reuse what `currentObject` already\n\t\t\t// computed.\n\t\t\tconst fromSchemaDriven = this.currentObject\n\t\t\tif (fromSchemaDriven) {\n\t\t\t\treturn fromSchemaDriven\n\t\t\t}\n\t\t\t// Direct-mount fallback: legacy callers pass `objectType` +\n\t\t\t// `objectId`. Read the same cache shape but keyed off the\n\t\t\t// explicit slug.\n\t\t\tconst store = this.effectiveObjectStore\n\t\t\tif (!store) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (!this.objectType || !this.objectId) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn store.objects?.[this.objectType]?.[this.objectId] ?? null\n\t\t},\n\n\t\t/**\n\t\t * The records `visibilityContext` names for the loaded record.\n\t\t *\n\t\t * @return {Array<{key: string, register: string, schema: string, id: string}>}\n\t\t * @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-page-can-load-records-a-condition-names\n\t\t */\n\t\tvisibilityRefs() {\n\t\t\treturn visibilityContextRefs(this.visibilityContext, this.resolvedObject)\n\t\t},\n\n\t\t/**\n\t\t * Whether a record `visibilityContext` names is still on its way.\n\t\t *\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-page-can-load-records-a-condition-names\n\t\t */\n\t\tvisibilityRowsPending() {\n\t\t\treturn this.visibilityRefs.length > 0 && this.visibilityRowsFor !== this.visibilityRefsKey\n\t\t},\n\n\t\t/**\n\t\t * The named references as one key, so another record (or another case\n\t\t * type on the same record) reads them again.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-page-can-load-records-a-condition-names\n\t\t */\n\t\tvisibilityRefsKey() {\n\t\t\treturn JSON.stringify(this.visibilityRefs)\n\t\t},\n\n\t\t/**\n\t\t * What a grid widget's `visibleWhen` reads: the record, with the\n\t\t * `visibilityContext` records under `@context.<name>`. Null until the record loads.\n\t\t *\n\t\t * @return {object|null}\n\t\t * @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-grid-widget-shows-only-while-its-visiblewhen-holds-for-the-record\n\t\t */\n\t\twidgetVisibilityData() {\n\t\t\treturn widgetConditionData(this.resolvedObject, this.visibilityRows)\n\t\t},\n\n\t\t/**\n\t\t * The page's widget definitions with each `titleWhen` resolved against\n\t\t * the record, as the cards, the tab strip and the leaves read them.\n\t\t *\n\t\t * @return {object[]}\n\t\t * @spec openspec/changes/widget-title-when/specs/widget-title-when/spec.md#requirement-a-widget-title-can-depend-on-the-record\n\t\t */\n\t\ttitledWidgets() {\n\t\t\treturn withTitlesWhen(this.widgets, this.titleData)\n\t\t},\n\n\t\t/**\n\t\t * The body grid's widget definitions with each `titleWhen` resolved.\n\t\t *\n\t\t * @return {object[]}\n\t\t * @spec openspec/changes/widget-title-when/specs/widget-title-when/spec.md#requirement-a-widget-title-can-depend-on-the-record\n\t\t */\n\t\ttitledBodyGridWidgets() {\n\t\t\treturn withTitlesWhen(this.bodyGridWidgets, this.titleData)\n\t\t},\n\n\t\t/**\n\t\t * What a `titleWhen` reads: the record with its `@context` rows, null\n\t\t * while the record or those rows are on their way.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\ttitleData() {\n\t\t\treturn this.visibilityRowsPending ? null : this.widgetVisibilityData\n\t\t},\n\n\t\t/**\n\t\t * The grid widgets whose `visibleWhen` needs a request (endpoint or\n\t\t * source), with what they ask, as one key: a change re-asks them.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-grid-widget-shows-only-while-its-visiblewhen-holds-for-the-record\n\t\t */\n\t\tremoteWidgetConditionsKey() {\n\t\t\tif (!this.widgetVisibilityData || this.visibilityRowsPending) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst defs = [...(this.bodyGridWidgets || []), ...((this.activeView && this.activeView.widgets) || []), ...this.declaredSideWidgets]\n\t\t\tconst remote = defs.filter((def) => def && def.visibleWhen && typeof def.visibleWhen === 'object'\n\t\t\t\t&& !isLocallyDecidableVisibleWhen(def.visibleWhen))\n\t\t\tif (remote.length === 0) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn JSON.stringify([String(this.objectId || ''), remote.map((def) => [def.id, def.visibleWhen])])\n\t\t},\n\n\t\t/**\n\t\t * The `@self` block of the resolved object (interaction markers live here).\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\tresolvedSelf() {\n\t\t\tconst obj = this.resolvedObject\n\t\t\treturn (obj && typeof obj === 'object' && obj['@self'] && typeof obj['@self'] === 'object') ? obj['@self'] : {}\n\t\t},\n\n\t\t/**\n\t\t * Whether Mark as unread is offered: the object carries `@self.unread`.\n\t\t *\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/record-unread-markers/tasks.md#task-2\n\t\t */\n\t\tshowMarkUnread() {\n\t\t\treturn this.actionsMenu?.showMarkUnread !== false\n\t\t\t\t&& typeof this.resolvedSelf.unread === 'boolean'\n\t\t\t\t&& this.register !== '' && this.schema !== '' && this.objectId !== ''\n\t\t},\n\n\t\t/**\n\t\t * Whether the header menu offers \"Open sidebar\": the board look, where the\n\t\t * sidebar's own toggle is hidden, on a page whose sidebar is active and\n\t\t * closed.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tshowOpenSidebar() {\n\t\t\treturn this.actionsMenu?.showOpenSidebar !== false\n\t\t\t\t&& this.isBoard && this.hasExternalSidebar && this.sidebarActive && this.objectSidebarState.open !== true\n\t\t},\n\n\t\t/** @return {string} Label of the Open sidebar entry. */\n\t\topenSidebarLabel() {\n\t\t\treturn t('nextcloud-vue', 'Open sidebar')\n\t\t},\n\n\t\t/** @return {string} Label of the Mark as unread entry. */\n\t\tmarkUnreadLabel() {\n\t\t\treturn t('nextcloud-vue', 'Mark as unread')\n\t\t},\n\n\t\t/**\n\t\t * True once the object has rendered unread: loaded, not missing, and\n\t\t * carrying `@self.unread: true`. Sending `PUT .../read-state` hangs on this.\n\t\t *\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/record-unread-markers/tasks.md#task-2\n\t\t */\n\t\treadStateDue() {\n\t\t\treturn this.markRead !== false\n\t\t\t\t&& !this.objectFetchPending\n\t\t\t\t&& !this.objectNotFound\n\t\t\t\t&& this.resolvedSelf.unread === true\n\t\t\t\t&& this.register !== '' && this.schema !== '' && this.objectId !== ''\n\t\t},\n\n\t\t/**\n\t\t * Whether the Follow toggle renders: the object carries `@self.watching` and the page did not turn it off.\n\t\t *\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/record-favourite-and-follow/tasks.md#task-3\n\t\t */\n\t\tshowFollowToggle() {\n\t\t\treturn this.follow !== false\n\t\t\t\t&& typeof this.resolvedSelf.watching === 'boolean'\n\t\t\t\t&& this.register !== '' && this.schema !== '' && this.objectId !== ''\n\t\t},\n\n\t\t/**\n\t\t * Who else has the record open now, in words, for the follow row:\n\t\t * one name, or the first name and how many more. Empty without\n\t\t * `followRow.presence` or while nobody else is there.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/follow-row-presence/specs/board-follow-row/spec.md#requirement-the-follow-row-can-say-who-else-is-looking\n\t\t */\n\t\tpresenceSentence() {\n\t\t\tconst others = this.followRow?.presence === true && Array.isArray(this.presentOthers) ? this.presentOthers : []\n\t\t\tif (others.length === 0) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst first = String(others[0]?.user || '')\n\t\t\tconst name = userDisplayName(first) || first\n\t\t\tif (others.length === 1) {\n\t\t\t\treturn t('nextcloud-vue', '{name} is also looking at this record now', { name })\n\t\t\t}\n\t\t\treturn n('nextcloud-vue', '{name} and %n other are also looking at this record now', '{name} and %n others are also looking at this record now', others.length - 1, { name })\n\t\t},\n\n\t\t/**\n\t\t * The props of the Follow control, wherever the header draws it.\n\t\t *\n\t\t * @return {object} The CnFollowToggle props.\n\t\t * @spec openspec/changes/board-follow-row/specs/board-follow-row/spec.md#requirement-the-board-follow-control-sits-under-the-breadcrumb\n\t\t */\n\t\tfollowToggleProps() {\n\t\t\tconst self = this.resolvedSelf\n\t\t\treturn {\n\t\t\t\tregister: this.register,\n\t\t\t\tschema: this.schema,\n\t\t\t\tobjectId: String(this.objectId),\n\t\t\t\twatching: self.watching === true,\n\t\t\t\tnotify: typeof self.watchNotify === 'boolean' ? self.watchNotify : null,\n\t\t\t\twatcherCount: typeof self.watcherCount === 'number' && this.followRow?.count !== false ? self.watcherCount : null,\n\t\t\t\tcanManage: !!(self.can && self.can.manage === true),\n\t\t\t\tnotifies: this.followNotifies,\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The `_extend[]` values of the object read: the page's own, plus\n\t\t * `@self.can` while the Follow toggle can render (OpenRegister returns\n\t\t * the rights only on request, and the picker needs `manage`).\n\t\t *\n\t\t * @return {string[]}\n\t\t * @spec openspec/changes/record-favourite-and-follow/tasks.md#task-3\n\t\t */\n\t\treadExtend() {\n\t\t\tconst out = [...this.extend]\n\t\t\tif (this.follow !== false && !out.includes('@self.can')) {\n\t\t\t\tout.push('@self.can')\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/**\n\t\t * The resolved record's human display name: the `titleField` value\n\t\t * when the page names one and it is filled, else the same fallback\n\t\t * chain the relation resolver uses: `@self.name` → `name` / `title` /\n\t\t * `displayName` → `firstName lastName`. Empty string while the object is still loading or when no\n\t\t * name-like field exists (a raw id is never used as a name — ADR-062).\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tobjectDisplayName() {\n\t\t\tconst obj = this.resolvedObject\n\t\t\tif (!obj || typeof obj !== 'object') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tif (this.titleField) {\n\t\t\t\tconst named = stageOf(obj, this.titleField)\n\t\t\t\tif (named !== '') {\n\t\t\t\t\treturn named\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst self = obj['@self'] || {}\n\t\t\tconst id = this.objectId !== null && this.objectId !== undefined ? String(this.objectId) : ''\n\t\t\tconst candidates = [self.name, self.title, obj.name, obj.title, obj.displayName]\n\t\t\tfor (const c of candidates) {\n\t\t\t\tif (c !== null && c !== undefined && c !== '' && String(c) !== id) {\n\t\t\t\t\treturn String(c)\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst composed = ((obj.firstName || '') + ' ' + (obj.lastName || '')).trim()\n\t\t\treturn composed || ''\n\t\t},\n\n\t\t/**\n\t\t * The header `<h2>` text. Prefers the resolved record's display name\n\t\t * so the header names the record (\"ACME Corp\") rather than its type\n\t\t * (\"Case\"); falls back to the `title` prop (the type/label) while the\n\t\t * object is loading or has no name field.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tdisplayTitle() {\n\t\t\treturn this.objectDisplayName || this.resolvedTitle\n\t\t},\n\n\t\t/**\n\t\t * The declared crumbs: `config.breadcrumb` as one crumb or a list,\n\t\t * without the crumbs that have no label.\n\t\t *\n\t\t * @spec openspec/changes/detail-breadcrumb-trail/specs/detail-page/spec.md#requirement-a-record-page-breadcrumb-can-name-more-than-one-level\n\t\t * @return {Array<object>}\n\t\t */\n\t\tdeclaredCrumbs() {\n\t\t\tconst list = Array.isArray(this.breadcrumb) ? this.breadcrumb : (this.breadcrumb ? [this.breadcrumb] : [])\n\t\t\treturn list.filter((crumb) => crumb && typeof crumb.label === 'string' && crumb.label !== '')\n\t\t},\n\n\t\t/**\n\t\t * The breadcrumb trail: the declared crumbs (each a router target from\n\t\t * `route` + `params`, or an `href`), then the record's display name\n\t\t * as the current crumb. Empty without a declared crumb. A crumb\n\t\t * carries `icon` only when the manifest declares one.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-detail-page-drops-its-eyebrow-and-shows-a-breadcrumb\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-declared-breadcrumb-label-shows-as-text\n\t\t * @spec openspec/changes/detail-breadcrumb-trail/specs/detail-page/spec.md#requirement-a-record-page-breadcrumb-can-name-more-than-one-level\n\t\t * @return {Array<{label: string, to?: object, href?: string, icon?: string}>}\n\t\t */\n\t\tbreadcrumbCrumbs() {\n\t\t\tif (this.declaredCrumbs.length === 0) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\tconst crumbs = this.declaredCrumbs.map((crumb) => {\n\t\t\t\tconst entry = { label: this.effectiveTranslate(crumb.label) }\n\t\t\t\tif (typeof crumb.icon === 'string' && crumb.icon !== '') {\n\t\t\t\t\tentry.icon = crumb.icon\n\t\t\t\t}\n\t\t\t\tif (crumb.route) {\n\t\t\t\t\tentry.to = { name: crumb.route, ...(crumb.params ? { params: crumb.params } : {}), ...(crumb.query ? { query: crumb.query } : {}) }\n\t\t\t\t} else if (crumb.href) {\n\t\t\t\t\tentry.href = crumb.href\n\t\t\t\t}\n\t\t\t\treturn entry\n\t\t\t})\n\t\t\treturn [...crumbs, { label: this.breadcrumbCurrentLabel }]\n\t\t},\n\n\t\t/**\n\t\t * The current crumb: the record's `breadcrumb.currentField` value\n\t\t * (the case number on a case page) when declared and filled, else the\n\t\t * display name.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-breadcrumb-can-name-the-record-by-a-field-and-use-a-text-separator\n\t\t * @return {string}\n\t\t */\n\t\tbreadcrumbCurrentLabel() {\n\t\t\tconst field = this.breadcrumbOption('currentField')\n\t\t\tif (field !== '') {\n\t\t\t\tconst value = stageOf(this.resolvedObject, field)\n\t\t\t\tif (value !== '') {\n\t\t\t\t\treturn value\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn this.displayTitle\n\t\t},\n\n\t\t/**\n\t\t * The text drawn between the crumbs (`breadcrumb.separator`), or ''\n\t\t * for the chevron.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-breadcrumb-can-name-the-record-by-a-field-and-use-a-text-separator\n\t\t * @return {string}\n\t\t */\n\t\tbreadcrumbSeparator() {\n\t\t\treturn this.breadcrumbOption('separator')\n\t\t},\n\n\t\t/**\n\t\t * Effective translate function: the injected `cnTranslate` (the host\n\t\t * app's bound `t()`), identity by default.\n\t\t *\n\t\t * @return {(key: string) => string}\n\t\t */\n\t\teffectiveTranslate() {\n\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate : (key) => key\n\t\t},\n\n\t\t/**\n\t\t * The `title` prop (the manifest-authored type label) run through the\n\t\t * host translate function. Used for the header fallback and the type\n\t\t * eyebrow; the record's own display name is data and stays untouched.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedTitle() {\n\t\t\treturn this.title ? this.effectiveTranslate(this.title) : this.title\n\t\t},\n\n\t\t/**\n\t\t * The page description run through the host translate function.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedDescription() {\n\t\t\treturn this.description ? this.effectiveTranslate(this.description) : this.description\n\t\t},\n\n\t\t/**\n\t\t * The statistics section heading run through the host translate function.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedStatsTitle() {\n\t\t\treturn this.statsTitle ? this.effectiveTranslate(this.statsTitle) : this.statsTitle\n\t\t},\n\n\t\t/**\n\t\t * The small type-label eyebrow shown above the record name. Only\n\t\t * rendered once a record name is resolved AND it differs from the\n\t\t * `title` prop, so the type context (\"Case\", \"Publication\") is kept\n\t\t * without shadowing the record name. Empty while loading (the type\n\t\t * label is then the `<h2>` itself).\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\ttypeEyebrow() {\n\t\t\tconst name = this.objectDisplayName\n\t\t\tif (!name || !this.title || name === this.title) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn this.resolvedTitle\n\t\t},\n\n\t\t/**\n\t\t * The fetched JSON Schema for the schema-driven mode. Read from\n\t\t * the store's `schemas[type]` cache populated by `fetchSchema`.\n\t\t * Required to render `CnObjectDataWidget` (which takes a schema\n\t\t * Object, not a slug).\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tcurrentSchema() {\n\t\t\tconst store = this.effectiveObjectStore\n\t\t\tif (!store) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst type = this.resolvedObjectType\n\t\t\tif (!type) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn store.schemas?.[type] ?? null\n\t\t},\n\n\t\t/**\n\t\t * True when consumer-supplied default slot content IS present.\n\t\t * Treats comment placeholders and whitespace-only text vnodes as\n\t\t * no content so a stray newline in the template doesn't\n\t\t * accidentally suppress the auto-body.\n\t\t *\n\t\t * `$slots.default` is a FUNCTION in Vue 3 — it has to be called to\n\t\t * get the vnodes. Reading `.length` off it yields the function's\n\t\t * arity (always 0 for a compiled slot), which made this read\n\t\t * `false` unconditionally and let the auto-body render on top of\n\t\t * whatever the consumer had put in the slot.\n\t\t */\n\t\thasDefaultSlotContent() {\n\t\t\treturn slotRenders(this.$slots.default)\n\t\t},\n\n\t\t/**\n\t\t * True when the auto-body (CnObjectDataWidget + CnObjectMetadataWidget)\n\t\t * should render. Conditions: schema-driven mount, the object has\n\t\t * loaded, no consumer slot wins, and the grid-layout mode is not\n\t\t * active (grid mode owns the body when present).\n\t\t */\n\t\tshouldRenderAutoBody() {\n\t\t\treturn this.hasSchemaDrivenFetch\n\t\t\t\t&& this.currentObject\n\t\t\t\t&& !this.hasDefaultSlotContent\n\t\t\t\t&& !this.hasGridLayout\n\t\t\t\t&& !this.hasViews\n\t\t},\n\n\t\t/**\n\t\t * The object context handed to integration widgets.\n\t\t *\n\t\t * An explicit `integrationContext` prop wins; otherwise it is derived the\n\t\t * way it always was, falling back through the resolved sidebar config and\n\t\t * `sidebarProps` before the page's own register/schema. That fallback\n\t\t * chain is why this is computed HERE and passed down rather than derived\n\t\t * inside CnDetailWidgetHost: `resolvedSidebar` and `sidebarProps` are page\n\t\t * state the host has no view of, and deriving from `register`/`schema`\n\t\t * alone hands the widget two empty strings.\n\t\t *\n\t\t * @return {object} `{ register, schema, objectId }`.\n\t\t */\n\t\teffectiveIntegrationContext() {\n\t\t\tif (this.integrationContext) {\n\t\t\t\treturn this.integrationContext\n\t\t\t}\n\t\t\tconst resolved = this.resolvedSidebar || {}\n\t\t\treturn {\n\t\t\t\tregister: resolved.register || this.sidebarProps?.register || this.register || '',\n\t\t\t\tschema: resolved.schema || this.schema || this.resolvedObjectType || this.sidebarProps?.schema || '',\n\t\t\t\tobjectId: this.objectId ? String(this.objectId) : '',\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Effective layout array driving the body grid. An explicit `layout` prop\n\t\t * (manifest grid page) wins; otherwise the materialized default auto-body\n\t\t * (Data + Related) is used when the schema-driven object has loaded.\n\t\t *\n\t\t * @return {Array} Layout items for CnDashboardGrid.\n\t\t */\n\t\tbodyGridLayout() {\n\t\t\t// Depend on the (always-reactive) edit flag so this re-evaluates\n\t\t\t// when in-app edit mode flips: the manifest arrays are raw until\n\t\t\t// useManifestEditor.enter() observes them in place, so a cache\n\t\t\t// built pre-edit would stay frozen for in-place pushes (Add widget\n\t\t\t// on a detail page / grid ejection). See CnDashboardPage.hasWidgets.\n\t\t\t// eslint-disable-next-line @typescript-eslint/no-unused-expressions -- reading the flag IS the effect: the read registers the reactive dependency, and there is nothing to assign it to\n\t\t\tthis.editingBody\n\t\t\tif (this.hasGridLayout) {\n\t\t\t\treturn this.closeDroppedRows(this.layout)\n\t\t\t}\n\t\t\tif (this.shouldRenderAutoBody) {\n\t\t\t\treturn this.closeDroppedRows(this.autoBodyLayout || [])\n\t\t\t}\n\t\t\treturn []\n\t\t},\n\n\t\t/**\n\t\t * Effective widget-definition array paired with `bodyGridLayout`. Used by\n\t\t * the overridden `findWidget` so both the prop-driven and the default\n\t\t * auto-body grids resolve their widget defs from one place.\n\t\t *\n\t\t * @return {Array} Widget definitions.\n\t\t */\n\t\tbodyGridWidgets() {\n\t\t\tif (this.hasGridLayout) {\n\t\t\t\treturn this.pickTabs(this.widgets)\n\t\t\t}\n\t\t\tif (this.shouldRenderAutoBody) {\n\t\t\t\treturn this.autoBodyWidgets || []\n\t\t\t}\n\t\t\treturn []\n\t\t},\n\n\t\t/**\n\t\t * The tab ids `tabsFrom` reads off this record, or null when it reads\n\t\t * no non-empty list (or the body is being edited).\n\t\t *\n\t\t * @spec openspec/changes/detail-tabs-from/specs/detail-page/spec.md#requirement-a-record-can-pick-its-tabs-and-their-order\n\t\t * @return {Array<string>|null}\n\t\t */\n\t\tpickedTabIds() {\n\t\t\tif (!this.tabsFrom || this.editingBody || !this.widgetVisibilityData) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst ids = readVisibleWhenPath(this.widgetVisibilityData, this.tabsFrom)\n\t\t\treturn Array.isArray(ids) && ids.length > 0 ? ids.map((id) => String(id)) : null\n\t\t},\n\n\t\t/**\n\t\t * Whether the body renders as an adjustable widget grid (vs. the\n\t\t * hand-authored default slot). True for explicit grid pages and for the\n\t\t * schema-driven default Data + Related body.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\thasBodyGrid() {\n\t\t\treturn this.bodyGridLayout.length > 0\n\t\t},\n\n\t\t/**\n\t\t * The chosen view's layout as the grid draws it: items that draw\n\t\t * nothing leave and their rows close, as for the body grid.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-view-with-nothing-to-draw-says-so\n\t\t */\n\t\tviewDisplayLayout() {\n\t\t\t// eslint-disable-next-line @typescript-eslint/no-unused-expressions -- reading the flag IS the effect, as in bodyGridLayout\n\t\t\tthis.editingBody\n\t\t\treturn this.activeView ? this.closeDroppedRows(this.activeViewLayout) : []\n\t\t},\n\n\t\t/**\n\t\t * Whether the chosen view has nothing to draw.\n\t\t *\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-view-with-nothing-to-draw-says-so\n\t\t */\n\t\tviewIsEmpty() {\n\t\t\treturn this.hasViews && this.viewDisplayLayout.length === 0\n\t\t},\n\n\t\t/**\n\t\t * The grids the body draws: the body grid when it has items, then the\n\t\t * chosen view's grid in the region the view switch controls (left out\n\t\t * when the view is empty; the empty state takes the region then).\n\t\t *\n\t\t * @return {Array<{key: string, view: boolean, layout: Array<object>, editable: boolean, attrs: object}>}\n\t\t * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-page-declares-views-that-each-hold-a-widget-grid\n\t\t */\n\t\tbodyGridSections() {\n\t\t\tconst sections = this.hasBodyGrid ? [{ key: 'body', view: false, layout: this.bodyGridLayout, editable: this.editingBody, attrs: {} }] : []\n\t\t\tif (this.hasViews && !this.viewIsEmpty) {\n\t\t\t\tsections.push({\n\t\t\t\t\tkey: `view-${this.activeViewId}`,\n\t\t\t\t\tview: true,\n\t\t\t\t\tlayout: this.viewDisplayLayout,\n\t\t\t\t\t// The user arranges the view while Arrange view is on.\n\t\t\t\t\teditable: this.editingBody || this.arrangingView,\n\t\t\t\t\tattrs: {\n\t\t\t\t\t\tid: this.viewRegionId,\n\t\t\t\t\t\trole: 'region',\n\t\t\t\t\t\t'aria-label': this.activeView.label,\n\t\t\t\t\t\t'data-testid': 'cn-detail-page-view-region',\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t}\n\t\t\treturn sections\n\t\t},\n\n\t\t/**\n\t\t * Whether the sidebar is rendered externally (via objectSidebarState inject)\n\t\t * rather than inline. When external, CnDetailPage only manages state —\n\t\t * the parent App renders the actual NcAppSidebar.\n\t\t */\n\t\thasExternalSidebar() {\n\t\t\treturn !!this.objectSidebarState\n\t\t},\n\n\t\t/**\n\t\t * Normalised sidebar config object regardless of input shape.\n\t\t *\n\t\t * - Boolean `true` → `{ show: true, enabled: true }`\n\t\t * - Boolean `false` → `{ show: false, enabled: false }`\n\t\t * - Object → fields passed through; `show` and\n\t\t * `enabled` default to `true` when omitted.\n\t\t *\n\t\t * Centralising the normalisation here keeps `syncSidebarState`\n\t\t * and the deprecation/conflict-warning logic single-sourced.\n\t\t */\n\t\tresolvedSidebar() {\n\t\t\tconst cfg = this.sidebar\n\t\t\t// A non-empty manifest `sidebarTabs` prop is itself an opt-in:\n\t\t\t// a `type:\"detail\"` page that declares tabs clearly wants the\n\t\t\t// sidebar. The `sidebar` prop defaults to Boolean `false`, and\n\t\t\t// CnPageRenderer only forwards a `sidebar` config when the\n\t\t\t// manifest page sets one — so a page that declares ONLY\n\t\t\t// `config.sidebarTabs` (procest CaseDetail) would otherwise fall\n\t\t\t// into the Boolean-`false` branch below and never publish its\n\t\t\t// strip. When tabs are present we treat the sidebar as enabled\n\t\t\t// (the explicit Object form below still overrides show/enabled).\n\t\t\tconst hasTabs = Array.isArray(this.sidebarTabs) && this.sidebarTabs.length > 0\n\t\t\tif (typeof cfg === 'boolean') {\n\t\t\t\tif (cfg) {\n\t\t\t\t\treturn { show: true, enabled: true }\n\t\t\t\t}\n\t\t\t\treturn hasTabs ? { show: true, enabled: true } : { show: false, enabled: false }\n\t\t\t}\n\t\t\tif (cfg && typeof cfg === 'object') {\n\t\t\t\treturn {\n\t\t\t\t\tshow: cfg.show !== false,\n\t\t\t\t\tenabled: cfg.enabled !== false,\n\t\t\t\t\t...cfg,\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn hasTabs ? { show: true, enabled: true } : { show: false, enabled: false }\n\t\t},\n\n\t\t/**\n\t\t * True when the sidebar should be wired into the external\n\t\t * `objectSidebarState` channel. Both `show` and `enabled`\n\t\t * must be non-`false` (defaults are `true`); either can\n\t\t * suppress the sidebar declaratively.\n\t\t */\n\t\tsidebarActive() {\n\t\t\tconst r = this.resolvedSidebar\n\t\t\t// An explicit `show: false` always suppresses the sidebar. Otherwise a\n\t\t\t// non-empty `sidebarTabs` opt-in activates it even past the prop's\n\t\t\t// default `{ enabled: false }` — the procest CaseDetail manifest\n\t\t\t// pattern, where a page declares only `config.sidebarTabs`.\n\t\t\tif (r.show === false) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif (r.enabled === false) {\n\t\t\t\treturn Array.isArray(this.sidebarTabs) && this.sidebarTabs.length > 0\n\t\t\t}\n\t\t\treturn true\n\t\t},\n\n\t\thasStats() {\n\t\t\treturn this.statsColumns.length > 0 && (this.statsRows.length > 0 || !!this.$slots['stats-rows'])\n\t\t},\n\n\t\t/** Whether any declarative in-body sections are configured. */\n\t\thasBodyWidgets() {\n\t\t\treturn Array.isArray(this.bodyWidgets) && this.bodyWidgets.length > 0\n\t\t},\n\n\t\t/**\n\t\t * Sections that land at the END placement: those with no `placement`\n\t\t * (the default) or an explicit `placement: \"end\"`. The three named\n\t\t * placement mounts (before-body / after-data / after-related) filter by\n\t\t * exact value; this catches the rest so nothing is silently dropped.\n\t\t *\n\t\t * @return {Array}\n\t\t */\n\t\tendPlacementSections() {\n\t\t\tif (!this.hasBodyWidgets) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\tconst named = ['before-body', 'after-data', 'after-related']\n\t\t\treturn this.bodyWidgets.filter((s) => !s || !s.placement || !named.includes(s.placement))\n\t\t},\n\n\t\t/**\n\t\t * Page/object context forwarded to CnBodySections for token resolution\n\t\t * (`@objectId` / `@object.<field>` / `@workspace.<key>`) AND provided on\n\t\t * `cnSectionContext` for host section components that inject. Mirrors the\n\t\t * `cnObjectContext` ref the abstract list/stat widgets already read.\n\t\t * `setObject(object)` shows an object a section saved without a re-fetch.\n\t\t *\n\t\t * @return {{objectId: (string|null), object: (object|null), register: string, schema: string, setObject: function(object): void}}\n\t\t */\n\t\tsectionContext() {\n\t\t\tconst resolved = this.resolvedSidebar || {}\n\t\t\treturn {\n\t\t\t\tobjectId: (this.objectId !== undefined && this.objectId !== null) ? String(this.objectId) : null,\n\t\t\t\tobject: this.resolvedObject || null,\n\t\t\t\tregister: resolved.register || this.register || this.sidebarProps?.register || '',\n\t\t\t\tschema: resolved.schema || this.schema || this.resolvedObjectType || this.sidebarProps?.schema || '',\n\t\t\t\tconfig: this.cnAppConfigRef,\n\t\t\t\tsetObject: (object) => this.setCurrentObject(object),\n\t\t\t}\n\t\t},\n\t},\n\n\twatch: {\n\t\theaderMetaCountKey: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.loadHeaderMetaCounts()\n\t\t\t},\n\t\t},\n\n\t\tlastChangeKey: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.loadLastChange()\n\t\t\t},\n\t\t},\n\n\t\t// The records a condition names are read again for another record.\n\t\tvisibilityRefsKey: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.refreshVisibilityRows()\n\t\t\t},\n\t\t},\n\n\t\t// Endpoint and source conditions on grid widgets are asked once per record.\n\t\tremoteWidgetConditionsKey: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.refreshWidgetConditions()\n\t\t\t},\n\t\t},\n\n\t\t// A pill with `resolve` looks its record up again for another value.\n\t\tpillResolveKey: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.refreshPillRefLabels()\n\t\t\t},\n\t\t},\n\n\t\t// The header's count pills count again for another record or other lists.\n\t\tpillCountKey: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.refreshPillCounts()\n\t\t\t},\n\t\t},\n\n\t\t// Opening a record marks it read, once, after its data has rendered.\n\t\treadStateDue: {\n\t\t\timmediate: true,\n\t\t\thandler(due) {\n\t\t\t\tif (due) {\n\t\t\t\t\tthis.sendReadState()\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\n\t\t// A load just settled (true → false) — remember it so later loads\n\t\t// refresh in place (full-page spinner only on the first load). Not\n\t\t// `immediate`: `loading` starts false before the first fetch begins,\n\t\t// so reacting to that initial false would suppress the first spinner.\n\t\tloading(val) {\n\t\t\tif (!val) {\n\t\t\t\tthis.hasLoadedOnce = true\n\t\t\t\t// Refresh settled — re-assert the sidebar state that\n\t\t\t\t// syncSidebarState skipped while loading was in flight.\n\t\t\t\tthis.syncSidebarState()\n\t\t\t}\n\t\t},\n\n\t\tawaitingObject() {\n\t\t\tthis.syncSidebarState()\n\t\t},\n\n\t\tobjectNotFound() {\n\t\t\tthis.syncSidebarState()\n\t\t},\n\n\t\tcontentWithheld() {\n\t\t\tthis.syncSidebarState()\n\t\t},\n\n\t\t/** Another record asks its own purpose: the dialog opens again for it. */\n\t\taccessPurposeKey() {\n\t\t\tthis.accessPurposeAsking = true\n\t\t\tthis.accessPurposeError = ''\n\t\t},\n\n\t\t// Deep, like `sidebarTabs` / `sidebarProps` below: the manifest editors\n\t\t// mutate `page.config.sidebar` IN PLACE, so the reference CnPageRenderer\n\t\t// forwards never changes. A shallow watcher missed every such edit —\n\t\t// switching the sidebar off in CnEditSidebarModal did nothing until the\n\t\t// page was reloaded. assignSidebarState() only writes keys that actually\n\t\t// changed, so the extra calls cost a comparison.\n\t\tsidebar: {\n\t\t\timmediate: true,\n\t\t\tdeep: true,\n\t\t\thandler() {\n\t\t\t\tthis.syncSidebarState()\n\t\t\t},\n\t\t},\n\n\t\t// Keep the provided object context current so detail-page abstract\n\t\t// widgets (CnObjectListWidget / CnStatWidget) can resolve `@objectId` /\n\t\t// `@object.<field>` tokens against the live object.\n\t\tcurrentObject: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.syncObjectContext()\n\t\t\t\t// Re-publish the object into objectSidebarState so the\n\t\t\t\t// hoisted sidebar's data/metadata tab widgets stay current.\n\t\t\t\tthis.syncSidebarState()\n\t\t\t},\n\t\t},\n\n\t\t// Re-publish once the schema Object resolves (fetched async), so the\n\t\t// hoisted sidebar's `data` tab widget gets its `schema` prop.\n\t\tcurrentSchema(schema) {\n\t\t\tthis.syncSidebarState()\n\t\t\treportBindingProblems(schema, { register: this.register, includeFields: this.includeFields })\n\n\t\t\t// Put the Data widget back if the body materialized before the\n\t\t\t// schema arrived. See materializeAutoBody(): fetchObject and\n\t\t\t// fetchSchema race, `shouldRenderAutoBody` gates on the OBJECT\n\t\t\t// only, and the body materializes once — so losing that race meant\n\t\t\t// the page rendered its title and Related panel with every scalar\n\t\t\t// field permanently missing (shillinq#1073).\n\t\t\t//\n\t\t\t// Guarded by `dataWidgetDeferred` rather than by \"is the data\n\t\t\t// widget absent\", so a body the user has edited is never rewritten\n\t\t\t// underneath them. The flag is cleared by the re-materialize, so\n\t\t\t// this runs at most once per materialized body.\n\t\t\tif (schema && this.dataWidgetDeferred) {\n\t\t\t\tthis.materializeAutoBody()\n\t\t\t}\n\t\t},\n\n\t\t// Materialize the default body grid (Data + Related) the first time the\n\t\t// schema-driven object resolves so the detail body is an adjustable grid.\n\t\tshouldRenderAutoBody: {\n\t\t\timmediate: true,\n\t\t\thandler(active) {\n\t\t\t\tif (active && !this.autoBodyLayout) {\n\t\t\t\t\tthis.materializeAutoBody()\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\n\t\t// A different schema means a fresh default grid — drop the materialized\n\t\t// one so it rebuilds for the new object type.\n\t\tresolvedObjectType() {\n\t\t\tthis.autoBodyLayout = null\n\t\t\tthis.autoBodyWidgets = null\n\t\t\t// Cleared with the body it described. materializeAutoBody() sets it\n\t\t\t// afresh, so this only avoids carrying a stale \"deferred\" across a\n\t\t\t// type change.\n\t\t\tthis.dataWidgetDeferred = false\n\t\t},\n\n\t\ttitle() {\n\t\t\tthis.syncSidebarState()\n\t\t},\n\n\t\tsubtitle() {\n\t\t\tthis.syncSidebarState()\n\t\t},\n\n\t\tobjectType() {\n\t\t\tthis.syncSidebarState()\n\t\t},\n\n\t\t// Schema-driven props feed both the sidebar state (via\n\t\t// resolvedObjectType) and the auto-fetch path. Re-sync + re-fetch\n\t\t// whenever any of the three move so the page stays consistent if\n\t\t// a parent component swaps the active object.\n\t\tregister() {\n\t\t\tthis.syncSidebarState()\n\t\t\tthis.pushAiContext()\n\t\t\tthis.fetchObjectIfNeeded()\n\t\t},\n\n\t\tschema() {\n\t\t\tthis.syncSidebarState()\n\t\t\tthis.pushAiContext()\n\t\t\tthis.fetchObjectIfNeeded()\n\t\t},\n\n\t\tobjectId() {\n\t\t\tthis.syncSidebarState()\n\t\t\tthis.pushAiContext()\n\t\t\tthis.fetchObjectIfNeeded()\n\t\t\tthis.syncObjectContext()\n\t\t},\n\n\t\tsidebarTabs: {\n\t\t\tdeep: true,\n\t\t\thandler() {\n\t\t\t\tthis.syncSidebarState()\n\t\t\t},\n\t\t},\n\n\t\tsidebarProps: {\n\t\t\tdeep: true,\n\t\t\thandler() {\n\t\t\t\tthis.syncSidebarState()\n\t\t\t},\n\t\t},\n\t},\n\n\tcreated() {\n\t\tthis.pushAiContext()\n\t\t// Seeded before the first render, so a widget's first request already\n\t\t// carries the page filter's value.\n\t\tthis.initPageFilters()\n\t\tthis.loadPageFilterOptions()\n\t},\n\n\tmounted() {\n\t\t// Kick the schema-driven fetch once the component has mounted —\n\t\t// `effectiveObjectStore` relies on a live Pinia context, which\n\t\t// is guaranteed by mounted() but not by created().\n\t\tthis.fetchObjectIfNeeded()\n\t\tthis.scheduleCellOverflowAudit()\n\n\t\t// `cn:page:refresh` is the channel a successful write announces itself\n\t\t// on — the manifest `refresh` and `api-call` actions bump it, and so do\n\t\t// app dialogs after a save. Until now nothing on a schema-driven detail\n\t\t// page listened: `useEndpointSource` and CnChartWidget answered it, but\n\t\t// every widget reading the page's OBJECT (`type: \"data\"` above all)\n\t\t// reads the copy this component fetched once, so a page kept showing\n\t\t// values the backend had already changed. Measured on a dossiq draft:\n\t\t// publish returned `{\"published\":true,\"version\":1}`, a re-read showed\n\t\t// `isDraft: false`, and the page still said draft.\n\t\t//\n\t\t// The subscription lives HERE rather than in each widget on purpose.\n\t\t// One page carries several object-bound widgets; if each answered the\n\t\t// channel itself, one write would become one read per widget. This\n\t\t// component owns the object, so one write is one read however many\n\t\t// widgets are bound to it.\n\t\tthis._onPageRefresh = (payload) => this.onPageRefreshBus(payload)\n\t\tsubscribe(PAGE_REFRESH_CHANNEL, this._onPageRefresh)\n\t},\n\n\tupdated() {\n\t\tthis.scheduleCellOverflowAudit()\n\t},\n\n\tbeforeUnmount() {\n\t\tif (this._onPageRefresh) {\n\t\t\tunsubscribe(PAGE_REFRESH_CHANNEL, this._onPageRefresh)\n\t\t\tthis._onPageRefresh = null\n\t\t}\n\t\tif (this.hasExternalSidebar) {\n\t\t\tthis.objectSidebarState.active = false\n\t\t\t// Clear manifest-driven tabs so the next mount starts fresh\n\t\t\t// rather than inheriting the previous page's tab strip.\n\t\t\tthis.objectSidebarState.tabs = undefined\n\t\t}\n\t\t// Reset AI context fields so stale detail context doesn't leak\n\t\tif (this.cnAiContext) {\n\t\t\tthis.cnAiContext.pageKind = 'custom'\n\t\t\tthis.cnAiContext.objectUuid = undefined\n\t\t\tthis.cnAiContext.registerSlug = undefined\n\t\t\tthis.cnAiContext.schemaSlug = undefined\n\t\t}\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Read the record's newest create or update audit entry into\n\t\t * `lastChange`; a failed read leaves it null.\n\t\t *\n\t\t * @spec openspec/changes/header-meta-last-change/specs/detail-page/spec.md#requirement-the-header-meta-line-can-name-who-changed-the-record-last\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadLastChange() {\n\t\t\tconst key = this.lastChangeKey\n\t\t\tthis.lastChange = null\n\t\t\tif (key === '') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([\n\t\t\t\t\timport('@nextcloud/axios'),\n\t\t\t\t\timport('@nextcloud/router'),\n\t\t\t\t])\n\t\t\t\tconst url = generateUrl(\n\t\t\t\t\t'/apps/openregister/api/objects/{register}/{schema}/{id}/audit-trails',\n\t\t\t\t\t{ register: this.register, schema: this.schema, id: this.objectId },\n\t\t\t\t)\n\t\t\t\tconst res = await axios.get(url, { params: { _limit: 1, sort: 'created', order: 'DESC', action: 'create,update' } })\n\t\t\t\tconst rows = (res && res.data && Array.isArray(res.data.results)) ? res.data.results : []\n\t\t\t\tif (key === this.lastChangeKey) {\n\t\t\t\t\tthis.lastChange = rows[0] || null\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// No trail to read: the meta line drops the tokens.\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * A key of the declared trail: the last crumb that sets it wins.\n\t\t *\n\t\t * @spec openspec/changes/detail-breadcrumb-trail/specs/detail-page/spec.md#requirement-a-record-page-breadcrumb-can-name-more-than-one-level\n\t\t * @param {string} key `currentField` or `separator`.\n\t\t * @return {string} Its value, or ''.\n\t\t */\n\t\tbreadcrumbOption(key) {\n\t\t\tconst found = [...this.declaredCrumbs].reverse().find((crumb) => typeof crumb[key] === 'string' && crumb[key] !== '')\n\t\t\treturn found ? found[key] : ''\n\t\t},\n\n\t\t/**\n\t\t * A body widget starts or stops showing its own cards in the side\n\t\t * column. While any does, the generic side cards stand aside.\n\t\t *\n\t\t * @param {string|number} owner Who claims, unique per widget.\n\t\t * @param {boolean} on Whether it shows its cards now.\n\t\t * @return {void}\n\t\t */\n\t\tclaimSideColumn(owner, on) {\n\t\t\tconst rest = this.sideClaims.filter((id) => id !== owner)\n\t\t\tthis.sideClaims = on ? [...rest, owner] : rest\n\t\t},\n\n\t\t/**\n\t\t * A text of the access purpose gate through the app's translate, '' when unset.\n\t\t *\n\t\t * @param {string} key `title` or `description`.\n\t\t * @return {string}\n\t\t */\n\t\taccessPurposeText(key) {\n\t\t\tconst text = this.accessPurpose && this.accessPurpose[key]\n\t\t\treturn typeof text === 'string' && text !== '' ? this.cnTranslate(text) : ''\n\t\t},\n\n\t\t/**\n\t\t * Record the reader's purpose with the app's endpoint, then open the\n\t\t * record and remember it for this browser session. A refusal (or a\n\t\t * failure to record) keeps the record closed and says why.\n\t\t *\n\t\t * @spec openspec/changes/detail-access-purpose/specs/detail-access-purpose/spec.md#requirement-a-detail-page-can-ask-for-a-purpose-before-it-opens\n\t\t * @param {{purpose: string, note: string}} answer The chosen purpose.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync recordAccessPurpose(answer) {\n\t\t\tconst url = String(this.accessPurpose?.url || '').replace(/\\{objectId\\}/g, encodeURIComponent(String(this.objectId || '')))\n\t\t\tif (url === '') {\n\t\t\t\tthis.accessPurposeError = t('nextcloud-vue', 'This page cannot record a purpose, so the record stays closed.')\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.accessPurposeBusy = true\n\t\t\tthis.accessPurposeError = ''\n\t\t\ttry {\n\t\t\t\tconst { default: axios } = await import('@nextcloud/axios')\n\t\t\t\tawait axios.post(prefixUrl(url), { purpose: answer.purpose, note: answer.note })\n\t\t\t\tconst key = this.accessPurposeKey\n\t\t\t\tthis.accessPurposeGranted = [...new Set([...this.accessPurposeGranted, key])]\n\t\t\t\ttry {\n\t\t\t\t\twindow.sessionStorage.setItem(ACCESS_PURPOSE_SESSION_KEY, JSON.stringify(this.accessPurposeGranted))\n\t\t\t\t} catch {\n\t\t\t\t\t// Without session storage the purpose is asked again on the next visit.\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tconst data = error?.response?.data\n\t\t\t\tthis.accessPurposeError = (data && (data.message || data.error)) || t('nextcloud-vue', 'Your purpose could not be recorded, so the record stays closed.')\n\t\t\t} finally {\n\t\t\t\tthis.accessPurposeBusy = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The widget definitions with each `tabs` widget's tabs narrowed and\n\t\t * ordered by `pickedTabIds`; the definitions as given without a pick.\n\t\t * A new definition is returned for a changed widget; the declared one\n\t\t * is never mutated.\n\t\t *\n\t\t * @spec openspec/changes/detail-tabs-from/specs/detail-page/spec.md#requirement-a-record-can-pick-its-tabs-and-their-order\n\t\t * @param {Array<object>} widgets The widget definitions.\n\t\t * @return {Array<object>}\n\t\t */\n\t\tpickTabs(widgets) {\n\t\t\tconst ids = this.pickedTabIds\n\t\t\tif (!ids || !Array.isArray(widgets)) {\n\t\t\t\treturn widgets\n\t\t\t}\n\t\t\tconst idOf = (tab) => String(typeof tab === 'string' ? tab : (tab && (tab.id ?? tab.widgetId)) ?? '')\n\t\t\treturn widgets.map((widget) => {\n\t\t\t\tif (!widget || widget.type !== 'tabs' || !Array.isArray(widget.content?.tabs)) {\n\t\t\t\t\treturn widget\n\t\t\t\t}\n\t\t\t\tconst tabs = ids\n\t\t\t\t\t.map((id) => widget.content.tabs.find((tab) => idOf(tab) === id))\n\t\t\t\t\t.filter(Boolean)\n\t\t\t\treturn tabs.length > 0 ? { ...widget, content: { ...widget.content, tabs } } : widget\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The tag after an admin-only entry in the board look (\"beheerder\",\n\t\t * DqZaakMeer), or null for any other entry or look. It is drawn by\n\t\t * detail-page.css from the attribute, since the menu renders its\n\t\t * entries' text as plain text.\n\t\t *\n\t\t * @param {{key: string}} group The menu group the entry is in.\n\t\t * @return {string|null} The tag, or null.\n\t\t */\n\t\tadminTagFor(group) {\n\t\t\treturn this.isBoard && group && group.key === 'cn-admin' ? t('nextcloud-vue', 'admin') : null\n\t\t},\n\n\t\t/**\n\t\t * Open the object sidebar through the shared channel.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\topenSidebar() {\n\t\t\tif (this.objectSidebarState) {\n\t\t\t\tthis.objectSidebarState.open = true\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Start or finish arranging the chosen view. Finishing stores the\n\t\t * arrangement of every view the user moved, once.\n\t\t *\n\t\t * @return {void}\n\t\t * @spec openspec/changes/page-view-user-layouts/specs/view-switch-containers/spec.md#requirement-a-user-arranges-each-view-for-themselves\n\t\t */\n\t\ttoggleArrangeView() {\n\t\t\tconst leaving = this.arrangingView\n\t\t\tthis.arrangingView = !this.arrangingView\n\t\t\tif (leaving) {\n\t\t\t\tthis.saveViewUserLayouts()\n\t\t\t}\n\t\t},\n\n\t\t// Expose the shared grid helpers to the template (grid mode + auto-body).\n\t\tcnGridCellStyle,\n\t\thasGridRow,\n\n\t\t/**\n\t\t * Emit a list step. When the control is a link it has navigated\n\t\t * already, so the payload `{ event, to }` tells the host not to push\n\t\t * again (`useListNavigation`'s `goNext`/`goPrevious` honour it).\n\t\t *\n\t\t * @param {string} name `previous-record` or `next-record`.\n\t\t * @param {object|string|null} to The link target, or null.\n\t\t * @param {MouseEvent} event The click event.\n\t\t */\n\t\tonListStep(name, to, event) {\n\t\t\tif (to && this.$router) {\n\t\t\t\tthis.$emit(name, { event, to })\n\t\t\t} else {\n\t\t\t\tthis.$emit(name)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Move focus to the primary action when the skip link is followed.\n\t\t *\n\t\t * The `href` anchor alone moves the DOCUMENT to the element without\n\t\t * moving FOCUS to it in every browser, which is the difference\n\t\t * between a skip link that works and one that scrolls and then hands\n\t\t * the next tab press back to the top of the page.\n\t\t *\n\t\t * @param {Event} event The click event.\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tonSkipToPrimaryAction(event) {\n\t\t\t// The ref covers the header button; the id also finds the button\n\t\t\t// when it sits inside the \"what now\" card.\n\t\t\tconst target = this.$refs.primaryActionButton?.$el\n\t\t\t\t?? this.$refs.primaryActionButton\n\t\t\t\t?? this.$el?.querySelector?.(`[id=\"${this.primaryActionAnchorId}\"]`)\n\t\t\tconst focusable = target?.matches?.('button, a') ? target : target?.querySelector?.('button, a')\n\t\t\tif (focusable?.focus) {\n\t\t\t\tevent.preventDefault()\n\t\t\t\tfocusable.focus()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The primary button was pressed.\n\t\t *\n\t\t * A stage's action is dispatched the way a header action is, through\n\t\t * the `run()` CnActionButtons bound for it, and announced to the host\n\t\t * with its declaration. A link action is left to the browser and\n\t\t * announces nothing. Without a stage action this is the page's\n\t\t * `primaryAction`, announced as before, and when it is an in-app link\n\t\t * a plain click routes there.\n\t\t *\n\t\t * @param {MouseEvent} [event] The click.\n\t\t * @return {void}\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-primary-action-follows-the-stage\n\t\t * @spec openspec/changes/header-action-href/specs/header-action-href/spec.md#requirement-a-header-action-with-href-is-a-link-that-runs-and-emits-nothing\n\t\t */\n\t\tonPrimaryActionClick(event) {\n\t\t\tconst entry = this.stagePrimaryEntry\n\t\t\tif (!entry) {\n\t\t\t\tthis.$emit('primary-action', this.primaryAction)\n\t\t\t\tif (this.primaryActionLink?.to) {\n\t\t\t\t\tfollowLinkClick(event, this.primaryActionLink.to, this.$router)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (entry.href) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.$emit('primary-action', this.headerActionsById[entry.id] || { id: entry.id, label: entry.label })\n\t\t\tthis.onPinnedActionClick(entry)\n\t\t},\n\n\t\t/**\n\t\t * A pinned action's button was pressed: run it, unless it is a link\n\t\t * the browser is already following.\n\t\t *\n\t\t * @param {object} entry The action's menu entry.\n\t\t * @return {void}\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-quick-actions\n\t\t */\n\t\tonPinnedActionClick(entry) {\n\t\t\tif (!entry || entry.href || typeof entry.run !== 'function') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tentry.run()\n\t\t},\n\n\t\t/**\n\t\t * The active tab changed: tell the host, and put it in the address\n\t\t * when the page declares that its tab is part of its address.\n\t\t *\n\t\t * `replace`, never `push`, for the FIRST tab of a visit: an address\n\t\t * that arrived without a tab is being corrected to its canonical form\n\t\t * (ADR-052), and a correction must not become a history entry the\n\t\t * back button walks into. Later changes push, so back and forward\n\t\t * walk the tabs the reader actually visited.\n\t\t *\n\t\t * @param {string} tabId The tab now active.\n\t\t * @param {boolean} [canonicalising] Whether this is the arrival correction rather than a reader's choice.\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tonTabChange(tabId, canonicalising = false) {\n\t\t\tif (typeof tabId !== 'string' || tabId === '') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\t/**\n\t\t\t * @event tab-change Emitted when the active tab changes.\n\t\t\t * @type {string}\n\t\t\t */\n\t\t\tthis.$emit('tab-change', tabId)\n\t\t\tif (!this.tabInAddress || !this.$router || !this.$route) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.$route.query?._tab === tabId) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst to = { name: this.$route.name, params: { ...this.$route.params }, query: { ...this.$route.query, _tab: tabId } }\n\t\t\tconst navigate = canonicalising ? this.$router.replace : this.$router.push\n\t\t\tnavigate.call(this.$router, to).catch(() => {})\n\t\t},\n\n\t\t/**\n\t\t * The tab the address asks for, or null.\n\t\t *\n\t\t * @return {string|null} The tab id.\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\trequestedTabFromAddress() {\n\t\t\tconst tab = this.$route?.query?._tab\n\t\t\treturn typeof tab === 'string' && tab !== '' ? tab : null\n\t\t},\n\n\t\t/**\n\t\t * The sidebar settled on a tab: put it in the address.\n\t\t *\n\t\t * The sidebar reports the tab it actually shows, not the one that was\n\t\t * asked for, so an address naming a tab this reader may not see is\n\t\t * corrected to the one they can. `canonicalising` is true for that\n\t\t * correction and for the tabless arrival, so neither becomes a\n\t\t * history entry the back button walks into (ADR-052).\n\t\t *\n\t\t * @param {string} tabId The tab now showing.\n\t\t * @param {boolean} [canonicalising] Whether this is a correction rather than a reader's choice.\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tonSidebarTabChange(tabId, canonicalising = false) {\n\t\t\tthis.onTabChange(tabId, canonicalising || this.requestedTabFromAddress() === null)\n\t\t},\n\n\t\t/**\n\t\t * Release the lock the current user holds on this record.\n\t\t *\n\t\t * Only reachable from the locked card's Unlock button, which the card\n\t\t * only renders for the viewer's OWN lock — so this never releases\n\t\t * somebody else's. A failure is left to `useObjectLock`, which logs it;\n\t\t * the flag is cleared either way so a failed release does not strand\n\t\t * the button disabled.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync onReleaseLock() {\n\t\t\tif (!this.lockState || this.releasingLock) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.releasingLock = true\n\t\t\ttry {\n\t\t\t\tawait this.lockState.release()\n\t\t\t} finally {\n\t\t\t\tthis.releasingLock = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Re-emit the page-header menu's Refresh to the host.\n\t\t *\n\t\t * @param {{ widgetId: string, title: string }} payload - Action payload.\n\t\t * @param {object} event - Synthetic event (host may preventDefault).\n\t\t */\n\t\tasync onHeaderRefresh(payload, event) {\n\t\t\t/**\n\t\t\t * @event refresh The page-header Refresh action was clicked.\n\t\t\t * @type {{ widgetId: string, title: string }}\n\t\t\t */\n\t\t\tthis.$emit('refresh', payload, event)\n\t\t\t// Schema-driven (manifest) detail pages self-fetch — without this\n\t\t\t// the Refresh action has no host listener to act on and does\n\t\t\t// nothing. Re-fetch the object + schema and spin the action.\n\t\t\tif (!this.selfFetchesRecord) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.internalRefreshing = true\n\t\t\tconst done = this.fetchObjectIfNeeded()\n\t\t\tevent?.waitUntil?.(done)\n\t\t\ttry {\n\t\t\t\tawait done\n\t\t\t} finally {\n\t\t\t\tthis.internalRefreshing = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Look up the records the `resolve` pills point at, through the same\n\t\t * reference-label resolver the index tables use. A failed lookup keeps\n\t\t * the raw value.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/detail-pill-resolve/specs/detail-page/spec.md#requirement-a-header-pill-can-show-a-referenced-record-s-label\n\t\t */\n\t\tasync refreshPillRefLabels() {\n\t\t\tconst targets = this.pillResolveTargets\n\t\t\tconst seq = ++this.pillRefSeq\n\t\t\tif (targets.length === 0) {\n\t\t\t\tthis.pillRefLabels = {}\n\t\t\t\tthis.pillRefLooks = {}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!this._pillRefResolver) {\n\t\t\t\tthis._pillRefResolver = createRefLabelResolver(() => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\treturn this.objectStore || useObjectStore()\n\t\t\t\t\t} catch {\n\t\t\t\t\t\treturn null\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t\tconst labels = {}\n\t\t\tconst looks = {}\n\t\t\tconst field = async (register, schema, id, name) => {\n\t\t\t\tif (!name) {\n\t\t\t\t\treturn null\n\t\t\t\t}\n\t\t\t\tconst found = await this._pillRefResolver.resolve(register, schema, [id], name, { strict: true }).catch(() => ({}))\n\t\t\t\treturn found && typeof found[id] === 'string' ? found[id] : null\n\t\t\t}\n\t\t\tawait Promise.all(targets.map(async ([key, register, schema, labelField, id, colorField, iconField]) => {\n\t\t\t\tconst [found, color, icon] = await Promise.all([\n\t\t\t\t\tthis._pillRefResolver.resolve(register, schema, [id], labelField).catch(() => ({})),\n\t\t\t\t\tfield(register, schema, id, colorField),\n\t\t\t\t\tfield(register, schema, id, iconField),\n\t\t\t\t])\n\t\t\t\tif (found && typeof found[id] === 'string') {\n\t\t\t\t\tlabels[key] = found[id]\n\t\t\t\t}\n\t\t\t\tif (color || icon) {\n\t\t\t\t\tlooks[key] = { color, icon }\n\t\t\t\t}\n\t\t\t}))\n\t\t\tif (seq === this.pillRefSeq) {\n\t\t\t\tthis.pillRefLabels = labels\n\t\t\t\tthis.pillRefLooks = looks\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Read the totals `headerMetaCounts` names: a register list through\n\t\t * one list request, a named source by loading it and counting its\n\t\t * rows. A failed count is null and leaves the line out.\n\t\t *\n\t\t * @spec openspec/changes/detail-header-meta-counts/specs/detail-page/spec.md#requirement-a-record-page-header-line-can-count-a-list\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadHeaderMetaCounts() {\n\t\t\tconst key = this.headerMetaCountKey\n\t\t\tif (key === '') {\n\t\t\t\tthis.headerMetaCountValues = {}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst ctx = { objectId: this.objectId, object: this.resolvedObject || undefined }\n\t\t\tconst values = {}\n\t\t\tawait Promise.all(JSON.parse(key)[0].map(async ([name, from]) => {\n\t\t\t\ttry {\n\t\t\t\t\tif (from.entitySource) {\n\t\t\t\t\t\tconst source = resolveIndexSource(from.entitySource)\n\t\t\t\t\t\tif (!source) {\n\t\t\t\t\t\t\tvalues[name] = null\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t\tawait source.load()\n\t\t\t\t\t\tvalues[name] = source.rows().length\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tvalues[name] = await fetchListTotal(from.register, from.schema, from.filter || {}, ctx)\n\t\t\t\t} catch {\n\t\t\t\t\tvalues[name] = null\n\t\t\t\t}\n\t\t\t}))\n\t\t\tif (key === this.headerMetaCountKey) {\n\t\t\t\tthis.headerMetaCountValues = values\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Count the lists behind the header's count pills (`countFrom`): one\n\t\t * list request each (`_limit=1`, the `total`). A failed count, or a\n\t\t * record without an id yet, shows no pill.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-a-header-pill-can-be-a-constant-or-a-count\n\t\t */\n\t\tasync refreshPillCounts() {\n\t\t\tconst pills = [['type', this.typePill], ['status', this.statusPill]]\n\t\t\t\t.map(([key, config]) => [key, pillCountSource(config)])\n\t\t\t\t.filter(([, source]) => source)\n\t\t\tconst seq = ++this.pillCountSeq\n\t\t\tif (pills.length === 0 || this.objectId === null || this.objectId === undefined || this.objectId === '') {\n\t\t\t\tthis.pillCounts = {}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst ctx = { objectId: this.objectId, object: this.resolvedObject || undefined }\n\t\t\tconst counts = {}\n\t\t\tawait Promise.all(pills.map(async ([key, source]) => {\n\t\t\t\tcounts[key] = await fetchListTotal(source.register, source.schema, source.filter, ctx).catch(() => null)\n\t\t\t}))\n\t\t\tif (seq === this.pillCountSeq) {\n\t\t\t\tthis.pillCounts = counts\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * A `cn:page:refresh` reached the page — re-read the object so every\n\t\t * object-bound widget re-renders from what the backend now holds.\n\t\t *\n\t\t * Deliberately a no-op while `internalRefreshing` is set. The header's\n\t\t * own Refresh item emits `@refresh` FIRST and only then emits on this\n\t\t * channel (see CnActionsMenu.onRefreshClick), and `onHeaderRefresh`\n\t\t * sets the flag synchronously before awaiting its fetch — so without\n\t\t * this guard one click on Refresh would fetch the object twice.\n\t\t *\n\t\t * @param {{ waitUntil?: (promise: Promise<unknown>) => void }} [payload] Bus payload.\n\t\t * @return {void}\n\t\t */\n\t\tonPageRefreshBus(payload) {\n\t\t\tthis.refreshPillCounts()\n\t\t\tif (!this.selfFetchesRecord) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.internalRefreshing) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.internalRefreshing = true\n\t\t\tconst done = Promise.resolve(this.fetchObjectIfNeeded()).finally(() => {\n\t\t\t\tthis.internalRefreshing = false\n\t\t\t})\n\t\t\tpayload?.waitUntil?.(done)\n\t\t},\n\n\t\t/**\n\t\t * Re-emit the page-header menu's Request-a-feature to the host.\n\t\t *\n\t\t * @param {{ widgetId: string, title: string }} payload - Action payload.\n\t\t * @param {object} event - Synthetic event (host may preventDefault).\n\t\t */\n\t\tonHeaderRequestFeature(payload, event) {\n\t\t\t/**\n\t\t\t * @event request-feature The page-header Request-a-feature action\n\t\t\t * was clicked.\n\t\t\t * @type {{ widgetId: string, title: string }}\n\t\t\t */\n\t\t\tthis.$emit('request-feature', payload, event)\n\t\t},\n\n\t\t/**\n\t\t * Open the sidebar leaf a \"Linked apps\" row on the auto-body\n\t\t * CnRelatedObjectsWidget points at. Publishes the requested tab and\n\t\t * opens the external sidebar via `objectSidebarState`, and re-emits\n\t\t * `open-integration` so a host can override the routing.\n\t\t *\n\t\t * @param {string} integrationId - The leaf integration id (tab id).\n\t\t */\n\t\tonAutoBodyOpenIntegration(integrationId) {\n\t\t\tif (this.hasExternalSidebar && this.objectSidebarState) {\n\t\t\t\tthis.objectSidebarState.open = true\n\t\t\t\tthis.objectSidebarState.requestedTab = integrationId\n\t\t\t}\n\t\t\t/**\n\t\t\t * @event open-integration A related-objects \"Linked apps\" row\n\t\t\t * was clicked on the auto-body. Payload is the leaf id.\n\t\t\t * @type {string}\n\t\t\t */\n\t\t\tthis.$emit('open-integration', integrationId)\n\t\t},\n\n\t\t/**\n\t\t * Re-emit a click on an object in the Related widget. The page does not\n\t\t * know the routes; the host (CnPageRenderer on a manifest page) opens\n\t\t * the object's own detail page.\n\t\t *\n\t\t * @param {object} raw The clicked object; `@self` names its register and schema.\n\t\t */\n\t\tonRelatedObjectSelect(raw) {\n\t\t\t/**\n\t\t\t * @event related-object-click An object in the Related widget was\n\t\t\t * clicked. Payload is the raw object; `@self` carries its register,\n\t\t\t * schema and id.\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('related-object-click', raw)\n\t\t},\n\n\t\t/**\n\t\t * Re-fetch the object after the geo widget persists `@self.geo` (so the\n\t\t * rest of the page — metadata, other widgets — reflects the new value),\n\t\t * and re-emit for the host.\n\t\t *\n\t\t * @param {object|null} geo The saved geo value (GeoJSON Point or null).\n\t\t */\n\t\tonGeoSaved(geo) {\n\t\t\tthis.fetchObjectIfNeeded()\n\t\t\t/**\n\t\t\t * @event geo-saved The object's `@self.geo` was saved via the geo\n\t\t\t * widget. Payload is the saved geo value (GeoJSON Point or null).\n\t\t\t * @type {object|null}\n\t\t\t */\n\t\t\tthis.$emit('geo-saved', geo)\n\t\t},\n\n\t\t/**\n\t\t * Re-emit a successful lifecycle transition to the host.\n\t\t *\n\t\t * @param {{ action: string, to: string, object: object }} payload The transition result.\n\t\t */\n\t\tonTransitioned(payload) {\n\t\t\t/**\n\t\t\t * @event transitioned A declarative lifecycle transition succeeded on this\n\t\t\t * page's object. Payload is `{ action, to, object }`.\n\t\t\t * @type {{ action: string, to: string, object: object }}\n\t\t\t */\n\t\t\tthis.$emit('transitioned', payload)\n\t\t},\n\n\t\t/**\n\t\t * Tick or untick one checklist item through the record source. The\n\t\t * source replaces its record with the answer; a refusal is shown with\n\t\t * the server's message and leaves the item as it was.\n\t\t *\n\t\t * @param {string} itemId The item id.\n\t\t * @param {boolean} checked The new state.\n\t\t * @return {Promise<boolean>} Whether the write was taken.\n\t\t * @spec openspec/changes/record-checklist-widget/specs/record-checklist-widget/spec.md#requirement-a-checklist-widget-ticks-items-through-the-record-source\n\t\t */\n\t\tasync checkSourceItem(itemId, checked) {\n\t\t\tconst source = this.recordSource\n\t\t\tconst objectId = this.objectId\n\t\t\tif (!this.recordCanCheckItems || !objectId) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tconst outcome = await source.checkItem(String(objectId), String(itemId), checked === true)\n\t\t\tif (!outcome || outcome.ok !== true) {\n\t\t\t\tconst { showError } = await import('@nextcloud/dialogs')\n\t\t\t\tshowError(String(outcome?.message || '').trim() || t('nextcloud-vue', 'This could not be done. Try again.'))\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn true\n\t\t},\n\n\t\t/**\n\t\t * Run a record-source verb on the record in view. The source keeps its\n\t\t * record in step with the answer, so the header, pills and widgets\n\t\t * redraw from it. A refusal is shown with the server's own message,\n\t\t * which names the verb and the reason.\n\t\t *\n\t\t * @param {{id: string, label: string, successMessage?: string}} verb The verb.\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/record-source-verbs/specs/record-source-verbs/spec.md#requirement-a-record-source-offers-header-verbs\n\t\t */\n\t\tasync runSourceVerb(verb) {\n\t\t\tconst source = this.recordSource\n\t\t\tconst objectId = this.objectId\n\t\t\tif (!source || !objectId || this.sourceVerbBusy) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (verb.form && verb.form.schema) {\n\t\t\t\tthis.sourceVerbForm = verb\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.sourceVerbBusy = true\n\t\t\ttry {\n\t\t\t\tconst url = this.verbEndpoints && typeof this.verbEndpoints[verb.id] === 'string' ? this.verbEndpoints[verb.id] : undefined\n\t\t\t\tconst outcome = await source.runVerb(String(objectId), verb.id, url ? { url } : {})\n\t\t\t\tconst { showError, showSuccess } = await import('@nextcloud/dialogs')\n\t\t\t\tif (!outcome || outcome.ok !== true) {\n\t\t\t\t\tshowError(String(outcome?.message || '').trim() || t('nextcloud-vue', 'This could not be done. Try again.'))\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif (verb.successMessage) {\n\t\t\t\t\tshowSuccess(verb.successMessage)\n\t\t\t\t}\n\t\t\t\tthis.$emit('transitioned', { action: verb.id, to: String(outcome.record?.state ?? ''), object: outcome.record || null })\n\t\t\t} finally {\n\t\t\t\tthis.sourceVerbBusy = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Save the open record-source verb form through the source: only the\n\t\t * form's own fields are sent. The form shows the server's refusal and\n\t\t * stays open; on success it closes and the page redraws from the\n\t\t * record the source kept.\n\t\t *\n\t\t * @param {object} formData The form's values.\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/task-edit-action/specs/record-source-verbs/spec.md#requirement-a-record-source-verb-can-ask-for-values-first\n\t\t */\n\t\tasync onSourceVerbFormConfirm(formData) {\n\t\t\tconst verb = this.sourceVerbForm\n\t\t\tconst source = this.recordSource\n\t\t\tconst dialog = this.$refs.sourceVerbFormDialog\n\t\t\tif (!verb || !source || !this.objectId) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst keys = Object.keys(verb.form.schema.properties || {})\n\t\t\tconst body = Object.fromEntries(keys.filter((key) => formData && key in formData).map((key) => [key, formData[key]]))\n\t\t\tconst outcome = await source.runVerb(String(this.objectId), verb.id, { body })\n\t\t\tif (!outcome || outcome.ok !== true) {\n\t\t\t\tdialog?.setResult({ error: String(outcome?.message || '').trim() || t('nextcloud-vue', 'This could not be done. Try again.') })\n\t\t\t\treturn\n\t\t\t}\n\t\t\tdialog?.setResult({ success: true })\n\t\t\tthis.sourceVerbForm = null\n\t\t\tif (verb.successMessage) {\n\t\t\t\tconst { showSuccess } = await import('@nextcloud/dialogs')\n\t\t\t\tshowSuccess(verb.successMessage)\n\t\t\t}\n\t\t\tthis.$emit('transitioned', { action: verb.id, to: String(outcome.record?.state ?? ''), object: outcome.record || null })\n\t\t},\n\n\t\t/**\n\t\t * Re-fetch the page's object after a lifecycle transition so the new state\n\t\t * (and any other server-side mutations the guard applied) render.\n\t\t */\n\t\tonLifecycleReload() {\n\t\t\tthis.fetchObjectIfNeeded()\n\t\t},\n\n\t\t/**\n\t\t * Open the relation-link modal for a configured `relationLinks` entry.\n\t\t *\n\t\t * @param {object} link The relation-link descriptor.\n\t\t */\n\t\topenRelationLink(link) {\n\t\t\tthis.activeRelationLink = link\n\t\t},\n\n\t\t/**\n\t\t * A relation-link modal saved a patched FK — re-fetch the object and\n\t\t * re-emit so the host can react.\n\t\t *\n\t\t * @param {object} saved The updated object.\n\t\t */\n\t\tonRelationLinked(saved) {\n\t\t\tthis.fetchObjectIfNeeded()\n\t\t\t/**\n\t\t\t * @event relation-linked A relation-link action patched a foreign key on\n\t\t\t * this page's object. Payload is the updated object.\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('relation-linked', saved)\n\t\t},\n\n\t\t/**\n\t\t * Re-emit a related-collection row click to the host.\n\t\t *\n\t\t * @param {{ collection: object, row: object, index: number }} payload The click payload.\n\t\t */\n\t\tonRelatedRowClick(payload) {\n\t\t\t/**\n\t\t\t * @event related-row-click A row in a `relatedCollections` section was\n\t\t\t * clicked. Payload is `{ collection, row, index }`.\n\t\t\t * @type {{ collection: object, row: object, index: number }}\n\t\t\t */\n\t\t\tthis.$emit('related-row-click', payload)\n\t\t},\n\n\t\t/**\n\t\t * Replace the page's object in the store cache, for a section that saved\n\t\t * it and holds the saved version. An object with another id is ignored.\n\t\t *\n\t\t * @param {object} object The saved object.\n\t\t */\n\t\tsetCurrentObject(object) {\n\t\t\tconst store = this.effectiveObjectStore\n\t\t\tconst type = this.resolvedObjectType\n\t\t\tif (!store || !type || !this.objectId || !object) {\n\t\t\t\t// eslint-disable-next-line no-console -- tells the integrating section its save is not shown\n\t\t\t\tconsole.warn('[CnDetailPage] setObject ignored: the page has no object store, type or id')\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst id = object.id ?? object['@self']?.id\n\t\t\tif (id !== undefined && id !== null && String(id) !== String(this.objectId)) {\n\t\t\t\t// eslint-disable-next-line no-console -- tells the integrating section its save is not shown\n\t\t\t\tconsole.warn(`[CnDetailPage] setObject ignored: object ${id} is not the page's object ${this.objectId}`)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tstore.objects = {\n\t\t\t\t...store.objects,\n\t\t\t\t[type]: { ...(store.objects?.[type] || {}), [this.objectId]: object },\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Schema-driven fetch entry point — no-op outside the\n\t\t * `register`+`schema`+`objectId` mode. Registers the type on\n\t\t * the store with the canonical 4-arg signature (matches what\n\t\t * `CnIndexPage` does), then fetches the object and its schema\n\t\t * in parallel. Errors land on `this._fetchError` (exposed via\n\t\t * the `error` template gate); the loading flag is intentionally\n\t\t * left to the store's own per-type `loading[type]` so it\n\t\t * cooperates with parallel fetches from sibling components.\n\t\t *\n\t\t * Called from `mounted()` and from the `register` / `schema` /\n\t\t * `objectId` watchers — every prop change re-runs in one\n\t\t * place so the request lifecycle stays predictable.\n\t\t */\n\t\t/**\n\t\t * `PUT .../read-state`, once per register, schema and object per page load.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/record-unread-markers/tasks.md#task-2\n\t\t */\n\t\tasync sendReadState() {\n\t\t\tconst key = `${this.register}/${this.schema}/${this.objectId}`\n\t\t\tif (this.readStateSentFor === key) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.readStateSentFor = key\n\t\t\tconst result = await setReadState(this.register, this.schema, String(this.objectId), true)\n\t\t\tif (result.ok) {\n\t\t\t\tpatchStoredSelf(this.register, this.schema, String(this.objectId), { unread: false })\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Mark as unread: `DELETE .../read-state`, then `marked-unread`.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/record-unread-markers/tasks.md#task-2\n\t\t */\n\t\tasync onMarkUnread() {\n\t\t\t// Fix the key first, so the unread marker coming back does not mark it read again.\n\t\t\tthis.readStateSentFor = `${this.register}/${this.schema}/${this.objectId}`\n\t\t\tconst result = await setReadState(this.register, this.schema, String(this.objectId), false)\n\t\t\tif (!result.ok) {\n\t\t\t\timport('@nextcloud/dialogs').then(({ showError }) => showError(result.message || t('nextcloud-vue', 'Could not mark this record as unread.'))).catch(() => {})\n\t\t\t\treturn\n\t\t\t}\n\t\t\tpatchStoredSelf(this.register, this.schema, String(this.objectId), { unread: true })\n\t\t\t/** @event marked-unread Emitted after the record was marked unread; the page stays open unless `markUnreadNavigatesBack`. */\n\t\t\tthis.$emit('marked-unread')\n\t\t\tif (this.markUnreadNavigatesBack && this.$router && typeof this.$router.back === 'function') {\n\t\t\t\tthis.$router.back()\n\t\t\t}\n\t\t},\n\n\t\tasync fetchObjectIfNeeded() {\n\t\t\tif (this.recordSource) {\n\t\t\t\tawait this.fetchSourceRecord()\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// Create archetype: no object to fetch, but the create form needs\n\t\t\t// the schema — register the type and fetch its schema, then stop.\n\t\t\tif (!this.hasSchemaDrivenFetch) {\n\t\t\t\tthis.objectFetchPending = false\n\t\t\t\tif (this.isCreateMode) {\n\t\t\t\t\tawait this.fetchSchemaForCreate()\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst store = this.effectiveObjectStore\n\t\t\tif (!store) {\n\t\t\t\tthis.objectFetchPending = false\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst type = this.resolvedObjectType\n\t\t\t// (slug, schemaId, registerId, slugs) — same shape as the\n\t\t\t// CnIndexPage / CnLogsPage fix. Passing the slug strings into\n\t\t\t// the positional id slots is intentional (OR's REST accepts\n\t\t\t// either numeric ids or kebab slugs there), and the 4th-arg\n\t\t\t// hints feed the live-updates transport.\n\t\t\t// Register the type ONCE. `registerObjectType` resets the type's\n\t\t\t// cache (`objects[type] = {}`, `schemas[type] = null`), so calling\n\t\t\t// it on every refresh blanks the object + schema until the\n\t\t\t// re-fetch lands — the content visibly disappears mid-refresh.\n\t\t\t// Skip re-registration when the type is already registered so a\n\t\t\t// refresh re-fetches and replaces the data in place.\n\t\t\tif (typeof store.registerObjectType === 'function'\n\t\t\t\t&& !store.objectTypeRegistry?.[type]) {\n\t\t\t\tstore.registerObjectType(\n\t\t\t\t\ttype,\n\t\t\t\t\tthis.schema,\n\t\t\t\t\tthis.register,\n\t\t\t\t\t{ registerSlug: this.register, schemaSlug: this.schema },\n\t\t\t\t)\n\t\t\t}\n\t\t\tconst objectId = this.objectId\n\t\t\tconst fetchesObject = typeof store.fetchObject === 'function'\n\t\t\tthis.objectFetchPending = fetchesObject\n\t\t\tif (fetchesObject) {\n\t\t\t\tthis.objectNotFound = false\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst tasks = []\n\t\t\t\tif (fetchesObject) {\n\t\t\t\t\ttasks.push(store.fetchObject(type, objectId, { extend: this.readExtend }))\n\t\t\t\t}\n\t\t\t\tif (typeof store.fetchSchema === 'function') {\n\t\t\t\t\ttasks.push(store.fetchSchema(type))\n\t\t\t\t}\n\t\t\t\tawait Promise.all(tasks)\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console -- the failure is already handled; the console is the only channel a host app can read the detail on\n\t\t\t\tconsole.error('[CnDetailPage] schema-driven fetch failed:', err)\n\t\t\t} finally {\n\t\t\t\t// A newer fetch for another id owns the flags now.\n\t\t\t\tif (fetchesObject && objectId === this.objectId) {\n\t\t\t\t\tthis.objectNotFound = !store.objects?.[type]?.[objectId]\n\t\t\t\t\t\t&& store.getError?.(type)?.status === 404\n\t\t\t\t\tthis.objectFetchPending = false\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Read the record through the record source. A refusal (403) or a\n\t\t * missing record (404) shows the not-found state; which records the\n\t\t * viewer may read is the server's decision.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync fetchSourceRecord() {\n\t\t\tconst source = this.recordSource\n\t\t\tconst objectId = this.objectId\n\t\t\tif (!objectId) {\n\t\t\t\tthis.objectFetchPending = false\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.objectFetchPending = true\n\t\t\tthis.objectNotFound = false\n\t\t\ttry {\n\t\t\t\tawait source.loadRecord(String(objectId))\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console -- the failure is already handled; the console is the only channel a host app can read the detail on\n\t\t\t\tconsole.error('[CnDetailPage] record source fetch failed:', err)\n\t\t\t} finally {\n\t\t\t\t// A newer fetch for another id owns the flags now.\n\t\t\t\tif (objectId === this.objectId) {\n\t\t\t\t\tconst status = typeof source.recordError === 'function' ? source.recordError(String(objectId)) : null\n\t\t\t\t\tthis.objectNotFound = !this.currentObject && (status === 403 || status === 404)\n\t\t\t\t\tthis.objectFetchPending = false\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Whether a grid-layout item resolves to an integration-typed\n\t\t * widget — `def.type === 'integration'` with a string\n\t\t * `integrationId`. Used by the grid template to render the\n\t\t * registry widget as the slot fallback.\n\t\t *\n\t\t * @param {object} item Layout item\n\t\t * @return {boolean} true when the matching widget def is an integration widget\n\t\t */\n\t\t/**\n\t\t * Register the type and fetch ONLY its schema, for the create archetype\n\t\t * (no object id to fetch). Populates `currentSchema` so the inline create\n\t\t * form can auto-generate its fields.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync fetchSchemaForCreate() {\n\t\t\tconst store = this.effectiveObjectStore\n\t\t\tif (!store) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst type = this.resolvedObjectType\n\t\t\tif (typeof store.registerObjectType === 'function'\n\t\t\t\t&& !store.objectTypeRegistry?.[type]) {\n\t\t\t\tstore.registerObjectType(\n\t\t\t\t\ttype,\n\t\t\t\t\tthis.schema,\n\t\t\t\t\tthis.register,\n\t\t\t\t\t{ registerSlug: this.register, schemaSlug: this.schema },\n\t\t\t\t)\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tif (typeof store.fetchSchema === 'function') {\n\t\t\t\t\tawait store.fetchSchema(type)\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console -- the failure is already handled; the console is the only channel a host app can read the detail on\n\t\t\t\tconsole.error('[CnDetailPage] create-mode schema fetch failed:', err)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Persist the create form: POST the new object to OpenRegister, then\n\t\t * navigate to the created object's detail route (`createRoute` with its\n\t\t * id) — or back in history when no route is configured.\n\t\t *\n\t\t * Returns the outcome as well as applying it. The built-in dialog reads\n\t\t * the outcome off its own ref (`createFormDialog.setResult`), which a\n\t\t * `#form-dialog` replacement does not have — the returned result is how\n\t\t * a replacement learns whether to unlock its own dialog. Nothing on the\n\t\t * default path consumes the return value.\n\t\t *\n\t\t * @param {object} formData The confirmed form values.\n\t\t * @return {Promise<{success?: boolean, data?: object, error?: string}>}\n\t\t */\n\t\tasync onCreateFormConfirm(formData) {\n\t\t\ttry {\n\t\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([\n\t\t\t\t\timport('@nextcloud/axios'),\n\t\t\t\t\timport('@nextcloud/router'),\n\t\t\t\t])\n\t\t\t\tconst url = generateUrl(\n\t\t\t\t\t'/apps/openregister/api/objects/{register}/{schema}',\n\t\t\t\t\t{ register: this.register, schema: this.schema },\n\t\t\t\t)\n\t\t\t\tconst res = await axios.post(url, formData)\n\t\t\t\tconst created = (res && res.data) ? res.data : {}\n\t\t\t\tconst newId = (created['@self'] && created['@self'].id) || created.id\n\t\t\t\tif (this.$refs.createFormDialog) {\n\t\t\t\t\tthis.$refs.createFormDialog.setResult({ success: true })\n\t\t\t\t}\n\t\t\t\t/**\n\t\t\t\t * @event created Emitted after a successful create-form save.\n\t\t\t\t * @type {object}\n\t\t\t\t */\n\t\t\t\tthis.$emit('created', created)\n\t\t\t\tif (this.createRoute && newId && this.$router) {\n\t\t\t\t\tthis.$router.push({ name: this.createRoute, params: { id: String(newId) } }).catch(() => {})\n\t\t\t\t} else if (this.$router) {\n\t\t\t\t\tthis.$router.back()\n\t\t\t\t}\n\t\t\t\treturn { success: true, data: created }\n\t\t\t} catch (e) {\n\t\t\t\tconst error = (e && e.message) || 'error'\n\t\t\t\tif (this.$refs.createFormDialog) {\n\t\t\t\t\tthis.$refs.createFormDialog.setResult({ error })\n\t\t\t\t}\n\t\t\t\treturn { error }\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * `confirm` for the `#form-dialog` slot: the page's own save path,\n\t\t * handed to a replacement dialog so a create or edit made there behaves\n\t\t * exactly like one made in the built-in dialog — same duplicate-id\n\t\t * refusal, same store-then-axios fallback, same `created` / `edited`\n\t\t * events, same lifecycle reload.\n\t\t *\n\t\t * Bound as a PROP rather than left as an `@confirm` listener, because\n\t\t * CnPageRenderer mounts a manifest-declared replacement with\n\t\t * `v-bind=\"slotProps\"` — props only. A listener is unreachable from a\n\t\t * manifest even in principle, which is what made the same slot on\n\t\t * CnIndexPage decorative until openconnector#1150.\n\t\t *\n\t\t * Dispatches on `isCreateMode`, the same flag that decides which of the\n\t\t * two built-in dialogs renders — they are mutually exclusive, since\n\t\t * create mode is by definition a page with no object id.\n\t\t *\n\t\t * @param {object} formData The submitted form values.\n\t\t * @return {Promise<{success?: boolean, data?: object, error?: string}>} The save outcome, for the replacement's own `setResult`.\n\t\t */\n\t\tonFormDialogConfirm(formData) {\n\t\t\treturn this.isCreateMode\n\t\t\t\t? this.onCreateFormConfirm(formData)\n\t\t\t\t: this.onEditFormConfirm(formData)\n\t\t},\n\n\t\t/**\n\t\t * `close` for the `#form-dialog` slot. Dispatches the same way\n\t\t * `onFormDialogConfirm` does, so a dismissed create still navigates\n\t\t * back rather than stranding the user on an id-less page.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonFormDialogClose() {\n\t\t\tif (this.isCreateMode) {\n\t\t\t\tthis.onCreateFormClose()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.closeEditForm()\n\t\t},\n\n\t\t/**\n\t\t * Open the record edit form.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\topenEditForm() {\n\t\t\t// 🔴 OPENING TAKES THE LOCK. Two people editing one record is not\n\t\t\t// resolved by whoever presses Save last: the first person's work is\n\t\t\t// gone and neither of them is told. The button is already withdrawn\n\t\t\t// while somebody else holds the lock (`canEditRecord`), so this is\n\t\t\t// the backstop for the seconds between the last read and the click.\n\t\t\t//\n\t\t\t// 🔑 THE FORM OPENS SYNCHRONOUSLY, AND THE ACQUIRE RUNS BESIDE IT.\n\t\t\t// Awaiting the round trip here would make `openEditForm()` async,\n\t\t\t// and every consumer that calls it and then asserts or renders on\n\t\t\t// the next tick would see a form that is not open yet: measured,\n\t\t\t// 10 of the 17 `CnDetailPageFormDialogSlot` tests went red on\n\t\t\t// exactly that. A conflict closes the form again and names the\n\t\t\t// holder, which is the same outcome one tick later.\n\t\t\tthis.editLockRefusal = ''\n\t\t\tthis.editFormOpen = true\n\t\t\tthis.acquireEditLock()\n\t\t},\n\n\t\t/**\n\t\t * Take the lock for the edit that just opened.\n\t\t *\n\t\t * A refusal CLOSES the form and shows the holder, because a form left\n\t\t * open over somebody else's lock is a form that will be filled in and\n\t\t * then refused. Any other failure leaves the form open: the server\n\t\t * re-checks on the write, so a lock that could not be taken costs an\n\t\t * optimistic edit rather than an editor who cannot work at all.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tacquireEditLock() {\n\t\t\tif (!this.lockState) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.lockState.acquire().catch((e) => {\n\t\t\t\tif (e?.name !== 'LockConflictError') {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tthis.editLockRefusal = e.message\n\t\t\t\tthis.editFormOpen = false\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Close the record edit form without saving.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tcloseEditForm() {\n\t\t\tthis.editFormOpen = false\n\t\t\tthis.releaseEditLock()\n\t\t},\n\n\t\t/**\n\t\t * Hand back the lock this page took for an edit.\n\t\t *\n\t\t * Fire and forget and never throws: a release that fails must not stop\n\t\t * a form from closing, and the lock has a server-side TTL behind it.\n\t\t * `release()` is a no-op when the viewer does not hold the lock, so\n\t\t * calling it on a page that opened no form takes nothing from anybody.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\treleaseEditLock() {\n\t\t\tif (!this.lockState || !this.lockState.lockedByMe.value) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.lockState.release().catch(() => {})\n\t\t},\n\n\t\t/**\n\t\t * Persist an edit of the record this page is showing.\n\t\t *\n\t\t * Saves through the object store when one is available so the page's own\n\t\t * subscription re-renders with the new values; the store is also what\n\t\t * every other surface reads from, so a direct HTTP write would leave\n\t\t * them showing the pre-edit record until something else refetched.\n\t\t * Falls back to a PUT for consumers with no Pinia.\n\t\t *\n\t\t * Returns the outcome as well as applying it, for the same reason\n\t\t * `onCreateFormConfirm` does: a `#form-dialog` replacement has no ref\n\t\t * on the built-in dialog and would otherwise never learn whether the\n\t\t * save landed. Nothing on the default path consumes the return value.\n\t\t *\n\t\t * @param {object} formData The submitted form values.\n\t\t * @return {Promise<{success?: boolean, data?: object, error?: string}>}\n\t\t */\n\t\tasync onEditFormConfirm(formData) {\n\t\t\t// `saveObject` picks PUT over POST on the presence of `id`, and an\n\t\t\t// OpenRegister object carries its id in `@self`, not at the top\n\t\t\t// level. Without this the edit would CREATE a duplicate record.\n\t\t\tconst dialog = this.$refs.editFormDialog\n\t\t\tif (!this.objectId) {\n\t\t\t\t// REFUSE rather than save. An empty id does not error — it makes\n\t\t\t\t// saveObject POST, which silently CREATES A DUPLICATE and leaves\n\t\t\t\t// the record the user was editing untouched. That presents as\n\t\t\t\t// \"my change did not save\", with a new orphan row behind it, and\n\t\t\t\t// nothing anywhere says so.\n\t\t\t\tconst error = t('nextcloud-vue', 'Cannot save: this record has no id, so the edit would create a duplicate instead of updating it.')\n\t\t\t\tif (dialog) {\n\t\t\t\t\tdialog.setResult({ error })\n\t\t\t\t}\n\t\t\t\treturn { error }\n\t\t\t}\n\t\t\tconst payload = { ...formData, id: this.objectId }\n\t\t\ttry {\n\t\t\t\tconst store = this.effectiveObjectStore\n\t\t\t\tlet saved = null\n\t\t\t\tif (store && typeof store.saveObject === 'function' && this.resolvedObjectType) {\n\t\t\t\t\t// Only the keys the form changed are written (a PATCH), so keys\n\t\t\t\t\t// the edit form does not show keep their stored value.\n\t\t\t\t\tsaved = await saveEdited(store, this.resolvedObjectType, this.currentObject || null, payload)\n\t\t\t\t\tif (!saved) {\n\t\t\t\t\t\tconst err = store.getError?.(this.resolvedObjectType)\n\t\t\t\t\t\tconst error = (err && err.message) || t('nextcloud-vue', 'Save failed')\n\t\t\t\t\t\tif (dialog) {\n\t\t\t\t\t\t\tdialog.setResult({ error })\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn { error }\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([\n\t\t\t\t\t\timport('@nextcloud/axios'),\n\t\t\t\t\t\timport('@nextcloud/router'),\n\t\t\t\t\t])\n\t\t\t\t\tconst url = generateUrl(\n\t\t\t\t\t\t'/apps/openregister/api/objects/{register}/{schema}/{id}',\n\t\t\t\t\t\t{ register: this.register, schema: this.schema, id: this.objectId },\n\t\t\t\t\t)\n\t\t\t\t\tconst res = await axios.put(url, payload)\n\t\t\t\t\tsaved = (res && res.data) ? res.data : payload\n\t\t\t\t}\n\t\t\t\tif (dialog) {\n\t\t\t\t\tdialog.setResult({ success: true })\n\t\t\t\t}\n\t\t\t\tthis.editFormOpen = false\n\t\t\t\tthis.releaseEditLock()\n\t\t\t\t/**\n\t\t\t\t * @event edited Emitted after the record edit form saves successfully.\n\t\t\t\t * @type {object} The saved record.\n\t\t\t\t */\n\t\t\t\tthis.$emit('edited', saved)\n\t\t\t\tthis.onLifecycleReload()\n\t\t\t\treturn { success: true, data: saved }\n\t\t\t} catch (e) {\n\t\t\t\tconst error = (e && e.message) || t('nextcloud-vue', 'Save failed')\n\t\t\t\tif (dialog) {\n\t\t\t\t\tdialog.setResult({ error })\n\t\t\t\t}\n\t\t\t\treturn { error }\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The create form was dismissed without saving — navigate back so the\n\t\t * user is not stranded on an id-less page.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonCreateFormClose() {\n\t\t\t/**\n\t\t\t * @event create-cancel Emitted when the create form is dismissed.\n\t\t\t */\n\t\t\tthis.$emit('create-cancel')\n\t\t\tif (this.$router) {\n\t\t\t\tthis.$router.back()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Whether a grid item is a schema-driven `data` widget — rendered via\n\t\t * CnObjectDataWidget with the page's loaded object + the def's overrides.\n\t\t *\n\t\t * @param {object} item Layout item\n\t\t * @return {boolean} true when the matching widget def is `type: 'data'`.\n\t\t */\n\t\tisDataWidget(item) {\n\t\t\treturn isDataWidgetDef(this.findWidget(item))\n\t\t},\n\n\t\t/**\n\t\t * Whether a grid item is the schema-driven `related` widget — rendered via\n\t\t * CnRelatedObjectsWidget with the page's loaded object. One of the two\n\t\t * default body widgets.\n\t\t *\n\t\t * @param {object} item Layout item\n\t\t * @return {boolean} true when the matching widget def is `type: 'related'`.\n\t\t */\n\t\tisRelatedWidget(item) {\n\t\t\treturn isRelatedWidgetDef(this.findWidget(item))\n\t\t},\n\n\t\t/**\n\t\t * Whether a grid item is the `object-geo` widget — rendered via\n\t\t * CnObjectGeoWidget with the page's loaded object so the user can view /\n\t\t * edit the object's `@self.geo` on a map.\n\t\t *\n\t\t * @param {object} item Layout item\n\t\t * @return {boolean} true when the matching widget def is `type: 'object-geo'`.\n\t\t */\n\t\tisGeoWidget(item) {\n\t\t\treturn isGeoWidgetDef(this.findWidget(item))\n\t\t},\n\n\t\t/**\n\t\t * Whether a grid item is a content-only catalog widget (a bare table\n\t\t * renderer with no chrome of its own). These get the titled\n\t\t * CnWidgetWrapper card in the grid (ADR-062); self-chromed catalog\n\t\t * widgets (stat / chart / gauge / tile / …) render bare.\n\t\t *\n\t\t * @param {object} item Layout item.\n\t\t * @return {boolean} true when the widget def's type is content-only.\n\t\t */\n\t\tisContentOnlyWidget(item) {\n\t\t\treturn isContentOnlyWidgetDef(this.findWidget(item))\n\t\t},\n\n\t\t/**\n\t\t * Whether a grid item is a registry \"card\" widget — a single self-\n\t\t * contained KPI / gauge / delta tile (registry entry `card: true`).\n\t\t * These render bare tile content, so on a detail page they need the\n\t\t * same titled CnWidgetWrapper card chrome the dashboard gives them\n\t\t * (ADR-062: a lone stat must not read as uncarded floating text —\n\t\t * pipelinq contracts / POS).\n\t\t *\n\t\t * @param {object} item Layout item.\n\t\t * @return {boolean} true when the widget def's registry entry is a card.\n\t\t */\n\t\tisCardWidget(item) {\n\t\t\treturn isCardWidgetDef(this.findWidget(item))\n\t\t},\n\n\t\t/**\n\t\t * Debounced dev-mode audit: warn when a grid widget's rendered content\n\t\t * overflows its fixed cell (ADR-062 — the cell is the budget; overflow\n\t\t * is a design bug, never a scroll surface). No-op in production builds.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tscheduleCellOverflowAudit() {\n\t\t\tif (process.env.NODE_ENV === 'production') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tclearTimeout(this._cellAuditTimer)\n\t\t\tthis._cellAuditTimer = setTimeout(() => this.auditCellOverflow(), 800)\n\t\t},\n\n\t\t/**\n\t\t * Measure every grid cell and console.warn the widget ids whose content\n\t\t * is taller than the cell. Dev aid only — called via\n\t\t * `scheduleCellOverflowAudit`.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tauditCellOverflow() {\n\t\t\tif (!this.$el || !this.$el.querySelectorAll) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.$el.querySelectorAll('.grid-stack-item').forEach((cell) => {\n\t\t\t\tconst content = cell.querySelector('.grid-stack-item-content')\n\t\t\t\tif (!content) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif (content.scrollHeight > content.clientHeight + 8) {\n\t\t\t\t\tconst inner = cell.querySelector('.cn-detail-page__grid-item')\n\t\t\t\t\tconst label = (inner && inner.getAttribute('aria-labelledby')) || cell.getAttribute('gs-id') || ''\n\t\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\t\tconsole.warn(`[CnDetailPage] widget cell ${label || '(unlabelled)'} content overflows its gridHeight `\n\t\t\t\t\t\t+ `(${content.scrollHeight}px in ${content.clientHeight}px) — enlarge the cell or scope the widget's content (ADR-062).`)\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Whether to render the grid section `<h3>` title above a widget. Only for\n\t\t * consumer-supplied `#widget-<id>` slots (whose content may be bare) — the\n\t\t * built-in widget renderers (data / related / integration / catalog) draw\n\t\t * their own titled card header, so a grid heading would show the title\n\t\t * twice. Honours `item.showTitle === false` to opt out entirely.\n\t\t *\n\t\t * @param {object} item Layout item.\n\t\t * @return {boolean} true when the grid heading should render.\n\t\t */\n\t\tshowGridTitle(item) {\n\t\t\tif (item.showTitle === false || !this.findWidget(item)) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn Boolean(this.$slots[`widget-${item.widgetId}`] || this.$slots[`widget-${item.widgetId}`])\n\t\t},\n\n\t\t/**\n\t\t * The grid section heading for a consumer slot widget — the widget\n\t\t * definition's manifest `title` run through the host translate\n\t\t * function. Returns the raw value untouched when there is none.\n\t\t *\n\t\t * @param {object} item Layout item.\n\t\t * @return {string|undefined} The heading text.\n\t\t */\n\t\tgridTitle(item) {\n\t\t\tconst title = this.findWidget(item)?.title\n\t\t\treturn title ? this.effectiveTranslate(title) : title\n\t\t},\n\n\t\t/**\n\t\t * Whether a card widget (stat / gauge / delta) shows the wrapper header.\n\t\t *\n\t\t * These renderers draw their own label from `content.label`, so a header\n\t\t * carrying the same manifest title printed it twice. `showTitle: false`\n\t\t * on the layout item switches it off, the same key and the same meaning\n\t\t * as on the grid `<h3>` (`showGridTitle`).\n\t\t *\n\t\t * @param {object} item Layout item.\n\t\t * @return {boolean} true when the wrapper header renders.\n\t\t */\n\t\tshowCardTitle(item) {\n\t\t\tif (item.showTitle === false) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tconst def = this.findWidget(item)\n\t\t\treturn def?.title !== undefined || widgetContentOf(def).title !== undefined\n\t\t},\n\n\t\t/**\n\t\t * Resolve the widget definition for a layout item. Overrides the\n\t\t * gridLayout mixin's `findWidget` (which only reads the `widgets` prop) so\n\t\t * the materialized default auto-body widgets resolve too. Searches the\n\t\t * effective `bodyGridWidgets`.\n\t\t *\n\t\t * @param {{ widgetId: string }} item Layout item.\n\t\t * @return {object|undefined} The matching widget definition.\n\t\t */\n\t\t/**\n\t\t * A widget definition with its `titleWhen` resolved against the record.\n\t\t *\n\t\t * @param {object|null|undefined} def The definition.\n\t\t * @return {object|null|undefined} The definition to hand on.\n\t\t * @spec openspec/changes/widget-title-when/specs/widget-title-when/spec.md#requirement-a-widget-title-can-depend-on-the-record\n\t\t */\n\t\ttitled(def) {\n\t\t\treturn withTitleWhen(def, this.titleData)\n\t\t},\n\n\t\tfindWidget(item) {\n\t\t\treturn this.bodyGridWidgets.find((w) => w.id === item.widgetId)\n\t\t\t\t|| this.findViewWidget(item.widgetId)\n\t\t\t\t|| undefined\n\t\t},\n\n\t\t/**\n\t\t * Read the records `visibilityContext` names for the loaded record. A\n\t\t * late answer for a record the page has left is dropped.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-page-can-load-records-a-condition-names\n\t\t */\n\t\tasync refreshVisibilityRows() {\n\t\t\tconst key = this.visibilityRefsKey\n\t\t\tconst refs = this.visibilityRefs\n\t\t\tconst seq = ++this.visibilityRowsSeq\n\t\t\tif (refs.length === 0) {\n\t\t\t\t// Write only on a change, so a page without `visibilityContext`\n\t\t\t\t// never touches the state its layout could read.\n\t\t\t\tif (Object.keys(this.visibilityRows).length > 0) {\n\t\t\t\t\tthis.visibilityRows = {}\n\t\t\t\t}\n\t\t\t\tif (this.visibilityRowsFor !== key) {\n\t\t\t\t\tthis.visibilityRowsFor = key\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!this._visibilityRowCache) {\n\t\t\t\tthis._visibilityRowCache = new Map()\n\t\t\t}\n\t\t\tconst rows = await fetchVisibilityRows(refs, this._visibilityRowCache)\n\t\t\tif (seq !== this.visibilityRowsSeq) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.visibilityRows = rows\n\t\t\tthis.visibilityRowsFor = key\n\t\t},\n\n\t\t/**\n\t\t * Ask the endpoint and source `visibleWhen` conditions of the grid\n\t\t * widgets, once per record. Each is fail-safe: an error hides the widget.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-grid-widget-shows-only-while-its-visiblewhen-holds-for-the-record\n\t\t */\n\t\tasync refreshWidgetConditions() {\n\t\t\tconst seq = ++this.widgetConditionSeq\n\t\t\tif (this.remoteWidgetConditionsKey === '') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst [, entries] = JSON.parse(this.remoteWidgetConditionsKey)\n\t\t\tconst ctx = { objectId: this.objectId, object: this.widgetVisibilityData || undefined }\n\t\t\tconst answers = {}\n\t\t\tawait Promise.all(entries.map(async ([id, cond]) => {\n\t\t\t\tanswers[id] = await evaluateVisibleWhen(cond, ctx)\n\t\t\t}))\n\t\t\tif (seq === this.widgetConditionSeq) {\n\t\t\t\tthis.widgetConditionAnswers = answers\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Drop the items that draw nothing, then close the rows they freed —\n\t\t * `float: true` leaves the item below pinned under a gap nobody owns.\n\t\t * Skipped when nothing was dropped, so an authored gap survives.\n\t\t *\n\t\t * @param {Array<object>} items The authored layout items.\n\t\t *\n\t\t * @return {Array<object>} What the grid should render.\n\t\t */\n\t\tcloseDroppedRows(items) {\n\t\t\tconst drawable = items.filter((item) => this.layoutItemCanDraw(item))\n\t\t\tif (drawable.length === items.length) {\n\t\t\t\treturn drawable\n\t\t\t}\n\n\t\t\treturn compactLayoutRows(drawable)\n\t\t},\n\n\t\t/**\n\t\t * Whether a side entry belongs beside the open body tab: an entry with\n\t\t * `tabs` (tab ids) shows only while one of them is open; an entry with\n\t\t * `notTabs` stands aside while one of those is open (a tab that draws\n\t\t * the side column as its own, DqZaakAssistent); an entry with neither\n\t\t * shows beside every tab. Edit mode shows them all.\n\t\t *\n\t\t * @spec openspec/changes/detail-side-cards-per-tab/specs/detail-page/spec.md#requirement-a-side-card-can-follow-the-open-tab\n\t\t * @spec openspec/changes/side-card-not-on-tabs/specs/detail-page/spec.md#requirement-a-side-card-can-stand-aside-for-a-tab\n\t\t * @param {object} widget The side entry's definition.\n\t\t * @return {boolean}\n\t\t */\n\t\tsideWidgetOnTab(widget) {\n\t\t\treturn this.editingBody || onTab(widget, this.activeBodyTab)\n\t\t},\n\n\t\t/**\n\t\t * Whether a side column widget shows: always in edit mode or without a\n\t\t * `visibleWhen`, else while it holds for the record and the records\n\t\t * `visibilityContext` names, read by the same `detailWidgetVisible` the\n\t\t * body grid uses (local field conditions, `@context.<name>.*`, and the\n\t\t * answers of endpoint and source conditions).\n\t\t *\n\t\t * @param {object} widget A side column widget definition.\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/detail-side-visible-when/specs/detail-page/spec.md#requirement-a-side-column-widget-shows-only-while-its-visiblewhen-holds\n\t\t */\n\t\tsideWidgetVisible(widget) {\n\t\t\tif (this.editingBody || widget.visibleWhen === undefined || widget.visibleWhen === null) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn detailWidgetVisible(widget, this.widgetVisibilityData, {\n\t\t\t\tpending: this.visibilityRowsPending,\n\t\t\t\tanswers: this.widgetConditionAnswers,\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Whether a layout item has anything to draw, so a grid cell is worth\n\t\t * spending on it.\n\t\t *\n\t\t * A widget whose definition carries `visibleWhen` draws only while the\n\t\t * condition holds for the record (see `detailWidgetVisible`).\n\t\t *\n\t\t * Beyond that, TWO widget types are judged, and no others.\n\t\t *\n\t\t * `integration`, in the case where nothing can ever appear: a leaf from\n\t\t * another app that is not registered, with no `requiredApp` to fall back\n\t\t * on. A cross-app feature is normally optional — humaniq's hours leaf on a\n\t\t * dossiq case — and without this the host still renders its wrapper, so the\n\t\t * absent app costs a bordered, full-height ghost card in the grid. An app\n\t\t * nobody installed should cost nothing.\n\t\t *\n\t\t * A widget that declares `requiredApp` is KEPT: the host answers a missing\n\t\t * app with a set-up state, which is content, and hiding it would turn an\n\t\t * actionable \"install humaniq\" into silence.\n\t\t *\n\t\t * `presence`, whenever nobody else is on the record. A runtime fact that\n\t\t * flips both ways, so the host holds the subscription (see setup) and the\n\t\t * row returns the moment somebody arrives.\n\t\t *\n\t\t * Deliberately NOT generalised to \"anything that resolves to nothing\".\n\t\t * A `custom` widget whose `#widget-<id>` slot is missing also draws an\n\t\t * empty host, but that is a WIRING FAULT rather than an absent optional\n\t\t * feature, and quietly dropping it would hide the bug instead of showing\n\t\t * it — CnPageRenderer's split pane forwards no slots, which is exactly\n\t\t * that fault and wants fixing, not concealing.\n\t\t *\n\t\t * The widget the header holds (`headerWidget`) draws nothing here\n\t\t * either: it is already drawn in the header.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-detail-header-can-be-a-card-that-holds-a-widget\n\t\t * @param {object} item A layout item.\n\t\t * @return {boolean} False only when the item provably draws nothing.\n\t\t */\n\t\tlayoutItemCanDraw(item) {\n\t\t\t// In edit mode nothing is hidden: an author has to see the placement to\n\t\t\t// move or delete it, and a widget that vanished from the editor while\n\t\t\t// staying in the manifest would be unmanageable.\n\t\t\tif (this.editingBody) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\t// The widget the header holds (`headerWidget`) is drawn there, so\n\t\t\t// its grid cell would be the same widget twice.\n\t\t\tif (this.headerWidgetDef !== null && item.widgetId === this.headerWidgetDef.id) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tconst widget = this.findWidget(item)\n\t\t\tif (!widget) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\t// The widget's own `visibleWhen`, read against the record and the\n\t\t\t// records `visibilityContext` names (a case's case type). Without\n\t\t\t// one the widget shows, as it always did, and the layout takes no\n\t\t\t// dependency on the record: a page without conditions re-lays out\n\t\t\t// exactly as often as before.\n\t\t\tif (widget.visibleWhen !== undefined && widget.visibleWhen !== null\n\t\t\t\t&& !detailWidgetVisible(widget, this.widgetVisibilityData, {\n\t\t\t\t\tpending: this.visibilityRowsPending,\n\t\t\t\t\tanswers: this.widgetConditionAnswers,\n\t\t\t\t})) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\t// The board look shows the activity as the History tab. When a tabs\n\t\t\t// widget already holds it, it is not also a body section.\n\t\t\tif (this.isBoard && isActivityWidget(widget) && this.tabbedWidgetIds.has(widget.id)) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\t// Presence is empty almost always, and no attribute can reclaim its\n\t\t\t// row: GridStack floors `sizeToContent` at the item's `gs-min-h`.\n\t\t\tif (widget.type === 'presence') {\n\t\t\t\treturn (this.presentOthers || []).length > 0\n\t\t\t}\n\t\t\tif (widget.type !== 'integration') {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tconst requiredApp = widget.requiredApp || (widget.content || {}).requiredApp || ''\n\t\t\tif (requiredApp !== '' && !isAppInstalled(requiredApp)) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tif (typeof this.getRegistryProvider !== 'function') {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn !!this.getRegistryProvider(widget.integrationId)\n\t\t},\n\n\t\t/**\n\t\t * Build the default body grid for the schema-driven auto-body: a full-width\n\t\t * Data widget with the Related-objects widget beneath it. Materialized once\n\t\t * (lazily) so the geometry is a mutable, reactive array the user can drag /\n\t\t * resize in edit mode. Both widgets carry `showTitle: false` because they\n\t\t * render their own card chrome.\n\t\t */\n\t\tmaterializeAutoBody() {\n\t\t\t// One source of truth (shared with the Buildiq edit button's\n\t\t\t// \"eject\" on edit) so the in-memory default and the manifest-ejected\n\t\t\t// default are identical. The data widget's content carries the page's\n\t\t\t// register/schema so its per-property editor can resolve the schema\n\t\t\t// (empty on legacy objectType-only mounts → the editor falls back to\n\t\t\t// the injected `cnObjectContext`).\n\t\t\tconst grid = defaultDetailGrid({\n\t\t\t\tregister: this.register || '',\n\t\t\t\tschema: this.schema || '',\n\t\t\t\tshowRelated: this.showRelatedObjects,\n\t\t\t})\n\t\t\t// Drop the Data widget when there's no schema to render (the helper\n\t\t\t// always includes it; the auto-body only shows it with a schema).\n\t\t\t//\n\t\t\t// This can happen because the schema HAS NOT ARRIVED YET, not\n\t\t\t// because there is none: `schemaDrivenFetch` fires fetchObject and\n\t\t\t// fetchSchema in parallel, and this body materializes as soon as\n\t\t\t// `shouldRenderAutoBody` turns true — which tests `currentObject`\n\t\t\t// and NOT `currentSchema`. When the object wins that race the Data\n\t\t\t// widget is filtered out, and because the body materializes only\n\t\t\t// once (`if (active && !this.autoBodyLayout)`) and resets only when\n\t\t\t// `resolvedObjectType` changes, the schema landing a moment later\n\t\t\t// never puts it back.\n\t\t\t//\n\t\t\t// The symptom is a detail page showing its title and Related panel\n\t\t\t// with EVERY declared scalar field missing, intermittently, on\n\t\t\t// identical code — shillinq#1073, where the object fetch returned\n\t\t\t// 200 carrying the values the page then did not render.\n\t\t\t//\n\t\t\t// `dataWidgetDeferred` records that this filtering was provisional\n\t\t\t// so the watcher below can re-materialize exactly once, when the\n\t\t\t// schema arrives. It is deliberately NOT a plain \"re-materialize if\n\t\t\t// the data widget is absent\": a body the user has edited must not\n\t\t\t// have the widget silently restored underneath them.\n\t\t\tthis.dataWidgetDeferred = !this.currentSchema\n\t\t\tif (!this.currentSchema) {\n\t\t\t\tgrid.widgets = grid.widgets.filter((w) => w.widgetId !== 'data')\n\t\t\t\tgrid.layout = grid.layout.filter((l) => l.widgetId !== 'data')\n\t\t\t}\n\t\t\tthis.autoBodyWidgets = grid.widgets\n\t\t\tthis.autoBodyLayout = grid.layout\n\t\t},\n\n\t\t/**\n\t\t * Persist a drag/resize from the body grid. For the default auto-body the\n\t\t * new geometry is kept locally so the move sticks; for an explicit `layout`\n\t\t * prop the change is emitted upward (`layout-change` + `update:layout`) for\n\t\t * the manifest renderer / host to persist.\n\t\t *\n\t\t * @param {Array} updated The new layout array from CnDashboardGrid.\n\t\t *\n\t\t * @event layout-change Emitted with the updated body-grid layout. The\n\t\t * sibling `update:layout` event is also emitted so `:layout.sync`\n\t\t * consumers stay in sync.\n\t\t */\n\t\tonBodyLayoutChange(updated) {\n\t\t\tif (this.hasGridLayout) {\n\t\t\t\t// Config-backed grid (manifest editor, ADR-041): the `layout` prop is\n\t\t\t\t// the SAME array the working manifest holds (the route-sentinel\n\t\t\t\t// resolver is reference-preserving), so write the new geometry back\n\t\t\t\t// onto its items in place — Save page then persists the resize.\n\t\t\t\tfor (const u of updated) {\n\t\t\t\t\tconst item = this.layout.find((l) => String(l.id) === String(u.id))\n\t\t\t\t\tif (item) {\n\t\t\t\t\t\titem.gridX = u.gridX\n\t\t\t\t\t\titem.gridY = u.gridY\n\t\t\t\t\t\titem.gridWidth = u.gridWidth\n\t\t\t\t\t\titem.gridHeight = u.gridHeight\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// In-memory default grid — keep the new geometry locally so the move\n\t\t\t\t// sticks for the session (un-customised page, nothing to persist).\n\t\t\t\tthis.autoBodyLayout = updated\n\t\t\t}\n\t\t\t/**\n\t\t\t * @event layout-change Emitted when a body-grid widget is dragged or\n\t\t\t * resized. Payload is the updated layout array.\n\t\t\t * @type {Array}\n\t\t\t */\n\t\t\tthis.$emit('layout-change', updated)\n\t\t\t// Description goes ABOVE `@event`, not inline after it:\n\t\t\t// vue-docgen-api's event-name splitter stops at the first `:`, so\n\t\t\t// `@event update:layout <description>` is read as one long event\n\t\t\t// NAME and the generated docs show an empty description.\n\t\t\t/**\n\t\t\t * Sibling of `layout-change` so `v-model:layout` consumers stay in\n\t\t\t * sync.\n\t\t\t *\n\t\t\t * @event update:layout\n\t\t\t * @type {Array}\n\t\t\t */\n\t\t\tthis.$emit('update:layout', updated)\n\t\t},\n\n\t\t/**\n\t\t * The registered config FORM for a grid item's widget type, or null.\n\t\t * Used to gate the per-widget configure cog (only configurable widgets\n\t\t * show one) in Buildiq edit mode.\n\t\t *\n\t\t * @param {object} item Layout item\n\t\t * @return {object|null} The form component, or null.\n\t\t */\n\t\tregistryFormFor(item) {\n\t\t\tconst def = this.findWidget(item)\n\t\t\tif (!def || !def.type) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst entry = getWidgetTypeEntry(def.type)\n\t\t\treturn (entry && entry.form) || null\n\t\t},\n\n\t\t/**\n\t\t * Whether a grid item shows the configure cog: the page is in edit\n\t\t * mode and the item resolves to a widget definition. Not gated on a\n\t\t * registered form: the style editor modal handles a type without one\n\t\t * (title, icon, style, delete), and the delete is what a custom widget\n\t\t * or an integration leaf needs the cog for.\n\t\t *\n\t\t * @param {object} item Layout item.\n\t\t * @return {boolean} true when the cog renders.\n\t\t */\n\t\tshowsCog(item) {\n\t\t\treturn this.editingBody === true && Boolean(this.findWidget(item))\n\t\t},\n\n\t\t/**\n\t\t * The registered RENDERER for a grid item's content-driven catalog\n\t\t * widget type (stat / chart / delta / gauge / object-list / …), or null.\n\t\t * Integration widgets resolve separately; the `data` renderer needs\n\t\t * object context the slot supplies, so it is excluded from this generic\n\t\t * fallback.\n\t\t *\n\t\t * @param {object} item Layout item\n\t\t * @return {object|null} The renderer component, or null.\n\t\t */\n\t\tregistryRendererFor(item) {\n\t\t\treturn resolveRegistryRenderer(this.findWidget(item), this.cnRegistry)\n\t\t},\n\n\t\t/**\n\t\t * The stored content/config blob for a grid item's catalog widget\n\t\t * (passed to its renderer).\n\t\t *\n\t\t * @param {object} item Layout item\n\t\t * @return {object} The widget def's `content`, or an empty object.\n\t\t */\n\t\twidgetContentFor(item) {\n\t\t\treturn widgetContentOf(this.findWidget(item))\n\t\t},\n\n\t\t/**\n\t\t * Resolve the effective title for a grid widget.\n\t\t *\n\t\t * A title-owning widget type (registry `ownsTitle`, e.g. `related`) keeps\n\t\t * its editable title in `content.title` — its config sub-form owns the\n\t\t * title field and the generic chrome title input is hidden. For those,\n\t\t * `content.title` MUST win over the chrome `def.title`, otherwise a\n\t\t * non-empty seed title (e.g. the default detail grid seeds the related\n\t\t * widget with `title: 'Related'`) permanently shadows the user's edit and\n\t\t * the title can never be changed. For every other type the chrome\n\t\t * `def.title` is authoritative, falling back to `content.title`. Empty\n\t\t * resolves to `undefined` so the widget renders its own default label.\n\t\t *\n\t\t * @param {object} item Layout item.\n\t\t * @return {string|undefined} The title to render, or undefined for the default.\n\t\t */\n\t\twidgetDisplayTitle(item) {\n\t\t\treturn widgetTitleOf(this.findWidget(item))\n\t\t},\n\n\t\t/**\n\t\t * Open the per-widget style/config editor for a grid item (cog click).\n\t\t *\n\t\t * @param {object} item Layout item to configure.\n\t\t */\n\t\tconfigureWidget(item) {\n\t\t\tthis.configWidgetId = item.widgetId\n\t\t\tthis.showWidgetConfig = true\n\t\t},\n\n\t\t/**\n\t\t * Persist the editor's changes onto the matching widget definition IN\n\t\t * PLACE (so an injected in-place manifest editor picks them up), close\n\t\t * the modal, and emit `widget-config-change` for consumers that persist\n\t\t * the page config themselves.\n\t\t *\n\t\t * @param {object} edited The widget object mutated by the editor.\n\t\t *\n\t\t * @event widget-config-change Emitted after a widget's config is saved.\n\t\t * @type {object}\n\t\t */\n\t\tonWidgetConfigSave(edited) {\n\t\t\tconst def = this.bodyGridWidgets.find((w) => w.id === this.configWidgetId)\n\t\t\tif (def) {\n\t\t\t\tif (edited.title !== undefined) {\n\t\t\t\t\tdef.title = edited.title\n\t\t\t\t}\n\t\t\t\tif (edited.content !== undefined) {\n\t\t\t\t\tdef.content = edited.content\n\t\t\t\t}\n\t\t\t\tdef.styleConfig = edited.styleConfig || {}\n\t\t\t}\n\t\t\tthis.showWidgetConfig = false\n\t\t\t/**\n\t\t\t * @event widget-config-change Emitted after a grid widget's config is\n\t\t\t * saved via the cog editor, or after the widget is removed (payload\n\t\t\t * null). Consumers persist the updated page/widget config.\n\t\t\t * @type {object|null}\n\t\t\t */\n\t\t\tthis.$emit('widget-config-change', def || null)\n\t\t},\n\n\t\t/**\n\t\t * Delete from inside the editor — drop the widget def + its layout\n\t\t * placement and close.\n\t\t *\n\t\t * @param {object} _w The widget the editor wants deleted (unused; routed\n\t\t * by `configWidgetId`).\n\t\t *\n\t\t * @event widget-config-change Emitted after the widget is removed.\n\t\t * @type {null}\n\t\t */\n\t\tonWidgetConfigDelete(_w) {\n\t\t\tconst id = this.configWidgetId\n\t\t\t// Drop from whichever pair backs the active grid: the explicit\n\t\t\t// `layout`/`widgets` props (manifest grid page) or the materialized\n\t\t\t// default auto-body arrays.\n\t\t\tconst widgetArr = this.hasGridLayout ? this.widgets : this.autoBodyWidgets\n\t\t\tconst layoutArr = this.hasGridLayout ? this.layout : this.autoBodyLayout\n\t\t\tif (Array.isArray(widgetArr)) {\n\t\t\t\tconst wIdx = widgetArr.findIndex((w) => w.id === id)\n\t\t\t\tif (wIdx !== -1) {\n\t\t\t\t\twidgetArr.splice(wIdx, 1)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (Array.isArray(layoutArr)) {\n\t\t\t\tconst lIdx = layoutArr.findIndex((l) => l.widgetId === id)\n\t\t\t\tif (lIdx !== -1) {\n\t\t\t\t\tlayoutArr.splice(lIdx, 1)\n\t\t\t\t}\n\t\t\t}\n\t\t\tthis.showWidgetConfig = false\n\t\t\tthis.$emit('widget-config-change', null)\n\t\t},\n\n\t\t/**\n\t\t * Push pageKind + objectUuid + register/schema context into the\n\t\t * reactive cnAiContext holder so the AI Chat Companion knows\n\t\t * what object the user is viewing.\n\t\t */\n\t\tpushAiContext() {\n\t\t\tif (!this.cnAiContext) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst resolved = this.resolvedSidebar || {}\n\t\t\tthis.cnAiContext.pageKind = 'detail'\n\t\t\tthis.cnAiContext.objectUuid = this.objectId ? String(this.objectId) : undefined\n\t\t\tthis.cnAiContext.registerSlug = resolved.register || this.register || this.sidebarProps?.register || undefined\n\t\t\tthis.cnAiContext.schemaSlug = resolved.schema || this.schema || this.resolvedObjectType || this.sidebarProps?.schema || undefined\n\t\t},\n\n\t\t/**\n\t\t * Refresh the provided `cnObjectContext` ref so detail-page abstract\n\t\t * widgets can resolve `@objectId` / `@object.<field>` filter tokens\n\t\t * against the current object, and hand back an object they saved\n\t\t * (`setObject`). No-op when setup() didn't expose the ref\n\t\t * (standalone / read-only mounts).\n\t\t */\n\t\tsyncObjectContext() {\n\t\t\tif (!this.cnObjectContextRef) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst resolved = this.resolvedSidebar || {}\n\t\t\tthis.cnObjectContextRef.value = {\n\t\t\t\tobjectId: this.objectId !== undefined && this.objectId !== null ? String(this.objectId) : null,\n\t\t\t\tobject: this.currentObject || null,\n\t\t\t\tregister: resolved.register || this.register || this.sidebarProps?.register || '',\n\t\t\t\tschema: resolved.schema || this.schema || this.resolvedObjectType || this.sidebarProps?.schema || '',\n\t\t\t\tsetObject: (object) => this.setCurrentObject(object),\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Pushes the resolved sidebar config into the\n\t\t * `objectSidebarState` provide/inject channel for the host\n\t\t * App's mounted `<CnObjectSidebar>` to consume.\n\t\t *\n\t\t * Object form fields take precedence over `sidebarProps` for\n\t\t * any field they declare; `sidebarProps` continues to fill\n\t\t * fields the Object form omits, preserving back-compat.\n\t\t *\n\t\t * Suppression (`show: false` or `enabled: false`) clears\n\t\t * `tabs` so a hidden detail page does not leak prior tab\n\t\t * state to the next mount.\n\t\t *\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tsyncSidebarState() {\n\t\t\tif (!this.hasExternalSidebar) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.warnIfDeprecatedSidebarShape()\n\t\t\t// During a background refresh (content stays in place — see\n\t\t\t// `hasLoadedOnce`), a transient loading-driven `enabled: false`\n\t\t\t// must NOT tear down the sidebar. Hosts commonly bind\n\t\t\t// `:sidebar=\"{ enabled: !loading }\"`, so without this guard every\n\t\t\t// refresh unmounts the host's CnObjectSidebar and re-fetches all\n\t\t\t// its sub-resources (files/notes/tags/tasks/audit). Skip the sync\n\t\t\t// while refreshing; the `loading` watcher re-syncs once it settles.\n\t\t\tif (this.loading && this.hasLoadedOnce) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst r = this.resolvedSidebar\n\t\t\t// A record held back for its access purpose shows nothing in the sidebar either.\n\t\t\tconst hasRecord = !this.objectNotFound && !this.awaitingObject && !this.contentWithheld\n\t\t\tif (this.sidebarActive && hasRecord && this.resolvedObjectType && this.objectId) {\n\t\t\t\tconst merged = this.mergeSidebarSources(r)\n\t\t\t\t// Seed `open` only on the inactive→active edge (first activation\n\t\t\t\t// of this object). Subsequent syncs must NOT clobber it, otherwise\n\t\t\t\t// the user's close/toggle would be undone on the next reactive\n\t\t\t\t// change. The shared channel owns `open` after seeding.\n\t\t\t\t//\n\t\t\t\t// `sidebarSeeded` used to be written here and never read, so\n\t\t\t\t// `open` went into assignSidebarState() on EVERY sync and the\n\t\t\t\t// paragraph above described behaviour the code did not have. A\n\t\t\t\t// detail page emits several syncs while it hydrates (the object\n\t\t\t\t// resolves, then the schema), and each one reset `open` to the\n\t\t\t\t// prop default — reclosing a sidebar the user had just opened.\n\t\t\t\t// Downstream that made openbuild's e2e race the UI: the tab was\n\t\t\t\t// clicked and its panel was hidden again before the assertion\n\t\t\t\t// ran (openbuild#268 — \"resolves 30×, hidden\").\n\t\t\t\tconst seedOpen = (this.sidebarSeeded === false)\n\t\t\t\tif (seedOpen) {\n\t\t\t\t\tthis.sidebarSeeded = true\n\t\t\t\t}\n\t\t\t\t// Manifest-driven open-enum tabs (forwarded to the host\n\t\t\t\t// app's mounted CnObjectSidebar via inject). When the\n\t\t\t\t// top-level `sidebarTabs` prop is non-empty it provides\n\t\t\t\t// the tabs (manifest pattern); otherwise the merged\n\t\t\t\t// `sidebar.tabs` / `sidebarProps.tabs` legacy paths win.\n\t\t\t\t// Falls back to `undefined` so the host's CnObjectSidebar\n\t\t\t\t// renders its built-in tab set.\n\t\t\t\tconst tabs = (this.sidebarTabs && this.sidebarTabs.length > 0)\n\t\t\t\t\t? this.sidebarTabs\n\t\t\t\t\t: merged.tabs\n\t\t\t\tthis.assignSidebarState({\n\t\t\t\t\tactive: true,\n\t\t\t\t\t// Omitted after the seeding sync — assignSidebarState()\n\t\t\t\t\t// only writes the keys it is given, so leaving `open` out\n\t\t\t\t\t// preserves whatever the shared channel currently holds.\n\t\t\t\t\t...(seedOpen ? { open: this.sidebarOpen } : {}),\n\t\t\t\t\tobjectType: this.resolvedObjectType,\n\t\t\t\t\tobjectId: this.objectId,\n\t\t\t\t\t// The loaded object, so coordinate-blind sidebar-tab widgets\n\t\t\t\t\t// (the `data` / `metadata` built-ins) receive it as `objectData`\n\t\t\t\t\t// — the hoisted sidebar is a CnAppRoot sibling, not a\n\t\t\t\t\t// CnDetailPage child, so it can't inject our object context.\n\t\t\t\t\tobject: this.resolvedObject || null,\n\t\t\t\t\t// The resolved schema OBJECT (not the slug), which the `data`\n\t\t\t\t\t// built-in (CnObjectDataWidget) needs alongside `objectData`.\n\t\t\t\t\tschemaObject: this.currentSchema || null,\n\t\t\t\t\ttitle: merged.title || this.title || '',\n\t\t\t\t\tsubtitle: merged.subtitle || this.subtitle || '',\n\t\t\t\t\tregister: merged.register || this.register || '',\n\t\t\t\t\tschema: merged.schema || this.schema || '',\n\t\t\t\t\thiddenTabs: merged.hiddenTabs || [],\n\t\t\t\t\ttabs,\n\t\t\t\t\t// ADR-019. `config.sidebar.useRegistry` reaches\n\t\t\t\t\t// `resolvedSidebar` intact — the Object branch returns\n\t\t\t\t\t// `{show, enabled, ...cfg}` — and survives\n\t\t\t\t\t// mergeSidebarSources(), which spreads `resolved` before\n\t\t\t\t\t// its own field list. It was simply never published here,\n\t\t\t\t\t// so the host's CnObjectSidebar always saw the prop default\n\t\t\t\t\t// `false` and rendered its BACKWARDS-COMPATIBLE branch: the\n\t\t\t\t\t// five hard-coded built-in tabs (files, notes, tags, tasks,\n\t\t\t\t\t// audit-trail) instead of one tab per registered provider.\n\t\t\t\t\t//\n\t\t\t\t\t// The declaration therefore did nothing at all. Measured on\n\t\t\t\t\t// decidiq run 32702211376, whose MeetingIntegrations page\n\t\t\t\t\t// sets `config.sidebar.useRegistry: true`:\n\t\t\t\t\t//\n\t\t\t\t\t// JS registry carries every expected provider passed\n\t\t\t\t\t// every component leaf has a tab + widget passed\n\t\t\t\t\t// OCS caps and JS registry agree passed\n\t\t\t\t\t// providers the server reports available 10\n\t\t\t\t\t// tabs the sidebar rendered 5\n\t\t\t\t\t//\n\t\t\t\t\t// Five, exactly, because that is the fallback branch. Every\n\t\t\t\t\t// app whose manifest opts a page into the registry sidebar\n\t\t\t\t\t// has been getting the built-ins.\n\t\t\t\t\t//\n\t\t\t\t\t// `excludeIntegrations` rides the same channel and was\n\t\t\t\t\t// dropped for the same reason; without it a page cannot\n\t\t\t\t\t// suppress a provider it does not want.\n\t\t\t\t\tuseRegistry: merged.useRegistry === true,\n\t\t\t\t\texcludeIntegrations: merged.excludeIntegrations || [],\n\t\t\t\t\t// The tab the address asks for, and the way back. The\n\t\t\t\t\t// sidebar lives outside this component's tree (the host\n\t\t\t\t\t// mounts it at NcContent level), so the address and the\n\t\t\t\t\t// tab strip can only meet on this shared channel.\n\t\t\t\t\t...(this.tabInAddress\n\t\t\t\t\t\t? { requestedTab: this.requestedTabFromAddress(), onTabChange: this.onSidebarTabChange }\n\t\t\t\t\t\t: {}),\n\t\t\t\t})\n\t\t\t} else {\n\t\t\t\tthis.assignSidebarState({ active: false, tabs: undefined })\n\t\t\t\t// Re-arm the seed so the next activation re-applies `sidebarOpen`.\n\t\t\t\tthis.sidebarSeeded = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Apply fields onto the shared `objectSidebarState` only when they\n\t\t * actually change. Writing the same logical value — notably a fresh\n\t\t * `[]` for `hiddenTabs` when none is configured — would otherwise put\n\t\t * a new array reference on the reactive object every call and trigger\n\t\t * a host re-render. That churn fed an infinite render loop: the host\n\t\t * App re-renders on the new `hiddenTabs` ref → its `<router-view>`\n\t\t * re-renders the routed detail page → the page's inline `:sidebar`\n\t\t * prop re-fires this sync → repeat. Arrays are compared shallowly so\n\t\t * an equivalent array is treated as unchanged.\n\t\t *\n\t\t * @param {object} fields Partial `objectSidebarState` to apply.\n\t\t */\n\t\tassignSidebarState(fields) {\n\t\t\tconst state = this.objectSidebarState\n\t\t\tfor (const key of Object.keys(fields)) {\n\t\t\t\tconst next = fields[key]\n\t\t\t\tconst cur = state[key]\n\t\t\t\tif (Array.isArray(next) && Array.isArray(cur)\n\t\t\t\t\t&& next.length === cur.length\n\t\t\t\t\t&& next.every((v, i) => v === cur[i])) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif (cur !== next) {\n\t\t\t\t\tstate[key] = next\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Merge the Object-form `sidebar` fields with the legacy\n\t\t * `sidebarProps` fields. Object form wins on conflict; first\n\t\t * conflict per instance fires a one-shot console.warn naming\n\t\t * the conflicting fields.\n\t\t *\n\t\t * @param {object} resolved Pre-normalised sidebar config object.\n\t\t * @return {object} Merged config — Object form fields override\n\t\t * sidebarProps fields where both are set.\n\t\t */\n\t\tmergeSidebarSources(resolved) {\n\t\t\tconst objectForm = (this.sidebar && typeof this.sidebar === 'object') ? this.sidebar : null\n\t\t\tconst props = this.sidebarProps || {}\n\t\t\tconst merged = {\n\t\t\t\ttitle: objectForm?.title ?? props.title,\n\t\t\t\tsubtitle: objectForm?.subtitle ?? props.subtitle,\n\t\t\t\tregister: objectForm?.register ?? props.register,\n\t\t\t\tschema: objectForm?.schema ?? props.schema,\n\t\t\t\thiddenTabs: objectForm?.hiddenTabs ?? props.hiddenTabs,\n\t\t\t\ttabs: objectForm?.tabs ?? props.tabs,\n\t\t\t}\n\t\t\tif (objectForm && !this.__sidebarConflictWarned) {\n\t\t\t\tconst overlap = ['title', 'subtitle', 'register', 'schema', 'hiddenTabs', 'tabs']\n\t\t\t\t\t.filter((field) => objectForm[field] !== undefined && props[field] !== undefined)\n\t\t\t\tif (overlap.length > 0) {\n\t\t\t\t\t// eslint-disable-next-line no-console -- a deliberate warning to the developer integrating this component\n\t\t\t\t\tconsole.warn(`[CnDetailPage] :sidebar (Object) and :sidebarProps both set ${overlap.join(', ')}; the :sidebar values win. Move all fields to :sidebar to silence this warning.`)\n\t\t\t\t\tthis.__sidebarConflictWarned = true\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Re-anchor `resolved` for the contract — callers expect\n\t\t\t// the returned object to reflect the normalised shape.\n\t\t\treturn { ...resolved, ...merged }\n\t\t},\n\n\t\t/**\n\t\t * Log a one-shot deprecation warning when the legacy Boolean\n\t\t * form of the `sidebar` prop is observed. Tracked via a\n\t\t * non-reactive instance flag so subsequent renders/toggles\n\t\t * don't spam the console.\n\t\t */\n\t\twarnIfDeprecatedSidebarShape() {\n\t\t\tif (typeof this.sidebar !== 'boolean') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.__sidebarBooleanWarned) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.__sidebarBooleanWarned = true\n\n\t\t\t// eslint-disable-next-line no-console -- a deliberate warning to the developer integrating this component\n\t\t\tconsole.warn('[CnDetailPage] :sidebar=Boolean is deprecated; pass an Object — see docs/components/cn-detail-page.md for the new shape.')\n\t\t},\n\t},\n}\n</script>\n\n<!-- Styles in css/detail-page.css -->\n"],"names":["_createElementBlock","_normalizeClass","_renderSlot","_createElementVNode","_openBlock","_Fragment","_renderList","_createBlock","_createVNode","_toDisplayString","_resolveDynamicComponent","_createTextVNode","_normalizeStyle","_mergeProps","_withCtx"],"mappings":"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"}
1
+ {"version":3,"file":"CnDetailPage.vue3.js","sources":["../../../../src/components/CnDetailPage/CnDetailPage.vue?vue&type=template&id=8cb7d154&lang.js"],"sourcesContent":["<!--\n CnDetailPage — Generic detail/overview page.\n\n The detail page equivalent of CnDashboardPage. Assembles a complete entity detail\n view from card-based sections, matching the dashboard visual style (rounded cards\n with headers). Uses a fixed declarative layout (no drag-and-drop).\n\n Features:\n - Header with back button, title, subtitle, and action buttons\n - Card-based content area (via default slot with CnDetailCard components)\n - Optional 12-column CSS grid layout mode (via layout + widgets props)\n - Optional right sidebar (CnObjectSidebar) for files, notes, tags, tasks, audit trail\n - Loading and error states\n - Edit mode toggle\n\n A simpler alternative to CnIndexPage for detail, stats, and overview pages.\n No multi-object table, no CRUD dialogs — just a clean layout with:\n - Header (title, description, icon, action buttons)\n - Loading / error / empty states\n - Statistics table section\n - Content sections via slots\n-->\n<template>\n\t<div\n\t\tclass=\"cn-detail-page\"\n\t\t:class=\"[{ 'cn-detail-page--with-side': showsSideColumn, 'cn-detail-page--card-buttons-regular': cardButtons === 'regular' }, boardClass]\"\n\t\tdata-testid=\"cn-detail-page\"\n\t\t:style=\"{ maxWidth: maxWidth }\">\n\t\t<!-- Skip link to the page's primary action. Off screen until it takes\n\t\t focus, so it costs a sighted mouse user nothing and saves a\n\t\t keyboard user every widget on the page. Targets the action the\n\t\t page DECLARES rather than a guessed first button: a skip link\n\t\t that lands somewhere arbitrary is worse than none, because it is\n\t\t the one link a keyboard user is told to trust. -->\n\t\t<a\n\t\t\tv-if=\"effectivePrimaryLabel && !objectNotFound\"\n\t\t\t:href=\"`#${primaryActionAnchorId}`\"\n\t\t\tclass=\"cn-detail-page__skip-link\"\n\t\t\tdata-testid=\"cn-detail-page-skip-link\"\n\t\t\t@click=\"onSkipToPrimaryAction\">\n\t\t\t{{ skipLinkLabel }}\n\t\t</a>\n\n\t\t<!-- Breadcrumb line (manifest `config.breadcrumb`): the list the record\n\t\t belongs to, then the record itself as the current crumb. -->\n\t\t<CnBreadcrumbs\n\t\t\tv-if=\"breadcrumbCrumbs.length > 0 && !objectNotFound && !contentWithheld && !isBoard\"\n\t\t\tclass=\"cn-detail-page__breadcrumbs\"\n\t\t\t:crumbs=\"breadcrumbCrumbs\"\n\t\t\t:rootText=\"breadcrumbCrumbs[0].icon === undefined\"\n\t\t\t:separator=\"breadcrumbSeparator\"\n\t\t\tdata-testid=\"cn-detail-page-breadcrumbs\" />\n\t\t<!-- Header -->\n\t\t<div\n\t\t\tv-if=\"!objectNotFound && !contentWithheld\"\n\t\t\tclass=\"cn-detail-page__header\"\n\t\t\t:class=\"{ 'cn-detail-page__header--card': headerCard && !isBoard, 'cn-detail-page__header--with-widget': headerWidgetDef !== null, 'cn-detail-page__header--board': isBoard }\"\n\t\t\tdata-testid=\"cn-detail-page-header\">\n\t\t\t<!-- Header (left block) — overridable via #header slot. Default\n\t\t\t renders the icon + title + description. The right-hand\n\t\t\t #actions slot remains separate so headerComponent and\n\t\t\t actionsComponent can be replaced independently. -->\n\t\t\t<!--\n\t\t\t\t@slot header\n\t\t\t\t@description Replace the entire left header block (icon + title + description).\n\t\t\t\t@binding {string} title Page title.\n\t\t\t\t@binding {string} description Subtitle / description.\n\t\t\t\t@binding {string} icon MDI icon name resolved by CnIcon.\n\t\t\t\t@binding {number} icon-size Icon size in pixels.\n\t\t\t-->\n\t\t\t<slot\n\t\t\t\tname=\"header\"\n\t\t\t\t:title=\"title\"\n\t\t\t\t:description=\"description\"\n\t\t\t\t:icon=\"icon\"\n\t\t\t\t:iconSize=\"iconSize\">\n\t\t\t\t<div class=\"cn-detail-page__header-left\">\n\t\t\t\t\t<!--\n\t\t\t\t\t\t@slot icon\n\t\t\t\t\t\t@description Replace only the header icon (keeping the lib's title +\n\t\t\t\t\t\tdescription rendering). Default content: a `<CnIcon>` resolved from\n\t\t\t\t\t\tthe `icon` prop.\n\t\t\t\t\t-->\n\t\t\t\t\t<slot name=\"icon\">\n\t\t\t\t\t\t<CnIcon\n\t\t\t\t\t\t\tv-if=\"icon && !isBoard\"\n\t\t\t\t\t\t\t:name=\"icon\"\n\t\t\t\t\t\t\t:size=\"iconSize\"\n\t\t\t\t\t\t\tclass=\"cn-detail-page__icon\" />\n\t\t\t\t\t</slot>\n\t\t\t\t\t<div class=\"cn-detail-page__header-text\">\n\t\t\t\t\t\t<!-- Type and status pills (manifest `config.typePill` /\n\t\t\t\t\t\t `config.statusPill`). What kind of record this is and\n\t\t\t\t\t\t where it stands, readable before the title. Nothing\n\t\t\t\t\t\t renders for a page that declares neither. -->\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tv-if=\"headerPills.length > 0 && !isBoard\"\n\t\t\t\t\t\t\tclass=\"cn-detail-page__pills\"\n\t\t\t\t\t\t\tdata-testid=\"cn-detail-page-pills\">\n\t\t\t\t\t\t\t<CnStatusBadge\n\t\t\t\t\t\t\t\tv-for=\"pill in headerPills\"\n\t\t\t\t\t\t\t\t:key=\"pill.key\"\n\t\t\t\t\t\t\t\t:label=\"pill.label\"\n\t\t\t\t\t\t\t\t:colorKey=\"pill.colorKey\"\n\t\t\t\t\t\t\t\t:colorMap=\"pill.colorMap\"\n\t\t\t\t\t\t\t\t:variant=\"pill.variant\"\n\t\t\t\t\t\t\t\t:data-testid=\"`cn-detail-page-pill-${pill.key}`\">\n\t\t\t\t\t\t\t\t<template v-if=\"pill.icon\" #icon>\n\t\t\t\t\t\t\t\t\t<CnIcon :name=\"pill.icon\" :size=\"14\" class=\"cn-detail-page__pill-icon\" />\n\t\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t</CnStatusBadge>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<!-- Type name as a small eyebrow above the record name, so\n\t\t\t\t\t\t the header reads \"the record\" not \"the kind of record\"\n\t\t\t\t\t\t (ADR-062). Only shown once the object resolves to a\n\t\t\t\t\t\t display name that differs from the type label. -->\n\t\t\t\t\t\t<p\n\t\t\t\t\t\t\tv-if=\"typeEyebrow && showTypeEyebrow && !isBoard\"\n\t\t\t\t\t\t\tclass=\"cn-detail-page__type-eyebrow\"\n\t\t\t\t\t\t\tdata-testid=\"cn-detail-page-type-eyebrow\">\n\t\t\t\t\t\t\t{{ typeEyebrow }}\n\t\t\t\t\t\t</p>\n\t\t\t\t\t\t<div v-if=\"displayTitle || showFollowToggle\" class=\"cn-detail-page__title-row\">\n\t\t\t\t\t\t\t<component\n\t\t\t\t\t\t\t\t:is=\"isBoard ? 'h1' : 'h2'\"\n\t\t\t\t\t\t\t\tv-if=\"displayTitle\"\n\t\t\t\t\t\t\t\tclass=\"cn-detail-page__title\"\n\t\t\t\t\t\t\t\t:title=\"displayTitle\">\n\t\t\t\t\t\t\t\t{{ displayTitle }}\n\t\t\t\t\t\t\t</component>\n\t\t\t\t\t\t\t<!-- The one Follow control beside the title (manifest `config.follow`):\n\t\t\t\t\t\t\t shown when the object carries `@self.watching`. There is no star:\n\t\t\t\t\t\t\t a favourite is a follow with notifications off. The board look\n\t\t\t\t\t\t\t draws it on a row of its own under the breadcrumb. -->\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tv-if=\"showFollowToggle && !isBoard\"\n\t\t\t\t\t\t\t\tclass=\"cn-detail-page__interactions\"\n\t\t\t\t\t\t\t\tdata-testid=\"cn-detail-page-interactions\">\n\t\t\t\t\t\t\t\t<CnFollowToggle v-bind=\"followToggleProps\" />\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<!--\n\t\t\t\t\t\t\t@slot translation-badge\n\t\t\t\t\t\t\t@description Replace the default `CnTranslatedBadge` rendered when the\n\t\t\t\t\t\t\tresolved object's `_translationMeta.translatedFrom` is set. Receives\n\t\t\t\t\t\t\t`:object=\"resolvedObject\"`. Default renders `<CnTranslatedBadge :object=\"resolvedObject\" />`\n\t\t\t\t\t\t\t(which auto-hides when there's nothing to surface). See\n\t\t\t\t\t\t\t`openspec/changes/cn-detail-translation-aware-surfacing`.\n\t\t\t\t\t\t-->\n\t\t\t\t\t\t<slot name=\"translation-badge\" :object=\"resolvedObject\">\n\t\t\t\t\t\t\t<CnTranslatedBadge v-if=\"resolvedObject\" :object=\"resolvedObject\" />\n\t\t\t\t\t\t</slot>\n\t\t\t\t\t\t<p v-if=\"description\" class=\"cn-detail-page__description\">\n\t\t\t\t\t\t\t{{ resolvedDescription }}\n\t\t\t\t\t\t</p>\n\t\t\t\t\t\t<!-- Identifying field chips (manifest `config.headerFields`):\n\t\t\t\t\t\t number, status, assignee, deadline. Absent without the key. -->\n\t\t\t\t\t\t<CnDetailHeaderChips\n\t\t\t\t\t\t\tv-if=\"headerFields.length > 0 && !isBoard\"\n\t\t\t\t\t\t\t:fields=\"headerFields\"\n\t\t\t\t\t\t\t:object=\"resolvedObject\"\n\t\t\t\t\t\t\t:schema=\"currentSchema\"\n\t\t\t\t\t\t\t:register=\"register\" />\n\t\t\t\t\t\t<!-- Declarative cross-schema summary chips (manifest\n\t\t\t\t\t\t `config.summaryAggregates`). Count/sum/avg over a\n\t\t\t\t\t\t related schema scoped to this object via @objectId. -->\n\t\t\t\t\t\t<CnSummaryAggregates\n\t\t\t\t\t\t\tv-if=\"summaryAggregates && summaryAggregates.length > 0\"\n\t\t\t\t\t\t\t:aggregates=\"summaryAggregates\" />\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</slot>\n\t\t\t<div class=\"cn-detail-page__header-actions\">\n\t\t\t\t<!-- The page's view switch (`views`), unless a greeting header\n\t\t\t\t widget on the page draws it. Its options control the view\n\t\t\t\t region in the body. -->\n\t\t\t\t<CnSegmentedControl\n\t\t\t\t\tv-if=\"showsPageViewSwitch\"\n\t\t\t\t\tclass=\"cn-page-view-switch\"\n\t\t\t\t\t:options=\"viewSwitchOptions\"\n\t\t\t\t\t:modelValue=\"activeViewId\"\n\t\t\t\t\t:ariaLabel=\"viewSwitchLabel\"\n\t\t\t\t\t:controls=\"viewRegionId\"\n\t\t\t\t\tdata-testid=\"cn-detail-page-view-switch\"\n\t\t\t\t\t@update:modelValue=\"selectView\" />\n\t\t\t\t<!-- With `userLayout`, the user arranges the chosen view for\n\t\t\t\t themselves: Arrange view makes its grid draggable, Done\n\t\t\t\t stores the arrangement, Reset view returns the view to the\n\t\t\t\t layout the app ships. -->\n\t\t\t\t<NcButton\n\t\t\t\t\tv-if=\"userLayout && viewUserLayoutOn && !viewIsEmpty\"\n\t\t\t\t\t:variant=\"arrangingView ? 'primary' : 'secondary'\"\n\t\t\t\t\tdata-testid=\"cn-detail-page-arrange-view\"\n\t\t\t\t\t@click=\"toggleArrangeView\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<Check v-if=\"arrangingView\" :size=\"20\" />\n\t\t\t\t\t\t<Pencil v-else :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t{{ arrangingView ? t('nextcloud-vue', 'Done') : t('nextcloud-vue', 'Arrange view') }}\n\t\t\t\t</NcButton>\n\t\t\t\t<NcButton\n\t\t\t\t\tv-if=\"viewUserLayoutOn && arrangingView\"\n\t\t\t\t\tvariant=\"tertiary\"\n\t\t\t\t\tdata-testid=\"cn-detail-page-reset-view\"\n\t\t\t\t\t@click=\"resetViewUserLayout()\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<Restore :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t{{ t('nextcloud-vue', 'Reset view') }}\n\t\t\t\t</NcButton>\n\t\t\t\t<!-- Next and previous inside the list this record was opened\n\t\t\t\t from. Rendered only when the address names that list: a\n\t\t\t\t record reached by a bare link offers neither, rather than\n\t\t\t\t stepping through an order nobody chose. Both ends say so\n\t\t\t\t instead of wrapping. -->\n\t\t\t\t<div\n\t\t\t\t\tv-if=\"listNavigation && listNavigation.available\"\n\t\t\t\t\tclass=\"cn-detail-page__list-nav\"\n\t\t\t\t\tdata-testid=\"cn-detail-page-list-nav\">\n\t\t\t\t\t<!-- With previousTo/nextTo the control is a link; a disabled\n\t\t\t\t\t end drops `to` so it renders as a disabled button. -->\n\t\t\t\t\t<NcButton\n\t\t\t\t\t\tvariant=\"tertiary\"\n\t\t\t\t\t\t:disabled=\"listNavigation.isFirst\"\n\t\t\t\t\t\t:to=\"listNavigation.isFirst ? undefined : (previousTo || undefined)\"\n\t\t\t\t\t\t:aria-label=\"previousRecordLabel\"\n\t\t\t\t\t\t:title=\"previousRecordLabel\"\n\t\t\t\t\t\tdata-testid=\"cn-detail-page-previous\"\n\t\t\t\t\t\t@click=\"onListStep('previous-record', previousTo, $event)\">\n\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t<ChevronLeft :size=\"20\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t</NcButton>\n\t\t\t\t\t<span class=\"cn-detail-page__list-nav-position\" data-testid=\"cn-detail-page-list-position\">\n\t\t\t\t\t\t{{ listPositionLabel }}\n\t\t\t\t\t</span>\n\t\t\t\t\t<NcButton\n\t\t\t\t\t\tvariant=\"tertiary\"\n\t\t\t\t\t\t:disabled=\"listNavigation.isLast\"\n\t\t\t\t\t\t:to=\"listNavigation.isLast ? undefined : (nextTo || undefined)\"\n\t\t\t\t\t\t:aria-label=\"nextRecordLabel\"\n\t\t\t\t\t\t:title=\"nextRecordLabel\"\n\t\t\t\t\t\tdata-testid=\"cn-detail-page-next\"\n\t\t\t\t\t\t@click=\"onListStep('next-record', nextTo, $event)\">\n\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t<ChevronRight :size=\"20\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t</NcButton>\n\t\t\t\t</div>\n\t\t\t\t<!-- The action the page declares as its primary one, and the\n\t\t\t\t target the skip link above lands on. -->\n\t\t\t\t<!-- With a \"what now\" card on the page the button lives in that\n\t\t\t\t card, next to the checklist that explains it, and not here\n\t\t\t\t as well: one primary button per page. -->\n\t\t\t\t<NcButton\n\t\t\t\t\tv-if=\"effectivePrimaryLabel && !showsNextStepCard && stagePrimaryOnTab\"\n\t\t\t\t\t:id=\"primaryActionAnchorId\"\n\t\t\t\t\tref=\"primaryActionButton\"\n\t\t\t\t\tvariant=\"primary\"\n\t\t\t\t\t:href=\"primaryActionHref || undefined\"\n\t\t\t\t\t:target=\"primaryActionHref ? primaryActionTarget : undefined\"\n\t\t\t\t\t:disabled=\"primaryActionDisabled\"\n\t\t\t\t\tdata-testid=\"cn-detail-page-primary-action\"\n\t\t\t\t\t@click=\"onPrimaryActionClick\">\n\t\t\t\t\t{{ effectivePrimaryLabel }}\n\t\t\t\t</NcButton>\n\t\t\t\t<!-- The record source's own verbs (a task's claim, hand back,\n\t\t\t\t complete), offered from the record's state. The ones a source\n\t\t\t\t marks `menu` join the Actions menu instead. -->\n\t\t\t\t<NcButton\n\t\t\t\t\tv-for=\"verb in sourceVerbButtons\"\n\t\t\t\t\t:key=\"`verb-${verb.id}`\"\n\t\t\t\t\tvariant=\"secondary\"\n\t\t\t\t\t:disabled=\"sourceVerbBusy\"\n\t\t\t\t\t:data-testid=\"`cn-detail-page-verb-${verb.id}`\"\n\t\t\t\t\t@click=\"runSourceVerb(verb)\">\n\t\t\t\t\t<template v-if=\"verb.icon\" #icon>\n\t\t\t\t\t\t<CnIcon v-if=\"typeof verb.icon === 'string'\" :name=\"verb.icon\" :size=\"20\" />\n\t\t\t\t\t\t<component :is=\"verb.icon\" v-else :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t{{ verb.label }}\n\t\t\t\t</NcButton>\n\t\t\t\t<!-- Quick actions (manifest `config.quickActions`): the few\n\t\t\t\t actions a handler reaches for on every record, always\n\t\t\t\t visible. At most three, so the row cannot grow back into\n\t\t\t\t the button wall the Actions menu replaced. -->\n\t\t\t\t<NcButton\n\t\t\t\t\tv-for=\"entry in quickActionEntries\"\n\t\t\t\t\t:key=\"`quick-${entry.id}`\"\n\t\t\t\t\tvariant=\"secondary\"\n\t\t\t\t\t:href=\"entry.href || undefined\"\n\t\t\t\t\t:target=\"entry.href ? (entry.linkTarget || undefined) : undefined\"\n\t\t\t\t\t:disabled=\"entry.disabled\"\n\t\t\t\t\t:data-testid=\"`cn-detail-page-quick-${entry.id}`\"\n\t\t\t\t\t@click=\"onPinnedActionClick(entry)\">\n\t\t\t\t\t<template v-if=\"entry.iconName\" #icon>\n\t\t\t\t\t\t<CnIcon :name=\"entry.iconName\" :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t{{ entry.label }}\n\t\t\t\t</NcButton>\n\t\t\t\t<!-- Declarative lifecycle transitions (manifest\n\t\t\t\t `config.lifecycleActions`). Status-gated; driven by the\n\t\t\t\t object's x-openregister-lifecycle. They land in the Actions\n\t\t\t\t menu below, since a record with several transitions filled\n\t\t\t\t the header with buttons. -->\n\t\t\t\t<CnLifecycleActions\n\t\t\t\t\tv-if=\"showsLifecycleActions\"\n\t\t\t\t\t:objectId=\"objectId\"\n\t\t\t\t\t:object=\"currentObject\"\n\t\t\t\t\t:config=\"lifecycleActions\"\n\t\t\t\t\t:register=\"register\"\n\t\t\t\t\t:schema=\"currentSchema\"\n\t\t\t\t\tdisplay=\"menu\"\n\t\t\t\t\t@entries=\"lifecycleMenuEntries = $event\"\n\t\t\t\t\t@transitioned=\"onTransitioned\"\n\t\t\t\t\t@reload=\"onLifecycleReload\" />\n\t\t\t\t<!-- Declarative header actions (#91 Wave 3): a manifest\n\t\t\t\t `config.headerActions[]` dispatched with visibleWhen gating —\n\t\t\t\t the object context this page provides drives `@objectId` /\n\t\t\t\t `@object.<field>` tokens + local predicates (the shillinq\n\t\t\t\t PaymentRunDetailActions contract).\n\n\t\t\t\t They render as entries in the Actions menu below, not as a\n\t\t\t\t row of buttons, which is what the manifest schema has always\n\t\t\t\t said they are and what CnIndexPage already does with the same\n\t\t\t\t key. A dossiq case declares twelve, and twelve buttons pushed\n\t\t\t\t the record's own title down to a truncated stub. This\n\t\t\t\t instance draws nothing itself; it owns the dialogs the\n\t\t\t\t actions open and feeds the menu through `entries`. -->\n\t\t\t\t<CnActionButtons\n\t\t\t\t\tv-if=\"effectiveHeaderActions.length > 0\"\n\t\t\t\t\t:actions=\"effectiveHeaderActions\"\n\t\t\t\t\tdisplay=\"menu\"\n\t\t\t\t\tdata-testid=\"cn-detail-page-header-actions\"\n\t\t\t\t\t@entries=\"menuHeaderActions = $event\"\n\t\t\t\t\t@created=\"onLifecycleReload\" />\n\t\t\t\t<!--\n\t\t\t\t\t@slot actions\n\t\t\t\t\t@description Right-hand action surface in the page header (typically NcActions\n\t\t\t\t\tor buttons). Renders alongside (not inside) the `header` slot. Receives the\n\t\t\t\t\tresolved object context so an actionsComponent (resolved by CnPageRenderer)\n\t\t\t\t\tcan act on the current record without re-fetching.\n\t\t\t\t\t@binding {object} object The resolved record.\n\t\t\t\t\t@binding {string} objectId The resolved object id.\n\t\t\t\t\t@binding {object} schema The resolved schema.\n\t\t\t\t\t@binding {string} objectType The resolved object type.\n\t\t\t\t\t@binding {object} store The effective object store.\n\t\t\t\t\t@binding {Function} openEditForm Opens this page's own record edit form. Lets an actions component put Edit inside ITS action cluster instead of leaving the page's button beside it — the page renders no button of its own once `showEditAction` is false, but this still opens the same dialog.\n\t\t\t\t\t@binding {boolean} canEditRecord Whether the page would render its own Edit button (`showEditAction` plus a record and schema to edit).\n\t\t\t\t-->\n\t\t\t\t<slot\n\t\t\t\t\tname=\"actions\"\n\t\t\t\t\t:object=\"resolvedObject\"\n\t\t\t\t\t:objectId=\"objectId\"\n\t\t\t\t\t:schema=\"currentSchema\"\n\t\t\t\t\t:objectType=\"resolvedObjectType\"\n\t\t\t\t\t:store=\"effectiveObjectStore\"\n\t\t\t\t\t:openEditForm=\"openEditForm\"\n\t\t\t\t\t:canEditRecord=\"canEditRecord\" />\n\t\t\t\t<!-- Record edit (ADR-062): the detail page's own way to change the\n\t\t\t\t record it is showing. Without it a `type:\"detail\"` page is\n\t\t\t\t read-only and the ONLY edit surface is the modal launched\n\t\t\t\t from the index table — which is why that modal could not\n\t\t\t\t simply be removed. Opens the same schema-driven CnFormDialog\n\t\t\t\t the index used, scoped to this record. -->\n\t\t\t\t<!-- Standalone unless `inlineActions` is set, in which case Edit\n\t\t\t\t joins the header actions above and lands in the Actions menu\n\t\t\t\t with them. Kept standalone by default so every existing\n\t\t\t\t consumer's header is byte-identical. -->\n\t\t\t\t<NcButton\n\t\t\t\t\tv-if=\"canEditRecord && !foldsEditIntoActions\"\n\t\t\t\t\tvariant=\"secondary\"\n\t\t\t\t\tdata-testid=\"cn-detail-page-edit\"\n\t\t\t\t\t:aria-label=\"editActionLabel\"\n\t\t\t\t\t@click=\"openEditForm\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<Pencil :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t{{ editActionLabel }}\n\t\t\t\t</NcButton>\n\t\t\t\t<!-- NB: no sidebar-toggle here — NcObjectSidebar/NcAppSidebar\n\t\t\t\t renders its own open toggle (`.app-sidebar__toggle`) when\n\t\t\t\t closed and an X (`.app-sidebar__close`) when open, so a custom\n\t\t\t\t button would duplicate it. -->\n\t\t\t\t<!-- In-app edit button (ADR-041): icon-only, self-wires from CnAppRoot. -->\n\t\t\t\t<CnBuildiqEditButton />\n\t\t\t\t<CnActionsMenu\n\t\t\t\t\t:showRefresh=\"menuShowsRefresh\"\n\t\t\t\t\t:refreshing=\"effectiveRefreshing\"\n\t\t\t\t\t:showRequestFeature=\"showRequestFeature && menuShowsHelpLinks\"\n\t\t\t\t\t:showReportBug=\"showReportBug && menuShowsHelpLinks\"\n\t\t\t\t\t:showDocumentation=\"showDocumentation && menuShowsHelpLinks\"\n\t\t\t\t\t:actionsMenuLabel=\"boardMenuLabel\"\n\t\t\t\t\t:variant=\"isBoard ? 'secondary' : undefined\"\n\t\t\t\t\t:trailingChevron=\"isBoard\"\n\t\t\t\t\t:placement=\"isBoard ? 'bottom-end' : 'bottom'\"\n\t\t\t\t\t:hasPrimaryItems=\"headerMenuGroups.length > 0 || showMarkUnread || showOpenSidebar\"\n\t\t\t\t\t:documentationUrl=\"documentationUrl\"\n\t\t\t\t\t:docsAnchor=\"resolvedPageId\"\n\t\t\t\t\t:documentationLabel=\"documentationLabel || undefined\"\n\t\t\t\t\t:widgetId=\"resolvedPageId\"\n\t\t\t\t\t:title=\"title\"\n\t\t\t\t\t:surface=\"`detail:${resolvedPageId}`\"\n\t\t\t\t\trefreshChannel=\"cn:page:refresh\"\n\t\t\t\t\ttestidBase=\"cn-detail-page\"\n\t\t\t\t\t@refresh=\"onHeaderRefresh\"\n\t\t\t\t\t@requestFeature=\"onHeaderRequestFeature\">\n\t\t\t\t\t<template v-if=\"headerMenuGroups.length || showMarkUnread || showOpenSidebar\" #primary-items>\n\t\t\t\t\t\t<!-- One block per group. A page that groups nothing has\n\t\t\t\t\t\t a single captionless group, and renders exactly the\n\t\t\t\t\t\t flat list it always did. -->\n\t\t\t\t\t\t<template v-for=\"(group, groupIndex) in headerMenuGroups\">\n\t\t\t\t\t\t\t<NcActionSeparator\n\t\t\t\t\t\t\t\tv-if=\"groupIndex > 0\"\n\t\t\t\t\t\t\t\t:key=\"`${group.key}-separator`\" />\n\t\t\t\t\t\t\t<NcActionCaption\n\t\t\t\t\t\t\t\tv-if=\"group.label\"\n\t\t\t\t\t\t\t\t:key=\"`${group.key}-caption`\"\n\t\t\t\t\t\t\t\t:name=\"group.label\"\n\t\t\t\t\t\t\t\t:data-testid=\"`cn-detail-page-action-group-${group.key}`\" />\n\t\t\t\t\t\t\t<template v-for=\"entry in group.entries\">\n\t\t\t\t\t\t\t\t<!-- An action that goes to a URL is a LINK. The browser\n\t\t\t\t\t\t\t\t then supplies middle-click, \"open in new tab\" and the\n\t\t\t\t\t\t\t\t semantics assistive tech announces, none of which a\n\t\t\t\t\t\t\t\t click handler can. -->\n\t\t\t\t\t\t\t\t<NcActionLink\n\t\t\t\t\t\t\t\t\tv-if=\"entry.href\"\n\t\t\t\t\t\t\t\t\t:key=\"`${entry.id}-link`\"\n\t\t\t\t\t\t\t\t\t:href=\"entry.href\"\n\t\t\t\t\t\t\t\t\t:target=\"entry.linkTarget || undefined\"\n\t\t\t\t\t\t\t\t\t:data-testid=\"entry.testid\"\n\t\t\t\t\t\t\t\t\t:data-cn-admin-tag=\"adminTagFor(group)\">\n\t\t\t\t\t\t\t\t\t<template v-if=\"entry.iconName || entry.iconClass\" #icon>\n\t\t\t\t\t\t\t\t\t\t<CnIcon v-if=\"entry.iconName\" :name=\"entry.iconName\" :size=\"20\" />\n\t\t\t\t\t\t\t\t\t\t<span v-else :class=\"entry.iconClass\" />\n\t\t\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t\t\t{{ entry.label }}\n\t\t\t\t\t\t\t\t</NcActionLink>\n\t\t\t\t\t\t\t\t<NcActionButton\n\t\t\t\t\t\t\t\t\tv-else\n\t\t\t\t\t\t\t\t\t:key=\"entry.id\"\n\t\t\t\t\t\t\t\t\t:data-testid=\"entry.testid\"\n\t\t\t\t\t\t\t\t\t:disabled=\"entry.disabled\"\n\t\t\t\t\t\t\t\t\t:aria-pressed=\"entry.pressed === null ? null : String(entry.pressed)\"\n\t\t\t\t\t\t\t\t\t:closeAfterClick=\"true\"\n\t\t\t\t\t\t\t\t\t:data-cn-admin-tag=\"adminTagFor(group)\"\n\t\t\t\t\t\t\t\t\t@click=\"entry.run()\">\n\t\t\t\t\t\t\t\t\t<template v-if=\"entry.iconName || entry.iconClass\" #icon>\n\t\t\t\t\t\t\t\t\t\t<CnIcon v-if=\"entry.iconName\" :name=\"entry.iconName\" :size=\"20\" />\n\t\t\t\t\t\t\t\t\t\t<span v-else :class=\"entry.iconClass\" />\n\t\t\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t\t\t{{ entry.label }}\n\t\t\t\t\t\t\t\t</NcActionButton>\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t</template>\n\t\t\t\t\t\t<!-- Mark as unread (record-unread-markers): offered when the\n\t\t\t\t\t\t object carries @self.unread. -->\n\t\t\t\t\t\t<NcActionButton\n\t\t\t\t\t\t\tv-if=\"showMarkUnread\"\n\t\t\t\t\t\t\tkey=\"mark-unread\"\n\t\t\t\t\t\t\tdata-testid=\"cn-detail-page-mark-unread\"\n\t\t\t\t\t\t\t:closeAfterClick=\"true\"\n\t\t\t\t\t\t\t@click=\"onMarkUnread\">\n\t\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t\t<EyeOffOutline :size=\"20\" />\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t{{ markUnreadLabel }}\n\t\t\t\t\t\t</NcActionButton>\n\t\t\t\t\t\t<!-- Board look: the sidebar's own open toggle is hidden (it sat\n\t\t\t\t\t\t over the header's last button, where the board draws none), so\n\t\t\t\t\t\t the menu opens it. -->\n\t\t\t\t\t\t<NcActionButton\n\t\t\t\t\t\t\tv-if=\"showOpenSidebar\"\n\t\t\t\t\t\t\tkey=\"open-sidebar\"\n\t\t\t\t\t\t\tdata-testid=\"cn-detail-page-open-sidebar\"\n\t\t\t\t\t\t\t:closeAfterClick=\"true\"\n\t\t\t\t\t\t\t@click=\"openSidebar\">\n\t\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t\t<DockRight :size=\"20\" />\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t{{ openSidebarLabel }}\n\t\t\t\t\t\t</NcActionButton>\n\t\t\t\t\t\t<!-- Divides the page's own actions from the help links\n\t\t\t\t\t\t below. With the help links switched off there is\n\t\t\t\t\t\t nothing below to divide from. -->\n\t\t\t\t\t\t<NcActionSeparator v-if=\"menuShowsHelpLinks\" />\n\t\t\t\t\t</template>\n\t\t\t\t</CnActionsMenu>\n\t\t\t</div>\n\t\t\t<!-- Row 2 of the board look's header (screens-detail-page-parity): the\n\t\t\t pills, the breadcrumb, a middle dot, the meta line and the Follow\n\t\t\t control, on the\n\t\t\t ground under the title. The breadcrumb does not render above the\n\t\t\t header in this look, so this is its only place. -->\n\t\t\t<div\n\t\t\t\tv-if=\"isBoard && showsHeaderRow2\"\n\t\t\t\tclass=\"cn-detail-page__header-row2\"\n\t\t\t\tdata-testid=\"cn-detail-page-header-row2\">\n\t\t\t\t<div\n\t\t\t\t\tv-if=\"headerPills.length > 0\"\n\t\t\t\t\tclass=\"cn-detail-page__pills\"\n\t\t\t\t\tdata-testid=\"cn-detail-page-pills\">\n\t\t\t\t\t<CnStatusBadge\n\t\t\t\t\t\tv-for=\"pill in headerPills\"\n\t\t\t\t\t\t:key=\"pill.key\"\n\t\t\t\t\t\t:label=\"pill.label\"\n\t\t\t\t\t\t:colorKey=\"pill.colorKey\"\n\t\t\t\t\t\t:colorMap=\"pill.colorMap\"\n\t\t\t\t\t\t:variant=\"pill.variant\"\n\t\t\t\t\t\t:data-testid=\"`cn-detail-page-pill-${pill.key}`\">\n\t\t\t\t\t\t<template v-if=\"pill.icon\" #icon>\n\t\t\t\t\t\t\t<CnIcon :name=\"pill.icon\" :size=\"14\" class=\"cn-detail-page__pill-icon\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t</CnStatusBadge>\n\t\t\t\t</div>\n\t\t\t\t<CnBreadcrumbs\n\t\t\t\t\tv-if=\"breadcrumbCrumbs.length > 0\"\n\t\t\t\t\tclass=\"cn-detail-page__breadcrumbs cn-detail-page__breadcrumbs--row2\"\n\t\t\t\t\t:crumbs=\"breadcrumbCrumbs\"\n\t\t\t\t\t:rootText=\"breadcrumbCrumbs[0].icon === undefined\"\n\t\t\t\t\t:separator=\"breadcrumbSeparator || '/'\"\n\t\t\t\t\tdata-testid=\"cn-detail-page-breadcrumbs\" />\n\t\t\t\t<span\n\t\t\t\t\tv-if=\"breadcrumbCrumbs.length > 0 && hasHeaderMeta\"\n\t\t\t\t\tclass=\"cn-detail-page__header-dot\"\n\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\tdata-testid=\"cn-detail-page-header-dot\">&middot;</span>\n\t\t\t\t<span\n\t\t\t\t\tv-if=\"headerMetaText\"\n\t\t\t\t\tclass=\"cn-detail-page__header-meta\"\n\t\t\t\t\tdata-testid=\"cn-detail-page-header-meta\">{{ headerMetaText }}</span>\n\t\t\t\t<CnDetailHeaderChips\n\t\t\t\t\tv-if=\"headerFields.length > 0\"\n\t\t\t\t\t:fields=\"headerFields\"\n\t\t\t\t\t:object=\"resolvedObject\"\n\t\t\t\t\t:schema=\"currentSchema\"\n\t\t\t\t\t:register=\"register\" />\n\t\t\t</div>\n\t\t\t<!-- Row 3 of the board look's header: the Follow control under the\n\t\t\t breadcrumb, clear of the title row and its actions (DqZaak\n\t\t\t \"Volgen\"). -->\n\t\t\t<div\n\t\t\t\tv-if=\"isBoard && showFollowToggle\"\n\t\t\t\tclass=\"cn-detail-page__header-row3\"\n\t\t\t\tdata-testid=\"cn-detail-page-header-row3\">\n\t\t\t\t<span\n\t\t\t\t\tv-if=\"presenceSentence\"\n\t\t\t\t\tclass=\"cn-detail-page__presence\"\n\t\t\t\t\tdata-testid=\"cn-detail-page-presence\">\n\t\t\t\t\t<CnPresenceAvatars :present=\"presentOthers\" :max=\"3\" :size=\"24\" />\n\t\t\t\t\t<span>{{ presenceSentence }}</span>\n\t\t\t\t\t<span aria-hidden=\"true\">·</span>\n\t\t\t\t</span>\n\t\t\t\t<span class=\"cn-detail-page__interactions\" data-testid=\"cn-detail-page-interactions\">\n\t\t\t\t\t<CnFollowToggle v-bind=\"followToggleProps\" />\n\t\t\t\t</span>\n\t\t\t</div>\n\t\t\t<!-- A widget inside the header (manifest `config.headerWidget`), on\n\t\t\t a row of its own under the title and the actions: the stages\n\t\t\t bars of the board's case card. Drawn without a card of its own,\n\t\t\t and taken out of the body grid. Nothing renders without the\n\t\t\t key. -->\n\t\t\t<div\n\t\t\t\tv-if=\"headerWidgetDef !== null\"\n\t\t\t\tclass=\"cn-detail-page__header-widget\"\n\t\t\t\tdata-testid=\"cn-detail-page-header-widget\">\n\t\t\t\t<!--\n\t\t\t\t\t@slot `widget-${headerWidgetDef.id}`\n\t\t\t\t\t@description The same per-widget slot the body grid and the side\n\t\t\t\t\tcolumn offer, for the widget placed in the header\n\t\t\t\t\t(`headerWidget`). Same name and same bindings.\n\t\t\t\t-->\n\t\t\t\t<slot\n\t\t\t\t\t:name=\"`widget-${headerWidgetDef.id}`\"\n\t\t\t\t\t:item=\"{ id: headerWidgetDef.id, widgetId: headerWidgetDef.id }\"\n\t\t\t\t\t:widget=\"titled(headerWidgetDef)\"\n\t\t\t\t\t:objectId=\"objectId\"\n\t\t\t\t\t:object=\"resolvedObject\"\n\t\t\t\t\t:objectData=\"resolvedObject\"\n\t\t\t\t\t:objectType=\"resolvedObjectType\"\n\t\t\t\t\t:register=\"register\"\n\t\t\t\t\t:schema=\"schema\">\n\t\t\t\t\t<CnDetailWidgetHost\n\t\t\t\t\t\t:widget=\"titled(headerWidgetDef)\"\n\t\t\t\t\t\tchrome=\"bare\"\n\t\t\t\t\t\t:objectId=\"objectId\"\n\t\t\t\t\t\t:object=\"currentObject\"\n\t\t\t\t\t\t:objectType=\"resolvedObjectType\"\n\t\t\t\t\t\t:schemaObject=\"currentSchema\"\n\t\t\t\t\t\t:register=\"register\"\n\t\t\t\t\t\t:schema=\"schema\"\n\t\t\t\t\t\t:store=\"effectiveObjectStore\"\n\t\t\t\t\t\t:surface=\"surface\"\n\t\t\t\t\t\t:integrationContext=\"effectiveIntegrationContext\"\n\t\t\t\t\t\t:hideEmpty=\"hideEmpty\"\n\t\t\t\t\t\t:cnRegistry=\"cnRegistry\"\n\t\t\t\t\t\t:availableWidgets=\"titledWidgets\"\n\t\t\t\t\t\t@geoSaved=\"onGeoSaved\"\n\t\t\t\t\t\t@openIntegration=\"onAutoBodyOpenIntegration\"\n\t\t\t\t\t\t@selectObject=\"onRelatedObjectSelect\" />\n\t\t\t\t</slot>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- The locked card, under the title, for ANY active lock — not only a\n\t\t remote one. It used to be suppressed when the current user held the\n\t\t lock, which meant the one person who could do something about a\n\t\t stale lock was also the only person the UI never told about it.\n\t\t The card carries the tone: an error for somebody else's lock, a\n\t\t neutral notice plus Unlock for your own. -->\n\t\t<CnLockedBanner\n\t\t\tv-if=\"lockState && lockState.locked.value\"\n\t\t\t:lockedBy=\"lockState.lockedBy.value\"\n\t\t\t:lockedByMe=\"lockState.lockedByMe.value\"\n\t\t\t:expiresAt=\"lockState.expiresAt.value\"\n\t\t\t:unlocking=\"releasingLock\"\n\t\t\t@unlock=\"onReleaseLock\" />\n\n\t\t<!-- The sentence a refused acquire left behind. Shown SEPARATELY from\n\t\t the banner above and not folded into it: a refused acquire does not\n\t\t refresh the cached object, so `@self.locked` is still whatever the\n\t\t last read said and the banner may not have appeared yet. This\n\t\t carries the server's own message, which names the holder. -->\n\t\t<p\n\t\t\tv-if=\"editLockRefusal\"\n\t\t\tclass=\"cn-detail-page__lock-refusal\"\n\t\t\trole=\"alert\"\n\t\t\tdata-testid=\"cn-detail-page-lock-refusal\">\n\t\t\t{{ editLockRefusal }}\n\t\t</p>\n\n\t\t<!-- The \"what now\" card (manifest `config.nextStep`): what this stage\n\t\t still needs, next to the button that takes the step. Above the\n\t\t body and the side column, at the full width of both. Hidden for a\n\t\t stage that declares no checklist. -->\n\t\t<CnNextStepCard\n\t\t\tv-if=\"showsNextStepCard\"\n\t\t\tclass=\"cn-detail-page__next-step\"\n\t\t\t:title=\"nextStepCard.title\"\n\t\t\t:titleTag=\"isBoard ? 'h2' : 'h3'\"\n\t\t\t:items=\"nextStepCard.items\"\n\t\t\t:after=\"nextStepCard.after\"\n\t\t\tdata-testid=\"cn-detail-page-next-step\">\n\t\t\t<template v-if=\"effectivePrimaryLabel\" #action>\n\t\t\t\t<NcButton\n\t\t\t\t\t:id=\"primaryActionAnchorId\"\n\t\t\t\t\tref=\"primaryActionButton\"\n\t\t\t\t\tvariant=\"primary\"\n\t\t\t\t\t:href=\"primaryActionHref || undefined\"\n\t\t\t\t\t:target=\"primaryActionHref ? primaryActionTarget : undefined\"\n\t\t\t\t\t:disabled=\"primaryActionDisabled\"\n\t\t\t\t\tdata-testid=\"cn-detail-page-primary-action\"\n\t\t\t\t\t@click=\"onPrimaryActionClick\">\n\t\t\t\t\t{{ effectivePrimaryLabel }}\n\t\t\t\t</NcButton>\n\t\t\t</template>\n\t\t</CnNextStepCard>\n\n\t\t<!-- The band (manifest `config.band`): a row of widgets across the whole\n\t\t page, above the body and the side column (PtPortal: the traffic\n\t\t figures and the start-page notice). Each entry takes `span`\n\t\t twelfths of the row and the row wraps. -->\n\t\t<div\n\t\t\tv-if=\"showsBand\"\n\t\t\tclass=\"cn-detail-page__band\"\n\t\t\tdata-testid=\"cn-detail-page-band\">\n\t\t\t<!-- The page filters (`config.pageFilters`), right-aligned above the\n\t\t\t band's widgets; each writes the page's workspace context. -->\n\t\t\t<div\n\t\t\t\tv-if=\"detailPageFilters.length > 0\"\n\t\t\t\tclass=\"cn-detail-page__page-filters\"\n\t\t\t\tdata-testid=\"cn-detail-page-page-filters\">\n\t\t\t\t<template v-for=\"pf in detailPageFilters\" :key=\"pf.key\">\n\t\t\t\t\t<div v-if=\"pf.control === 'segmented'\" class=\"cn-detail-page__page-filter\">\n\t\t\t\t\t\t<span v-if=\"pf.label\" class=\"cn-detail-page__page-filter-label\">{{ effectiveTranslate(pf.label) }}</span>\n\t\t\t\t\t\t<CnSegmentedControl\n\t\t\t\t\t\t\tsize=\"compact\"\n\t\t\t\t\t\t\tmode=\"toggle\"\n\t\t\t\t\t\t\t:options=\"pageFilterSegments(pf)\"\n\t\t\t\t\t\t\t:modelValue=\"workspaceContext[pf.key]\"\n\t\t\t\t\t\t\t:ariaLabel=\"pf.label ? effectiveTranslate(pf.label) : pf.key\"\n\t\t\t\t\t\t\t:data-testid=\"'cn-page-filter-' + pf.key\"\n\t\t\t\t\t\t\t@update:modelValue=\"onPageFilterChange(pf, $event)\" />\n\t\t\t\t\t</div>\n\t\t\t\t\t<label v-else class=\"cn-detail-page__page-filter\">\n\t\t\t\t\t\t<span v-if=\"pf.label\" class=\"cn-detail-page__page-filter-label\">{{ effectiveTranslate(pf.label) }}</span>\n\t\t\t\t\t\t<NcSelect\n\t\t\t\t\t\t\t:modelValue=\"selectedPageFilterOption(pf)\"\n\t\t\t\t\t\t\t:options=\"pageFilterSegments(pf)\"\n\t\t\t\t\t\t\t:clearable=\"pf.clearable === true\"\n\t\t\t\t\t\t\t:placeholder=\"pf.placeholder ? effectiveTranslate(pf.placeholder) : undefined\"\n\t\t\t\t\t\t\t:loading=\"pageFilterLoading[pf.key] === true\"\n\t\t\t\t\t\t\t:inputLabel=\"pf.label ? effectiveTranslate(pf.label) : pf.key\"\n\t\t\t\t\t\t\tlabel=\"label\"\n\t\t\t\t\t\t\t:data-testid=\"'cn-page-filter-' + pf.key\"\n\t\t\t\t\t\t\t@update:modelValue=\"onPageFilterChange(pf, $event)\" />\n\t\t\t\t\t</label>\n\t\t\t\t</template>\n\t\t\t</div>\n\t\t\t<div\n\t\t\t\tv-for=\"entry in bandWidgets\"\n\t\t\t\t:key=\"`band-${entry.widget.id}`\"\n\t\t\t\tclass=\"cn-detail-page__band-item\"\n\t\t\t\t:class=\"{ 'cn-detail-page__band-item--full': entry.span === 12 }\"\n\t\t\t\t:style=\"{ '--cn-band-span': entry.span }\"\n\t\t\t\t:data-testid=\"`cn-detail-page-band-${entry.widget.id}`\">\n\t\t\t\t<!--\n\t\t\t\t\t@slot `widget-${widget.id}`\n\t\t\t\t\t@description The same per-widget slot the body grid and the side\n\t\t\t\t\tcolumn offer, for a widget placed in the band.\n\t\t\t\t-->\n\t\t\t\t<slot\n\t\t\t\t\t:name=\"`widget-${entry.widget.id}`\"\n\t\t\t\t\t:item=\"{ id: entry.widget.id, widgetId: entry.widget.id }\"\n\t\t\t\t\t:widget=\"titled(entry.widget)\"\n\t\t\t\t\t:objectId=\"objectId\"\n\t\t\t\t\t:object=\"resolvedObject\"\n\t\t\t\t\t:objectData=\"resolvedObject\"\n\t\t\t\t\t:objectType=\"resolvedObjectType\"\n\t\t\t\t\t:register=\"register\"\n\t\t\t\t\t:schema=\"schema\">\n\t\t\t\t\t<CnDetailWidgetHost\n\t\t\t\t\t\t:widget=\"titled(entry.widget)\"\n\t\t\t\t\t\t:chrome=\"entry.chrome\"\n\t\t\t\t\t\t:showCardTitle=\"true\"\n\t\t\t\t\t\t:showActions=\"effectiveShowWidgetActions\"\n\t\t\t\t\t\t:objectId=\"objectId\"\n\t\t\t\t\t\t:object=\"currentObject\"\n\t\t\t\t\t\t:objectType=\"resolvedObjectType\"\n\t\t\t\t\t\t:schemaObject=\"currentSchema\"\n\t\t\t\t\t\t:register=\"register\"\n\t\t\t\t\t\t:schema=\"schema\"\n\t\t\t\t\t\t:store=\"effectiveObjectStore\"\n\t\t\t\t\t\t:surface=\"surface\"\n\t\t\t\t\t\t:integrationContext=\"effectiveIntegrationContext\"\n\t\t\t\t\t\t:hideEmpty=\"hideEmpty\"\n\t\t\t\t\t\t:cnRegistry=\"cnRegistry\"\n\t\t\t\t\t\t:availableWidgets=\"titledWidgets\" />\n\t\t\t\t</slot>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- Not-found state: the record behind the address does not exist. -->\n\t\t<div v-if=\"objectNotFound\" class=\"cn-detail-page__not-found\" data-testid=\"cn-detail-page-not-found\">\n\t\t\t<!--\n\t\t\t\t@slot not-found\n\t\t\t\t@description Replace the default not-found surface, shown instead of the\n\t\t\t\theader and body when the schema-driven fetch answers 404.\n\t\t\t\t@binding {object|string} target Router location of the back button.\n\t\t\t-->\n\t\t\t<slot name=\"not-found\" :target=\"notFoundTarget\">\n\t\t\t\t<NcEmptyContent :name=\"notFoundLabels.name\" :description=\"notFoundLabels.description\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<FileQuestionOutline :size=\"48\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t<template v-if=\"$router\" #action>\n\t\t\t\t\t\t<NcButton\n\t\t\t\t\t\t\tvariant=\"primary\"\n\t\t\t\t\t\t\tdata-testid=\"cn-detail-page-not-found-back\"\n\t\t\t\t\t\t\t:to=\"notFoundTarget\">\n\t\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t\t<ArrowLeft :size=\"20\" />\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t{{ notFoundButtonLabel }}\n\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t</template>\n\t\t\t\t</NcEmptyContent>\n\t\t\t</slot>\n\t\t</div>\n\n\t\t<!-- Access purpose (`accessPurpose`): nothing of the record shows until\n\t\t the reader has stated why they open it and the purpose is recorded. -->\n\t\t<div v-else-if=\"contentWithheld\" class=\"cn-detail-page__access-purpose\" data-testid=\"cn-detail-page-access-purpose\">\n\t\t\t<CnAccessPurposeDialog\n\t\t\t\tv-if=\"accessPurposeAsking\"\n\t\t\t\t:title=\"accessPurposeText('title')\"\n\t\t\t\t:description=\"accessPurposeText('description')\"\n\t\t\t\t:options=\"accessPurposeOptions\"\n\t\t\t\t:loading=\"accessPurposeBusy\"\n\t\t\t\t:error=\"accessPurposeError\"\n\t\t\t\t@submit=\"recordAccessPurpose\"\n\t\t\t\t@cancel=\"accessPurposeAsking = false\" />\n\t\t\t<NcEmptyContent\n\t\t\t\tv-else-if=\"accessPurposeRequired\"\n\t\t\t\t:name=\"t('nextcloud-vue', 'State a purpose to open this')\"\n\t\t\t\t:description=\"t('nextcloud-vue', 'This record holds special categories of personal data. It opens once your purpose is recorded.')\"\n\t\t\t\tdata-testid=\"cn-detail-page-access-purpose-refused\">\n\t\t\t\t<template #action>\n\t\t\t\t\t<NcButton variant=\"primary\" data-testid=\"cn-detail-page-access-purpose-ask\" @click=\"accessPurposeAsking = true\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'State a purpose') }}\n\t\t\t\t\t</NcButton>\n\t\t\t\t</template>\n\t\t\t</NcEmptyContent>\n\t\t\t<NcLoadingIcon v-else :size=\"32\" :name=\"loadingLabel\" />\n\t\t</div>\n\n\t\t<!-- Loading state -->\n\t\t<div v-else-if=\"showLoadingState\" class=\"cn-detail-page__loading\">\n\t\t\t<NcLoadingIcon :size=\"32\" />\n\t\t\t<span>{{ loadingLabel }}</span>\n\t\t</div>\n\n\t\t<!-- Error state -->\n\t\t<div v-else-if=\"error\" class=\"cn-detail-page__error\">\n\t\t\t<!--\n\t\t\t\t@slot error\n\t\t\t\t@description Replace the default `<NcEmptyContent>` error surface. Default\n\t\t\t\tcontent uses `errorMessage` + an MDI alert icon + an optional retry button.\n\t\t\t-->\n\t\t\t<slot name=\"error\">\n\t\t\t\t<NcEmptyContent :name=\"errorMessage\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<AlertCircleOutline :size=\"48\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t<template #action>\n\t\t\t\t\t\t<NcButton v-if=\"onRetry\" variant=\"primary\" @click=\"onRetry\">\n\t\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t\t<Refresh :size=\"20\" />\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t{{ retryLabel }}\n\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t\t<!--\n\t\t\t\t\t\t\t@slot error-actions\n\t\t\t\t\t\t\t@description Extra buttons rendered inside the default error\n\t\t\t\t\t\t\tsurface's action area, after the (optional) Retry button.\n\t\t\t\t\t\t-->\n\t\t\t\t\t\t<slot name=\"error-actions\" />\n\t\t\t\t\t</template>\n\t\t\t\t</NcEmptyContent>\n\t\t\t</slot>\n\t\t</div>\n\n\t\t<!-- Empty state -->\n\t\t<div v-else-if=\"empty\" class=\"cn-detail-page__empty\">\n\t\t\t<!--\n\t\t\t\t@slot empty\n\t\t\t\t@description Replace the default empty-state `<NcEmptyContent>` surface.\n\t\t\t-->\n\t\t\t<slot name=\"empty\">\n\t\t\t\t<NcEmptyContent :name=\"emptyLabel\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<InformationOutline :size=\"48\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t<template #action>\n\t\t\t\t\t\t<!--\n\t\t\t\t\t\t\t@slot empty-actions\n\t\t\t\t\t\t\t@description Buttons rendered inside the default empty-state\n\t\t\t\t\t\t\taction area.\n\t\t\t\t\t\t-->\n\t\t\t\t\t\t<slot name=\"empty-actions\" />\n\t\t\t\t\t</template>\n\t\t\t\t</NcEmptyContent>\n\t\t\t</slot>\n\t\t</div>\n\n\t\t<!-- Main content -->\n\t\t<div v-else class=\"cn-detail-page__body\">\n\t\t\t<div v-if=\"$slots['before-body'] || $slots['before-body']\" class=\"cn-detail-page__before-body\">\n\t\t\t\t<!--\n\t\t\t\t\t@slot before-body\n\t\t\t\t\t@description Content rendered at the top of the body, above the grid\n\t\t\t\t\tlayout / stats / schema-driven auto-body (Data + Related). Use it to\n\t\t\t\t\tsurface page-level widgets (e.g. a KPI/insights dashboard) in the body\n\t\t\t\t\tbelow the header+actions line without suppressing the auto Data/Related\n\t\t\t\t\tsections (which a `default` slot override would).\n\t\t\t\t\t@binding {object} object The resolved record.\n\t\t\t\t\t@binding {object} schema The resolved schema.\n\t\t\t\t\t@binding {string} objectType The resolved object type.\n\t\t\t\t\t@binding {string} objectId The resolved object id.\n\t\t\t\t\t@binding {object} store The effective object store.\n\t\t\t\t-->\n\t\t\t\t<slot\n\t\t\t\t\tname=\"before-body\"\n\t\t\t\t\t:object=\"resolvedObject\"\n\t\t\t\t\t:schema=\"currentSchema\"\n\t\t\t\t\t:objectType=\"resolvedObjectType\"\n\t\t\t\t\t:objectId=\"objectId\"\n\t\t\t\t\t:store=\"effectiveObjectStore\" />\n\t\t\t</div>\n\n\t\t\t<!-- Declarative in-body sections, `placement: \"before-body\"` —\n\t\t\t above the grid / stats / auto-body. -->\n\t\t\t<CnBodySections\n\t\t\t\tv-if=\"hasBodyWidgets\"\n\t\t\t\t:sections=\"bodyWidgets\"\n\t\t\t\t:context=\"sectionContext\"\n\t\t\t\tplacement=\"before-body\"\n\t\t\t\tdata-testid=\"cn-detail-body-sections-before-body\" />\n\n\t\t\t<!-- Adjustable widget grid (GridStack). The detail body is, at its\n\t\t\t core, a real drag/resize grid: by default it is seeded with the\n\t\t\t schema-driven Data + Related widgets (see `bodyGridLayout`), but in\n\t\t\t Buildiq edit mode widgets can be moved, resized, configured and\n\t\t\t added. Explicit `layout` + `widgets` props (manifest grid pages)\n\t\t\t feed the same engine, so hand-authored grid pages also become\n\t\t\t draggable. -->\n\t\t\t<!-- :key remounts the grid when in-app edit mode flips (ADR-041):\n\t\t\t the manifest is raw (non-reactive) until useManifestEditor\n\t\t\t observes it in place, so a grid mounted pre-edit never\n\t\t\t subscribed to the layout/widgets arrays — in-place pushes\n\t\t\t (Add widget) keep the same array identity and can't re-render\n\t\t\t it. One remount per edit flip re-subscribes it. -->\n\t\t\t<!-- One grid per section: the body grid, then the chosen view\n\t\t\t (`views`) in a region the view switch controls. A view grid is\n\t\t\t keyed on its view, so switching mounts the other view's grid. -->\n\t\t\t<CnDashboardGrid\n\t\t\t\tv-for=\"section in bodyGridSections\"\n\t\t\t\t:key=\"`cn-detail-grid-${section.key}-${section.editable ? 'editing' : 'live'}`\"\n\t\t\t\tv-bind=\"section.attrs\"\n\t\t\t\t:layout=\"section.layout\"\n\t\t\t\t:editable=\"section.editable\"\n\t\t\t\t:columns=\"12\"\n\t\t\t\t:columnOpts=\"bodyColumnOpts\"\n\t\t\t\t:float=\"gridFloat\"\n\t\t\t\tclass=\"cn-detail-page__grid\"\n\t\t\t\t@layoutChange=\"section.view ? onViewLayoutChange($event) : onBodyLayoutChange($event)\">\n\t\t\t\t<template #widget=\"{ item }\">\n\t\t\t\t\t<div\n\t\t\t\t\t\tclass=\"cn-detail-page__grid-item\"\n\t\t\t\t\t\t:class=\"{ 'cn-detail-page__grid-item--fit': !!item.sizeToContent }\"\n\t\t\t\t\t\t:aria-labelledby=\"showGridTitle(item) ? `widget-title-${item.id}` : undefined\">\n\t\t\t\t\t\t<!-- In-app edit overlay (ADR-041): a configure cog appears on\n\t\t\t\t\t\t EVERY grid widget while the page is in Buildiq edit mode,\n\t\t\t\t\t\t as it does on CnDashboardPage. It was gated on the type\n\t\t\t\t\t\t having a registered config form, which left a `custom`\n\t\t\t\t\t\t widget and an `integration` leaf with no cog at all: they\n\t\t\t\t\t\t could be dragged and resized but never configured or\n\t\t\t\t\t\t removed, because the modal's own Delete affordance is the\n\t\t\t\t\t\t only way off the page. The modal already handles a type\n\t\t\t\t\t\t without a form (title, icon, style, delete), so the gate\n\t\t\t\t\t\t bought nothing. -->\n\t\t\t\t\t\t<div v-if=\"showsCog(item)\" class=\"cn-detail-page__widget-edit\">\n\t\t\t\t\t\t\t<NcButton variant=\"tertiary\" :aria-label=\"t('nextcloud-vue', 'Configure widget')\" @click=\"configureWidget(item)\">\n\t\t\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t\t\t<Cog :size=\"18\" />\n\t\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t</NcButton>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<!-- Section heading ONLY for consumer-supplied bare-content slots.\n\t\t\t\t\t\t Built-in widgets (data / related / integration / catalog)\n\t\t\t\t\t\t render their own titled card header, so a grid <h3> here would\n\t\t\t\t\t\t duplicate the title above the widget. -->\n\t\t\t\t\t\t<component :is=\"isBoard ? 'h2' : 'h3'\"\n\t\t\t\t\t\t\tv-if=\"showGridTitle(item)\"\n\t\t\t\t\t\t\t:id=\"`widget-title-${item.id}`\"\n\t\t\t\t\t\t\tclass=\"cn-detail-page__widget-title\">\n\t\t\t\t\t\t\t{{ gridTitle(item) }}\n\t\t\t\t\t\t</component>\n\t\t\t\t\t\t<!--\n\t\t\t\t\t\t\t@slot `widget-${item.widgetId}`\n\t\t\t\t\t\t\t@description Per-widget slot whose name is `widget-<widgetId>`. Use\n\t\t\t\t\t\t\tit to inject custom widget content into a grid slot. Default\n\t\t\t\t\t\t\tfallback renders a registry widget: `type: 'data'` renders the\n\t\t\t\t\t\t\tschema-driven data widget; `type: 'related'` the related-objects\n\t\t\t\t\t\t\twidget; `type: 'integration'` resolves from the integration\n\t\t\t\t\t\t\tregistry; any other content-driven catalog type (stat / chart /\n\t\t\t\t\t\t\tdelta / gauge / object-list / …) renders its registered renderer.\n\n\t\t\t\t\t\t\tThe page's object context is bound alongside the widget\n\t\t\t\t\t\t\tdescriptor because a `type: \"custom\"` widget is not a template —\n\t\t\t\t\t\t\tCnPageRenderer mounts the mapped component as\n\t\t\t\t\t\t\t`<component v-bind=\"slotProps\" />`, so whatever THIS slot binds\n\t\t\t\t\t\t\tIS that widget's entire prop set. Binding only `item`/`widget`\n\t\t\t\t\t\t\ttherefore left every custom widget that declares `objectId`\n\t\t\t\t\t\t\tsitting on its own `default: ''` — and an empty id does not\n\t\t\t\t\t\t\terror, it widens the widget's OpenRegister filter to the whole\n\t\t\t\t\t\t\tcollection (decidiq#968: a voting-round panel filtering on\n\t\t\t\t\t\t\t`relations.motion: ''` listed every round in the instance, so a\n\t\t\t\t\t\t\tvote could be cast against another motion's round). Widgets that\n\t\t\t\t\t\t\tdeclare none of these props are unaffected — this is purely\n\t\t\t\t\t\t\tadditive.\n\n\t\t\t\t\t\t\t`objectId` is bound from the PROP, never read back off the loaded\n\t\t\t\t\t\t\trecord: `currentObject` / `resolvedObject` are themselves keyed by\n\t\t\t\t\t\t\t`objectId`, and the record is null for the first frames of a\n\t\t\t\t\t\t\tdetail page — the same \"the record has not arrived yet\" race\n\t\t\t\t\t\t\t`editFormAwaitingRecord` guards (#850). The route param reaches\n\t\t\t\t\t\t\tthe prop through CnPageRenderer, which maps `params.id` →\n\t\t\t\t\t\t\t`objectId`, so the id is there on the first render while `object`\n\t\t\t\t\t\t\tis not.\n\t\t\t\t\t\t\t@binding {object} item Layout item descriptor.\n\t\t\t\t\t\t\t@binding {object} widget Resolved widget definition.\n\t\t\t\t\t\t\t@binding {string|number} objectId This record's id. Unlike `object`\n\t\t\t\t\t\t\tit is present on the very first render, because it comes from the route.\n\t\t\t\t\t\t\t@binding {object} object The loaded record, or null while it is\n\t\t\t\t\t\t\tstill being fetched.\n\t\t\t\t\t\t\t@binding {object} objectData Alias of `object`, spelled the way widget\n\t\t\t\t\t\t\tcomponents declare the prop (`objectData`).\n\t\t\t\t\t\t\t@binding {string} objectType Resolved object type slug.\n\t\t\t\t\t\t\t@binding {string} register Register slug of this page.\n\t\t\t\t\t\t\t@binding {string} schema Schema slug of this page.\n\t\t\t\t\t\t-->\n\t\t\t\t\t\t<slot\n\t\t\t\t\t\t\t:name=\"`widget-${item.widgetId}`\"\n\t\t\t\t\t\t\t:item=\"item\"\n\t\t\t\t\t\t\t:widget=\"titled(findWidget(item))\"\n\t\t\t\t\t\t\t:objectId=\"objectId\"\n\t\t\t\t\t\t\t:object=\"resolvedObject\"\n\t\t\t\t\t\t\t:objectData=\"resolvedObject\"\n\t\t\t\t\t\t\t:objectType=\"resolvedObjectType\"\n\t\t\t\t\t\t\t:register=\"register\"\n\t\t\t\t\t\t\t:schema=\"schema\">\n\t\t\t\t\t\t\t<CnDetailWidgetHost\n\t\t\t\t\t\t\t\t:widget=\"titled(findWidget(item))\"\n\t\t\t\t\t\t\t\tchrome=\"card\"\n\t\t\t\t\t\t\t\tinGridCell\n\t\t\t\t\t\t\t\t:showCardTitle=\"showCardTitle(item)\"\n\t\t\t\t\t\t\t\t:showActions=\"effectiveShowWidgetActions\"\n\t\t\t\t\t\t\t\t:objectId=\"objectId\"\n\t\t\t\t\t\t\t\t:object=\"currentObject\"\n\t\t\t\t\t\t\t\t:objectType=\"resolvedObjectType\"\n\t\t\t\t\t\t\t\t:schemaObject=\"currentSchema\"\n\t\t\t\t\t\t\t\t:register=\"register\"\n\t\t\t\t\t\t\t\t:schema=\"schema\"\n\t\t\t\t\t\t\t\t:store=\"effectiveObjectStore\"\n\t\t\t\t\t\t\t\t:surface=\"surface\"\n\t\t\t\t\t\t\t\t:integrationContext=\"effectiveIntegrationContext\"\n\t\t\t\t\t\t\t\t:hideEmpty=\"hideEmpty\"\n\t\t\t\t\t\t\t\t:cnRegistry=\"cnRegistry\"\n\t\t\t\t\t\t\t\t:availableWidgets=\"titledBodyGridWidgets\"\n\t\t\t\t\t\t\t\t@geoSaved=\"onGeoSaved\"\n\t\t\t\t\t\t\t\t@openIntegration=\"onAutoBodyOpenIntegration\"\n\t\t\t\t\t\t\t\t@selectObject=\"onRelatedObjectSelect\" />\n\t\t\t\t\t\t</slot>\n\t\t\t\t\t</div>\n\t\t\t\t</template>\n\t\t\t</CnDashboardGrid>\n\n\t\t\t<!-- The chosen view has nothing to draw: say so in the region,\n\t\t\t never leave a blank area under the switch. -->\n\t\t\t<div\n\t\t\t\tv-if=\"viewIsEmpty\"\n\t\t\t\t:id=\"viewRegionId\"\n\t\t\t\trole=\"region\"\n\t\t\t\t:aria-label=\"activeView ? activeView.label : null\"\n\t\t\t\tclass=\"cn-page-view-region cn-page-view-region--empty\"\n\t\t\t\tdata-testid=\"cn-detail-page-view-empty\">\n\t\t\t\t<NcEmptyContent :description=\"viewEmptyText\">\n\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t<ViewDashboardOutline :size=\"48\" />\n\t\t\t\t\t</template>\n\t\t\t\t</NcEmptyContent>\n\t\t\t</div>\n\n\t\t\t<!-- Statistics table -->\n\t\t\t<div v-if=\"hasStats\" class=\"cn-detail-page__stats\">\n\t\t\t\t<!--\n\t\t\t\t\t@slot stats-header\n\t\t\t\t\t@description Replace the heading above the stats table. Default\n\t\t\t\t\tcontent: an `<h3>` showing `statsTitle`.\n\t\t\t\t-->\n\t\t\t\t<slot name=\"stats-header\">\n\t\t\t\t\t<component :is=\"isBoard ? 'h2' : 'h3'\" v-if=\"statsTitle\" class=\"cn-detail-page__section-title\">\n\t\t\t\t\t\t{{ resolvedStatsTitle }}\n\t\t\t\t\t</component>\n\t\t\t\t</slot>\n\t\t\t\t<table class=\"cn-detail-page__stats-table\">\n\t\t\t\t\t<thead v-if=\"statsColumns.length > 0\">\n\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t<th v-for=\"col in statsColumns\"\n\t\t\t\t\t\t\t\t:key=\"col.key\"\n\t\t\t\t\t\t\t\tscope=\"col\"\n\t\t\t\t\t\t\t\t:class=\"col.align ? 'cn-detail-page__stats-cell--' + col.align : ''\">\n\t\t\t\t\t\t\t\t{{ col.label }}\n\t\t\t\t\t\t\t</th>\n\t\t\t\t\t\t</tr>\n\t\t\t\t\t</thead>\n\t\t\t\t\t<tbody>\n\t\t\t\t\t\t<!--\n\t\t\t\t\t\t\t@slot stats-rows\n\t\t\t\t\t\t\t@description Replace the default rendering of the `statsRows`\n\t\t\t\t\t\t\tprop. Use when rows need custom formatting per column.\n\t\t\t\t\t\t-->\n\t\t\t\t\t\t<slot name=\"stats-rows\">\n\t\t\t\t\t\t\t<tr v-for=\"(row, index) in statsRows\" :key=\"index\" :class=\"{ 'cn-detail-page__stats-row--sub': row.indent }\">\n\t\t\t\t\t\t\t\t<td v-for=\"col in statsColumns\" :key=\"col.key\" :class=\"[row.indent ? 'cn-detail-page__stats-cell--indented' : '', col.align ? 'cn-detail-page__stats-cell--' + col.align : '']\">\n\t\t\t\t\t\t\t\t\t{{ row[col.key] !== undefined ? row[col.key] : '-' }}\n\t\t\t\t\t\t\t\t</td>\n\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t</slot>\n\t\t\t\t\t</tbody>\n\t\t\t\t</table>\n\t\t\t</div>\n\n\t\t\t<!-- Default vertical stacking mode. The schema-driven auto-body (Data +\n\t\t\t Related) is now rendered by the adjustable grid above (seeded by\n\t\t\t `bodyGridLayout`), so this branch only carries hand-authored\n\t\t\t default-slot content when no widget grid is active. -->\n\t\t\t<div v-else class=\"cn-detail-page__content\">\n\t\t\t\t<!-- Default content -->\n\t\t\t\t<div v-if=\"!hasBodyGrid\" class=\"cn-detail-page__content\">\n\t\t\t\t\t<!--\n\t\t\t\t\t\t@slot default\n\t\t\t\t\t\t@description Main body content rendered when no grid layout, no\n\t\t\t\t\t\tstats table, and no schema-driven auto-body apply. Use it to ship\n\t\t\t\t\t\ta hand-authored detail page.\n\t\t\t\t\t-->\n\t\t\t\t\t<slot />\n\t\t\t\t</div>\n\n\t\t\t\t<!-- Sections slot — additional content below stats -->\n\t\t\t\t<div v-if=\"$slots.sections\" class=\"cn-detail-page__sections\">\n\t\t\t\t\t<!--\n\t\t\t\t\t\t@slot sections\n\t\t\t\t\t\t@description Additional vertically-stacked content rendered below\n\t\t\t\t\t\tthe default body / stats table.\n\t\t\t\t\t-->\n\t\t\t\t\t<slot name=\"sections\" />\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t<!-- Declarative in-body sections, `placement: \"after-data\"` —\n\t\t\t below the data/auto-body, above the related-object lists. -->\n\t\t\t<CnBodySections\n\t\t\t\tv-if=\"hasBodyWidgets\"\n\t\t\t\t:sections=\"bodyWidgets\"\n\t\t\t\t:context=\"sectionContext\"\n\t\t\t\tplacement=\"after-data\"\n\t\t\t\tdata-testid=\"cn-detail-body-sections-after-data\" />\n\n\t\t\t<!-- Declarative related-object list sections (manifest\n\t\t\t `config.relatedCollections`). Rendered below the detail body;\n\t\t\t each section's filter is scoped to this object via @objectId.\n\t\t\t An optional relation-link button opens CnRelationLinkModal. -->\n\t\t\t<div\n\t\t\t\tv-if=\"(relatedCollections && relatedCollections.length > 0) || (relationLinks && relationLinks.length > 0)\"\n\t\t\t\tclass=\"cn-detail-page__related-collections\">\n\t\t\t\t<div v-if=\"relationLinks && relationLinks.length > 0\" class=\"cn-detail-page__relation-links\">\n\t\t\t\t\t<NcButton\n\t\t\t\t\t\tv-for=\"(link, li) in relationLinks\"\n\t\t\t\t\t\t:key=\"li\"\n\t\t\t\t\t\tclass=\"cn-detail-page__relation-link-button\"\n\t\t\t\t\t\t:data-testid=\"`cn-detail-relation-link-${li}`\"\n\t\t\t\t\t\t@click=\"openRelationLink(link)\">\n\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t<Plus :size=\"18\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t\t{{ link.label || t('nextcloud-vue', 'Link related object') }}\n\t\t\t\t\t</NcButton>\n\t\t\t\t</div>\n\t\t\t\t<CnRelatedCollections\n\t\t\t\t\tv-if=\"relatedCollections && relatedCollections.length > 0\"\n\t\t\t\t\t:collections=\"relatedCollections\"\n\t\t\t\t\t@rowClick=\"onRelatedRowClick\" />\n\t\t\t</div>\n\n\t\t\t<!-- Declarative in-body sections, `placement: \"after-related\"` —\n\t\t\t below the related-object lists. -->\n\t\t\t<CnBodySections\n\t\t\t\tv-if=\"hasBodyWidgets\"\n\t\t\t\t:sections=\"bodyWidgets\"\n\t\t\t\t:context=\"sectionContext\"\n\t\t\t\tplacement=\"after-related\"\n\t\t\t\tdata-testid=\"cn-detail-body-sections-after-related\" />\n\n\t\t\t<!-- Declarative in-body sections, `placement: \"end\"` (the default\n\t\t\t placement) — the last body content, above the footer. -->\n\t\t\t<CnBodySections\n\t\t\t\tv-if=\"hasBodyWidgets\"\n\t\t\t\t:sections=\"endPlacementSections\"\n\t\t\t\t:context=\"sectionContext\"\n\t\t\t\t:placement=\"null\"\n\t\t\t\tdata-testid=\"cn-detail-body-sections-end\" />\n\n\t\t\t<!-- Footer -->\n\t\t\t<div v-if=\"$slots.footer\" class=\"cn-detail-page__footer\">\n\t\t\t\t<!--\n\t\t\t\t\t@slot footer\n\t\t\t\t\t@description Footer surface rendered below the detail-page body. Use\n\t\t\t\t\tit for save/cancel button rows or status text.\n\t\t\t\t-->\n\t\t\t\t<slot name=\"footer\" />\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- Side column (manifest `config.sideColumn`): the record's facts as\n\t\t a column of cards beside the body. A sibling of the body, after it\n\t\t in the document, so it reads after the body and stacks under it\n\t\t when the page is too narrow for both. -->\n\t\t<aside\n\t\t\tv-if=\"showsSideColumn\"\n\t\t\tclass=\"cn-detail-page__side\"\n\t\t\t:aria-label=\"sideColumnLabel\"\n\t\t\tdata-testid=\"cn-detail-page-side\">\n\t\t\t<!-- Where a body widget puts its own cards about what is selected in\n\t\t\t it (`cnDetailSideColumn`); while it does, the generic cards\n\t\t\t below stand aside. -->\n\t\t\t<div :id=\"sideTakeoverId\"\n\t\t\t\tclass=\"cn-detail-page__side-takeover\"\n\t\t\t\tdata-testid=\"cn-detail-page-side-takeover\" />\n\t\t\t<div\n\t\t\t\tv-for=\"widget in sideClaims.length > 0 ? [] : sideColumnWidgets\"\n\t\t\t\t:key=\"`side-${widget.id}`\"\n\t\t\t\tclass=\"cn-detail-page__side-item\"\n\t\t\t\t:class=\"{ 'cn-detail-page__side-item--notice': isBoard && widget.type === 'banner', 'cn-detail-page__side-item--history': isBoard && widget.id === sideHistoryId, 'cn-detail-page__side-item--with-identifier': isBoard && widget.id === sideHistoryId && identifierLine !== '' }\"\n\t\t\t\t:data-testid=\"`cn-detail-page-side-${widget.id}`\">\n\t\t\t\t<!--\n\t\t\t\t\t@slot `widget-${widget.id}`\n\t\t\t\t\t@description The same per-widget slot the body grid offers,\n\t\t\t\t\tfor a widget placed in the side column. Same name and same\n\t\t\t\t\tbindings, so a custom widget placed here needs nothing it\n\t\t\t\t\twould not need in the grid. Default content renders the\n\t\t\t\t\twidget through `CnDetailWidgetHost` as a card.\n\t\t\t\t\t@binding {object} item `{ id, widgetId }`, for parity with the grid slot.\n\t\t\t\t\t@binding {object} widget The widget definition.\n\t\t\t\t\t@binding {string|number} objectId This record's id.\n\t\t\t\t\t@binding {object} object The loaded record, or null while it is still being fetched.\n\t\t\t\t\t@binding {object} objectData Alias of `object`.\n\t\t\t\t\t@binding {string} objectType Resolved object type slug.\n\t\t\t\t\t@binding {string} register Register slug of this page.\n\t\t\t\t\t@binding {string} schema Schema slug of this page.\n\t\t\t\t-->\n\t\t\t\t<slot\n\t\t\t\t\t:name=\"`widget-${widget.id}`\"\n\t\t\t\t\t:item=\"{ id: widget.id, widgetId: widget.id }\"\n\t\t\t\t\t:widget=\"titled(widget)\"\n\t\t\t\t\t:objectId=\"objectId\"\n\t\t\t\t\t:object=\"resolvedObject\"\n\t\t\t\t\t:objectData=\"resolvedObject\"\n\t\t\t\t\t:objectType=\"resolvedObjectType\"\n\t\t\t\t\t:register=\"register\"\n\t\t\t\t\t:schema=\"schema\">\n\t\t\t\t\t<component\n\t\t\t\t\t\t:is=\"widget.follows ? 'CnFollowingWidgetHost' : 'CnDetailWidgetHost'\"\n\t\t\t\t\t\t:widget=\"titled(widget)\"\n\t\t\t\t\t\tchrome=\"card\"\n\t\t\t\t\t\t:showCardTitle=\"true\"\n\t\t\t\t\t\t:showActions=\"effectiveShowWidgetActions\"\n\t\t\t\t\t\t:objectId=\"objectId\"\n\t\t\t\t\t\t:object=\"currentObject\"\n\t\t\t\t\t\t:objectType=\"resolvedObjectType\"\n\t\t\t\t\t\t:schemaObject=\"currentSchema\"\n\t\t\t\t\t\t:register=\"register\"\n\t\t\t\t\t\t:schema=\"schema\"\n\t\t\t\t\t\t:store=\"effectiveObjectStore\"\n\t\t\t\t\t\t:surface=\"surface\"\n\t\t\t\t\t\t:integrationContext=\"effectiveIntegrationContext\"\n\t\t\t\t\t\t:hideEmpty=\"hideEmpty\"\n\t\t\t\t\t\t:cnRegistry=\"cnRegistry\"\n\t\t\t\t\t\t:availableWidgets=\"titledWidgets\"\n\t\t\t\t\t\t@geoSaved=\"onGeoSaved\"\n\t\t\t\t\t\t@openIntegration=\"onAutoBodyOpenIntegration\"\n\t\t\t\t\t\t@selectObject=\"onRelatedObjectSelect\" />\n\t\t\t\t</slot>\n\t\t\t\t<!-- The identifier line that ends the History card in the board\n\t\t\t\t look (`config.identifierField`). -->\n\t\t\t\t<p\n\t\t\t\t\tv-if=\"isBoard && widget.id === sideHistoryId && identifierLine !== ''\"\n\t\t\t\t\tclass=\"cn-detail-page__side-identifier\"\n\t\t\t\t\tdata-testid=\"cn-detail-page-side-identifier\">\n\t\t\t\t\t{{ identifierLine }}\n\t\t\t\t</p>\n\t\t\t</div>\n\t\t</aside>\n\n\t\t<!-- Per-widget style/config editor (ADR-041). Opened by the per-widget\n\t\t configure cog while in Buildiq edit mode; the modal's own Delete\n\t\t button removes the widget from the grid. -->\n\t\t<CnWidgetStyleEditorModal\n\t\t\tv-if=\"showWidgetConfig\"\n\t\t\t:show=\"showWidgetConfig\"\n\t\t\t:widget=\"titled(configWidget)\"\n\t\t\t:deletable=\"true\"\n\t\t\t@save=\"onWidgetConfigSave\"\n\t\t\t@delete=\"onWidgetConfigDelete\"\n\t\t\t@close=\"showWidgetConfig = false\" />\n\n\t\t<!-- @slot form-dialog Replace the built-in create/edit form dialog (use CnFormDialog or CnAdvancedFormDialog). Deliberately the SAME slot name and the same scope as CnIndexPage's `form-dialog`, so one replacement component serves both pages and an app author learns one contract, not two. Covers both of this page's built-in dialogs — the create archetype's empty form and the record edit form — which are mutually exclusive, exactly as CnIndexPage's single slot covers create and edit. -->\n\t\t<!-- @binding {boolean} show Whether the form dialog is currently visible. -->\n\t\t<!-- @binding {?object} item The record being edited; in create mode the route-derived prefill, or null when there is nothing to prefill. -->\n\t\t<!-- @binding {object} schema The effective JSON schema driving the form. Transform this — rather than replacing the save path — to splice a runtime vocabulary into an enum. -->\n\t\t<!-- @binding {Function} confirm Persists the form data through the page's own save path and emits `created`/`edited`. Call this instead of saving in the replacement dialog, so a create or edit made there behaves exactly like one made in the built-in dialog. RESOLVES to a `{ success: true, data }` / `{ error }` result, which a replacement rendering its own CnFormDialog must hand back to that dialog's `setResult()`: CnFormDialog sets `loading` on submit and only `setResult` clears it, and `no-close` is bound to `loading`, so a replacement that ignores the result locks its own modal open on both success and failure. -->\n\t\t<!-- @binding {Function} close Closes the form dialog — in create mode this is the \"dismissed without saving\" path, which navigates back so the user is not stranded on an id-less page. -->\n\t\t<!--\n\t\t `show` and `confirm` are bound as PROPS, not left as listeners on\n\t\t the default children. A manifest-declared replacement is mounted by\n\t\t CnPageRenderer as `<component :is=… v-bind=\"slotProps\" />`, which\n\t\t binds props only — so a listener is unreachable from a manifest\n\t\t even in principle, and a replacement dialog could render and close\n\t\t but never save (openconnector#1150, fixed the same way on\n\t\t CnIndexPage).\n\n\t\t The two default children below are UNCHANGED and still wire their\n\t\t own `@confirm` / `@close` directly, so a page that passes no slot\n\t\t renders and behaves exactly as it did before this slot existed.\n\t\t-->\n\t\t<slot\n\t\t\tname=\"form-dialog\"\n\t\t\t:show=\"formDialogVisible\"\n\t\t\t:item=\"formDialogItem\"\n\t\t\t:schema=\"currentSchema\"\n\t\t\t:confirm=\"onFormDialogConfirm\"\n\t\t\t:close=\"onFormDialogClose\">\n\t\t\t<!-- Create archetype (ADR-062): a schema-bound `type:\"detail\"` page with\n\t\t\t no object id renders an empty create form for its schema instead of a\n\t\t\t blank page. Save POSTs the object and navigates to its detail route\n\t\t\t (or back); prefill comes from the route query. -->\n\t\t\t<CnFormDialog\n\t\t\t\tv-if=\"isCreateMode && currentSchema\"\n\t\t\t\tref=\"createFormDialog\"\n\t\t\t\t:schema=\"currentSchema\"\n\t\t\t\t:item=\"createPrefill\"\n\t\t\t\t:register=\"register\"\n\t\t\t\t:dialogTitle=\"title || undefined\"\n\t\t\t\t:excludeFields=\"excludeFields\"\n\t\t\t\t:includeFields=\"includeFields\"\n\t\t\t\t:fieldOverrides=\"fieldOverrides\"\n\t\t\t\t:size=\"formSize\"\n\t\t\t\t:columns=\"formColumns\"\n\t\t\t\t@confirm=\"onCreateFormConfirm\"\n\t\t\t\t@close=\"onCreateFormClose\" />\n\n\t\t\t<!-- Record edit form. Same component and same schema the index table's\n\t\t\t modal used; the difference is that it is reached from the record,\n\t\t\t with the record's own page around it.\n\n\t\t\t Waits for the RECORD as well as the schema. `currentObject` is a\n\t\t\t store read that is null until this page's own fetch lands, and a\n\t\t\t dialog opened before then shows an empty form for a record that\n\t\t\t has values — which the user can type into, and whose Save would\n\t\t\t PUT those blanks over the record. -->\n\t\t\t<CnFormDialog\n\t\t\t\tv-if=\"editFormOpen && currentSchema && !editFormAwaitingRecord\"\n\t\t\t\tref=\"editFormDialog\"\n\t\t\t\t:schema=\"currentSchema\"\n\t\t\t\t:item=\"currentObject\"\n\t\t\t\t:register=\"register\"\n\t\t\t\t:dialogTitle=\"editFormTitle\"\n\t\t\t\t:subtitle=\"editFormSubtitle\"\n\t\t\t\t:excludeFields=\"excludeFields\"\n\t\t\t\t:includeFields=\"includeFields\"\n\t\t\t\t:fieldOverrides=\"fieldOverrides\"\n\t\t\t\t:size=\"formSize\"\n\t\t\t\t:columns=\"formColumns\"\n\t\t\t\t@confirm=\"onEditFormConfirm\"\n\t\t\t\t@close=\"closeEditForm\" />\n\t\t</slot>\n\n\t\t<!-- A record-source verb that asks for values first (the tasks source's\n\t\t Edit): its own form, saved through the source. -->\n\t\t<CnFormDialog\n\t\t\tv-if=\"sourceVerbForm\"\n\t\t\tref=\"sourceVerbFormDialog\"\n\t\t\t:schema=\"sourceVerbForm.form.schema\"\n\t\t\t:item=\"sourceVerbForm.form.item || {}\"\n\t\t\t:dialogTitle=\"sourceVerbForm.label\"\n\t\t\t:fieldOverrides=\"sourceVerbForm.form.fieldOverrides || {}\"\n\t\t\t@confirm=\"onSourceVerbFormConfirm\"\n\t\t\t@close=\"sourceVerbForm = null\" />\n\n\t\t<!-- Relation-link modal (manifest `config.relationLinks`): async-search a\n\t\t target schema and patch a FK on the current object. -->\n\t\t<CnRelationLinkModal\n\t\t\tv-if=\"activeRelationLink\"\n\t\t\t:title=\"activeRelationLink.title || undefined\"\n\t\t\t:selectLabel=\"activeRelationLink.selectLabel || undefined\"\n\t\t\t:register=\"activeRelationLink.register\"\n\t\t\t:schema=\"activeRelationLink.schema\"\n\t\t\t:labelField=\"activeRelationLink.labelField || 'name'\"\n\t\t\t:allowCreate=\"activeRelationLink.allowCreate === true\"\n\t\t\t:currentType=\"resolvedObjectType\"\n\t\t\t:currentObject=\"currentObject || {}\"\n\t\t\t:fkField=\"activeRelationLink.fkField\"\n\t\t\t@linked=\"onRelationLinked\"\n\t\t\t@close=\"activeRelationLink = null\" />\n\t</div>\n</template>\n\n<script>\nimport { getCurrentUser } from '@nextcloud/auth'\nimport { subscribe, unsubscribe } from '@nextcloud/event-bus'\nimport { translatePlural as n, translate as t } from '@nextcloud/l10n'\nimport { NcActionButton, NcActionCaption, NcActionLink, NcActionSeparator, NcButton, NcEmptyContent, NcLoadingIcon, NcSelect } from '@nextcloud/vue'\nimport { computed, markRaw, provide, ref, watch } from 'vue'\nimport AlertCircleOutline from 'vue-material-design-icons/AlertCircleOutline.vue'\nimport ArrowLeft from 'vue-material-design-icons/ArrowLeft.vue'\nimport Check from 'vue-material-design-icons/Check.vue'\nimport ChevronLeft from 'vue-material-design-icons/ChevronLeft.vue'\nimport ChevronRight from 'vue-material-design-icons/ChevronRight.vue'\nimport Cog from 'vue-material-design-icons/Cog.vue'\nimport DockRight from 'vue-material-design-icons/DockRight.vue'\nimport EyeOffOutline from 'vue-material-design-icons/EyeOffOutline.vue'\nimport FileQuestionOutline from 'vue-material-design-icons/FileQuestionOutline.vue'\nimport InformationOutline from 'vue-material-design-icons/InformationOutline.vue'\nimport Pencil from 'vue-material-design-icons/Pencil.vue'\nimport Plus from 'vue-material-design-icons/Plus.vue'\nimport Refresh from 'vue-material-design-icons/Refresh.vue'\nimport Restore from 'vue-material-design-icons/Restore.vue'\nimport ViewDashboardOutline from 'vue-material-design-icons/ViewDashboardOutline.vue'\nimport CnAccessPurposeDialog from '../../dialogs/CnAccessPurposeDialog.vue'\nimport CnRelationLinkModal from '../../dialogs/CnRelationLinkModal.vue'\nimport CnWidgetStyleEditorModal from '../../dialogs/CnWidgetStyleEditorModal.vue'\nimport CnActionsMenu from '../CnActionsMenu/CnActionsMenu.vue'\nimport CnBodySections from '../CnBodySections/CnBodySections.vue'\nimport CnBuildiqEditButton from '../CnBuildiqEditButton/CnBuildiqEditButton.vue'\nimport CnDashboardGrid from '../CnDashboardGrid/CnDashboardGrid.vue'\nimport CnDetailWidgetHost from '../CnDetailWidgetHost/CnDetailWidgetHost.vue'\nimport CnFollowingWidgetHost from '../CnFollowingWidgetHost/CnFollowingWidgetHost.vue'\nimport CnFollowToggle from '../CnFollowToggle/CnFollowToggle.vue'\nimport CnFormDialog from '../CnFormDialog/CnFormDialog.vue'\nimport CnLifecycleActions from '../CnLifecycleActions/CnLifecycleActions.vue'\nimport CnLockedBanner from '../CnLockedBanner/CnLockedBanner.vue'\nimport CnNextStepCard from '../CnNextStepCard/CnNextStepCard.vue'\nimport CnPresenceAvatars from '../CnPresenceAvatars/CnPresenceAvatars.vue'\nimport CnRelatedCollections from '../CnRelatedCollections/CnRelatedCollections.vue'\nimport CnSegmentedControl from '../CnSegmentedControl/CnSegmentedControl.vue'\nimport CnSummaryAggregates from '../CnSummaryAggregates/CnSummaryAggregates.vue'\nimport CnTranslatedBadge from '../CnTranslatedBadge/CnTranslatedBadge.vue'\nimport CnDetailHeaderChips from './CnDetailHeaderChips.vue'\nimport { resolveIndexSource, resolveRecordSource } from '../../composables/indexSources.js'\nimport { useIntegrationRegistry } from '../../composables/useIntegrationRegistry.js'\nimport { normalizeLook } from '../../composables/useLook.js'\nimport { useObjectLock } from '../../composables/useObjectLock.js'\nimport { useObjectPresence } from '../../composables/useObjectPresence.js'\nimport { useObjectSubscription } from '../../composables/useObjectSubscription.js'\nimport { createRefLabelResolver } from '../../composables/useRefLabels.js'\nimport { gridLayout } from '../../mixins/gridLayout.js'\nimport { pageFilters } from '../../mixins/pageFilters.js'\nimport { pageViews } from '../../mixins/pageViews.js'\nimport { useObjectStore } from '../../store/index.js'\nimport { headerActionAsLink, resolveManifestHref, routeHref } from '../../utils/actionLink.js'\nimport { isAppInstalled } from '../../utils/appInstalled.js'\nimport { compactLayoutRows } from '../../utils/dashboardPlacement.js'\nimport { defaultDetailGrid } from '../../utils/defaultDetailGrid.js'\nimport {\n\tgroupMenuEntries,\n\tMAX_QUICK_ACTIONS,\n\tnormalisePinnedAction,\n\tonTab,\n\tpillCountSource,\n\tpillLook,\n\tresolveCountPill,\n\tresolveNextStep,\n\tresolvePill,\n\tstageEntry,\n\tstageOf,\n} from '../../utils/detailActionModel.js'\nimport { detailWidgetVisible, fetchVisibilityRows, visibilityContextRefs, widgetConditionData } from '../../utils/detailWidgetVisibility.js'\nimport { reportBindingProblems } from '../../utils/diagnostics.js'\nimport { fetchListTotal } from '../../utils/fetchFilterCounts.js'\nimport { fillTemplate, firstFilled } from '../../utils/fillTemplate.js'\nimport { cnGridCellStyle, hasGridRow } from '../../utils/grid.js'\nimport { isActivityWidget, resolveHeaderMeta } from '../../utils/headerMeta.js'\nimport { prefixUrl } from '../../utils/headers.js'\nimport { HEADING_LEVEL } from '../../utils/headingLevel.js'\nimport { interpolateUrlTokens } from '../../utils/interpolateUrlTokens.js'\nimport { followLinkClick } from '../../utils/linkNavigation.js'\nimport { patchStoredSelf } from '../../utils/patchStoredSelf.js'\nimport { setReadState } from '../../utils/recordInteractions.js'\nimport { saveEdited } from '../../utils/saveEdited.js'\nimport { slotRenders } from '../../utils/slotContent.js'\nimport { userDisplayName } from '../../utils/userDisplayName.js'\nimport { evaluateVisibleWhen, isLocallyDecidableVisibleWhen, readVisibleWhenPath } from '../../utils/visibleWhen.js'\nimport {\n\tisCardWidgetDef,\n\tisContentOnlyWidgetDef,\n\tisDataWidgetDef,\n\tisGeoWidgetDef,\n\tisRelatedWidgetDef,\n\tresolveRegistryRenderer,\n\twidgetContentOf,\n\twidgetTitleOf,\n} from '../../utils/widgetDispatch.js'\nimport { createWidgetSelection, WIDGET_SELECTION } from '../../utils/widgetSelection.js'\nimport { withTitlesWhen, withTitleWhen } from '../../utils/widgetTitleWhen.js'\nimport { CnActionButtons } from '../CnActionButtons/index.js'\nimport { CnBreadcrumbs } from '../CnBreadcrumbs/index.js'\nimport { CnIcon } from '../CnIcon/index.js'\nimport { CnStatusBadge } from '../CnStatusBadge/index.js'\nimport { getWidgetTypeEntry } from '../CnWidgetGrid/dashboardWidgetRegistry.js'\n\nimport '../CnWidgetGrid/registerDashboardWidgets.js'\n\n/**\n * Responsive column table for a detail body, measured against the GRID element.\n *\n * ONE threshold, and deliberately no steps between. `moveScale` rescales the\n * authored geometry into the new count, so an intermediate width turns a\n * 12-column record into fractional positions nobody placed: a `gridWidth: 3`\n * tile becomes 1 of 4, a `gridWidth: 9` panel becomes 3 of 4, and widgets land\n * left, centre or right depending purely on how the division rounded. That reads\n * as a broken page rather than a narrow one. A record is authored at 12 columns\n * or it is a single stacked column; the sizes in between are not designs.\n *\n * 1000px is where the 12-column geometry stops working rather than merely\n * tightening: it puts a `gridWidth: 3` side panel — Timeline, Flow runs — under\n * 250px, which is below its own title.\n *\n * `breakpointForWindow` is FALSE, unlike `getDashboardColumnOpts()`: a page in a\n * split pane sits in a narrow container on a wide window, so measuring the window\n * is measuring the wrong box and the grid would never reflow.\n *\n * `columnMax` is set EXPLICITLY. Above the top breakpoint GridStack assigns\n * `newColumn = columnOpts.columnMax`, and `column(undefined)` silently no-ops, so\n * a grid that once narrowed would never widen again.\n */\nconst DEFAULT_DETAIL_COLUMN_OPTS = Object.freeze({\n\tcolumnMax: 12,\n\tbreakpoints: [\n\t\t{ w: 1000, c: 1 },\n\t],\n\tlayout: 'moveScale',\n\tbreakpointForWindow: false,\n})\n\n/**\n * Where the default table stacks when a side column sits beside the body grid.\n *\n * The side column takes about 320px, so the grid it leaves is far narrower than\n * the page: 790px at a 1100px window. Stacking that at 1000px made a row of\n * small tiles (a KPI row of 5 or 6 stat tiles) impossible beside a side column.\n * 1000px exists to keep a `gridWidth: 3` side PANEL inside the grid readable; a\n * page with a real side column has no such panel, and 12 columns at 600px is\n * still 50px a column, enough for a tile. Below 600px it stacks as before.\n */\nconst SIDE_COLUMN_STACK_WIDTH = 600\n\n/**\n * Whether `opts` is the untouched default table, so a page that passed its own\n * `columnOpts` keeps exactly what it asked for.\n *\n * @param {object|null} opts The `columnOpts` value.\n * @return {boolean}\n */\nfunction isDefaultColumnOpts(opts) {\n\tconst breakpoints = opts && opts.breakpoints\n\treturn !!opts\n\t\t&& opts.columnMax === DEFAULT_DETAIL_COLUMN_OPTS.columnMax\n\t\t&& Array.isArray(breakpoints)\n\t\t&& breakpoints.length === 1\n\t\t&& breakpoints[0].w === DEFAULT_DETAIL_COLUMN_OPTS.breakpoints[0].w\n\t\t&& breakpoints[0].c === 1\n}\n\n/**\n * Event-bus channel a page-level refresh is announced on. The page's own\n * Actions menu emits it, the manifest `refresh` / `api-call` actions bump it,\n * and an app's own dialog emits it after a successful write.\n */\nconst PAGE_REFRESH_CHANNEL = 'cn:page:refresh'\n\n/** Surfaces understood by the pluggable integration registry (AD-19). */\nconst INTEGRATION_SURFACES = ['user-dashboard', 'app-dashboard', 'detail-page', 'single-entity']\n\n/** The session storage key that lists the records whose access purpose is recorded. */\nconst ACCESS_PURPOSE_SESSION_KEY = 'cn-access-purposes'\n\n/** Counter for unique side column slot ids across instances. */\nlet sideTakeoverCounter = 0\n\n/**\n * The records whose access purpose this browser session recorded. Empty when\n * session storage cannot be read (a private window, blocked site data).\n *\n * @return {Array<string>} The record keys.\n */\nfunction readGrantedPurposes() {\n\ttry {\n\t\tconst raw = window.sessionStorage.getItem(ACCESS_PURPOSE_SESSION_KEY)\n\t\tconst list = raw ? JSON.parse(raw) : []\n\t\treturn Array.isArray(list) ? list.filter((key) => typeof key === 'string') : []\n\t} catch {\n\t\treturn []\n\t}\n}\n\n/**\n * CnDetailPage — Generic detail/overview page.\n *\n * Supports two layout modes:\n * 1. **Default (vertical stacking):** Content provided via default slot, cards stack vertically.\n * 2. **Grid layout:** When `layout` and `widgets` props are provided, content renders in a\n * 12-column CSS grid with `#widget-{widgetId}` scoped slots. Same API as CnDashboardPage.\n *\n * Basic usage (vertical stacking)\n *\n * A simpler alternative to CnIndexPage for pages that display detail info,\n * statistics, charts, or card grids — without multi-object tables or CRUD\n * dialogs. Provides a consistent layout with header, loading/error/empty\n * states, a statistics table, and flexible content slots.\n *\n * Basic usage with stats table and content\n * ```vue\n * <CnDetailPage\n * title=\"Register Overview\"\n * description=\"Statistics and schema details\"\n * icon=\"DatabaseOutline\"\n * :stats-title=\"'Register Statistics'\"\n * :stats-columns=\"[\n * { key: 'type', label: 'Type' },\n * { key: 'total', label: 'Total' },\n * { key: 'size', label: 'Size' },\n * ]\"\n * :stats-rows=\"[\n * { type: 'Objects', total: 150, size: '2.4 MB' },\n * { type: 'Files', total: 42, size: '1.1 MB' },\n * ]\"\n * :loading=\"isLoading\">\n * <ChartGrid :data=\"chartData\" />\n * <SchemaCards :schemas=\"schemas\" />\n * </CnDetailPage>\n * ```\n *\n * Grid layout mode\n * ```vue\n * <CnDetailPage\n * title=\"Character Detail\"\n * :layout=\"[\n * { id: 1, widgetId: 'info', gridX: 0, gridY: 0, gridWidth: 8 },\n * { id: 2, widgetId: 'stats', gridX: 8, gridY: 0, gridWidth: 4 },\n * ]\"\n * :widgets=\"[\n * { id: 'info', title: 'Character Info' },\n * { id: 'stats', title: 'Statistics' },\n * ]\">\n * <template #widget-info=\"{ item, widget }\">\n * <CharacterInfoCard :character=\"character\" />\n * </template>\n * <template #widget-stats=\"{ item, widget }\">\n * <StatsCard :stats=\"character.stats\" />\n * </template>\n * ```\n *\n * With header actions and error handling\n * ```vue\n * <CnDetailPage\n * title=\"Schema Details\"\n * :error=\"hasError\"\n * error-message=\"Failed to load schema\"\n * :on-retry=\"loadSchema\">\n * <template #actions>\n * <NcButton @click=\"editSchema\">Edit</NcButton>\n * </template>\n * <DetailContent :schema=\"schema\" />\n * </CnDetailPage>\n * ```\n */\n/**\n * A header pill's label with the record's fields filled in (\"Version\n * {version} published\" reads \"Version 4 published\"). A field the record does\n * not have is left out rather than shown as a token.\n *\n * @param {string} label The translated label.\n * @param {object|null} object The record.\n * @return {string} The label to show.\n * @spec openspec/changes/detail-pill-label-tokens/specs/detail-page/spec.md#requirement-a-header-pill-label-can-name-the-records-fields\n */\nfunction pillLabelFilled(label, object) {\n\tif (typeof label !== 'string' || !label.includes('{')) {\n\t\treturn label\n\t}\n\tconst filled = fillTemplate(label, object)\n\tif (filled !== '') {\n\t\treturn filled\n\t}\n\treturn label.replace(/\\{[^{}]*\\}/g, '').replace(/\\s+/g, ' ').trim()\n}\n\nexport default {\n\tname: 'CnDetailPage',\n\n\tcomponents: {\n\t\tChevronLeft,\n\t\tCnAccessPurposeDialog,\n\t\tChevronRight,\n\t\tNcActionButton,\n\t\tNcActionCaption,\n\t\tNcActionLink,\n\t\tNcActionSeparator,\n\t\tNcButton,\n\t\tNcEmptyContent,\n\t\tNcLoadingIcon,\n\t\tCnIcon,\n\t\tAlertCircleOutline,\n\t\tArrowLeft,\n\t\tFileQuestionOutline,\n\t\tInformationOutline,\n\t\tRefresh,\n\t\tCnActionsMenu,\n\t\tCnBuildiqEditButton,\n\t\tCnLockedBanner,\n\t\tCnNextStepCard,\n\t\tCnDetailWidgetHost,\n\t\tCnFollowingWidgetHost,\n\t\tCnFormDialog,\n\t\tCnDashboardGrid,\n\t\tCnSegmentedControl,\n\t\tNcSelect,\n\t\tViewDashboardOutline,\n\t\tCnLifecycleActions,\n\t\tCnActionButtons,\n\t\tCnBreadcrumbs,\n\t\tCnStatusBadge,\n\t\tCnSummaryAggregates,\n\t\tCnDetailHeaderChips,\n\t\tCnFollowToggle,\n\t\tCnPresenceAvatars,\n\t\tDockRight,\n\t\tEyeOffOutline,\n\t\tCnRelatedCollections,\n\t\tCnBodySections,\n\t\tCnTranslatedBadge,\n\t\tCnWidgetStyleEditorModal,\n\t\tCnRelationLinkModal,\n\t\tCheck,\n\t\tCog,\n\t\tPlus,\n\t\tPencil,\n\t\tRestore,\n\t},\n\n\tmixins: [gridLayout, pageFilters, pageViews],\n\n\tinject: {\n\t\t/**\n\t\t * Consumer widget registry provided by CnAppRoot — the SAME injection\n\t\t * CnWidgetGrid reads. A consuming app registers its own components\n\t\t * here (hrmq registers `stat`, `chart`, `actions`,\n\t\t * `lifecycle-actions`, …), and per REQ-MVR-005 a consumer entry\n\t\t * OVERRIDES a built-in of the same name.\n\t\t *\n\t\t * Without this, a detail page could only render library widgets: an\n\t\t * app's own widget resolved on the CnWidgetGrid path and vanished on\n\t\t * this one, which is half of why the two paths disagreed.\n\t\t */\n\t\tcnRegistry: { default: () => ({}) },\n\t\tobjectSidebarState: { default: null },\n\t\t/**\n\t\t * Reactive AI context holder provided by CnAppRoot. This page\n\t\t * component writes pageKind, objectUuid, registerSlug, schemaSlug\n\t\t * in created() and watches for changes. Resets on beforeUnmount().\n\t\t */\n\t\tcnAiContext: { default: null },\n\t\t/**\n\t\t * Buildiq in-app edit state (ADR-041), a ref provided by CnAppRoot /\n\t\t * the edit button. When truthy, configurable grid widgets show a\n\t\t * configure cog. Defaults to null (no edit affordance) for standalone use.\n\t\t */\n\t\tcnEditingBody: { default: null },\n\t\t/**\n\t\t * Host translate function provided by CnAppRoot as\n\t\t * `cnTranslate: this.translate` (bound to the host app's id). The\n\t\t * manifest-authored page/section/widget titles are run through it.\n\t\t * Defaults to an identity function so an untranslated key renders\n\t\t * as itself. Record data (the resolved object's display name) is\n\t\t * NEVER translated — only manifest chrome is.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t\t/** The app's look, provided by CnAppRoot or CnPageRenderer (`nextcloud` or `board`). */\n\t\tcnLook: { default: 'nextcloud' },\n\t},\n\n\t/**\n\t * Offer the board look's tab list name to the tabs widget below this page\n\t * (`config.tabsLabel`, default \"<type> parts\"), and the records\n\t * `visibilityContext` names. Getter objects, so the widget reads them live.\n\t *\n\t * @return {object} The provided values.\n\t * @spec openspec/changes/screens-detail-page-parity/specs/detail-page-board-look/spec.md#requirement-folder-tabs-take-the-board-strip\n\t */\n\tprovide() {\n\t\tconst isBoard = computed(() => this.isBoard)\n\t\tconst tabsLabel = computed(() => this.resolvedTabsLabel)\n\t\tconst visibilityRows = computed(() => (this.visibilityRowsPending ? null : this.visibilityRows))\n\t\tconst canCheckItems = computed(() => this.recordCanCheckItems)\n\t\tconst sideAvailable = computed(() => this.showsSideColumn)\n\t\tconst canEditRecord = computed(() => this.canEditRecord)\n\t\tconst lastChange = computed(() => this.lastChange)\n\t\treturn {\n\t\t\t// The page's own record edit form, for a widget header button whose\n\t\t\t// action is `edit-record` (DqZaaktype \"Termijnen\" Bewerken).\n\t\t\tcnRecordEdit: {\n\t\t\t\tget available() {\n\t\t\t\t\treturn canEditRecord.value\n\t\t\t\t},\n\n\t\t\t\topen: () => this.openEditForm(),\n\t\t\t},\n\n\t\t\t// The record's newest create or update entry, for a widget that names\n\t\t\t// who changed it last (`recordLastChange`); null until read.\n\t\t\tcnRecordLastChange: lastChange,\n\n\t\t\t// Widget titles sit one level under the record title: h1 in the\n\t\t\t// board look, h2 in the Nextcloud look.\n\t\t\t[HEADING_LEVEL]: () => (this.isBoard ? 2 : 3),\n\t\t\t// The rows a list on this page shows and the one selected, for a side\n\t\t\t// card that follows them (side-card-follows-row).\n\t\t\t[WIDGET_SELECTION]: this.widgetSelection,\n\t\t\t// The side column, for a widget in the body that describes what is\n\t\t\t// selected in it (a files list's cards about the selected file).\n\t\t\t// `available` says whether the page draws a side column; `target`\n\t\t\t// is the selector to teleport the cards to; `claim(owner, on)`\n\t\t\t// puts the generic side cards aside while an owner shows its own.\n\t\t\tcnDetailSideColumn: {\n\t\t\t\tget available() {\n\t\t\t\t\treturn sideAvailable.value\n\t\t\t\t},\n\n\t\t\t\ttarget: `#${this.sideTakeoverId}`,\n\t\t\t\tclaim: (owner, on) => this.claimSideColumn(owner, on),\n\t\t\t},\n\n\t\t\t// The page's own look reaches the tabs, the cards and the activity\n\t\t\t// below it, also when it came from the `look` prop and not the app.\n\t\t\tcnLook: computed(() => (isBoard.value ? 'board' : 'nextcloud')),\n\t\t\tcnDetailTabsLabel: {\n\t\t\t\tget value() {\n\t\t\t\t\treturn tabsLabel.value\n\t\t\t\t},\n\t\t\t},\n\n\t\t\t// The record source's checklist writes, for a `checklist` widget.\n\t\t\tcnRecordChecklist: {\n\t\t\t\tget canCheck() {\n\t\t\t\t\treturn canCheckItems.value\n\t\t\t\t},\n\n\t\t\t\tcheck: (itemId, checked) => this.checkSourceItem(itemId, checked),\n\t\t\t},\n\n\t\t\t// The open body tab, reported by a `tabs` widget, so a side entry\n\t\t\t// with `tabs` shows only beside those tabs.\n\t\t\tcnDetailReportTab: (id) => {\n\t\t\t\tthis.activeBodyTab = typeof id === 'string' ? id : ''\n\t\t\t},\n\n\t\t\t// The records `visibilityContext` names, keyed `@context.<name>`, so a tab's\n\t\t\t// `visibleWhen` reads the case type as a grid widget's does. Null\n\t\t\t// while they are on their way.\n\t\t\tcnDetailVisibilityRows: {\n\t\t\t\tget value() {\n\t\t\t\t\treturn visibilityRows.value\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t},\n\n\tprops: {\n\t\t/** Page title */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * GridStack's `float` for the body grid (manifest `config.gridFloat`).\n\t\t * True (the default) keeps every widget on its authored row; false\n\t\t * packs widgets upward, so a size-to-content widget ends at its content\n\t\t * and the one below closes up (DqHandhaving: the violation card sits\n\t\t * 20px under the matrix).\n\t\t */\n\t\tgridFloat: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * How tall the labelled buttons in the page's card title rows stand in\n\t\t * the board look: `compact` (34px, setting and case-type pages) or\n\t\t * `regular` (the 40px control height, the case page's tab cards:\n\t\t * DqZaakContact \"Live gesprek\"). Manifest `config.cardButtons`.\n\t\t */\n\t\tcardButtons: {\n\t\t\ttype: String,\n\t\t\tdefault: 'compact',\n\t\t\tvalidator: (value) => ['compact', 'regular'].includes(value),\n\t\t},\n\n\t\t/**\n\t\t * NcDialog size for this page's create and edit form dialogs.\n\t\t *\n\t\t * CnFormDialog has taken a `size` since it shipped; this page never\n\t\t * passed one, so both its forms were stuck at `normal` however many\n\t\t * properties the schema declared.\n\t\t */\n\t\tformSize: {\n\t\t\ttype: String,\n\t\t\tdefault: 'normal',\n\t\t},\n\n\t\t/**\n\t\t * How many columns the create and edit forms flow their fields into.\n\t\t *\n\t\t * Pair `2` with `formSize: 'large'`, or the two columns are merely two\n\t\t * narrow ones. CnFormDialog collapses back to one column below 700px\n\t\t * on its own, so this is safe on a narrow viewport.\n\t\t */\n\t\tformColumns: {\n\t\t\ttype: Number,\n\t\t\tdefault: 1,\n\t\t\tvalidator: (value) => value === 1 || value === 2,\n\t\t},\n\n\t\t/**\n\t\t * Fields the create and edit forms ask for, in this order. Null asks for\n\t\t * every property the schema declares.\n\t\t *\n\t\t * Same three keys as CnIndexPage, so one manifest describes the record's\n\t\t * form wherever it opens rather than the list page and the detail page\n\t\t * each having their own.\n\t\t */\n\t\tincludeFields: {\n\t\t\ttype: Array,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Fields the create and edit forms never ask for. */\n\t\texcludeFields: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Per-field widget / label overrides for the create and edit forms. */\n\t\tfieldOverrides: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** Page description (shown below title) */\n\t\tdescription: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Optional MDI icon name (rendered via CnIcon) */\n\t\ticon: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Icon size in pixels */\n\t\ticonSize: {\n\t\t\ttype: Number,\n\t\t\tdefault: 28,\n\t\t},\n\n\t\t/** Whether the page is in a loading state */\n\t\tloading: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Message shown during loading */\n\t\tloadingLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Loading…'),\n\t\t},\n\n\t\t/**\n\t\t * Sidebar configuration. Accepts EITHER form:\n\t\t *\n\t\t * - **Boolean (legacy, deprecated):** `true` activates the\n\t\t * external sidebar, `false` deactivates. The first time this\n\t\t * form is observed per component instance a one-shot\n\t\t * `console.warn` is logged pointing at the migration path.\n\t\t * Continues to work in v1.x for back-compat.\n\t\t *\n\t\t * - **Object (preferred):** mirrors `CnIndexPage.sidebar` plus\n\t\t * the detail-specific fields previously on `sidebarProps`:\n\t\t * ```ts\n\t\t * {\n\t\t * show?: boolean, // default true; false suppresses sidebar\n\t\t * enabled?: boolean, // default true; false bypasses external sidebar\n\t\t * register?: string,\n\t\t * schema?: string,\n\t\t * hiddenTabs?: string[],\n\t\t * title?: string,\n\t\t * subtitle?: string,\n\t\t * tabs?: Array<TabDef>, // see manifest-abstract-sidebar\n\t\t * }\n\t\t * ```\n\t\t * When BOTH `sidebar` (Object) and `sidebarProps` are set\n\t\t * with overlapping fields, the Object form wins and a\n\t\t * `console.warn` lists the conflicting fields once per\n\t\t * component instance.\n\t\t *\n\t\t * @type {boolean | object}\n\t\t */\n\t\tsidebar: {\n\t\t\ttype: [Boolean, Object],\n\t\t\t// Object default (not Boolean `false`) so apps that omit the\n\t\t\t// prop don't trip the Boolean-form deprecation warning. Equivalent\n\t\t\t// to `false`: resolves to a suppressed/inactive sidebar.\n\t\t\tdefault: () => ({ enabled: false }),\n\t\t},\n\n\t\t/**\n\t\t * Initial open state of the object sidebar when it first activates.\n\t\t * Defaults to closed so the detail content fills the page; the user\n\t\t * opens it on demand via the header sidebar-toggle (and the sidebar's\n\t\t * own X closes it). After the first activation the open state is owned\n\t\t * by the shared `objectSidebarState` channel (toggle / close), so this\n\t\t * prop only seeds the initial value.\n\t\t */\n\t\tsidebarOpen: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** The registered object type slug for the sidebar */\n\t\tobjectType: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The object ID to display in the sidebar */\n\t\tobjectId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Whether a schema-bound page without an object id shows an empty\n\t\t * CnFormDialog for its schema instead of a blank page.\n\t\t *\n\t\t * Set from the manifest as `config.createForm`:\n\t\t * - `'auto'` (default) — heuristic: only when the page is schema-bound,\n\t\t * has no object id, and supplies no body of its own (no default slot,\n\t\t * no grid layout). Existing behaviour.\n\t\t * - `'never'` — never render it. Use when the body owns data entry, e.g.\n\t\t * a page whose CnObjectDataWidget (or a registry component) already\n\t\t * provides the form; this is what stops two form dialogs stacking.\n\t\t * - `'always'` — render it even when the page has a body, for a page\n\t\t * that deliberately pairs custom content with a create form.\n\t\t *\n\t\t * The dialog itself is always the generic, schema-driven CnFormDialog, so\n\t\t * whichever way it is summoned it follows the same OpenRegister/schema\n\t\t * form rules (required, readOnly, enum/$ref, `visibleWhen`).\n\t\t *\n\t\t * @type {'auto'|'never'|'always'}\n\t\t */\n\t\tcreateForm: {\n\t\t\ttype: String,\n\t\t\tdefault: 'auto',\n\t\t\tvalidator: (value) => ['auto', 'never', 'always'].includes(value),\n\t\t},\n\n\t\t/** Subtitle shown in the sidebar header */\n\t\tsubtitle: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Additional sidebar configuration (register, schema, hiddenTabs, title, subtitle) */\n\t\tsidebarProps: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Rendering surface forwarded to integration widgets in the\n\t\t * grid layout (widget defs with `type === 'integration'`).\n\t\t * Drives the AD-19 surface fallback. Defaults to\n\t\t * `'detail-page'` — the surface this page represents.\n\t\t *\n\t\t * @type {'user-dashboard'|'app-dashboard'|'detail-page'|'single-entity'}\n\t\t */\n\t\tsurface: {\n\t\t\ttype: String,\n\t\t\tdefault: 'detail-page',\n\t\t\tvalidator: (value) => INTEGRATION_SURFACES.includes(value),\n\t\t},\n\n\t\t/**\n\t\t * Object context forwarded to integration widgets:\n\t\t * `{ register, schema, objectId }`. When omitted it is derived\n\t\t * from `sidebarProps.register` / `sidebarProps.schema` (or\n\t\t * `objectType`) and `objectId`, so `CnFilesCard` etc. can\n\t\t * fetch the right object's sub-resources without extra wiring.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tintegrationContext: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Whether the page is in an error state */\n\t\terror: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Error message shown in error state */\n\t\terrorMessage: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'An error occurred'),\n\t\t},\n\n\t\t/** Callback for retry button in error state. If null, no retry button is shown. */\n\t\tonRetry: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Label for the retry button */\n\t\tretryLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Retry'),\n\t\t},\n\n\t\t/**\n\t\t * Router location the not-found state's button opens, shown when the\n\t\t * schema-driven fetch answers 404. Usually the record's list; null\n\t\t * sends the user to the app's root (`/`). CnPageRenderer fills it with\n\t\t * the index page the record was opened from.\n\t\t */\n\t\tnotFoundRoute: {\n\t\t\ttype: [Object, String],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Name of the `notFoundRoute` page, used in the button's \"Back to …\" label. */\n\t\tnotFoundRouteLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Whether the page has no data to show */\n\t\tempty: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Message shown when page is empty */\n\t\temptyLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'No data available'),\n\t\t},\n\n\t\t/** Title shown above the statistics table */\n\t\tstatsTitle: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Column definitions for the statistics table.\n\t\t * Each column: `{ key: string, label: string, align?: 'left'|'center'|'right' }`\n\t\t *\n\t\t * @type {Array<{ key: string, label: string, align: string }>}\n\t\t */\n\t\tstatsColumns: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Row data for the statistics table. Each row is an object keyed by\n\t\t * column keys. Set `indent: true` on a row for sub-row styling.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\tstatsRows: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Maximum width of the page content */\n\t\tmaxWidth: {\n\t\t\ttype: String,\n\t\t\tdefault: '1800px',\n\t\t},\n\n\t\t/**\n\t\t * Responsive `columnOpts` for the body grid, measured against the GRID's own\n\t\t * width rather than the window's — so a page rendered in a narrow container\n\t\t * (a split pane beside a list) reflows even though the viewport is wide.\n\t\t * Defaults to a table that stacks to one column at or below 1000px. Pass\n\t\t * `null` for a fixed 12-column grid at every size.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tcolumnOpts: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({ ...DEFAULT_DETAIL_COLUMN_OPTS }),\n\t\t},\n\n\t\t/**\n\t\t * Whether to auto-subscribe to live updates for this object.\n\t\t * Defaults to true. The page calls\n\t\t * `store.subscribe(type, objectId)` on mount and unsubscribes on\n\t\t * unmount, against the explicit `objectStore` prop when passed,\n\t\t * else (schema-driven / manifest mode: `register` + `schema` set)\n\t\t * against the library's default `useObjectStore()`. Set `false`\n\t\t * (manifest: `config.subscribe: false`) for read-only / archive\n\t\t * views.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tsubscribe: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Whether the schema-driven auto-body renders the related-objects widget\n\t\t * beneath the data widget. Defaults to true (back-compat). Set false on a\n\t\t * page that surfaces relations elsewhere (e.g. in the sidebar) to drop the\n\t\t * \"Related\" section from the body.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tshowRelatedObjects: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Optional explicit Pinia store instance to subscribe / lock\n\t\t * against; always wins when passed. When omitted, a page in\n\t\t * schema-driven mode (`register` + `schema` — the manifest\n\t\t * renderer path) falls back to the library's default\n\t\t * `useObjectStore()` so auto-subscribe still engages; the\n\t\t * resolution is defensive so consumer apps that haven't\n\t\t * activated Pinia yet (e.g. tests) don't crash.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tobjectStore: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Name of a registered entity source that reads ONE record (manifest\n\t\t * `config.entitySource`), for a record that is not a register/schema\n\t\t * object. The same registry and the same name as `CnIndexPage`'s\n\t\t * `entitySource`: `organisations` shows the OpenRegister organisation\n\t\t * whose uuid is `objectId`. The record feeds the header, pills, facts,\n\t\t * widgets and the `@objectId` / `@object.*` tokens exactly as a fetched\n\t\t * object does. Wins over `register` + `schema`. Empty (the default)\n\t\t * leaves the page on its object store.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tentitySource: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * App routes that replace a record source's own route for a verb, by\n\t\t * verb id (manifest `config.verbEndpoints`), such as\n\t\t * `{ \"complete\": \"/apps/dossiq/api/case-tasks/{id}/complete\" }` for an\n\t\t * app that checks something of its own before the engine completes the\n\t\t * task. `{id}` is the record id. Only read with `entitySource`.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tverbEndpoints: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * OpenRegister register slug. Pair with `schema` to opt into the\n\t\t * schema-driven mode: the page fuses the two into an internal\n\t\t * `${register}-${schema}` object-type slug, registers it on the\n\t\t * store, fetches the object identified by `objectId`, and auto-\n\t\t * renders `CnObjectDataWidget` + `CnObjectMetadataWidget` when no\n\t\t * default-slot content is supplied. Compatible with the existing\n\t\t * `objectType` prop — `objectType` wins on collision, so legacy\n\t\t * direct mounts are unaffected.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tregister: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * OpenRegister schema slug. See `register` for the schema-driven\n\t\t * contract.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tschema: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Deprecated and ignored: the star is gone, a favourite is a follow with\n\t\t * notifications off (OpenRegister `merge-follow-and-favourites`). Kept so a\n\t\t * manifest that still sets `config.favourite` stays valid; use `follow`.\n\t\t *\n\t\t * @deprecated Use `follow`.\n\t\t * @type {boolean|null}\n\t\t */\n\t\tfavourite: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The board look's follow row (manifest `config.followRow`):\n\t\t * `presence: true` says who else has the record open now, with their\n\t\t * initials, before the Follow control (DqZaak \"Anouk Bakker kijkt nu\n\t\t * ook naar deze zaak\"); `count: false` draws no follower count.\n\t\t *\n\t\t * @type {{presence?: boolean, count?: boolean}|null}\n\t\t * @spec openspec/changes/follow-row-presence/specs/board-follow-row/spec.md#requirement-the-follow-row-can-say-who-else-is-looking\n\t\t */\n\t\tfollowRow: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Follow toggle beside the title (manifest `config.follow`). Automatic\n\t\t * (null): shown when the object carries `@self.watching`. `false`\n\t\t * removes it, and the read then no longer asks for `@self.can`.\n\t\t *\n\t\t * @type {boolean|null}\n\t\t */\n\t\tfollow: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** False when the register sends no change notifications to followers; the Follow tooltip then says so. */\n\t\tfollowNotifies: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Send `PUT .../read-state` once per page load after the object has\n\t\t * rendered, when it carries `@self.unread`. `false` (manifest\n\t\t * `config.markRead: false`) keeps the page from marking anything read.\n\t\t */\n\t\tmarkRead: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** After Mark as unread, go back one step in the router history. Default: stay, and emit `marked-unread`. */\n\t\tmarkUnreadNavigatesBack: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Extra `_extend[]` values for the object read (for markers OpenRegister\n\t\t * returns only on request). `@self.can` is added whenever the Follow\n\t\t * toggle can render.\n\t\t *\n\t\t * @type {string[]}\n\t\t */\n\t\textend: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Hide valueless fields on the auto-rendered data widget, instead of\n\t\t * showing them with an em dash. See `CnObjectDataWidget`'s `hide-empty`.\n\t\t *\n\t\t * Set this from the manifest (`config.hideEmpty`) for a discriminated\n\t\t * supertype — one schema holding several variants, where each object only\n\t\t * carries the fields its variant uses. A widget that declares its own\n\t\t * `content.hideEmpty` still wins over this page-level default.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\thideEmpty: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Tab definitions forwarded to the host App's `CnObjectSidebar`\n\t\t * via the injected `objectSidebarState`. Each entry follows the\n\t\t * `CnObjectSidebar` tab shape (see that component for the exact\n\t\t * fields). When empty (default) the sidebar falls back to its\n\t\t * own default tab set. The actual `<CnObjectSidebar>` is rendered\n\t\t * at `NcContent` level by `CnAppRoot` (ADR-017 — external sidebar\n\t\t * pattern); this page only publishes the tabs.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\tsidebarTabs: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Documentation link surfaced in the page-header Actions menu. When\n\t\t * set, the menu renders a \"Documentation\" entry that opens this URL\n\t\t * in a new tab. The Refresh and Request-a-feature entries render by\n\t\t * default regardless of this value.\n\t\t */\n\t\tdocumentationUrl: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Label for the Documentation entry in the header menu. */\n\t\tdocumentationLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Stable id for the page-header Actions menu — forwarded as the\n\t\t * `widgetId` on the `@refresh` / `cn:page:refresh` payloads and as\n\t\t * the `surface: \"detail:<pageId>\"` on the feature-request modal.\n\t\t * Falls back to a slugified title when empty.\n\t\t */\n\t\tpageId: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Whether the user arranges each view (`views`) for themselves. On,\n\t\t * the header offers Arrange view next to the view switch; the\n\t\t * arrangement is stored per view and per user (the server, mirrored\n\t\t * in the browser) and a reset returns the view to its manifest\n\t\t * layout. Off (the default), a view renders as the manifest says.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tuserLayout: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Whether the Refresh entry renders in the page-header menu.\n\t\t * Tri-state:\n\t\t * - `true` / `false` — force it on or off.\n\t\t * - `null` (the default) — **auto**: show Refresh only when it will\n\t\t * do something, i.e. a consumer attached an `@refresh` listener OR\n\t\t * the page is in schema-driven mode (`register` + `schema` +\n\t\t * `objectId`) and can self-fetch. Legacy `objectType`-mode detail\n\t\t * pages that never wire `@refresh` therefore show no dead button.\n\t\t *\n\t\t * @type {boolean|null}\n\t\t */\n\t\tshowRefresh: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Show an Edit button in the header that opens the record's schema\n\t\t * form. Needs `register` + `schema` + `objectId` to have something to\n\t\t * edit; without them the button stays hidden however this is set.\n\t\t *\n\t\t * `CnPageRenderer` turns this on for every schema-bound `type:\"detail\"`\n\t\t * page, which is the same set of pages whose index tables stop offering\n\t\t * an edit modal — the two are one rule, and shipping either half alone\n\t\t * would leave records with two edit surfaces or none.\n\t\t */\n\t\tshowEditAction: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Label for the header Edit button, run through the host translate. Defaults to a translated \"Edit\". */\n\t\teditLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Title of the record edit form, run through the host translate. Empty uses the Edit button's label. */\n\t\teditTitle: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** One line under the edit form's title, run through the host translate. Empty shows none. */\n\t\teditSubtitle: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Whether a header refresh is in flight. Disables the Refresh entry\n\t\t * and swaps its icon for a spinner. Wire this to the same flag the\n\t\t * host toggles around its `@refresh` handler.\n\t\t */\n\t\trefreshing: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Whether the Request-a-feature entry renders in the page-header menu. */\n\t\tshowRequestFeature: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Whether the \"Report a bug\" entry renders in the page-header menu.\n\t\t * On by default — the trio Request a feature / Report a bug /\n\t\t * Documentation is the contract for every Conduction surface.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tshowReportBug: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Whether the Documentation entry renders in the page-header menu. On\n\t\t * by default; the shared menu resolves the target itself, so leaving\n\t\t * it on costs the host nothing.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tshowDocumentation: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Declarative lifecycle/transition actions (manifest `config.lifecycleActions`).\n\t\t * When set, renders status-gated transition buttons in the page header driven\n\t\t * by the object's `x-openregister-lifecycle`. Shape:\n\t\t * `{ field?: 'status', transitions?: [{ from, to, action, label, confirm?, variant? }], autoFetch?: boolean }`.\n\t\t * With just `{ field: 'status' }` the allowed transitions are fetched live from\n\t\t * OpenRegister's `/available-actions` endpoint (server-authoritative). An\n\t\t * explicit `transitions` array is filtered client-side by the object's current\n\t\t * state. Omit (default `null`) to keep the current behaviour (no transition\n\t\t * buttons). See `CnLifecycleActions`.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tlifecycleActions: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Declarative header actions (manifest `config.headerActions`, #91\n\t\t * Wave 3) rendered as items in the header's Actions menu via\n\t\t * CnActionButtons — `api-call` (POST/PUT + toast + refresh),\n\t\t * `open-form`, `toggle`, `navigate` / `open-modal`, each with an\n\t\t * optional `visibleWhen` predicate. Distinct from `lifecycleActions`\n\t\t * (state-machine transitions): these are free-form record actions\n\t\t * (approve, send, archive). The page's object context drives\n\t\t * `@objectId` / `@object.<field>` token + local-predicate resolution.\n\t\t * Empty (the default) renders nothing.\n\t\t *\n\t\t * An entry with `href` is a link, never dispatched: an absolute\n\t\t * `http(s)`, `mailto:`, `tel:` or `//host/...` URL as written, or an instance path\n\t\t * (leading `/`) through `generateUrl()`, opening in place unless\n\t\t * `target` is `'_blank'`.\n\t\t *\n\t\t * The menu, not a row of buttons: that is what the manifest schema has\n\t\t * always said this key is, and what CnIndexPage already does with it.\n\t\t * The two surfaces had drifted, and on a dossiq case — twelve actions —\n\t\t * the button row squeezed the record's own title to a truncated stub.\n\t\t * Edit stays a button because it is the one action a handler reaches\n\t\t * for on nearly every visit.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\theaderActions: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Set to fold this page's own Edit button in with the header actions,\n\t\t * so it becomes an entry in the Actions menu instead of a button\n\t\t * standing beside it. `null` (the default) keeps Edit standalone, so\n\t\t * every existing consumer's header is unchanged.\n\t\t *\n\t\t * The number itself no longer caps anything HERE: this page draws its\n\t\t * header actions as Actions-menu entries (`display: \"menu\"`), where\n\t\t * there are no buttons to cap. It is still CnActionButtons' `inline`\n\t\t * count for a host that mounts that component in `buttons` mode — see\n\t\t * that component's `inline` prop.\n\t\t *\n\t\t * @type {number|null}\n\t\t */\n\t\tinlineActions: {\n\t\t\ttype: Number,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Declarative related-object list sections (manifest\n\t\t * `config.relatedCollections`). Each entry renders a titled\n\t\t * `CnObjectListWidget` below the detail body, filtered to this object via\n\t\t * `@objectId` / `@object.<field>` tokens. Shape per entry:\n\t\t * `{ title?, register, schema, filter?, columns?, sort?, limit?, rowRoute? }`.\n\t\t * Empty / omitted (default `[]`) renders nothing. See `CnRelatedCollections`.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\trelatedCollections: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Declarative cross-schema summary chips (manifest\n\t\t * `config.summaryAggregates`). Each entry runs ONE count/sum/avg over a\n\t\t * related schema scoped to this object and shows it as a stat chip in the\n\t\t * header. Shape per entry:\n\t\t * `{ label, register, schema, metric?, field?, filter?, format? }`.\n\t\t * Empty / omitted (default `[]`) renders nothing. See `CnSummaryAggregates`.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\tsummaryAggregates: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Declarative relation-link actions (manifest `config.relationLinks`).\n\t\t * Each entry renders a button that opens a search-and-link modal which\n\t\t * patches a foreign-key field on THIS object with the chosen object's id.\n\t\t * Shape per entry:\n\t\t * `{ label?, register, schema, fkField, labelField?, allowCreate?, title?, selectLabel? }`.\n\t\t * Empty / omitted (default `[]`) renders nothing.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\trelationLinks: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Declarative IN-BODY sections (manifest `config.bodyWidgets`). Each\n\t\t * entry renders a REGISTERED host-app component as a titled section in\n\t\t * the page BODY (not the sidebar), with the object/page context injected.\n\t\t * This is the primitive that lets a rich bespoke detail page (BRP panels,\n\t\t * activity timelines, comms history, relationship graphs, bookings, …)\n\t\t * become declarative while keeping its sections in the body. Shape per\n\t\t * entry:\n\t\t * `{ id?, component, title?, props?, placement?, colSpan? }`.\n\t\t * - `component` — registry name resolved from the app's v2 `registry`\n\t\t * (any `kind` exposing a `.component`, e.g. `kind:\"section\"` or\n\t\t * `kind:\"widget\"`) or the legacy `customComponents` map. NO sidebar\n\t\t * tab is required (unlike integration widgets).\n\t\t * - `props` — values are token-resolved: `@objectId` → this object's id,\n\t\t * `@object.<field>` → a field off it, `@workspace.<key>` → page\n\t\t * context; unset `@…?` optional / unresolved tokens are dropped.\n\t\t * - `placement` — `before-body` | `after-data` | `after-related` | `end`\n\t\t * (default `end`). Controls where in the body the section lands.\n\t\t * - `colSpan` — 1–12 grid span when sections share a placement.\n\t\t * The loaded object + objectId are also `provide`d on `cnSectionContext`\n\t\t * so a host component can inject them instead of taking explicit props.\n\t\t * Empty / omitted (default `[]`) renders nothing. See `CnBodySections`.\n\t\t *\n\t\t * @type {Array<{id?: string, component: string, title?: string, props?: object, placement?: string, colSpan?: number}>}\n\t\t */\n\t\tbodyWidgets: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Vue-router route NAME to navigate to after a create-form save (the\n\t\t * create archetype: a `type:\"detail\"` page whose route carries no `:id`).\n\t\t * The created object's id is passed as the `id` route param. When empty\n\t\t * (the default) the page navigates back in history after a successful\n\t\t * create instead. See `isCreateMode`.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tcreateRoute: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Page-level app config map exposed to declarative widget / section\n\t\t * config via the `@config.<key>` token (e.g. the reporting `currency`\n\t\t * the setup wizard captures). Provided to descendants on `cnAppConfig`,\n\t\t * so a stat widget's `format: { style: 'currency', currency:\n\t\t * '@config.currency' }` formats with the configured value (falling back\n\t\t * to the literal default when unset). A manifest renderer typically seeds\n\t\t * this from `loadState(appId, 'config', {})`. Empty (default) leaves every\n\t\t * `@config.<key>` token to fall back to its literal default.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tappConfig: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Next and previous within the list this record was opened from, as\n\t\t * returned by `useListNavigation`. Omit it and neither control\n\t\t * renders, which is what a record reached by a bare link must do:\n\t\t * stepping through an order the reader never chose is worse than\n\t\t * offering no step at all.\n\t\t *\n\t\t * @type {{ available: boolean, isFirst: boolean, isLast: boolean, position: number, total: number }}\n\t\t */\n\t\tlistNavigation: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Router location of the previous record, as `useListNavigation`'s\n\t\t * `previousRoute`. When set, the previous control is a real link\n\t\t * (middle-click, open in new tab) and `previous-record` is emitted\n\t\t * with `{ event, to }` after it navigated. Null keeps it a button.\n\t\t *\n\t\t * @type {object|string|null}\n\t\t */\n\t\tpreviousTo: {\n\t\t\ttype: [Object, String],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Router location of the next record, as `useListNavigation`'s\n\t\t * `nextRoute`. Same behaviour as `previousTo`.\n\t\t *\n\t\t * @type {object|string|null}\n\t\t */\n\t\tnextTo: {\n\t\t\ttype: [Object, String],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The page's primary action, as declared on its manifest page. Renders\n\t\t * as the header's primary button and is the target the skip link\n\t\t * lands on. Omit it and neither renders.\n\t\t *\n\t\t * @type {{ id?: string, label: string, icon?: string, route?: string, href?: string }}\n\t\t */\n\t\tprimaryAction: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Puts the active tab in the address as `?_tab=<id>`, so a link points\n\t\t * at a tab of this record rather than at the record. Off by default,\n\t\t * which leaves the address exactly as it is today.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\ttabInAddress: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t// ── The action model (detail-action-model-and-case-surfaces) ────\n\n\t\t/**\n\t\t * The primary action per stage (manifest `config.primaryActionByStage`):\n\t\t * a map from a stage value to an action. When the record's stage has\n\t\t * an entry, that action is the page's primary button, so the button\n\t\t * names the next step: \"Take on\" on a new case, \"Publish\" on a\n\t\t * decided one. An entry is an action object in the `headerActions`\n\t\t * shape, or the id of a declared header action. A stage without an\n\t\t * entry falls back to `primaryAction`.\n\t\t *\n\t\t * @type {Record<string, string|object>|null}\n\t\t */\n\t\tprimaryActionByStage: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Dot-path to the field that holds the record's stage. Read by\n\t\t * `primaryActionByStage` and `nextStep`.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tstageField: {\n\t\t\ttype: String,\n\t\t\tdefault: 'status',\n\t\t},\n\n\t\t/**\n\t\t * Always visible buttons in the header (manifest `config.quickActions`)\n\t\t * for the few actions a handler uses on every record. At most three\n\t\t * render; later entries are dropped. An entry is an action object in\n\t\t * the `headerActions` shape, or the id of a declared header action,\n\t\t * which then leaves the Actions menu. `{ id, tabs?, notTabs? }` names\n\t\t * a declared action that is a button only beside those body tabs, as\n\t\t * a side card follows them; elsewhere it stays in the Actions menu.\n\t\t *\n\t\t * @type {Array<string|object>}\n\t\t */\n\t\tquickActions: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * What the Actions menu itself shows (manifest `config.actionsMenu`):\n\t\t * `{ showRefresh?, showHelpLinks?, showMarkUnread?, showOpenSidebar?, label? }`.\n\t\t * `showRefresh: false` removes Refresh. `showHelpLinks: false` removes\n\t\t * Request a feature, Report a bug and Documentation, for an app that\n\t\t * offers them in its own help menu. `showMarkUnread: false` and\n\t\t * `showOpenSidebar: false` remove those two built-in entries, for a\n\t\t * menu the board draws without them (DqZaakMeer). `label` names the\n\t\t * menu, for example \"More\". `null` (the default) changes nothing.\n\t\t *\n\t\t * @type {{showRefresh?: boolean, showHelpLinks?: boolean, showMarkUnread?: boolean, showOpenSidebar?: boolean, label?: string}|null}\n\t\t */\n\t\tactionsMenu: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The \"what now\" card (manifest `config.nextStep`):\n\t\t * `{ field?, stagesFrom?, stages: { <stage>: { title?, checklist, after? } } }`.\n\t\t * Each checklist item is `{ label, doneField?, doneWhen?, hint? }`.\n\t\t * `stagesFrom` is a field path read like a `visibleWhen` field\n\t\t * (`@context.caseType.nextStep`): when it holds a stages map (or an\n\t\t * object with `stages`), that map replaces `stages` for this record, so\n\t\t * a case type keeps its own stage texts. Otherwise `stages` stays.\n\t\t * Rendered above the body for a stage that declares a checklist, with\n\t\t * the primary button inside it. `tabs` / `notTabs` tie the card to\n\t\t * body tabs as a side card. See `CnNextStepCard`.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tnextStep: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * A pill above the title saying what kind of record this is\n\t\t * (manifest `config.typePill`): `{ field, colorMap?, labels?, variant? }`.\n\t\t * Rendered through `CnStatusBadge`; `colorMap` is keyed on the field's\n\t\t * raw value, `labels` maps a raw value to the text to show. Instead of\n\t\t * `field`: `text`, a constant (\"Inwoner\"), or `countFrom`\n\t\t * (`{ register, schema, filter? }`) with `countLabel` (`{ one, other }`\n\t\t * with `{count}`), a count of a related list (\"1 lopende zaak\") that\n\t\t * shows nothing at 0. With `resolve: { register, schema, labelField?,\n\t\t * colorField?, iconField? }` the pill shows the referenced record's\n\t\t * label, takes its tone from that record's colour (an NL Design System\n\t\t * hue such as `red`, or a badge role) and shows its icon. `values`\n\t\t * draws a `field` pill only for those raw values (a priority pill for\n\t\t * `high` and `urgent` only).\n\t\t *\n\t\t * @type {{field?: string, text?: string, countFrom?: object, countLabel?: object, colorMap?: object, labels?: object, variant?: string, resolve?: object, values?: Array<string>}|null}\n\t\t */\n\t\ttypePill: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * A pill above the title saying where the record stands (manifest\n\t\t * `config.statusPill`). Same shape as `typePill`.\n\t\t *\n\t\t * @type {{field: string, colorMap?: object, labels?: object, variant?: string}|null}\n\t\t */\n\t\tstatusPill: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Fields shown as chips under the title (manifest `config.headerFields`):\n\t\t * each entry a property key, or `{ key, format, labelField, colorField,\n\t\t * warnWhenPast }`. `format` is `text` (default), `mono`, `badge`, `user`\n\t\t * or `date`. An empty value shows no chip; no row without chips.\n\t\t *\n\t\t * @type {Array<string|{key: string, format?: string, labelField?: string, colorField?: string, warnWhenPast?: boolean}>}\n\t\t */\n\t\theaderFields: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Whether the type eyebrow (the type label above the record name)\n\t\t * renders once the record resolves. `false` (manifest\n\t\t * `config.showTypeEyebrow: false`) drops it for a header that says\n\t\t * the type in a pill instead.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-detail-page-drops-its-eyebrow-and-shows-a-breadcrumb\n\t\t */\n\t\tshowTypeEyebrow: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * A breadcrumb line above the header (manifest `config.breadcrumb`):\n\t\t * `{ label, route?, params?, href? }` names the list the record\n\t\t * belongs to; the record's display name follows as the current\n\t\t * crumb. `label` goes through the host translate function and shows\n\t\t * as text; `icon` (an MDI name, e.g. `Home`) draws that icon instead,\n\t\t * with the label as its accessible name. `currentField` names a field\n\t\t * of the record (a dotted path, e.g. `identifier`) whose value is the\n\t\t * current crumb instead of the display name; an empty value falls back\n\t\t * to the display name. `separator` draws that text (e.g. `/`) between\n\t\t * the crumbs instead of the chevron. Null (the default) draws no\n\t\t * trail.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-detail-page-drops-its-eyebrow-and-shows-a-breadcrumb\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-declared-breadcrumb-label-shows-as-text\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-breadcrumb-can-name-the-record-by-a-field-and-use-a-text-separator\n\t\t * A list of such crumbs draws a deeper trail (\"Settings / Case types /\"\n\t\t * and then the record); `currentField` and `separator` may sit on any\n\t\t * of them.\n\t\t *\n\t\t * @spec openspec/changes/detail-breadcrumb-trail/specs/detail-page/spec.md#requirement-a-record-page-breadcrumb-can-name-more-than-one-level\n\t\t * @type {{label: string, route?: string, params?: object, href?: string, icon?: string, currentField?: string, separator?: string}|Array<object>|null}\n\t\t */\n\t\tbreadcrumb: {\n\t\t\ttype: [Object, Array],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Whether the record's widget cards (the body grid and the side\n\t\t * column) keep their overflow Actions menu when their definition does\n\t\t * not say (`showActions`). `false` (manifest\n\t\t * `config.showWidgetActions: false`) drops it, as on a dashboard; a\n\t\t * widget with `showActions: true` keeps its menu. A card that offers\n\t\t * an Add action keeps the menu that holds it. When the key is not set\n\t\t * it is true, and false under the board look.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-detail-page-can-drop-the-widget-actions-menu\n\t\t */\n\t\tshowWidgetActions: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * A field path whose value picks the tabs of the page's `tabs` widgets\n\t\t * and their order for this record (manifest `config.tabsFrom`), read\n\t\t * like a `visibleWhen` field: `@context.caseType.tabs` reads the list a\n\t\t * case type keeps. A non-empty list of tab ids (a tab's `id`, else its\n\t\t * `widgetId`, or the string entry itself) keeps those tabs in that\n\t\t * order and leaves the rest out. Any other value keeps the declared\n\t\t * tabs. Edit mode shows them all.\n\t\t *\n\t\t * @spec openspec/changes/detail-tabs-from/specs/detail-page/spec.md#requirement-a-record-can-pick-its-tabs-and-their-order\n\t\t */\n\t\ttabsFrom: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * A column of cards beside the body (manifest `config.sideColumn`),\n\t\t * for the record's facts: deadline, requester, handler, links. Each\n\t\t * entry is a widget definition (`{ id?, type, title?, content? }`) or\n\t\t * the id of a widget declared in `widgets`. The column stacks under\n\t\t * the body on a narrow page. Empty (the default) leaves the layout as\n\t\t * it is. An entry with `tabs` (tab ids of the body's `tabs` widget)\n\t\t * shows only while one of those tabs is open (DqZaaktypeVelden: the\n\t\t * option list beside Velden).\n\t\t *\n\t\t * @type {Array<string|object>}\n\t\t */\n\t\tsideColumn: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * A row of widgets across the whole page, above the body and the side\n\t\t * column (manifest `config.band`). Each entry is the id of a widget\n\t\t * declared in `widgets`, a widget definition, or `{ widgetId, span,\n\t\t * chrome }`; `span` is the entry's share of the row in twelfths\n\t\t * (default 12) and the row wraps; `chrome` is `card` or `bare` (a\n\t\t * banner defaults to bare, anything else to card, so a widget that\n\t\t * draws its own card, such as a KPI, asks for bare). Each entry\n\t\t * follows its `visibleWhen`. Empty (the default) draws no band.\n\t\t *\n\t\t * @type {Array<string|object>}\n\t\t * @spec openspec/changes/detail-band/specs/detail-page/spec.md#requirement-a-detail-page-can-draw-a-band-across-the-page-above-the-body-and-the-side-column\n\t\t */\n\t\tband: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Page-level filters (manifest `config.pageFilters`), the same a\n\t\t * dashboard takes: each writes its choice into the page's workspace\n\t\t * context, which every widget on the page reads through\n\t\t * `@page.<key>` / `@workspace.<key>`. Drawn in the band, above its\n\t\t * widgets (PtPortal \"Periode\").\n\t\t *\n\t\t * @type {Array<{key: string, label?: string, type?: string, control?: ('select'|'segmented'), options?: Array<{value: (string|number), label: string}>, optionsFrom?: object, clearable?: boolean, placeholder?: string, default?: (string|number)}>}\n\t\t * @spec openspec/changes/detail-page-filters/specs/detail-page/spec.md#requirement-a-detail-page-can-draw-page-filters-that-its-widgets-read\n\t\t */\n\t\tpageFilters: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * A field path whose value picks the side column and its order for\n\t\t * this record (manifest `config.sideColumnFrom`), read like a\n\t\t * `visibleWhen` field: `@context.caseType.sideCards` reads the list a\n\t\t * case type keeps. A non-empty list of ids keeps those `sideColumn`\n\t\t * entries, in that order, and leaves the rest out; each still follows\n\t\t * its own `visibleWhen`. Any other value keeps `sideColumn` as\n\t\t * declared, so a record whose list is empty changes nothing.\n\t\t *\n\t\t * @spec openspec/changes/detail-side-column-from/specs/detail-page/spec.md#requirement-a-record-can-pick-the-side-column-and-its-order\n\t\t */\n\t\tsideColumnFrom: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * A field path, read like a `visibleWhen` field, whose list of\n\t\t * declared header action ids picks this record's quick actions\n\t\t * (manifest `config.quickActionsFrom`, e.g.\n\t\t * `@context.caseType.quickActions`). Only ids of `headerActions` are\n\t\t * taken, so the record's data never defines an action. An empty or\n\t\t * missing list keeps `quickActions`.\n\t\t *\n\t\t * @spec openspec/changes/detail-quick-actions-from/specs/detail-page/spec.md#requirement-a-record-can-pick-its-quick-actions\n\t\t */\n\t\tquickActionsFrom: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Draw the header as a card (manifest `config.headerCard: true`): the\n\t\t * pills, the title and the actions on the page's surface colour inside\n\t\t * a bordered, rounded box, as the board's case header. Off by default,\n\t\t * which keeps the header on the page ground.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-detail-header-can-be-a-card-that-holds-a-widget\n\t\t */\n\t\theaderCard: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * The id of a widget declared in `widgets` to render inside the header,\n\t\t * on its own row under the title and the actions (manifest\n\t\t * `config.headerWidget`), e.g. the stages bars. It renders without a\n\t\t * card of its own and leaves the body grid. Empty (the default), or an\n\t\t * id that names no widget, renders nothing extra.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-detail-header-can-be-a-card-that-holds-a-widget\n\t\t */\n\t\theaderWidget: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Records the page reads beside the loaded one, so a `visibleWhen`\n\t\t * can ask about them (manifest `config.visibilityContext`). Each entry\n\t\t * is `{ <name>: { field, register, schema } }`: `field` is the\n\t\t * property of the loaded record holding the other record's id, and a\n\t\t * condition reads that record as `@context.<name>.<path>`. A case page that\n\t\t * declares `{ caseType: { field: \"caseType\", register: \"dossiq\",\n\t\t * schema: \"caseType\" } }` can show a widget for some case types only\n\t\t * with `{ field: \"@context.caseType.category\", op: \"in\", value: [...] }`.\n\t\t * A record that cannot be read is absent, so its condition fails safe.\n\t\t * Empty (the default) reads nothing extra.\n\t\t *\n\t\t * @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-page-can-load-records-a-condition-names\n\t\t */\n\t\tvisibilityContext: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Ask why the reader opens the record before the page shows it, and\n\t\t * record the answer (manifest `config.accessPurpose`). `{ when, options,\n\t\t * url, title?, description? }`: `when` is a condition in the\n\t\t * `visibleWhen` grammar on the record and its `visibilityContext`\n\t\t * records (DqSociaalDomein: the case type lists special categories of\n\t\t * personal data); `options` the purposes offered (`{ value, label }`,\n\t\t * \"Other\" with a written purpose is always added); `url` the app\n\t\t * endpoint that records the purpose (POST `{ purpose, note }`, `{objectId}`\n\t\t * filled in). The page shows nothing of the record until the endpoint\n\t\t * accepts, and does not ask again for the same record in the same\n\t\t * browser session. A refusal keeps the record closed. `null` (the\n\t\t * default) asks nothing.\n\t\t *\n\t\t * @spec openspec/changes/detail-access-purpose/specs/detail-access-purpose/spec.md#requirement-a-detail-page-can-ask-for-a-purpose-before-it-opens\n\t\t * @type {object|null}\n\t\t */\n\t\taccessPurpose: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Whether the current user administers this instance. Shows the\n\t\t * `adminOnly` header actions. `null` (the default) reads it from\n\t\t * `getCurrentUser()`. Visibility only, never an authorization\n\t\t * decision: the endpoint behind an action decides who may call it.\n\t\t *\n\t\t * @type {boolean|null}\n\t\t */\n\t\tisAdmin: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * The look this page is drawn in (`nextcloud` or `board`). Empty (the\n\t\t * default) takes the app's look, which CnAppRoot or CnPageRenderer\n\t\t * provides, so a manifest's `look` reaches the page without a prop.\n\t\t *\n\t\t * @spec openspec/changes/screens-detail-page-parity/specs/detail-page-board-look/spec.md\n\t\t */\n\t\tlook: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * The meta line on row 2 of the board look's header (manifest\n\t\t * `config.headerMeta`): a field template such as `\"via {channel}\"`.\n\t\t * Every `{path}` is read from the record (an enum field as its label);\n\t\t * `{path|format}` formats it (`weekday`, `longdate`, `time`, `user`,\n\t\t * `currency:EUR`, ...). An empty token drops itself and its separator;\n\t\t * a line with no value at all is dropped. Only the board look draws it.\n\t\t * `{ count, one, other, zero? }` picks a form by the `headerMetaCounts`\n\t\t * total named `count` (\"1 lopende zaak\" / \"2 lopende zaken\"); each form\n\t\t * is translated on its own, and `zero` (when given) is used for none.\n\t\t *\n\t\t * @spec openspec/changes/screens-detail-page-parity/specs/detail-page-board-look/spec.md#requirement-the-detail-header-is-two-rows-on-the-ground\n\t\t * @spec openspec/changes/header-meta-plural/specs/detail-page/spec.md#requirement-a-header-line-can-name-a-count-in-the-singular-or-the-plural\n\t\t */\n\t\theaderMeta: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * The lists `headerMeta` counts (manifest `config.headerMetaCounts`), as\n\t\t * on a list page: `{ <name>: { register, schema, filter? } | { entitySource } }`,\n\t\t * each read as one total and filled in for `{count:<name>}` (\"een van\n\t\t * {count:organisations} organisaties op deze omgeving\", DqTenant). While\n\t\t * a count loads, or when it fails, the line is left out.\n\t\t *\n\t\t * @spec openspec/changes/detail-header-meta-counts/specs/detail-page/spec.md#requirement-a-record-page-header-line-can-count-a-list\n\t\t * @type {Record<string, object>|null}\n\t\t */\n\t\theaderMetaCounts: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Read the record's newest create or update entry from its audit\n\t\t * trail and offer it to `headerMeta` as `{_lastChange.userName}`,\n\t\t * `{_lastChange.user|user}` and `{_lastChange.created|date}`\n\t\t * (\"laatst gewijzigd 2 okt door Lars de Koning\", DqZaaktype). A record\n\t\t * does not say who changed it last; its trail does. A trail that\n\t\t * cannot be read leaves the tokens empty.\n\t\t *\n\t\t * @spec openspec/changes/header-meta-last-change/specs/detail-page/spec.md#requirement-the-header-meta-line-can-name-who-changed-the-record-last\n\t\t */\n\t\theaderMetaLastChange: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Read the record's newest create or update entry (as\n\t\t * `headerMetaLastChange` does) for the widgets on the page: a facts\n\t\t * card reads it as `_lastChange.userName` / `_lastChange.created`\n\t\t * (\"Laatst gewijzigd op 3 oktober door Pieter Jansen\", DqContact\n\t\t * \"Historie\"). `headerMetaLastChange` provides it as well.\n\t\t *\n\t\t * @spec openspec/changes/record-last-change-in-widgets/specs/record-last-change-in-widgets/spec.md#requirement-a-widget-can-read-who-changed-the-record-last\n\t\t */\n\t\trecordLastChange: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * The accessible name of the tab list under the board look (manifest\n\t\t * `config.tabsLabel`). Empty gives \"<type> parts\", for example \"Case\n\t\t * parts\", from the page title.\n\t\t *\n\t\t * @spec openspec/changes/screens-detail-page-parity/specs/detail-page-board-look/spec.md#requirement-folder-tabs-take-the-board-strip\n\t\t */\n\t\ttabsLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * The field holding the record's identifier (manifest\n\t\t * `config.identifierField`, for example `identifier`). Under the board\n\t\t * look the side column's History card ends with a line naming it.\n\t\t *\n\t\t * @spec openspec/changes/screens-detail-page-parity/specs/detail-page-board-look/spec.md#requirement-body-and-side-cards-take-the-board-anatomy\n\t\t */\n\t\tidentifierField: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * The record field the header names the record by (manifest\n\t\t * `config.titleField`, a dotted path such as `number`), as an invoice\n\t\t * page names the invoice number. An empty value falls back to the\n\t\t * display name. Empty (the default) changes nothing.\n\t\t *\n\t\t * @spec openspec/changes/detail-title-field/specs/detail-page/spec.md#requirement-a-detail-page-can-name-the-record-by-a-field\n\t\t */\n\t\ttitleField: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\temits: [\n\t\t'create-cancel',\n\t\t'created',\n\t\t'edited',\n\t\t'geo-saved',\n\t\t'layout-change',\n\t\t'marked-unread',\n\t\t'open-integration',\n\t\t'refresh',\n\t\t'related-object-click',\n\t\t'related-row-click',\n\t\t'relation-linked',\n\t\t'request-feature',\n\t\t'transitioned',\n\t\t'update:layout',\n\t\t'widget-config-change',\n\t\t/**\n\t\t * The reader asked for the next record of the list this one was\n\t\t * opened from. The host steps, because the host owns the router.\n\t\t * With `nextTo` set the link has navigated already and the payload\n\t\t * is `{ event, to }`.\n\t\t */\n\t\t'next-record',\n\t\t/** The reader asked for the previous record of that same list. Payload as `next-record`. */\n\t\t'previous-record',\n\t\t/** The page's declared primary action was pressed. Payload is the declaration. */\n\t\t'primary-action',\n\t\t/**\n\t\t * The active tab changed. Payload is the tab id. Emitted whether or\n\t\t * not `tabInAddress` is set, so a host may mirror it somewhere else.\n\t\t */\n\t\t'tab-change',\n\t],\n\n\tsetup(props) {\n\t\t// Pluggable integration registry — used to resolve `type:\n\t\t// 'integration'` widgets in the grid layout to their Vue\n\t\t// component (AD-19 surface fallback). Always wired; cheap when\n\t\t// no integration widgets are configured.\n\t\tconst { resolveWidget, getById } = useIntegrationRegistry()\n\t\tconst registryExposed = { resolveRegistryWidget: resolveWidget, getRegistryProvider: getById }\n\n\t\t// The host reads presence because only the host can act on it: the grid\n\t\t// reserves the row before the widget renders. See `layoutItemCanDraw`.\n\t\t// A getter, not a one-shot read: a page whose widgets arrive after mount\n\t\t// would otherwise never subscribe, and `layoutItemCanDraw` would drop the\n\t\t// presence row for good instead of until somebody arrives.\n\t\tconst presencePlaced = () => (props.widgets || []).some((w) => w && w.type === 'presence') || props.followRow?.presence === true\n\t\tconst { others: presentOthers } = useObjectPresence(\n\t\t\t() => props.register || '',\n\t\t\t() => props.schema || '',\n\t\t\t() => String(props.objectId || ''),\n\t\t\t{ enabled: presencePlaced },\n\t\t)\n\t\tregistryExposed.presentOthers = presentOthers\n\n\t\t// Object context for detail-page abstract widgets (ADR-041): a reactive\n\t\t// `{ objectId, object, register, schema }` holder kept current by the\n\t\t// Options watcher below. Provided so CnObjectListWidget / CnStatWidget\n\t\t// can resolve `@objectId` / `@object.<field>` filter tokens and scope\n\t\t// their query to the object on this page. Null fields until loaded.\n\t\tconst cnObjectContext = ref({ objectId: props.objectId || null, object: null, register: props.register || '', schema: props.schema || '' })\n\t\tprovide('cnObjectContext', cnObjectContext)\n\t\tregistryExposed.cnObjectContextRef = cnObjectContext\n\n\t\t// Page-level APP CONFIG exposed to declarative widget / section config via\n\t\t// the `@config.<key>` token (e.g. the reporting currency the setup wizard\n\t\t// captures). Provided so CnStatWidget / CnBodySections can format with the\n\t\t// configured value. Kept in sync with the `appConfig` prop.\n\t\tconst cnAppConfig = ref({ ...(props.appConfig || {}) })\n\t\tprovide('cnAppConfig', cnAppConfig)\n\t\tregistryExposed.cnAppConfigRef = cnAppConfig\n\t\twatch(\n\t\t\t() => props.appConfig,\n\t\t\t(next) => {\n\t\t\t\tcnAppConfig.value = { ...(next || {}) }\n\t\t\t},\n\t\t\t{ deep: true },\n\t\t)\n\n\t\t// Auto-subscribe + reactive lock state for the current object.\n\t\t// Both are no-ops when no store resolves (no Pinia active), when\n\t\t// subscribe is false (read-only / archive views), or when\n\t\t// objectType / objectId aren't yet known. Using composables in\n\t\t// setup() keeps the lifecycle bound to the component scope —\n\t\t// `tryOnScopeDispose` releases the subscription on unmount.\n\t\t//\n\t\t// Store resolution (manifest-live-updates): an explicit\n\t\t// `objectStore` prop always wins (existing behaviour, unchanged\n\t\t// for apps that pass their own store). In schema-driven mode —\n\t\t// `register` + `schema` set, the manifest renderer path — fall\n\t\t// back to the library's default store, the same one the\n\t\t// schema-driven fetch (`effectiveObjectStore`) uses, so\n\t\t// manifest-rendered detail pages auto-subscribe with zero\n\t\t// app-side wiring. Legacy `objectType`-only mounts without an\n\t\t// explicit store keep today's behaviour (no subscription).\n\t\tlet subscriptionStore = props.objectStore\n\t\tif (!subscriptionStore && props.subscribe && props.register && props.schema) {\n\t\t\ttry {\n\t\t\t\tsubscriptionStore = useObjectStore()\n\t\t\t} catch {\n\t\t\t\t// Pinia not active (stand-alone test mounts) — no live updates.\n\t\t\t\tsubscriptionStore = null\n\t\t\t}\n\t\t\tif (subscriptionStore) {\n\t\t\t\t// Register the EFFECTIVE type slug synchronously — the same\n\t\t\t\t// slug `resolveType()`/`resolvedObjectType` yields (an explicit\n\t\t\t\t// `objectType` prop wins; note the manifest renderer aliases\n\t\t\t\t// `config.schema` → `objectType`), with the same idempotence\n\t\t\t\t// guard as fetchObjectIfNeeded (which therefore skips its own\n\t\t\t\t// registration). This ensures the subscription's mount-time\n\t\t\t\t// subscribe() never races the mounted() fetch path into a\n\t\t\t\t// \"type not registered\" rejection.\n\t\t\t\tconst effectiveType = props.objectType || `${props.register}-${props.schema}`\n\t\t\t\tif (typeof subscriptionStore.registerObjectType === 'function'\n\t\t\t\t\t&& !subscriptionStore.objectTypeRegistry?.[effectiveType]) {\n\t\t\t\t\tsubscriptionStore.registerObjectType(\n\t\t\t\t\t\teffectiveType,\n\t\t\t\t\t\tprops.schema,\n\t\t\t\t\t\tprops.register,\n\t\t\t\t\t\t{ registerSlug: props.register, schemaSlug: props.schema },\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (!subscriptionStore || !props.subscribe) {\n\t\t\treturn { ...registryExposed, lockState: null }\n\t\t}\n\t\t// Resolve the effective object-type slug the same way `computed.resolvedObjectType`\n\t\t// does — the explicit `objectType` prop wins, else fuse `register` + `schema`.\n\t\t// The composables take getter functions so the resolution stays reactive when\n\t\t// `register` / `schema` / `objectType` change at runtime.\n\t\tconst resolveType = () => props.objectType || (props.register && props.schema ? `${props.register}-${props.schema}` : '')\n\t\tconst subscription = useObjectSubscription(\n\t\t\tsubscriptionStore,\n\t\t\tresolveType,\n\t\t\t() => props.objectId,\n\t\t\t{ enabled: () => Boolean(resolveType() && props.objectId) },\n\t\t)\n\t\t// Getters, read at call time: a register or schema that arrives after\n\t\t// mount (route params, a resolved sidebar) must reach the lock URL. They\n\t\t// used to be consts captured once at setup.\n\t\tconst sidebarReg = () => props.sidebarProps?.register || props.resolvedSidebar?.register || props.register || ''\n\t\tconst sidebarSchema = () => props.sidebarProps?.schema || props.resolvedSidebar?.schema || props.schema || ''\n\t\tconst lock = useObjectLock(\n\t\t\tsubscriptionStore,\n\t\t\tsidebarReg,\n\t\t\t() => resolveType() || sidebarSchema(),\n\t\t\t() => props.objectId,\n\t\t\t// The third argument is the OBJECT CACHE KEY (`<register>-<schema>`)\n\t\t\t// and the lock URL needs the schema SLUG. They were one string, and\n\t\t\t// the acquire went to `/api/objects/<register>/<register>-<schema>/`,\n\t\t\t// which no register answers.\n\t\t\t{ schemaSlug: () => props.schema || sidebarSchema() },\n\t\t)\n\t\t// The page's workspace context, as a dashboard provides it: page\n\t\t// filters write their choice here and a widget reads it through\n\t\t// `@page.<key>` / `@workspace.<key>`.\n\t\tconst workspaceContext = ref({})\n\t\tprovide('cnWorkspaceContext', workspaceContext)\n\t\treturn {\n\t\t\t...registryExposed,\n\t\t\tsubscriptionStatus: subscription.status,\n\t\t\tlockState: lock,\n\t\t\tworkspaceContext,\n\t\t}\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\t/** The `headerMetaCounts` totals, by name; a failed one is null. */\n\t\t\theaderMetaCountValues: {},\n\t\t\t/** The newest create or update entry of the record's audit trail (`headerMetaLastChange`). */\n\t\t\tlastChange: null,\n\t\t\t/** The rows a list on this page shows and the one selected (side-card-follows-row). */\n\t\t\twidgetSelection: markRaw(createWidgetSelection()),\n\t\t\t/** The id of the side column's slot for a body widget's own cards. */\n\t\t\tsideTakeoverId: `cn-detail-side-takeover-${++sideTakeoverCounter}`,\n\t\t\t/** The body widgets that show their own cards in the side column now. */\n\t\t\tsideClaims: [],\n\t\t\t/** The session keys of the records whose access purpose is recorded. */\n\t\t\taccessPurposeGranted: readGrantedPurposes(),\n\t\t\t/** Whether the purpose dialog is open; closing it without a purpose keeps the record closed. */\n\t\t\taccessPurposeAsking: true,\n\t\t\t/** Whether a purpose is being recorded. */\n\t\t\taccessPurposeBusy: false,\n\t\t\t/** Why the last purpose was refused. */\n\t\t\taccessPurposeError: '',\n\t\t\t/** The id of the open body tab a `tabs` widget reported, '' before one did. */\n\t\t\tactiveBodyTab: '',\n\t\t\t/**\n\t\t\t * The records `visibilityContext` names, keyed `@context.<name>`: the row,\n\t\t\t * `null` when it could not be read, absent while it is on its way.\n\t\t\t */\n\t\t\tvisibilityRows: {},\n\t\t\t/** The `visibilityRefsKey` the rows in `visibilityRows` were read for. */\n\t\t\tvisibilityRowsFor: '',\n\t\t\t/** Sequence of the last `visibilityContext` read, so a late answer for another record is dropped. */\n\t\t\tvisibilityRowsSeq: 0,\n\t\t\t/** The answers of endpoint and source `visibleWhen` conditions on grid widgets, by widget id. */\n\t\t\twidgetConditionAnswers: {},\n\t\t\t/** Sequence of the last remote condition round. */\n\t\t\twidgetConditionSeq: 0,\n\t\t\t/** The counts of the header's count pills (`countFrom`), by pill key (`type`, `status`). */\n\t\t\tpillCounts: {},\n\t\t\t/** Sequence of the last pill count round, so a late answer does not overwrite a newer one. */\n\t\t\tpillCountSeq: 0,\n\t\t\t/** The labels of the records pills with `resolve` point at, by pill key. */\n\t\t\tpillRefLabels: {},\n\t\t\t/** The colour and icon names read off the records pills with `resolve` point at, by pill key. */\n\t\t\tpillRefLooks: {},\n\t\t\t/** Sequence of the last pill label lookup, so a late answer for another record is dropped. */\n\t\t\tpillRefSeq: 0,\n\t\t\t/** `register/schema/id` the read-state call was last made for, so it is sent once per load. */\n\t\t\treadStateSentFor: '',\n\t\t\t/** Whether the user is arranging the chosen view (`userLayout`). */\n\t\t\tarrangingView: false,\n\t\t\t/** Whether the record edit form is open. */\n\t\t\teditFormOpen: false,\n\t\t\t/**\n\t\t\t * The sentence a refused lock left behind, shown in place of the\n\t\t\t * form. Empty whenever the form opened.\n\t\t\t */\n\t\t\teditLockRefusal: '',\n\t\t\t/**\n\t\t\t * Whether a lock release is in flight, so the locked card's Unlock\n\t\t\t * button can disable itself for the round trip. Without it a slow\n\t\t\t * release invites a second click, and the second `release()` lands\n\t\t\t * on a lock that is already gone.\n\t\t\t */\n\t\t\treleasingLock: false,\n\t\t\t/**\n\t\t\t * The manifest `headerActions[]` flattened into menu items by the\n\t\t\t * `display: \"menu\"` CnActionButtons instance, which keeps owning\n\t\t\t * their dialogs. Each carries its own pre-bound `run()`, so the\n\t\t\t * menu dispatches without reaching back into that component.\n\t\t\t */\n\t\t\tmenuHeaderActions: [],\n\t\t\t/** Whether a record-source verb is in flight, so a double click cannot run it twice. */\n\t\t\tsourceVerbBusy: false,\n\t\t\t/** The record-source verb whose form is open, or null. */\n\t\t\tsourceVerbForm: null,\n\t\t\t/** Lifecycle transitions CnLifecycleActions hands the Actions menu. */\n\t\t\tlifecycleMenuEntries: [],\n\t\t\t/** Whether the per-widget style/config editor modal is open. */\n\t\t\tshowWidgetConfig: false,\n\t\t\t/** The widgetId currently being configured via the cog. */\n\t\t\tconfigWidgetId: null,\n\t\t\t/**\n\t\t\t * Whether the object sidebar has been seeded with its initial open\n\t\t\t * state for the current activation. Gates the one-shot `sidebarOpen`\n\t\t\t * seed in `syncSidebarState` so user close/toggle is not clobbered.\n\t\t\t */\n\t\t\tsidebarSeeded: false,\n\t\t\t/** The relation-link descriptor whose modal is currently open (or null). */\n\t\t\tactiveRelationLink: null,\n\t\t\t/**\n\t\t\t * Materialized default body grid for the schema-driven auto-body (Data +\n\t\t\t * Related). Lazily built by `materializeAutoBody()` the first time the\n\t\t\t * object resolves so the detail body is a real, mutable GridStack grid\n\t\t\t * (drag / resize / configure) rather than a static stack. `null` until\n\t\t\t * materialized; reset when the schema-driven context changes.\n\t\t\t */\n\t\t\tautoBodyLayout: null,\n\t\t\t/** Widget definitions paired with `autoBodyLayout` (id ↔ widgetId). */\n\t\t\tautoBodyWidgets: null,\n\t\t\t/**\n\t\t\t * True when `materializeAutoBody()` dropped the Data widget only\n\t\t\t * because the schema had not arrived yet — the object won the race\n\t\t\t * against `fetchSchema`. The `currentSchema` watcher re-materializes\n\t\t\t * once on that signal, which is what stops a lost race from\n\t\t\t * permanently hiding every scalar field (shillinq#1073).\n\t\t\t *\n\t\t\t * A flag rather than an \"is the widget missing?\" test, so a body the\n\t\t\t * user has edited is never rewritten underneath them.\n\t\t\t */\n\t\t\tdataWidgetDeferred: false,\n\t\t\t/**\n\t\t\t * Drives the Actions-menu Refresh spinner during a schema-driven\n\t\t\t * self-fetch refresh, where the host has no promise to bind\n\t\t\t * `:refreshing` to. Mirrors CnIndexPage.internalRefreshing.\n\t\t\t */\n\t\t\tinternalRefreshing: false,\n\t\t\t/**\n\t\t\t * Whether a load has completed at least once. Gates the\n\t\t\t * full-page loading state to the FIRST load only: once content\n\t\t\t * has been shown, later loads (refresh, re-fetch) stay in place\n\t\t\t * and surface as the action-button spinner instead of blanking\n\t\t\t * the page. Set true the first time `loading` falls to false.\n\t\t\t */\n\t\t\thasLoadedOnce: false,\n\t\t\t/**\n\t\t\t * Whether the schema-driven object fetch is in flight. Starts true\n\t\t\t * so the immediate sidebar sync at creation already waits for it.\n\t\t\t */\n\t\t\tobjectFetchPending: Boolean(this.register && this.schema && this.objectId),\n\t\t\t/** Whether the schema-driven fetch answered 404 for `objectId`. */\n\t\t\tobjectNotFound: false,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t// ── The board look (screens-detail-page-parity) ─────────────────\n\n\t\t/**\n\t\t * Whether the page is drawn in the board look: its own `look` prop,\n\t\t * else the look the app provides.\n\t\t *\n\t\t * @return {boolean} True in the board look.\n\t\t * @spec openspec/changes/screens-detail-page-parity/specs/detail-page-board-look/spec.md\n\t\t */\n\t\tisBoard() {\n\t\t\treturn normalizeLook(this.look || this.cnLook) === 'board'\n\t\t},\n\n\t\t/**\n\t\t * The class that scopes the board rules to this page, so they apply\n\t\t * whether the look came from the app or from this page's own prop.\n\t\t *\n\t\t * @return {string} `cn-look-board` or ''.\n\t\t * @spec openspec/changes/screens-detail-page-parity/specs/detail-page-board-look/spec.md\n\t\t */\n\t\tboardClass() {\n\t\t\treturn this.isBoard ? 'cn-look-board' : ''\n\t\t},\n\n\t\t/**\n\t\t * Whether widget cards keep their overflow Actions menu: the manifest's\n\t\t * `showWidgetActions` when it says, else true, and false under the\n\t\t * board look (side cards have no Actions menu).\n\t\t *\n\t\t * @return {boolean} True when the menu shows.\n\t\t * @spec openspec/changes/screens-detail-page-parity/specs/detail-page-board-look/spec.md#requirement-body-and-side-cards-take-the-board-anatomy\n\t\t */\n\t\teffectiveShowWidgetActions() {\n\t\t\tif (typeof this.showWidgetActions === 'boolean') {\n\t\t\t\treturn this.showWidgetActions\n\t\t\t}\n\t\t\treturn !this.isBoard\n\t\t},\n\n\t\t/**\n\t\t * The header line's template: `headerMeta` itself, or for the plural\n\t\t * form `{ count, one, other, zero? }` the form its count asks for; null\n\t\t * while that count is missing.\n\t\t *\n\t\t * @return {string|null} The untranslated template, or null.\n\t\t * @spec openspec/changes/header-meta-plural/specs/detail-page/spec.md#requirement-a-header-line-can-name-a-count-in-the-singular-or-the-plural\n\t\t */\n\t\theaderMetaForm() {\n\t\t\tconst meta = this.headerMeta\n\t\t\tif (!meta || typeof meta !== 'object') {\n\t\t\t\treturn typeof meta === 'string' ? meta : ''\n\t\t\t}\n\t\t\tconst total = this.headerMetaCountValues[meta.count]\n\t\t\tif (typeof total !== 'number') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (total === 0 && typeof meta.zero === 'string') {\n\t\t\t\treturn meta.zero\n\t\t\t}\n\t\t\tconst form = total === 1 ? meta.one : meta.other\n\t\t\treturn typeof form === 'string' ? form : ''\n\t\t},\n\n\t\t/**\n\t\t * The meta line of row 2, translated and filled from the record.\n\t\t *\n\t\t * @return {string} The line, or ''.\n\t\t * @spec openspec/changes/screens-detail-page-parity/specs/detail-page-board-look/spec.md#requirement-the-detail-header-is-two-rows-on-the-ground\n\t\t */\n\t\theaderMetaText() {\n\t\t\tconst record = this.lastChange && this.resolvedObject ? { ...this.resolvedObject, _lastChange: this.lastChange } : this.resolvedObject\n\t\t\tlet missing = false\n\t\t\tconst form = this.headerMetaForm\n\t\t\tif (form === null) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst template = (form ? this.effectiveTranslate(form) : '').replace(/\\{count:([\\w-]+)\\}/g, (match, name) => {\n\t\t\t\tconst value = this.headerMetaCountValues[name]\n\t\t\t\tif (typeof value !== 'number') {\n\t\t\t\t\tmissing = true\n\t\t\t\t\treturn ''\n\t\t\t\t}\n\t\t\t\treturn String(value)\n\t\t\t})\n\t\t\treturn missing ? '' : resolveHeaderMeta(template, record, { schema: this.currentSchema, translate: this.effectiveTranslate })\n\t\t},\n\n\t\t/**\n\t\t * What the header line's counts depend on, as one key.\n\t\t *\n\t\t * @spec openspec/changes/detail-header-meta-counts/specs/detail-page/spec.md#requirement-a-record-page-header-line-can-count-a-list\n\t\t * @return {string}\n\t\t */\n\t\theaderMetaCountKey() {\n\t\t\tconst sources = this.headerMetaCounts && typeof this.headerMetaCounts === 'object' ? this.headerMetaCounts : {}\n\t\t\tconst valid = Object.entries(sources).filter(([, from]) => from && ((from.register && from.schema) || from.entitySource))\n\t\t\treturn valid.length > 0 ? JSON.stringify([valid, this.objectId]) : ''\n\t\t},\n\n\t\t/**\n\t\t * What the last-change read depends on.\n\t\t *\n\t\t * @spec openspec/changes/header-meta-last-change/specs/detail-page/spec.md#requirement-the-header-meta-line-can-name-who-changed-the-record-last\n\t\t * @return {string} The key, '' when nothing is to be read.\n\t\t */\n\t\tlastChangeKey() {\n\t\t\tif (!(this.headerMetaLastChange || this.recordLastChange) || !this.register || !this.schema || !this.objectId) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn `${this.register}/${this.schema}/${this.objectId}`\n\t\t},\n\n\t\t/**\n\t\t * Whether row 2 has a meta line or header chips to draw after the\n\t\t * breadcrumb.\n\t\t *\n\t\t * @return {boolean} True with meta or chips.\n\t\t * @spec openspec/changes/screens-detail-page-parity/specs/detail-page-board-look/spec.md#requirement-the-detail-header-is-two-rows-on-the-ground\n\t\t */\n\t\thasHeaderMeta() {\n\t\t\treturn this.headerMetaText !== '' || this.headerFields.length > 0\n\t\t},\n\n\t\t/**\n\t\t * Whether row 2 of the header has anything to draw.\n\t\t *\n\t\t * @return {boolean} True when a pill, the breadcrumb, meta or chips exist.\n\t\t * @spec openspec/changes/screens-detail-page-parity/specs/detail-page-board-look/spec.md#requirement-the-detail-header-is-two-rows-on-the-ground\n\t\t */\n\t\tshowsHeaderRow2() {\n\t\t\treturn this.headerPills.length > 0 || this.breadcrumbCrumbs.length > 0 || this.hasHeaderMeta\n\t\t},\n\n\t\t/**\n\t\t * The name of the tab list: `tabsLabel`, else \"<type> parts\" from the\n\t\t * page title, else \"Details\".\n\t\t *\n\t\t * @return {string} The accessible name.\n\t\t * @spec openspec/changes/screens-detail-page-parity/specs/detail-page-board-look/spec.md#requirement-folder-tabs-take-the-board-strip\n\t\t */\n\t\tresolvedTabsLabel() {\n\t\t\tif (this.tabsLabel) {\n\t\t\t\treturn this.effectiveTranslate(this.tabsLabel)\n\t\t\t}\n\t\t\tconst type = this.resolvedTitle\n\t\t\treturn type ? t('nextcloud-vue', '{type} parts', { type }) : t('nextcloud-vue', 'Details')\n\t\t},\n\n\t\t/**\n\t\t * The record's identifier, read from `identifierField`.\n\t\t *\n\t\t * @return {string} The identifier, or ''.\n\t\t * @spec openspec/changes/screens-detail-page-parity/specs/detail-page-board-look/spec.md#requirement-body-and-side-cards-take-the-board-anatomy\n\t\t */\n\t\tidentifierLine() {\n\t\t\tif (!this.identifierField) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst value = stageOf(this.resolvedObject, this.identifierField)\n\t\t\treturn value === '' ? '' : t('nextcloud-vue', 'Identifier: {identifier}', { identifier: value })\n\t\t},\n\n\t\t/**\n\t\t * The ids of the widgets a `tabs` widget on this page holds as tabs.\n\t\t *\n\t\t * @return {Set<string>} The widget ids.\n\t\t * @spec openspec/changes/screens-detail-page-parity/specs/detail-page-board-look/spec.md#requirement-history-is-the-last-tab\n\t\t */\n\t\ttabbedWidgetIds() {\n\t\t\tconst ids = new Set()\n\t\t\tconst declared = Array.isArray(this.widgets) ? this.widgets : []\n\t\t\tdeclared\n\t\t\t\t.filter((widget) => widget && widget.type === 'tabs')\n\t\t\t\t.forEach((widget) => {\n\t\t\t\t\tconst tabs = Array.isArray(widget.content?.tabs) ? widget.content.tabs : []\n\t\t\t\t\ttabs.forEach((tab) => {\n\t\t\t\t\t\tconst id = typeof tab === 'string' ? tab : tab?.widgetId\n\t\t\t\t\t\tif (id) {\n\t\t\t\t\t\t\tids.add(id)\n\t\t\t\t\t\t}\n\t\t\t\t\t})\n\t\t\t\t})\n\t\t\treturn ids\n\t\t},\n\n\t\t/**\n\t\t * The id of the side column's History card (the activity widget), the\n\t\t * one that ends with the identifier line.\n\t\t *\n\t\t * @return {string} The widget id, or ''.\n\t\t * @spec openspec/changes/screens-detail-page-parity/specs/detail-page-board-look/spec.md#requirement-body-and-side-cards-take-the-board-anatomy\n\t\t */\n\t\tsideHistoryId() {\n\t\t\tconst history = this.sideColumnWidgets.find((widget) => isActivityWidget(widget))\n\t\t\treturn history ? history.id : ''\n\t\t},\n\n\t\t// ── The record as a place (case-page-and-list-as-a-place) ───────\n\n\t\t/**\n\t\t * The primary action's label, run through the host's translate\n\t\t * function. Empty when the page declares no primary action, which is\n\t\t * what gates both the button and the skip link.\n\t\t *\n\t\t * @return {string} The label, or an empty string.\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tprimaryActionLabel() {\n\t\t\tconst label = this.primaryAction?.label\n\t\t\treturn typeof label === 'string' && label !== '' ? this.effectiveTranslate(label) : ''\n\t\t},\n\n\t\t/**\n\t\t * The id the skip link's anchor points at.\n\t\t *\n\t\t * Namespaced by the page so two detail pages in one document, which\n\t\t * the split view makes possible, cannot both answer to `#primary`.\n\t\t *\n\t\t * @return {string} The element id.\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tprimaryActionAnchorId() {\n\t\t\tconst scope = this.stagePrimaryEntry?.id || this.primaryAction?.id || this.pageId || this.objectId || 'record'\n\t\t\treturn `cn-primary-action-${String(scope).replace(/[^A-Za-z0-9_-]/g, '-')}`\n\t\t},\n\n\t\t/**\n\t\t * What the skip link says.\n\t\t *\n\t\t * It names the action rather than saying \"skip to main content\",\n\t\t * because a keyboard user who cannot see the page needs to know what\n\t\t * they are being offered before they take it.\n\t\t *\n\t\t * One library string with a placeholder, not \"Skip to\" from the host's\n\t\t * catalogue glued to the label: no app carried that fragment, so every\n\t\t * Dutch reader got \"Skip to In behandeling nemen\" (Zuiddrecht live\n\t\t * round 3), and a fragment cannot be put in Dutch word order anyway.\n\t\t *\n\t\t * @return {string} The link text.\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t * @spec openspec/changes/detail-page-strings-dutch/specs/detail-page-strings-dutch/spec.md#requirement-the-detail-page-speaks-the-readers-language\n\t\t */\n\t\tskipLinkLabel() {\n\t\t\treturn t('nextcloud-vue', 'Skip to {action}', { action: this.effectivePrimaryLabel })\n\t\t},\n\n\t\t// ── The action model (detail-action-model-and-case-surfaces) ────\n\n\t\t/**\n\t\t * The record's stage, as `primaryActionByStage` and `nextStep` key it.\n\t\t *\n\t\t * @return {string} The stage, or '' while the record has none.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-primary-action-follows-the-stage\n\t\t */\n\t\trecordStage() {\n\t\t\treturn stageOf(this.currentObject, this.stageField)\n\t\t},\n\n\t\t/**\n\t\t * The stage's primary action as an id, plus its definition when the\n\t\t * map carries one inline.\n\t\t *\n\t\t * @return {{id: string, inline: object|null}|null} The pinned action, or null when this stage declares none.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-primary-action-follows-the-stage\n\t\t */\n\t\tstagePrimary() {\n\t\t\treturn normalisePinnedAction(stageEntry(this.primaryActionByStage, this.recordStage), 'cn-stage-primary')\n\t\t},\n\n\t\t/**\n\t\t * The quick actions for this record: the declared header actions the\n\t\t * record lists through `quickActionsFrom`, in its order, else\n\t\t * `quickActions`. An entry is an id, or `{ id, ... }` without a label;\n\t\t * an id no header action declares is dropped.\n\t\t *\n\t\t * @return {Array<string|object>} The entries.\n\t\t * @spec openspec/changes/detail-quick-actions-from/specs/detail-page/spec.md#requirement-a-record-can-pick-its-quick-actions\n\t\t */\n\t\teffectiveQuickActions() {\n\t\t\tconst declared = Array.isArray(this.quickActions) ? this.quickActions : []\n\t\t\tif (!this.quickActionsFrom || !this.widgetVisibilityData) {\n\t\t\t\treturn declared\n\t\t\t}\n\t\t\tconst listed = readVisibleWhenPath(this.widgetVisibilityData, this.quickActionsFrom)\n\t\t\tif (!Array.isArray(listed)) {\n\t\t\t\treturn declared\n\t\t\t}\n\t\t\tconst known = new Set((Array.isArray(this.headerActions) ? this.headerActions : []).map((action) => action && action.id))\n\t\t\tconst idOf = (entry) => {\n\t\t\t\tif (typeof entry === 'string') {\n\t\t\t\t\treturn entry\n\t\t\t\t}\n\t\t\t\treturn entry && typeof entry === 'object' && !('label' in entry) && typeof entry.id === 'string' ? entry.id : ''\n\t\t\t}\n\t\t\tconst picked = listed.filter((entry) => known.has(idOf(entry)))\n\t\t\treturn picked.length > 0 ? picked : declared\n\t\t},\n\n\t\t/**\n\t\t * The quick actions as ids, plus definitions for the inline ones.\n\t\t * Capped at three, and never the action that is already the primary\n\t\t * button.\n\t\t *\n\t\t * @return {Array<{id: string, inline: object|null}>} The pinned actions.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-quick-actions\n\t\t */\n\t\tpinnedQuickActions() {\n\t\t\tconst primaryId = this.stagePrimary?.id\n\t\t\treturn this.effectiveQuickActions\n\t\t\t\t.map((entry, index) => normalisePinnedAction(entry, `cn-quick-${index}`))\n\t\t\t\t.filter((pinned) => pinned && pinned.id !== primaryId)\n\t\t\t\t.slice(0, MAX_QUICK_ACTIONS)\n\t\t},\n\n\t\t/**\n\t\t * Whether the stage's primary button belongs beside the open body tab:\n\t\t * an inline stage action with `tabs`/`notTabs` follows the tab like a\n\t\t * quick action (DqZaakAssistent: no \"Next step\" beside AI oversight).\n\t\t * It stays in the Actions menu either way.\n\t\t *\n\t\t * @return {boolean} true when the button may show.\n\t\t * @spec openspec/changes/stage-primary-follows-tab/specs/detail-page/spec.md#requirement-a-stage-action-can-follow-the-open-tab\n\t\t */\n\t\tstagePrimaryOnTab() {\n\t\t\treturn !this.stagePrimaryEntry || onTab(this.stagePrimary, this.activeBodyTab)\n\t\t},\n\n\t\t/**\n\t\t * The stage's primary action in dispatchable form: the menu entry\n\t\t * CnActionButtons built for it, with its `run()`. Null while the\n\t\t * action is hidden by its own `visibleWhen`, in which case the page\n\t\t * falls back to `primaryAction`.\n\t\t *\n\t\t * @return {object|null} The entry, or null.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-primary-action-follows-the-stage\n\t\t */\n\t\tstagePrimaryEntry() {\n\t\t\tconst id = this.stagePrimary?.id\n\t\t\treturn (id && this.menuHeaderActions.find((entry) => entry.id === id)) || null\n\t\t},\n\n\t\t/**\n\t\t * The quick actions in dispatchable form, in declaration order, less\n\t\t * the ones the open body tab leaves out.\n\t\t *\n\t\t * @return {Array<object>} The entries that are visible.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-quick-actions\n\t\t * @spec openspec/changes/detail-actions-per-tab/specs/detail-page/spec.md#requirement-quick-actions-and-the-next-step-card-can-follow-the-open-tab\n\t\t */\n\t\tquickActionEntries() {\n\t\t\treturn this.pinnedQuickActions\n\t\t\t\t.filter((pinned) => onTab(pinned, this.activeBodyTab))\n\t\t\t\t.map((pinned) => this.menuHeaderActions.find((entry) => entry.id === pinned.id))\n\t\t\t\t.filter(Boolean)\n\t\t},\n\n\t\t/**\n\t\t * What the primary button says: the stage's action when the stage\n\t\t * declares one, the page's `primaryAction` otherwise. Empty gates\n\t\t * both the button and the skip link.\n\t\t *\n\t\t * @return {string} The label, or an empty string.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-primary-action-follows-the-stage\n\t\t */\n\t\teffectivePrimaryLabel() {\n\t\t\treturn this.stagePrimaryEntry ? this.stagePrimaryEntry.label : this.primaryActionLabel\n\t\t},\n\n\t\t/**\n\t\t * The URL the primary button links to, when its action is a link.\n\t\t *\n\t\t * @return {string} The href, or '' for an action that is dispatched.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-primary-action-follows-the-stage\n\t\t */\n\t\tprimaryActionHref() {\n\t\t\tif (this.stagePrimaryEntry) {\n\t\t\t\treturn this.stagePrimaryEntry.href || ''\n\t\t\t}\n\t\t\treturn this.primaryActionLink?.href || ''\n\t\t},\n\n\t\t/**\n\t\t * The page's `primaryAction` as a link, when no stage action stands in\n\t\t * for it: `href` (its record tokens resolved) opens in a new tab, as the\n\t\t * schema says; `route` (with `query`) is an in-app link a plain click\n\t\t * routes to (PtPortal \"Nieuwe pagina\" opens the page create flow).\n\t\t *\n\t\t * @return {{href: string, to: object|null, target: string}|null} The link, or null.\n\t\t * @spec openspec/changes/detail-band/specs/detail-page/spec.md#requirement-a-detail-pages-own-primary-action-is-a-link\n\t\t */\n\t\tprimaryActionLink() {\n\t\t\tconst action = this.primaryAction\n\t\t\tif (!action || typeof action !== 'object') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (typeof action.href === 'string' && action.href !== '') {\n\t\t\t\tconst href = resolveManifestHref(interpolateUrlTokens(action.href, this.headerLinkTokenCtx))\n\t\t\t\treturn href ? { href, to: null, target: '_blank' } : null\n\t\t\t}\n\t\t\tif (typeof action.route === 'string' && action.route !== '') {\n\t\t\t\tconst to = { name: action.route, ...(action.query && typeof action.query === 'object' ? { query: action.query } : {}) }\n\t\t\t\tconst href = routeHref(to, this.$router)\n\t\t\t\treturn href ? { href, to, target: '_self' } : null\n\t\t\t}\n\t\t\treturn null\n\t\t},\n\n\t\t/**\n\t\t * The token context a header link resolves its URL against: this record.\n\t\t *\n\t\t * @return {{objectId: (string|number), object: object}} The context.\n\t\t * @spec openspec/changes/detail-band/specs/detail-page/spec.md#requirement-a-detail-header-link-builds-its-url-from-the-record\n\t\t */\n\t\theaderLinkTokenCtx() {\n\t\t\treturn { objectId: this.objectId, object: this.resolvedObject || {} }\n\t\t},\n\n\t\t/**\n\t\t * The link target that goes with `primaryActionHref`.\n\t\t *\n\t\t * @return {string|undefined} The target, or undefined.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-primary-action-follows-the-stage\n\t\t */\n\t\tprimaryActionTarget() {\n\t\t\tif (this.stagePrimaryEntry) {\n\t\t\t\treturn this.stagePrimaryEntry.linkTarget || undefined\n\t\t\t}\n\t\t\treturn this.primaryActionLink?.target || undefined\n\t\t},\n\n\t\t/**\n\t\t * Whether the primary button waits for its own request.\n\t\t *\n\t\t * @return {boolean} True while the stage's action is in flight.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-primary-action-follows-the-stage\n\t\t */\n\t\tprimaryActionDisabled() {\n\t\t\treturn Boolean(this.stagePrimaryEntry?.disabled)\n\t\t},\n\n\t\t/**\n\t\t * Whether the viewer administers this instance, for `adminOnly`\n\t\t * actions. The prop wins; otherwise Nextcloud says.\n\t\t *\n\t\t * @return {boolean} True for an instance admin.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-grouped-and-admin-only-menu-actions\n\t\t */\n\t\tviewerIsAdmin() {\n\t\t\tif (this.isAdmin !== null) {\n\t\t\t\treturn this.isAdmin\n\t\t\t}\n\t\t\ttry {\n\t\t\t\treturn getCurrentUser()?.isAdmin === true\n\t\t\t} catch {\n\t\t\t\treturn false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The declared actions by id, for the `group` and `adminOnly` a menu\n\t\t * entry does not carry itself.\n\t\t *\n\t\t * @return {Record<string, object>} The lookup.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-grouped-and-admin-only-menu-actions\n\t\t */\n\t\theaderActionsById() {\n\t\t\treturn Object.fromEntries(this.effectiveHeaderActions\n\t\t\t\t.filter((action) => action && action.id)\n\t\t\t\t.map((action) => [action.id, action]))\n\t\t},\n\n\t\t/**\n\t\t * The menu entries in the groups the menu draws: ungrouped first,\n\t\t * named groups in first appearance order, admin actions last and for\n\t\t * admins only. A page that groups nothing gets one captionless group.\n\t\t *\n\t\t * @return {Array<{key: string, label: string, entries: Array<object>}>} The groups.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-grouped-and-admin-only-menu-actions\n\t\t */\n\t\theaderMenuGroups() {\n\t\t\tconst groups = groupMenuEntries(this.headerMenuEntries, this.headerActionsById, {\n\t\t\t\tisAdmin: this.viewerIsAdmin,\n\t\t\t\tadminLabel: t('nextcloud-vue', 'Administration'),\n\t\t\t})\n\t\t\treturn groups.map((group) => ({ ...group, label: group.label ? this.effectiveTranslate(group.label) : '' }))\n\t\t},\n\n\t\t/**\n\t\t * Whether Refresh renders in the Actions menu.\n\t\t *\n\t\t * @return {boolean} The existing rule, unless `actionsMenu.showRefresh` is false.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-actions-menu-built-ins-are-optional\n\t\t */\n\t\tmenuShowsRefresh() {\n\t\t\treturn this.actionsMenu?.showRefresh === false ? false : this.effectiveHeaderShowRefresh\n\t\t},\n\n\t\t/**\n\t\t * Whether the help links render in the Actions menu.\n\t\t *\n\t\t * @return {boolean} True unless `actionsMenu.showHelpLinks` is false.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-actions-menu-built-ins-are-optional\n\t\t */\n\t\tmenuShowsHelpLinks() {\n\t\t\treturn this.actionsMenu?.showHelpLinks !== false\n\t\t},\n\n\t\t/**\n\t\t * The Actions menu's name.\n\t\t *\n\t\t * @return {string|undefined} `actionsMenu.label` translated, or undefined for the default \"Actions\".\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-actions-menu-built-ins-are-optional\n\t\t */\n\t\tactionsMenuName() {\n\t\t\tconst label = this.actionsMenu?.label\n\t\t\treturn typeof label === 'string' && label !== '' ? this.effectiveTranslate(label) : undefined\n\t\t},\n\n\t\t/**\n\t\t * The menu's name as the header draws it: under the board look always\n\t\t * a label (\"More\" unless the manifest names it), never icon-only.\n\t\t *\n\t\t * @return {string|undefined} The label, or undefined for the default \"Actions\".\n\t\t * @spec openspec/changes/screens-detail-page-parity/specs/detail-page-board-look/spec.md#requirement-the-detail-header-buttons-follow-one-order\n\t\t */\n\t\tboardMenuLabel() {\n\t\t\tif (this.actionsMenuName) {\n\t\t\t\treturn this.actionsMenuName\n\t\t\t}\n\t\t\treturn this.isBoard ? t('nextcloud-vue', 'More') : undefined\n\t\t},\n\n\t\t/**\n\t\t * Whether the page is showing its body: not the not-found, loading,\n\t\t * error or empty state. The \"what now\" card and the side column are\n\t\t * siblings of the body and follow it.\n\t\t *\n\t\t * @return {boolean} True when the body renders.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-next-step-card\n\t\t */\n\t\tshowsBody() {\n\t\t\treturn !this.objectNotFound && !this.contentWithheld && !this.showLoadingState && !this.error && !this.empty\n\t\t},\n\n\t\t/**\n\t\t * Whether the loaded record asks for a purpose before it opens\n\t\t * (`accessPurpose.when` holds for it). Undecided, and so withheld,\n\t\t * while the record or the records its condition reads are on their way.\n\t\t *\n\t\t * @spec openspec/changes/detail-access-purpose/specs/detail-access-purpose/spec.md#requirement-a-detail-page-can-ask-for-a-purpose-before-it-opens\n\t\t * @return {boolean}\n\t\t */\n\t\taccessPurposeRequired() {\n\t\t\tconst gate = this.accessPurpose\n\t\t\tif (!gate || typeof gate !== 'object' || !gate.when || !this.widgetVisibilityData || this.visibilityRowsPending) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn detailWidgetVisible({ visibleWhen: gate.when }, this.widgetVisibilityData)\n\t\t},\n\n\t\t/**\n\t\t * Whether the page holds the record back: it asks for a purpose and\n\t\t * none is recorded for this record in this session yet, or it cannot\n\t\t * tell yet whether it asks.\n\t\t *\n\t\t * @spec openspec/changes/detail-access-purpose/specs/detail-access-purpose/spec.md#requirement-a-detail-page-can-ask-for-a-purpose-before-it-opens\n\t\t * @return {boolean}\n\t\t */\n\t\tcontentWithheld() {\n\t\t\tconst gate = this.accessPurpose\n\t\t\tif (!gate || typeof gate !== 'object' || !gate.when || this.objectNotFound) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif (this.accessPurposeGranted.includes(this.accessPurposeKey)) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tconst undecided = !this.widgetVisibilityData || this.visibilityRowsPending\n\t\t\treturn undecided || this.accessPurposeRequired\n\t\t},\n\n\t\t/**\n\t\t * The session key of the purpose recorded for this record.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\taccessPurposeKey() {\n\t\t\treturn `cn-access-purpose:${this.resolvedObjectType || ''}:${this.objectId || ''}`\n\t\t},\n\n\t\t/**\n\t\t * The purposes offered, through the app's translate.\n\t\t *\n\t\t * @return {Array<{value: string, label: string}>}\n\t\t */\n\t\taccessPurposeOptions() {\n\t\t\tconst options = Array.isArray(this.accessPurpose?.options) ? this.accessPurpose.options : []\n\t\t\treturn options\n\t\t\t\t.filter((option) => option && typeof option.value === 'string' && option.value !== '')\n\t\t\t\t.map((option) => ({ value: option.value, label: this.cnTranslate(option.label || option.value) }))\n\t\t},\n\n\t\t/**\n\t\t * `nextStep` with its stages read from the record when `stagesFrom`\n\t\t * names a stages map there (a case type's own texts), else as declared.\n\t\t *\n\t\t * @spec openspec/changes/detail-next-step-from-record/specs/detail-page/spec.md#requirement-the-next-step-stages-can-come-from-the-record\n\t\t * @return {object|null}\n\t\t */\n\t\teffectiveNextStep() {\n\t\t\tconst config = this.nextStep\n\t\t\tif (!config || typeof config !== 'object' || typeof config.stagesFrom !== 'string' || config.stagesFrom === '' || !this.widgetVisibilityData) {\n\t\t\t\treturn config\n\t\t\t}\n\t\t\tconst found = readVisibleWhenPath(this.widgetVisibilityData, config.stagesFrom)\n\t\t\tconst stages = found && typeof found === 'object' && !Array.isArray(found)\n\t\t\t\t? (found.stages && typeof found.stages === 'object' && !Array.isArray(found.stages) ? found.stages : found)\n\t\t\t\t: null\n\t\t\treturn stages && Object.keys(stages).length > 0 ? { ...config, stages } : config\n\t\t},\n\n\t\t/**\n\t\t * The \"what now\" card for the record's stage, labels translated.\n\t\t *\n\t\t * @return {{stage: string, title: string, items: Array<object>, after: string}|null} The card, or null when the stage declares no checklist.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-next-step-card\n\t\t */\n\t\tnextStepCard() {\n\t\t\tconst card = resolveNextStep(this.effectiveNextStep, this.currentObject, this.stageField)\n\t\t\tif (!card) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst tr = (text) => (text ? this.effectiveTranslate(text) : '')\n\t\t\treturn {\n\t\t\t\t...card,\n\t\t\t\ttitle: firstFilled(card.title, this.currentObject, tr),\n\t\t\t\tafter: firstFilled(card.after, this.currentObject, tr),\n\t\t\t\titems: card.items.map((item) => ({ ...item, label: tr(item.label), hint: tr(item.hint) })),\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Whether the \"what now\" card renders.\n\t\t *\n\t\t * @return {boolean} True when the stage has a checklist, the body is showing and the open tab allows it.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-next-step-card\n\t\t * @spec openspec/changes/detail-actions-per-tab/specs/detail-page/spec.md#requirement-quick-actions-and-the-next-step-card-can-follow-the-open-tab\n\t\t */\n\t\tshowsNextStepCard() {\n\t\t\treturn Boolean(this.nextStepCard) && this.showsBody && onTab(this.nextStep, this.activeBodyTab)\n\t\t},\n\n\t\t/**\n\t\t * The pills above the title: type first, then status.\n\t\t *\n\t\t * @return {Array<{key: string, label: string, colorKey: string, colorMap: object|null, variant: string}>} The pills whose field has a value.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-header-pills\n\t\t */\n\t\theaderPills() {\n\t\t\treturn [['type', this.typePill], ['status', this.statusPill]]\n\t\t\t\t.map(([key, config]) => {\n\t\t\t\t\tconst pill = pillCountSource(config)\n\t\t\t\t\t\t? resolveCountPill(config, this.pillCounts[key], (text) => this.effectiveTranslate(text))\n\t\t\t\t\t\t: resolvePill(config, this.resolvedObject)\n\t\t\t\t\tif (!pill) {\n\t\t\t\t\t\treturn null\n\t\t\t\t\t}\n\t\t\t\t\t// `resolve`: the referenced record's label replaces the id, and\n\t\t\t\t\t// keys the colour too (\"Woo-verzoek\", not a uuid).\n\t\t\t\t\tconst resolved = this.pillRefLabels[key]\n\t\t\t\t\tconst look = pillLook(this.pillRefLooks[key])\n\t\t\t\t\tif (typeof resolved === 'string' && resolved !== '') {\n\t\t\t\t\t\treturn { ...pill, ...look, key, label: this.effectiveTranslate(resolved), colorKey: resolved }\n\t\t\t\t\t}\n\t\t\t\t\treturn { ...pill, ...look, key, label: pillLabelFilled(this.effectiveTranslate(pill.label), this.resolvedObject) }\n\t\t\t\t})\n\t\t\t\t.filter(Boolean)\n\t\t},\n\n\t\t/**\n\t\t * What the count pills depend on: the record id and the lists they\n\t\t * count. A change counts again.\n\t\t *\n\t\t * @return {string} The key.\n\t\t * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-a-header-pill-can-be-a-constant-or-a-count\n\t\t */\n\t\t/**\n\t\t * What the resolved pills depend on, as one key: a change looks again.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/detail-pill-resolve/specs/detail-page/spec.md#requirement-a-header-pill-can-show-a-referenced-record-s-label\n\t\t */\n\t\tpillResolveKey() {\n\t\t\treturn JSON.stringify(this.pillResolveTargets)\n\t\t},\n\n\t\t/**\n\t\t * The pills with `resolve: { register, schema, labelField? }` and the\n\t\t * id each one shows: `[key, register, schema, labelField, id]`. Empty\n\t\t * while the field is empty.\n\t\t *\n\t\t * @return {Array<Array<string>>}\n\t\t * @spec openspec/changes/detail-pill-resolve/specs/detail-page/spec.md#requirement-a-header-pill-can-show-a-referenced-record-s-label\n\t\t */\n\t\tpillResolveTargets() {\n\t\t\treturn [['type', this.typePill], ['status', this.statusPill]]\n\t\t\t\t.filter(([, config]) => config && config.resolve && config.resolve.register && config.resolve.schema && typeof config.field === 'string')\n\t\t\t\t.map(([key, config]) => {\n\t\t\t\t\tconst pill = resolvePill({ field: config.field }, this.resolvedObject)\n\t\t\t\t\tconst { register, schema, labelField, colorField, iconField } = config.resolve\n\t\t\t\t\treturn pill ? [key, register, schema, labelField || '', pill.colorKey, colorField || '', iconField || ''] : null\n\t\t\t\t})\n\t\t\t\t.filter(Boolean)\n\t\t},\n\n\t\tpillCountKey() {\n\t\t\treturn JSON.stringify([this.objectId, pillCountSource(this.typePill), pillCountSource(this.statusPill)])\n\t\t},\n\n\t\t/**\n\t\t * The widget the header holds (`headerWidget`): the definition in\n\t\t * `widgets` with that id, or null without the key or a match.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-detail-header-can-be-a-card-that-holds-a-widget\n\t\t * @return {object|null} The widget definition, or null.\n\t\t */\n\t\theaderWidgetDef() {\n\t\t\tif (!this.headerWidget) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst declared = Array.isArray(this.widgets) ? this.widgets : []\n\t\t\treturn declared.find((widget) => widget && widget.id === this.headerWidget) || null\n\t\t},\n\n\t\t/**\n\t\t * The side column's widget definitions. A string names a widget\n\t\t * declared in `widgets`; an object is a definition of its own. A\n\t\t * definition whose `visibleWhen` does not hold for the record (or the\n\t\t * records `visibilityContext` names) is left out, exactly as in the\n\t\t * body grid; edit mode shows them all.\n\t\t *\n\t\t * @return {Array<object>} The definitions that resolve, each with an `id`.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-side-column\n\t\t * @spec openspec/changes/detail-side-visible-when/specs/detail-page/spec.md#requirement-a-side-column-widget-shows-only-while-its-visiblewhen-holds\n\t\t */\n\t\tsideColumnWidgets() {\n\t\t\tconst resolved = this.pickedSideWidgets.filter((widget) => this.sideWidgetOnTab(widget) && this.sideWidgetVisible(widget))\n\t\t\tif (!this.isBoard) {\n\t\t\t\treturn resolved\n\t\t\t}\n\t\t\t// The board look: the notice first, the History card last.\n\t\t\tconst rank = (widget) => (widget.type === 'banner' ? 0 : isActivityWidget(widget) ? 2 : 1)\n\t\t\treturn resolved\n\t\t\t\t.map((widget, index) => ({ widget, index }))\n\t\t\t\t.sort((a, b) => (rank(a.widget) - rank(b.widget)) || (a.index - b.index))\n\t\t\t\t.map(({ widget }) => widget)\n\t\t},\n\n\t\t/**\n\t\t * The side column's definitions as the record picks them\n\t\t * (`sideColumnFrom`): the ids it lists, in its order, or every\n\t\t * declared entry when it lists none. Edit mode shows them all.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t * @spec openspec/changes/detail-side-column-from/specs/detail-page/spec.md#requirement-a-record-can-pick-the-side-column-and-its-order\n\t\t */\n\t\tpickedSideWidgets() {\n\t\t\tconst declared = this.declaredSideWidgets\n\t\t\tif (!this.sideColumnFrom || this.editingBody || !this.widgetVisibilityData) {\n\t\t\t\treturn declared\n\t\t\t}\n\t\t\tconst ids = readVisibleWhenPath(this.widgetVisibilityData, this.sideColumnFrom)\n\t\t\tif (!Array.isArray(ids) || ids.length === 0) {\n\t\t\t\treturn declared\n\t\t\t}\n\t\t\treturn ids\n\t\t\t\t.map((id) => declared.find((widget) => widget.id === id) || null)\n\t\t\t\t.filter(Boolean)\n\t\t},\n\n\t\t/**\n\t\t * The side column's entries resolved to definitions, before any\n\t\t * `visibleWhen` is read: a string names a widget declared in\n\t\t * `widgets`, an object is a definition of its own (given an id when it\n\t\t * has none).\n\t\t *\n\t\t * @return {Array<object>}\n\t\t * @spec openspec/changes/detail-side-visible-when/specs/detail-page/spec.md#requirement-a-side-column-widget-shows-only-while-its-visiblewhen-holds\n\t\t */\n\t\tdeclaredSideWidgets() {\n\t\t\tconst declared = Array.isArray(this.widgets) ? this.widgets : []\n\t\t\treturn (Array.isArray(this.sideColumn) ? this.sideColumn : [])\n\t\t\t\t.map((entry, index) => {\n\t\t\t\t\tif (typeof entry === 'string') {\n\t\t\t\t\t\treturn declared.find((widget) => widget && widget.id === entry) || null\n\t\t\t\t\t}\n\t\t\t\t\tif (!entry || typeof entry !== 'object' || Array.isArray(entry)) {\n\t\t\t\t\t\treturn null\n\t\t\t\t\t}\n\t\t\t\t\treturn entry.id ? entry : { ...entry, id: `cn-side-${index}` }\n\t\t\t\t})\n\t\t\t\t.filter(Boolean)\n\t\t},\n\n\t\t/**\n\t\t * The `columnOpts` the body grid gets: the default table stacks at 1000px\n\t\t * of grid width, or at 600px when a side column narrows the grid. An\n\t\t * explicit `columnOpts` prop is passed through untouched.\n\t\t *\n\t\t * @return {object|null} The column options.\n\t\t */\n\t\tbodyColumnOpts() {\n\t\t\tif (!this.showsSideColumn || !isDefaultColumnOpts(this.columnOpts)) {\n\t\t\t\treturn this.columnOpts\n\t\t\t}\n\t\t\treturn { ...this.columnOpts, breakpoints: [{ w: SIDE_COLUMN_STACK_WIDTH, c: 1 }] }\n\t\t},\n\n\t\t/**\n\t\t * Whether the side column renders.\n\t\t *\n\t\t * @return {boolean} True when it has widgets and the body is showing.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-side-column\n\t\t */\n\t\t/**\n\t\t * The band's widgets, resolved and visible, each with its span.\n\t\t *\n\t\t * @return {Array<{widget: object, span: number, chrome: string}>}\n\t\t * @spec openspec/changes/detail-band/specs/detail-page/spec.md#requirement-a-detail-page-can-draw-a-band-across-the-page-above-the-body-and-the-side-column\n\t\t */\n\t\tbandWidgets() {\n\t\t\tconst declared = Array.isArray(this.widgets) ? this.widgets : []\n\t\t\treturn (Array.isArray(this.band) ? this.band : [])\n\t\t\t\t.map((entry, index) => {\n\t\t\t\t\tlet widget = null\n\t\t\t\t\tlet span = 12\n\t\t\t\t\tif (typeof entry === 'string') {\n\t\t\t\t\t\twidget = declared.find((candidate) => candidate && candidate.id === entry) || null\n\t\t\t\t\t} else if (entry && typeof entry === 'object' && !Array.isArray(entry)) {\n\t\t\t\t\t\tspan = Number.isInteger(entry.span) ? Math.min(12, Math.max(1, entry.span)) : 12\n\t\t\t\t\t\tif (typeof entry.widgetId === 'string') {\n\t\t\t\t\t\t\twidget = declared.find((candidate) => candidate && candidate.id === entry.widgetId) || null\n\t\t\t\t\t\t} else if (typeof entry.type === 'string') {\n\t\t\t\t\t\t\twidget = entry.id ? entry : { ...entry, id: `cn-band-${index}` }\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif (!widget) {\n\t\t\t\t\t\treturn null\n\t\t\t\t\t}\n\t\t\t\t\tconst chrome = ['card', 'bare'].includes(entry?.chrome) ? entry.chrome : (widget.type === 'banner' ? 'bare' : 'card')\n\t\t\t\t\treturn { widget, span, chrome }\n\t\t\t\t})\n\t\t\t\t.filter((entry) => entry && this.sideWidgetVisible(entry.widget))\n\t\t},\n\n\t\t/**\n\t\t * Whether the band renders: it has a visible widget and the record shows.\n\t\t *\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/detail-band/specs/detail-page/spec.md#requirement-a-detail-page-can-draw-a-band-across-the-page-above-the-body-and-the-side-column\n\t\t */\n\t\tshowsBand() {\n\t\t\treturn (this.bandWidgets.length > 0 || this.detailPageFilters.length > 0) && this.showsBody\n\t\t},\n\n\t\t/**\n\t\t * The declared page filters that carry a key.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t * @spec openspec/changes/detail-page-filters/specs/detail-page/spec.md#requirement-a-detail-page-can-draw-page-filters-that-its-widgets-read\n\t\t */\n\t\tdetailPageFilters() {\n\t\t\treturn (Array.isArray(this.pageFilters) ? this.pageFilters : []).filter((pf) => pf && pf.key)\n\t\t},\n\n\t\tshowsSideColumn() {\n\t\t\treturn this.sideColumnWidgets.length > 0 && this.showsBody\n\t\t},\n\n\t\t/**\n\t\t * Accessible name of the side column.\n\t\t *\n\t\t * @return {string} The label.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-side-column\n\t\t */\n\t\tsideColumnLabel() {\n\t\t\treturn t('nextcloud-vue', 'Details')\n\t\t},\n\n\t\t/**\n\t\t * Where this record sits in the list it was opened from, as text.\n\t\t *\n\t\t * @return {string} For example \"4 of 40\".\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tlistPositionLabel() {\n\t\t\tconst nav = this.listNavigation\n\t\t\tif (!nav || !nav.total) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn `${nav.position} ${this.effectiveTranslate('of')} ${nav.total}`\n\t\t},\n\n\t\t/**\n\t\t * Accessible name for the previous control, which says what list it\n\t\t * steps through rather than only which direction it goes.\n\t\t *\n\t\t * @return {string} The label.\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tpreviousRecordLabel() {\n\t\t\treturn this.listNavigation?.isFirst\n\t\t\t\t? this.effectiveTranslate('This is the first record in the list')\n\t\t\t\t: this.effectiveTranslate('Previous record in the list')\n\t\t},\n\n\t\t/**\n\t\t * Accessible name for the next control.\n\t\t *\n\t\t * @return {string} The label.\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tnextRecordLabel() {\n\t\t\treturn this.listNavigation?.isLast\n\t\t\t\t? this.effectiveTranslate('This is the last record in the list')\n\t\t\t\t: this.effectiveTranslate('Next record in the list')\n\t\t},\n\n\t\t/**\n\t\t * Stable id for the page-header Actions menu. Prefers the explicit\n\t\t * `pageId` prop; falls back to a slugified `title` so the menu still\n\t\t * gets a usable id / surface.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\t/**\n\t\t * Whether the page is in Buildiq edit mode — unwraps the injected\n\t\t * `cnEditingBody` ref (or plain boolean). Drives the per-widget cog.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\teditingBody() {\n\t\t\tconst e = this.cnEditingBody\n\t\t\treturn Boolean(e && typeof e === 'object' && 'value' in e ? e.value : e)\n\t\t},\n\n\t\t/**\n\t\t * The widget definition currently being configured, shaped for\n\t\t * `CnWidgetStyleEditorModal` (carries `id`, `type`, `title`, `content`).\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tconfigWidget() {\n\t\t\tif (!this.configWidgetId) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst def = this.bodyGridWidgets.find((w) => w.id === this.configWidgetId)\n\t\t\treturn def || null\n\t\t},\n\n\t\tresolvedPageId() {\n\t\t\tif (this.pageId) {\n\t\t\t\treturn this.pageId\n\t\t\t}\n\t\t\treturn String(this.title || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')\n\t\t},\n\n\t\t/**\n\t\t * Whether to show the full-page loading state (spinner that replaces\n\t\t * the page content). Only on the FIRST load — when `loading` is true\n\t\t * and nothing has been shown yet. Once `hasLoadedOnce` is set, a\n\t\t * subsequent `loading` (refresh / re-fetch) keeps the existing\n\t\t * content in place; the action-button spinner signals the refresh\n\t\t * instead (see `effectiveRefreshing`).\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tshowLoadingState() {\n\t\t\treturn (this.loading && !this.hasLoadedOnce) || this.awaitingObject\n\t\t},\n\n\t\t/**\n\t\t * Whether the page is still waiting for its first answer on the record.\n\t\t * The body, lifecycle menu and sidebar stay unmounted meanwhile, because\n\t\t * each of them requests endpoints scoped to the id and a missing record\n\t\t * would turn every one of those into a 404 or 500.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tawaitingObject() {\n\t\t\tif (this.hasRecordSourceFetch) {\n\t\t\t\treturn this.objectFetchPending && !this.currentObject\n\t\t\t}\n\t\t\treturn this.hasSchemaDrivenFetch\n\t\t\t\t&& this.objectFetchPending\n\t\t\t\t&& typeof this.effectiveObjectStore?.fetchObject === 'function'\n\t\t\t\t&& !this.currentObject\n\t\t},\n\n\t\t/**\n\t\t * Where the not-found state's button goes: `notFoundRoute`, or the app's\n\t\t * root when the host names no list to return to.\n\t\t *\n\t\t * @return {object|string}\n\t\t */\n\t\tnotFoundTarget() {\n\t\t\treturn this.notFoundRoute || { path: '/' }\n\t\t},\n\n\t\t/** Heading and description of the not-found state. */\n\t\tnotFoundLabels() {\n\t\t\treturn {\n\t\t\t\tname: t('nextcloud-vue', 'Not found'),\n\t\t\t\tdescription: t('nextcloud-vue', 'This item does not exist or has been deleted.'),\n\t\t\t}\n\t\t},\n\n\t\t/** Label of the not-found state's button, unescaped because `{{ }}` escapes. */\n\t\tnotFoundButtonLabel() {\n\t\t\treturn this.notFoundRoute && this.notFoundRouteLabel\n\t\t\t\t? t('nextcloud-vue', 'Back to {page}', { page: this.notFoundRouteLabel }, undefined, { escape: false })\n\t\t\t\t: t('nextcloud-vue', 'Back to home')\n\t\t},\n\n\t\t/**\n\t\t * Refresh-spinner flag for the page-header Actions menu. True when:\n\t\t * an explicit `refreshing` prop is set; OR a schema-driven self-fetch\n\t\t * is in flight (`internalRefreshing`); OR a background load is running\n\t\t * after the first one completed (`loading && hasLoadedOnce`) — so\n\t\t * legacy `objectType` hosts get the spinner for free just by passing\n\t\t * `:loading`, without wiring `:refreshing` themselves.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\teffectiveRefreshing() {\n\t\t\treturn this.refreshing || this.internalRefreshing || (this.loading && this.hasLoadedOnce)\n\t\t},\n\n\t\t/**\n\t\t * Effective object-type slug, used for subscription, lock, store\n\t\t * registration, fetch, and sidebar state. Explicit `objectType`\n\t\t * prop wins (existing direct-mount call sites stay untouched);\n\t\t * otherwise `${register}-${schema}` fuses the schema-driven props\n\t\t * the manifest renderer passes. Returns `''` when neither path\n\t\t * yields a slug — the schema-driven gates downstream key off that.\n\t\t */\n\t\tresolvedObjectType() {\n\t\t\tif (this.objectType) {\n\t\t\t\treturn this.objectType\n\t\t\t}\n\t\t\tif (this.register && this.schema) {\n\t\t\t\treturn `${this.register}-${this.schema}`\n\t\t\t}\n\t\t\treturn ''\n\t\t},\n\n\t\t/**\n\t\t * True when this mount is wired for the manifest's schema-driven\n\t\t * contract: `register`, `schema`, and `objectId` are all set, so\n\t\t * the page should fetch the object + schema, register the type\n\t\t * on the store, and render the auto-body widgets when no slot\n\t\t * content is supplied. Legacy direct mounts (which pass an\n\t\t * explicit `objectType` instead) return `false` here and skip\n\t\t * the fetch path entirely.\n\t\t */\n\t\thasSchemaDrivenFetch() {\n\t\t\treturn !this.entitySource && Boolean(this.register && this.schema && this.objectId)\n\t\t},\n\n\t\t/**\n\t\t * The record source named by `entitySource`, or null. An unknown name,\n\t\t * or a source that cannot read one record, is null with a warning.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\trecordSource() {\n\t\t\tif (!this.entitySource) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\ttry {\n\t\t\t\treturn resolveRecordSource(this.entitySource)\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console -- the console is the only channel a host app can read the detail on\n\t\t\t\tconsole.warn('[CnDetailPage] the record source could not start; no record is shown.', err)\n\t\t\t\treturn null\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Whether the page reads its record through a record source.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\thasRecordSourceFetch() {\n\t\t\treturn Boolean(this.recordSource && this.objectId)\n\t\t},\n\n\t\t/**\n\t\t * Whether the page reads its own record, from the object store or a\n\t\t * record source, so Refresh and `cn:page:refresh` have something to do.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tselfFetchesRecord() {\n\t\t\treturn this.hasSchemaDrivenFetch || this.hasRecordSourceFetch\n\t\t},\n\n\t\t/**\n\t\t * Whether the header Edit button renders. Opted in via `showEditAction`,\n\t\t * but gated on there actually being a record and a schema to edit it\n\t\t * with: `isCreateMode` already owns the id-less page, and a button that\n\t\t * opens a form with no schema is a dead button.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\t/** Whether the lifecycle transitions are mounted. */\n\t\tshowsLifecycleActions() {\n\t\t\tif (this.objectNotFound || this.awaitingObject) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn Boolean(this.lifecycleActions && (this.objectId || this.currentObject))\n\t\t},\n\n\t\t/**\n\t\t * Everything the Actions menu lists above its built-in items: the\n\t\t * lifecycle transitions, then the declared header actions.\n\t\t *\n\t\t * @return {Array<object>} Menu-ready entries.\n\t\t */\n\t\theaderMenuEntries() {\n\t\t\tconst lifecycle = this.showsLifecycleActions ? this.lifecycleMenuEntries : []\n\t\t\t// An action drawn as a button, the stage's primary one or a quick\n\t\t\t// action, is not listed a second time in the menu.\n\t\t\tconst pinned = new Set([\n\t\t\t\t...(this.stagePrimaryEntry ? [this.stagePrimaryEntry.id] : []),\n\t\t\t\t...this.quickActionEntries.map((entry) => entry.id),\n\t\t\t])\n\t\t\tconst inMenu = pinned.size > 0\n\t\t\t\t? this.menuHeaderActions.filter((entry) => !pinned.has(entry.id))\n\t\t\t\t: this.menuHeaderActions\n\t\t\treturn [...this.sourceVerbMenuEntries, ...lifecycle, ...inMenu]\n\t\t},\n\n\t\t/**\n\t\t * The verbs the record source offers on the record in view, read off\n\t\t * the record's own state by the source. Empty without a source that\n\t\t * offers verbs, before the record is read, and when it was not found.\n\t\t *\n\t\t * @return {Array<object>} The verbs.\n\t\t * @spec openspec/changes/record-source-verbs/specs/record-source-verbs/spec.md#requirement-a-record-source-offers-header-verbs\n\t\t */\n\t\tsourceVerbs() {\n\t\t\tconst source = this.recordSource\n\t\t\tif (!source || typeof source.verbs !== 'function' || typeof source.runVerb !== 'function') {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\tif (this.objectNotFound || !this.currentObject) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\tconst verbs = source.verbs(this.currentObject)\n\t\t\treturn Array.isArray(verbs) ? verbs.filter((verb) => verb && typeof verb.id === 'string' && verb.id !== '') : []\n\t\t},\n\n\t\t/**\n\t\t * Whether the record source can tick the checklist of the record in\n\t\t * view: it writes items, and says this record can still be ticked.\n\t\t *\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/record-checklist-widget/specs/record-checklist-widget/spec.md#requirement-a-checklist-widget-ticks-items-through-the-record-source\n\t\t */\n\t\trecordCanCheckItems() {\n\t\t\tconst source = this.recordSource\n\t\t\tif (!source || typeof source.checkItem !== 'function' || !this.currentObject || this.objectNotFound) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn typeof source.canCheckItems !== 'function' || source.canCheckItems(this.currentObject) === true\n\t\t},\n\n\t\t/**\n\t\t * The source verbs drawn as header buttons.\n\t\t *\n\t\t * @return {Array<object>} The verbs.\n\t\t */\n\t\tsourceVerbButtons() {\n\t\t\treturn this.sourceVerbs.filter((verb) => verb.menu !== true)\n\t\t},\n\n\t\t/**\n\t\t * The source verbs listed in the Actions menu, as menu entries.\n\t\t *\n\t\t * @return {Array<object>} Menu-ready entries.\n\t\t */\n\t\tsourceVerbMenuEntries() {\n\t\t\treturn this.sourceVerbs\n\t\t\t\t.filter((verb) => verb.menu === true)\n\t\t\t\t.map((verb) => ({\n\t\t\t\t\tid: `cn-source-verb-${verb.id}`,\n\t\t\t\t\tlabel: verb.label,\n\t\t\t\t\ttestid: `cn-detail-page-verb-${verb.id}`,\n\t\t\t\t\ticonName: typeof verb.icon === 'string' ? verb.icon : '',\n\t\t\t\t\tdisabled: this.sourceVerbBusy,\n\t\t\t\t\tpressed: null,\n\t\t\t\t\trun: () => this.runSourceVerb(verb),\n\t\t\t\t}))\n\t\t},\n\n\t\t/**\n\t\t * Whether Edit joins the CnActionButtons cluster instead of standing\n\t\t * on its own. Opted into by `inlineActions`.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tfoldsEditIntoActions() {\n\t\t\t// Edit keeps its own labelled button in the board look.\n\t\t\treturn typeof this.inlineActions === 'number' && !this.isBoard\n\t\t},\n\n\t\t/**\n\t\t * The header actions actually handed to CnActionButtons: the declared\n\t\t * ones, plus this page's Edit appended when `inlineActions` folds it\n\t\t * in. Edit goes LAST so it reads after the app's own actions in the\n\t\t * Actions menu — the page's affordance yields to the app's.\n\t\t *\n\t\t * @spec openspec/changes/header-action-href/specs/header-action-href/spec.md#requirement-a-header-action-with-href-is-a-link-that-runs-and-emits-nothing\n\t\t * @return {Array<object>} The action descriptors.\n\t\t */\n\t\teffectiveHeaderActions() {\n\t\t\tconst authored = Array.isArray(this.headerActions) ? this.headerActions : []\n\t\t\t// Inline pinned actions (a stage's primary action, a quick action\n\t\t\t// written out in place) join the list CnActionButtons dispatches,\n\t\t\t// so they get the same dialogs, toasts and `visibleWhen` gating as\n\t\t\t// any header action. An id that is already declared is not added\n\t\t\t// twice. With nothing pinned inline and no `href` entry this is\n\t\t\t// `headerActions` itself.\n\t\t\tconst known = new Set(authored.map((action) => action && action.id))\n\t\t\tconst inline = [this.stagePrimary, ...this.pinnedQuickActions]\n\t\t\t\t.filter((pinned) => pinned && pinned.inline && !known.has(pinned.id))\n\t\t\t\t.map((pinned) => pinned.inline)\n\t\t\tconst combined = inline.length > 0 ? [...authored, ...inline] : authored\n\t\t\t// An entry (or child) with `href` becomes a link CnActionButtons renders as an anchor.\n\t\t\tconst linked = combined.map((action) => headerActionAsLink(action, this.headerLinkTokenCtx))\n\t\t\tconst declared = linked.some((action, index) => action !== combined[index]) ? linked : combined\n\t\t\tif (!(this.foldsEditIntoActions && this.canEditRecord)) {\n\t\t\t\treturn declared\n\t\t\t}\n\t\t\treturn [\n\t\t\t\t...declared,\n\t\t\t\t{\n\t\t\t\t\tid: 'cn-detail-page-edit',\n\t\t\t\t\tlabel: this.editActionLabel,\n\t\t\t\t\ticon: 'Pencil',\n\t\t\t\t\t// A function, not a dispatch type: `open-form` hardcodes\n\t\t\t\t\t// `item: null`, so it creates and cannot edit this record.\n\t\t\t\t\tonSelect: () => this.openEditForm(),\n\t\t\t\t},\n\t\t\t]\n\t\t},\n\n\t\tcanEditRecord() {\n\t\t\treturn this.showEditAction\n\t\t\t\t&& !this.isCreateMode\n\t\t\t\t&& this.hasSchemaDrivenFetch\n\t\t\t\t&& Boolean(this.currentSchema)\n\t\t\t\t&& !this.editLockedByOther\n\t\t},\n\n\t\t/**\n\t\t * Whether somebody else is editing this record right now.\n\t\t *\n\t\t * The banner above already names them, so the button is withdrawn\n\t\t * rather than shown and refused: an Edit that opens a form the write\n\t\t * will reject is a form somebody fills in twice.\n\t\t *\n\t\t * @return {boolean} True when another user holds the lock.\n\t\t */\n\t\teditLockedByOther() {\n\t\t\treturn Boolean(this.lockState\n\t\t\t\t&& this.lockState.locked.value\n\t\t\t\t&& !this.lockState.lockedByMe.value)\n\t\t},\n\n\t\t/**\n\t\t * Label for the header Edit button: the manifest's `editLabel` through\n\t\t * the host translate, else the library's translated \"Edit\".\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/detail-edit-label-translate/specs/detail-page/spec.md#requirement-the-edit-button-label-follows-the-reader-s-language\n\t\t */\n\t\teditActionLabel() {\n\t\t\treturn this.editLabel ? this.effectiveTranslate(this.editLabel) : t('nextcloud-vue', 'Edit')\n\t\t},\n\n\t\t/** @return {string} The edit form's title, translated; the Edit button's label when none is configured. */\n\t\teditFormTitle() {\n\t\t\treturn this.editTitle ? this.effectiveTranslate(this.editTitle) : this.editActionLabel\n\t\t},\n\n\t\t/** @return {string} The edit form's subtitle, translated; empty when none is configured. */\n\t\teditFormSubtitle() {\n\t\t\treturn this.editSubtitle ? this.effectiveTranslate(this.editSubtitle) : ''\n\t\t},\n\n\t\t/**\n\t\t * The create archetype: a schema-bound page (`register` + `schema`) whose\n\t\t * route carries NO object id, and which has no hand-authored body of its\n\t\t * own. Instead of rendering a blank page (procest `/tasks/new`), the page\n\t\t * shows an empty create form for the schema (ADR-062). Suppressed when a\n\t\t * default slot or an explicit grid `layout` supplies the body.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tisCreateMode() {\n\t\t\t// `createForm` (manifest `config.createForm`) is the explicit switch;\n\t\t\t// 'auto' keeps the original heuristic. A page still needs a schema to\n\t\t\t// build a form from, and one already showing an object is editing,\n\t\t\t// not creating — so those two conditions hold even for 'always'.\n\t\t\tif (this.createForm === 'never') {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tconst schemaBound = Boolean(this.register && this.schema) && !this.objectId\n\t\t\tif (this.createForm === 'always') {\n\t\t\t\treturn schemaBound\n\t\t\t}\n\t\t\treturn schemaBound && !this.hasDefaultSlotContent && !this.hasGridLayout\n\t\t},\n\n\t\t/**\n\t\t * Create-form prefill drawn from the route query, filtered to the\n\t\t * schema's own properties (e.g. `?caseId=x` seeds the `caseId` field so\n\t\t * a child created from a parent is pre-linked). Null when nothing\n\t\t * applies (true empty create).\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tcreatePrefill() {\n\t\t\tconst q = (this.$route && this.$route.query) || {}\n\t\t\tconst props = (this.currentSchema && this.currentSchema.properties) || {}\n\t\t\tconst out = {}\n\t\t\tfor (const [k, v] of Object.entries(q)) {\n\t\t\t\tif (Object.hasOwn(props, k)) {\n\t\t\t\t\tout[k] = v\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn Object.keys(out).length ? out : null\n\t\t},\n\n\t\t/**\n\t\t * `show` for the `#form-dialog` slot: whether the page's built-in form\n\t\t * dialog is on screen — the create archetype's empty form, or the\n\t\t * record edit form.\n\t\t *\n\t\t * Mirrors the `v-if` on the two default children exactly, so a\n\t\t * replacement opens and closes on the same conditions the built-in\n\t\t * dialog does. That includes the `editFormAwaitingRecord` guard (#850),\n\t\t * which is what stops an edit form opening over a record that has not\n\t\t * loaded yet — a replacement that reimplemented visibility from\n\t\t * `editFormOpen` alone would reintroduce that blank-form-overwrites-\n\t\t * the-record bug in every app that used it.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tformDialogVisible() {\n\t\t\tif (!this.currentSchema) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif (this.isCreateMode) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn this.editFormOpen && !this.editFormAwaitingRecord\n\t\t},\n\n\t\t/**\n\t\t * `item` for the `#form-dialog` slot: the record the built-in dialog is\n\t\t * bound to — the loaded object when editing, the route-derived prefill\n\t\t * when creating (null for a true empty create, matching CnIndexPage,\n\t\t * where a null `item` likewise means \"create\").\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tformDialogItem() {\n\t\t\treturn this.isCreateMode ? this.createPrefill : this.currentObject\n\t\t},\n\n\t\t/**\n\t\t * Effective Refresh visibility for the page header. An explicit\n\t\t * `showRefresh` prop wins; when unset (`null`), show Refresh only if\n\t\t * it will do something — a consumer attached an `@refresh` listener,\n\t\t * or the page can self-fetch (`hasSchemaDrivenFetch`).\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\teffectiveHeaderShowRefresh() {\n\t\t\tif (this.showRefresh !== null) {\n\t\t\t\treturn this.showRefresh\n\t\t\t}\n\t\t\t// `$.vnode.props`, not `$attrs`: `refresh` is a declared emit, and\n\t\t\t// Vue keeps declared emits out of `$attrs`.\n\t\t\treturn Boolean(this.$.vnode.props?.onRefresh) || this.selfFetchesRecord\n\t\t},\n\n\t\t/**\n\t\t * Pinia store instance used for the schema-driven fetch.\n\t\t * Mirrors `CnLogsPage.objectStore`: explicit `objectStore` prop\n\t\t * wins; otherwise falls back to the library's default\n\t\t * `useObjectStore()`. The composable is invoked lazily inside a\n\t\t * computed so test mounts that never activate Pinia don't crash.\n\t\t *\n\t\t * @return {object|null} The resolved store, or null when the page\n\t\t * is not in schema-driven mode (no need to touch Pinia at all).\n\t\t */\n\t\teffectiveObjectStore() {\n\t\t\tif (this.objectStore) {\n\t\t\t\treturn this.objectStore\n\t\t\t}\n\t\t\tif (!this.hasSchemaDrivenFetch && !this.isCreateMode) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\ttry {\n\t\t\t\treturn useObjectStore()\n\t\t\t} catch (err) {\n\t\t\t\t// Pinia not active in this consumer — fall back to no-op\n\t\t\t\t// and let the page render as if no store-driven content\n\t\t\t\t// were available. Real consumers (CnAppRoot-hosted apps)\n\t\t\t\t// always have Pinia active, so this branch only protects\n\t\t\t\t// stand-alone test mounts.\n\n\t\t\t\t// eslint-disable-next-line no-console -- a deliberate warning to the developer integrating this component\n\t\t\t\tconsole.warn('[CnDetailPage] useObjectStore() unavailable; schema-driven mode disabled.', err)\n\t\t\t\treturn null\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The fetched OR object for the schema-driven mode. Read straight\n\t\t * from the store's normalised `objects[type][id]` cache so any\n\t\t * other component fetching the same object (sidebar widgets,\n\t\t * locked-banner) shares state with no second request.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tcurrentObject() {\n\t\t\tif (this.recordSource) {\n\t\t\t\treturn this.objectId ? (this.recordSource.record(this.objectId) ?? null) : null\n\t\t\t}\n\t\t\tconst store = this.effectiveObjectStore\n\t\t\tif (!store) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst type = this.resolvedObjectType\n\t\t\tif (!type || !this.objectId) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn store.objects?.[type]?.[this.objectId] ?? null\n\t\t},\n\n\t\t/**\n\t\t * Whether the edit form is still waiting for the record it edits.\n\t\t *\n\t\t * True only while a record is actually RESOLVABLE and absent — a host\n\t\t * with no object store has nothing to wait for, and gating on it\n\t\t * there would make the Edit button dead.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\teditFormAwaitingRecord() {\n\t\t\tif (!this.effectiveObjectStore || !this.objectId) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn !this.currentObject\n\t\t},\n\n\t\t/**\n\t\t * The resolved OR object surfaced to the header's translation\n\t\t * badge slot (cn-detail-translation-aware-surfacing). Falls\n\t\t * back to `currentObject` from the schema-driven path AND, when\n\t\t * the schema-driven path is unavailable, attempts to read the\n\t\t * store via `objectType` + `objectId` directly (so legacy direct\n\t\t * mounts that don't pass `register` / `schema` still benefit).\n\t\t * Returns `null` when nothing can be resolved — the badge then\n\t\t * auto-hides.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tresolvedObject() {\n\t\t\t// Schema-driven path: reuse what `currentObject` already\n\t\t\t// computed.\n\t\t\tconst fromSchemaDriven = this.currentObject\n\t\t\tif (fromSchemaDriven) {\n\t\t\t\treturn fromSchemaDriven\n\t\t\t}\n\t\t\t// Direct-mount fallback: legacy callers pass `objectType` +\n\t\t\t// `objectId`. Read the same cache shape but keyed off the\n\t\t\t// explicit slug.\n\t\t\tconst store = this.effectiveObjectStore\n\t\t\tif (!store) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tif (!this.objectType || !this.objectId) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn store.objects?.[this.objectType]?.[this.objectId] ?? null\n\t\t},\n\n\t\t/**\n\t\t * The records `visibilityContext` names for the loaded record.\n\t\t *\n\t\t * @return {Array<{key: string, register: string, schema: string, id: string}>}\n\t\t * @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-page-can-load-records-a-condition-names\n\t\t */\n\t\tvisibilityRefs() {\n\t\t\treturn visibilityContextRefs(this.visibilityContext, this.resolvedObject)\n\t\t},\n\n\t\t/**\n\t\t * Whether a record `visibilityContext` names is still on its way.\n\t\t *\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-page-can-load-records-a-condition-names\n\t\t */\n\t\tvisibilityRowsPending() {\n\t\t\treturn this.visibilityRefs.length > 0 && this.visibilityRowsFor !== this.visibilityRefsKey\n\t\t},\n\n\t\t/**\n\t\t * The named references as one key, so another record (or another case\n\t\t * type on the same record) reads them again.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-page-can-load-records-a-condition-names\n\t\t */\n\t\tvisibilityRefsKey() {\n\t\t\treturn JSON.stringify(this.visibilityRefs)\n\t\t},\n\n\t\t/**\n\t\t * What a grid widget's `visibleWhen` reads: the record, with the\n\t\t * `visibilityContext` records under `@context.<name>`. Null until the record loads.\n\t\t *\n\t\t * @return {object|null}\n\t\t * @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-grid-widget-shows-only-while-its-visiblewhen-holds-for-the-record\n\t\t */\n\t\twidgetVisibilityData() {\n\t\t\treturn widgetConditionData(this.resolvedObject, this.visibilityRows)\n\t\t},\n\n\t\t/**\n\t\t * The page's widget definitions with each `titleWhen` resolved against\n\t\t * the record, as the cards, the tab strip and the leaves read them.\n\t\t *\n\t\t * @return {object[]}\n\t\t * @spec openspec/changes/widget-title-when/specs/widget-title-when/spec.md#requirement-a-widget-title-can-depend-on-the-record\n\t\t */\n\t\ttitledWidgets() {\n\t\t\treturn withTitlesWhen(this.widgets, this.titleData)\n\t\t},\n\n\t\t/**\n\t\t * The body grid's widget definitions with each `titleWhen` resolved.\n\t\t *\n\t\t * @return {object[]}\n\t\t * @spec openspec/changes/widget-title-when/specs/widget-title-when/spec.md#requirement-a-widget-title-can-depend-on-the-record\n\t\t */\n\t\ttitledBodyGridWidgets() {\n\t\t\treturn withTitlesWhen(this.bodyGridWidgets, this.titleData)\n\t\t},\n\n\t\t/**\n\t\t * What a `titleWhen` reads: the record with its `@context` rows, null\n\t\t * while the record or those rows are on their way.\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\ttitleData() {\n\t\t\treturn this.visibilityRowsPending ? null : this.widgetVisibilityData\n\t\t},\n\n\t\t/**\n\t\t * The grid widgets whose `visibleWhen` needs a request (endpoint or\n\t\t * source), with what they ask, as one key: a change re-asks them.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-grid-widget-shows-only-while-its-visiblewhen-holds-for-the-record\n\t\t */\n\t\tremoteWidgetConditionsKey() {\n\t\t\tif (!this.widgetVisibilityData || this.visibilityRowsPending) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst defs = [...(this.bodyGridWidgets || []), ...((this.activeView && this.activeView.widgets) || []), ...this.declaredSideWidgets]\n\t\t\tconst remote = defs.filter((def) => def && def.visibleWhen && typeof def.visibleWhen === 'object'\n\t\t\t\t&& !isLocallyDecidableVisibleWhen(def.visibleWhen))\n\t\t\tif (remote.length === 0) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn JSON.stringify([String(this.objectId || ''), remote.map((def) => [def.id, def.visibleWhen])])\n\t\t},\n\n\t\t/**\n\t\t * The `@self` block of the resolved object (interaction markers live here).\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\tresolvedSelf() {\n\t\t\tconst obj = this.resolvedObject\n\t\t\treturn (obj && typeof obj === 'object' && obj['@self'] && typeof obj['@self'] === 'object') ? obj['@self'] : {}\n\t\t},\n\n\t\t/**\n\t\t * Whether Mark as unread is offered: the object carries `@self.unread`.\n\t\t *\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/record-unread-markers/tasks.md#task-2\n\t\t */\n\t\tshowMarkUnread() {\n\t\t\treturn this.actionsMenu?.showMarkUnread !== false\n\t\t\t\t&& typeof this.resolvedSelf.unread === 'boolean'\n\t\t\t\t&& this.register !== '' && this.schema !== '' && this.objectId !== ''\n\t\t},\n\n\t\t/**\n\t\t * Whether the header menu offers \"Open sidebar\": the board look, where the\n\t\t * sidebar's own toggle is hidden, on a page whose sidebar is active and\n\t\t * closed.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tshowOpenSidebar() {\n\t\t\treturn this.actionsMenu?.showOpenSidebar !== false\n\t\t\t\t&& this.isBoard && this.hasExternalSidebar && this.sidebarActive && this.objectSidebarState.open !== true\n\t\t},\n\n\t\t/** @return {string} Label of the Open sidebar entry. */\n\t\topenSidebarLabel() {\n\t\t\treturn t('nextcloud-vue', 'Open sidebar')\n\t\t},\n\n\t\t/** @return {string} Label of the Mark as unread entry. */\n\t\tmarkUnreadLabel() {\n\t\t\treturn t('nextcloud-vue', 'Mark as unread')\n\t\t},\n\n\t\t/**\n\t\t * True once the object has rendered unread: loaded, not missing, and\n\t\t * carrying `@self.unread: true`. Sending `PUT .../read-state` hangs on this.\n\t\t *\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/record-unread-markers/tasks.md#task-2\n\t\t */\n\t\treadStateDue() {\n\t\t\treturn this.markRead !== false\n\t\t\t\t&& !this.objectFetchPending\n\t\t\t\t&& !this.objectNotFound\n\t\t\t\t&& this.resolvedSelf.unread === true\n\t\t\t\t&& this.register !== '' && this.schema !== '' && this.objectId !== ''\n\t\t},\n\n\t\t/**\n\t\t * Whether the Follow toggle renders: the object carries `@self.watching` and the page did not turn it off.\n\t\t *\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/record-favourite-and-follow/tasks.md#task-3\n\t\t */\n\t\tshowFollowToggle() {\n\t\t\treturn this.follow !== false\n\t\t\t\t&& typeof this.resolvedSelf.watching === 'boolean'\n\t\t\t\t&& this.register !== '' && this.schema !== '' && this.objectId !== ''\n\t\t},\n\n\t\t/**\n\t\t * Who else has the record open now, in words, for the follow row:\n\t\t * one name, or the first name and how many more. Empty without\n\t\t * `followRow.presence` or while nobody else is there.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/follow-row-presence/specs/board-follow-row/spec.md#requirement-the-follow-row-can-say-who-else-is-looking\n\t\t */\n\t\tpresenceSentence() {\n\t\t\tconst others = this.followRow?.presence === true && Array.isArray(this.presentOthers) ? this.presentOthers : []\n\t\t\tif (others.length === 0) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst first = String(others[0]?.user || '')\n\t\t\tconst name = userDisplayName(first) || first\n\t\t\tif (others.length === 1) {\n\t\t\t\treturn t('nextcloud-vue', '{name} is also looking at this record now', { name })\n\t\t\t}\n\t\t\treturn n('nextcloud-vue', '{name} and %n other are also looking at this record now', '{name} and %n others are also looking at this record now', others.length - 1, { name })\n\t\t},\n\n\t\t/**\n\t\t * The props of the Follow control, wherever the header draws it.\n\t\t *\n\t\t * @return {object} The CnFollowToggle props.\n\t\t * @spec openspec/changes/board-follow-row/specs/board-follow-row/spec.md#requirement-the-board-follow-control-sits-under-the-breadcrumb\n\t\t */\n\t\tfollowToggleProps() {\n\t\t\tconst self = this.resolvedSelf\n\t\t\treturn {\n\t\t\t\tregister: this.register,\n\t\t\t\tschema: this.schema,\n\t\t\t\tobjectId: String(this.objectId),\n\t\t\t\twatching: self.watching === true,\n\t\t\t\tnotify: typeof self.watchNotify === 'boolean' ? self.watchNotify : null,\n\t\t\t\twatcherCount: typeof self.watcherCount === 'number' && this.followRow?.count !== false ? self.watcherCount : null,\n\t\t\t\tcanManage: !!(self.can && self.can.manage === true),\n\t\t\t\tnotifies: this.followNotifies,\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The `_extend[]` values of the object read: the page's own, plus\n\t\t * `@self.can` while the Follow toggle can render (OpenRegister returns\n\t\t * the rights only on request, and the picker needs `manage`).\n\t\t *\n\t\t * @return {string[]}\n\t\t * @spec openspec/changes/record-favourite-and-follow/tasks.md#task-3\n\t\t */\n\t\treadExtend() {\n\t\t\tconst out = [...this.extend]\n\t\t\tif (this.follow !== false && !out.includes('@self.can')) {\n\t\t\t\tout.push('@self.can')\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/**\n\t\t * The resolved record's human display name: the `titleField` value\n\t\t * when the page names one and it is filled, else the same fallback\n\t\t * chain the relation resolver uses: `@self.name` → `name` / `title` /\n\t\t * `displayName` → `firstName lastName`. Empty string while the object is still loading or when no\n\t\t * name-like field exists (a raw id is never used as a name — ADR-062).\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tobjectDisplayName() {\n\t\t\tconst obj = this.resolvedObject\n\t\t\tif (!obj || typeof obj !== 'object') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tif (this.titleField) {\n\t\t\t\tconst named = stageOf(obj, this.titleField)\n\t\t\t\tif (named !== '') {\n\t\t\t\t\treturn named\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst self = obj['@self'] || {}\n\t\t\tconst id = this.objectId !== null && this.objectId !== undefined ? String(this.objectId) : ''\n\t\t\tconst candidates = [self.name, self.title, obj.name, obj.title, obj.displayName]\n\t\t\tfor (const c of candidates) {\n\t\t\t\tif (c !== null && c !== undefined && c !== '' && String(c) !== id) {\n\t\t\t\t\treturn String(c)\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst composed = ((obj.firstName || '') + ' ' + (obj.lastName || '')).trim()\n\t\t\treturn composed || ''\n\t\t},\n\n\t\t/**\n\t\t * The header `<h2>` text. Prefers the resolved record's display name\n\t\t * so the header names the record (\"ACME Corp\") rather than its type\n\t\t * (\"Case\"); falls back to the `title` prop (the type/label) while the\n\t\t * object is loading or has no name field.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tdisplayTitle() {\n\t\t\treturn this.objectDisplayName || this.resolvedTitle\n\t\t},\n\n\t\t/**\n\t\t * The declared crumbs: `config.breadcrumb` as one crumb or a list,\n\t\t * without the crumbs that have no label.\n\t\t *\n\t\t * @spec openspec/changes/detail-breadcrumb-trail/specs/detail-page/spec.md#requirement-a-record-page-breadcrumb-can-name-more-than-one-level\n\t\t * @return {Array<object>}\n\t\t */\n\t\tdeclaredCrumbs() {\n\t\t\tconst list = Array.isArray(this.breadcrumb) ? this.breadcrumb : (this.breadcrumb ? [this.breadcrumb] : [])\n\t\t\treturn list.filter((crumb) => crumb && typeof crumb.label === 'string' && crumb.label !== '')\n\t\t},\n\n\t\t/**\n\t\t * The breadcrumb trail: the declared crumbs (each a router target from\n\t\t * `route` + `params`, or an `href`), then the record's display name\n\t\t * as the current crumb. Empty without a declared crumb. A crumb\n\t\t * carries `icon` only when the manifest declares one.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-detail-page-drops-its-eyebrow-and-shows-a-breadcrumb\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-declared-breadcrumb-label-shows-as-text\n\t\t * @spec openspec/changes/detail-breadcrumb-trail/specs/detail-page/spec.md#requirement-a-record-page-breadcrumb-can-name-more-than-one-level\n\t\t * @return {Array<{label: string, to?: object, href?: string, icon?: string}>}\n\t\t */\n\t\tbreadcrumbCrumbs() {\n\t\t\tif (this.declaredCrumbs.length === 0) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\tconst crumbs = this.declaredCrumbs.map((crumb) => {\n\t\t\t\tconst entry = { label: this.effectiveTranslate(crumb.label) }\n\t\t\t\tif (typeof crumb.icon === 'string' && crumb.icon !== '') {\n\t\t\t\t\tentry.icon = crumb.icon\n\t\t\t\t}\n\t\t\t\tif (crumb.route) {\n\t\t\t\t\tentry.to = { name: crumb.route, ...(crumb.params ? { params: crumb.params } : {}), ...(crumb.query ? { query: crumb.query } : {}) }\n\t\t\t\t} else if (crumb.href) {\n\t\t\t\t\tentry.href = crumb.href\n\t\t\t\t}\n\t\t\t\treturn entry\n\t\t\t})\n\t\t\treturn [...crumbs, { label: this.breadcrumbCurrentLabel }]\n\t\t},\n\n\t\t/**\n\t\t * The current crumb: the record's `breadcrumb.currentField` value\n\t\t * (the case number on a case page) when declared and filled, else the\n\t\t * display name.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-breadcrumb-can-name-the-record-by-a-field-and-use-a-text-separator\n\t\t * @return {string}\n\t\t */\n\t\tbreadcrumbCurrentLabel() {\n\t\t\tconst field = this.breadcrumbOption('currentField')\n\t\t\tif (field !== '') {\n\t\t\t\tconst value = stageOf(this.resolvedObject, field)\n\t\t\t\tif (value !== '') {\n\t\t\t\t\treturn value\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn this.displayTitle\n\t\t},\n\n\t\t/**\n\t\t * The text drawn between the crumbs (`breadcrumb.separator`), or ''\n\t\t * for the chevron.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-breadcrumb-can-name-the-record-by-a-field-and-use-a-text-separator\n\t\t * @return {string}\n\t\t */\n\t\tbreadcrumbSeparator() {\n\t\t\treturn this.breadcrumbOption('separator')\n\t\t},\n\n\t\t/**\n\t\t * Effective translate function: the injected `cnTranslate` (the host\n\t\t * app's bound `t()`), identity by default.\n\t\t *\n\t\t * @return {(key: string) => string}\n\t\t */\n\t\teffectiveTranslate() {\n\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate : (key) => key\n\t\t},\n\n\t\t/**\n\t\t * The `title` prop (the manifest-authored type label) run through the\n\t\t * host translate function. Used for the header fallback and the type\n\t\t * eyebrow; the record's own display name is data and stays untouched.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedTitle() {\n\t\t\treturn this.title ? this.effectiveTranslate(this.title) : this.title\n\t\t},\n\n\t\t/**\n\t\t * The page description run through the host translate function.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedDescription() {\n\t\t\treturn this.description ? this.effectiveTranslate(this.description) : this.description\n\t\t},\n\n\t\t/**\n\t\t * The statistics section heading run through the host translate function.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedStatsTitle() {\n\t\t\treturn this.statsTitle ? this.effectiveTranslate(this.statsTitle) : this.statsTitle\n\t\t},\n\n\t\t/**\n\t\t * The small type-label eyebrow shown above the record name. Only\n\t\t * rendered once a record name is resolved AND it differs from the\n\t\t * `title` prop, so the type context (\"Case\", \"Publication\") is kept\n\t\t * without shadowing the record name. Empty while loading (the type\n\t\t * label is then the `<h2>` itself).\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\ttypeEyebrow() {\n\t\t\tconst name = this.objectDisplayName\n\t\t\tif (!name || !this.title || name === this.title) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn this.resolvedTitle\n\t\t},\n\n\t\t/**\n\t\t * The fetched JSON Schema for the schema-driven mode. Read from\n\t\t * the store's `schemas[type]` cache populated by `fetchSchema`.\n\t\t * Required to render `CnObjectDataWidget` (which takes a schema\n\t\t * Object, not a slug).\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tcurrentSchema() {\n\t\t\tconst store = this.effectiveObjectStore\n\t\t\tif (!store) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst type = this.resolvedObjectType\n\t\t\tif (!type) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn store.schemas?.[type] ?? null\n\t\t},\n\n\t\t/**\n\t\t * True when consumer-supplied default slot content IS present.\n\t\t * Treats comment placeholders and whitespace-only text vnodes as\n\t\t * no content so a stray newline in the template doesn't\n\t\t * accidentally suppress the auto-body.\n\t\t *\n\t\t * `$slots.default` is a FUNCTION in Vue 3 — it has to be called to\n\t\t * get the vnodes. Reading `.length` off it yields the function's\n\t\t * arity (always 0 for a compiled slot), which made this read\n\t\t * `false` unconditionally and let the auto-body render on top of\n\t\t * whatever the consumer had put in the slot.\n\t\t */\n\t\thasDefaultSlotContent() {\n\t\t\treturn slotRenders(this.$slots.default)\n\t\t},\n\n\t\t/**\n\t\t * True when the auto-body (CnObjectDataWidget + CnObjectMetadataWidget)\n\t\t * should render. Conditions: schema-driven mount, the object has\n\t\t * loaded, no consumer slot wins, and the grid-layout mode is not\n\t\t * active (grid mode owns the body when present).\n\t\t */\n\t\tshouldRenderAutoBody() {\n\t\t\treturn this.hasSchemaDrivenFetch\n\t\t\t\t&& this.currentObject\n\t\t\t\t&& !this.hasDefaultSlotContent\n\t\t\t\t&& !this.hasGridLayout\n\t\t\t\t&& !this.hasViews\n\t\t},\n\n\t\t/**\n\t\t * The object context handed to integration widgets.\n\t\t *\n\t\t * An explicit `integrationContext` prop wins; otherwise it is derived the\n\t\t * way it always was, falling back through the resolved sidebar config and\n\t\t * `sidebarProps` before the page's own register/schema. That fallback\n\t\t * chain is why this is computed HERE and passed down rather than derived\n\t\t * inside CnDetailWidgetHost: `resolvedSidebar` and `sidebarProps` are page\n\t\t * state the host has no view of, and deriving from `register`/`schema`\n\t\t * alone hands the widget two empty strings.\n\t\t *\n\t\t * @return {object} `{ register, schema, objectId }`.\n\t\t */\n\t\teffectiveIntegrationContext() {\n\t\t\tif (this.integrationContext) {\n\t\t\t\treturn this.integrationContext\n\t\t\t}\n\t\t\tconst resolved = this.resolvedSidebar || {}\n\t\t\treturn {\n\t\t\t\tregister: resolved.register || this.sidebarProps?.register || this.register || '',\n\t\t\t\tschema: resolved.schema || this.schema || this.resolvedObjectType || this.sidebarProps?.schema || '',\n\t\t\t\tobjectId: this.objectId ? String(this.objectId) : '',\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Effective layout array driving the body grid. An explicit `layout` prop\n\t\t * (manifest grid page) wins; otherwise the materialized default auto-body\n\t\t * (Data + Related) is used when the schema-driven object has loaded.\n\t\t *\n\t\t * @return {Array} Layout items for CnDashboardGrid.\n\t\t */\n\t\tbodyGridLayout() {\n\t\t\t// Depend on the (always-reactive) edit flag so this re-evaluates\n\t\t\t// when in-app edit mode flips: the manifest arrays are raw until\n\t\t\t// useManifestEditor.enter() observes them in place, so a cache\n\t\t\t// built pre-edit would stay frozen for in-place pushes (Add widget\n\t\t\t// on a detail page / grid ejection). See CnDashboardPage.hasWidgets.\n\t\t\t// eslint-disable-next-line @typescript-eslint/no-unused-expressions -- reading the flag IS the effect: the read registers the reactive dependency, and there is nothing to assign it to\n\t\t\tthis.editingBody\n\t\t\tif (this.hasGridLayout) {\n\t\t\t\treturn this.closeDroppedRows(this.layout)\n\t\t\t}\n\t\t\tif (this.shouldRenderAutoBody) {\n\t\t\t\treturn this.closeDroppedRows(this.autoBodyLayout || [])\n\t\t\t}\n\t\t\treturn []\n\t\t},\n\n\t\t/**\n\t\t * Effective widget-definition array paired with `bodyGridLayout`. Used by\n\t\t * the overridden `findWidget` so both the prop-driven and the default\n\t\t * auto-body grids resolve their widget defs from one place.\n\t\t *\n\t\t * @return {Array} Widget definitions.\n\t\t */\n\t\tbodyGridWidgets() {\n\t\t\tif (this.hasGridLayout) {\n\t\t\t\treturn this.pickTabs(this.widgets)\n\t\t\t}\n\t\t\tif (this.shouldRenderAutoBody) {\n\t\t\t\treturn this.autoBodyWidgets || []\n\t\t\t}\n\t\t\treturn []\n\t\t},\n\n\t\t/**\n\t\t * The tab ids `tabsFrom` reads off this record, or null when it reads\n\t\t * no non-empty list (or the body is being edited).\n\t\t *\n\t\t * @spec openspec/changes/detail-tabs-from/specs/detail-page/spec.md#requirement-a-record-can-pick-its-tabs-and-their-order\n\t\t * @return {Array<string>|null}\n\t\t */\n\t\tpickedTabIds() {\n\t\t\tif (!this.tabsFrom || this.editingBody || !this.widgetVisibilityData) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst ids = readVisibleWhenPath(this.widgetVisibilityData, this.tabsFrom)\n\t\t\treturn Array.isArray(ids) && ids.length > 0 ? ids.map((id) => String(id)) : null\n\t\t},\n\n\t\t/**\n\t\t * Whether the body renders as an adjustable widget grid (vs. the\n\t\t * hand-authored default slot). True for explicit grid pages and for the\n\t\t * schema-driven default Data + Related body.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\thasBodyGrid() {\n\t\t\treturn this.bodyGridLayout.length > 0\n\t\t},\n\n\t\t/**\n\t\t * The chosen view's layout as the grid draws it: items that draw\n\t\t * nothing leave and their rows close, as for the body grid.\n\t\t *\n\t\t * @return {Array<object>}\n\t\t * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-view-with-nothing-to-draw-says-so\n\t\t */\n\t\tviewDisplayLayout() {\n\t\t\t// eslint-disable-next-line @typescript-eslint/no-unused-expressions -- reading the flag IS the effect, as in bodyGridLayout\n\t\t\tthis.editingBody\n\t\t\treturn this.activeView ? this.closeDroppedRows(this.activeViewLayout) : []\n\t\t},\n\n\t\t/**\n\t\t * Whether the chosen view has nothing to draw.\n\t\t *\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-view-with-nothing-to-draw-says-so\n\t\t */\n\t\tviewIsEmpty() {\n\t\t\treturn this.hasViews && this.viewDisplayLayout.length === 0\n\t\t},\n\n\t\t/**\n\t\t * The grids the body draws: the body grid when it has items, then the\n\t\t * chosen view's grid in the region the view switch controls (left out\n\t\t * when the view is empty; the empty state takes the region then).\n\t\t *\n\t\t * @return {Array<{key: string, view: boolean, layout: Array<object>, editable: boolean, attrs: object}>}\n\t\t * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-page-declares-views-that-each-hold-a-widget-grid\n\t\t */\n\t\tbodyGridSections() {\n\t\t\tconst sections = this.hasBodyGrid ? [{ key: 'body', view: false, layout: this.bodyGridLayout, editable: this.editingBody, attrs: {} }] : []\n\t\t\tif (this.hasViews && !this.viewIsEmpty) {\n\t\t\t\tsections.push({\n\t\t\t\t\tkey: `view-${this.activeViewId}`,\n\t\t\t\t\tview: true,\n\t\t\t\t\tlayout: this.viewDisplayLayout,\n\t\t\t\t\t// The user arranges the view while Arrange view is on.\n\t\t\t\t\teditable: this.editingBody || this.arrangingView,\n\t\t\t\t\tattrs: {\n\t\t\t\t\t\tid: this.viewRegionId,\n\t\t\t\t\t\trole: 'region',\n\t\t\t\t\t\t'aria-label': this.activeView.label,\n\t\t\t\t\t\t'data-testid': 'cn-detail-page-view-region',\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t}\n\t\t\treturn sections\n\t\t},\n\n\t\t/**\n\t\t * Whether the sidebar is rendered externally (via objectSidebarState inject)\n\t\t * rather than inline. When external, CnDetailPage only manages state —\n\t\t * the parent App renders the actual NcAppSidebar.\n\t\t */\n\t\thasExternalSidebar() {\n\t\t\treturn !!this.objectSidebarState\n\t\t},\n\n\t\t/**\n\t\t * Normalised sidebar config object regardless of input shape.\n\t\t *\n\t\t * - Boolean `true` → `{ show: true, enabled: true }`\n\t\t * - Boolean `false` → `{ show: false, enabled: false }`\n\t\t * - Object → fields passed through; `show` and\n\t\t * `enabled` default to `true` when omitted.\n\t\t *\n\t\t * Centralising the normalisation here keeps `syncSidebarState`\n\t\t * and the deprecation/conflict-warning logic single-sourced.\n\t\t */\n\t\tresolvedSidebar() {\n\t\t\tconst cfg = this.sidebar\n\t\t\t// A non-empty manifest `sidebarTabs` prop is itself an opt-in:\n\t\t\t// a `type:\"detail\"` page that declares tabs clearly wants the\n\t\t\t// sidebar. The `sidebar` prop defaults to Boolean `false`, and\n\t\t\t// CnPageRenderer only forwards a `sidebar` config when the\n\t\t\t// manifest page sets one — so a page that declares ONLY\n\t\t\t// `config.sidebarTabs` (procest CaseDetail) would otherwise fall\n\t\t\t// into the Boolean-`false` branch below and never publish its\n\t\t\t// strip. When tabs are present we treat the sidebar as enabled\n\t\t\t// (the explicit Object form below still overrides show/enabled).\n\t\t\tconst hasTabs = Array.isArray(this.sidebarTabs) && this.sidebarTabs.length > 0\n\t\t\tif (typeof cfg === 'boolean') {\n\t\t\t\tif (cfg) {\n\t\t\t\t\treturn { show: true, enabled: true }\n\t\t\t\t}\n\t\t\t\treturn hasTabs ? { show: true, enabled: true } : { show: false, enabled: false }\n\t\t\t}\n\t\t\tif (cfg && typeof cfg === 'object') {\n\t\t\t\treturn {\n\t\t\t\t\tshow: cfg.show !== false,\n\t\t\t\t\tenabled: cfg.enabled !== false,\n\t\t\t\t\t...cfg,\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn hasTabs ? { show: true, enabled: true } : { show: false, enabled: false }\n\t\t},\n\n\t\t/**\n\t\t * True when the sidebar should be wired into the external\n\t\t * `objectSidebarState` channel. Both `show` and `enabled`\n\t\t * must be non-`false` (defaults are `true`); either can\n\t\t * suppress the sidebar declaratively.\n\t\t */\n\t\tsidebarActive() {\n\t\t\tconst r = this.resolvedSidebar\n\t\t\t// An explicit `show: false` always suppresses the sidebar. Otherwise a\n\t\t\t// non-empty `sidebarTabs` opt-in activates it even past the prop's\n\t\t\t// default `{ enabled: false }` — the procest CaseDetail manifest\n\t\t\t// pattern, where a page declares only `config.sidebarTabs`.\n\t\t\tif (r.show === false) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif (r.enabled === false) {\n\t\t\t\treturn Array.isArray(this.sidebarTabs) && this.sidebarTabs.length > 0\n\t\t\t}\n\t\t\treturn true\n\t\t},\n\n\t\thasStats() {\n\t\t\treturn this.statsColumns.length > 0 && (this.statsRows.length > 0 || !!this.$slots['stats-rows'])\n\t\t},\n\n\t\t/** Whether any declarative in-body sections are configured. */\n\t\thasBodyWidgets() {\n\t\t\treturn Array.isArray(this.bodyWidgets) && this.bodyWidgets.length > 0\n\t\t},\n\n\t\t/**\n\t\t * Sections that land at the END placement: those with no `placement`\n\t\t * (the default) or an explicit `placement: \"end\"`. The three named\n\t\t * placement mounts (before-body / after-data / after-related) filter by\n\t\t * exact value; this catches the rest so nothing is silently dropped.\n\t\t *\n\t\t * @return {Array}\n\t\t */\n\t\tendPlacementSections() {\n\t\t\tif (!this.hasBodyWidgets) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\tconst named = ['before-body', 'after-data', 'after-related']\n\t\t\treturn this.bodyWidgets.filter((s) => !s || !s.placement || !named.includes(s.placement))\n\t\t},\n\n\t\t/**\n\t\t * Page/object context forwarded to CnBodySections for token resolution\n\t\t * (`@objectId` / `@object.<field>` / `@workspace.<key>`) AND provided on\n\t\t * `cnSectionContext` for host section components that inject. Mirrors the\n\t\t * `cnObjectContext` ref the abstract list/stat widgets already read.\n\t\t * `setObject(object)` shows an object a section saved without a re-fetch.\n\t\t *\n\t\t * @return {{objectId: (string|null), object: (object|null), register: string, schema: string, setObject: function(object): void}}\n\t\t */\n\t\tsectionContext() {\n\t\t\tconst resolved = this.resolvedSidebar || {}\n\t\t\treturn {\n\t\t\t\tobjectId: (this.objectId !== undefined && this.objectId !== null) ? String(this.objectId) : null,\n\t\t\t\tobject: this.resolvedObject || null,\n\t\t\t\tregister: resolved.register || this.register || this.sidebarProps?.register || '',\n\t\t\t\tschema: resolved.schema || this.schema || this.resolvedObjectType || this.sidebarProps?.schema || '',\n\t\t\t\tconfig: this.cnAppConfigRef,\n\t\t\t\tsetObject: (object) => this.setCurrentObject(object),\n\t\t\t}\n\t\t},\n\t},\n\n\twatch: {\n\t\theaderMetaCountKey: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.loadHeaderMetaCounts()\n\t\t\t},\n\t\t},\n\n\t\tlastChangeKey: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.loadLastChange()\n\t\t\t},\n\t\t},\n\n\t\t// The records a condition names are read again for another record.\n\t\tvisibilityRefsKey: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.refreshVisibilityRows()\n\t\t\t},\n\t\t},\n\n\t\t// Header actions gate on `@context.<name>` through the object context.\n\t\tvisibilityRows() {\n\t\t\tthis.syncObjectContext()\n\t\t},\n\n\t\tvisibilityRowsPending() {\n\t\t\tthis.syncObjectContext()\n\t\t},\n\n\t\t// Endpoint and source conditions on grid widgets are asked once per record.\n\t\tremoteWidgetConditionsKey: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.refreshWidgetConditions()\n\t\t\t},\n\t\t},\n\n\t\t// A pill with `resolve` looks its record up again for another value.\n\t\tpillResolveKey: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.refreshPillRefLabels()\n\t\t\t},\n\t\t},\n\n\t\t// The header's count pills count again for another record or other lists.\n\t\tpillCountKey: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.refreshPillCounts()\n\t\t\t},\n\t\t},\n\n\t\t// Opening a record marks it read, once, after its data has rendered.\n\t\treadStateDue: {\n\t\t\timmediate: true,\n\t\t\thandler(due) {\n\t\t\t\tif (due) {\n\t\t\t\t\tthis.sendReadState()\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\n\t\t// A load just settled (true → false) — remember it so later loads\n\t\t// refresh in place (full-page spinner only on the first load). Not\n\t\t// `immediate`: `loading` starts false before the first fetch begins,\n\t\t// so reacting to that initial false would suppress the first spinner.\n\t\tloading(val) {\n\t\t\tif (!val) {\n\t\t\t\tthis.hasLoadedOnce = true\n\t\t\t\t// Refresh settled — re-assert the sidebar state that\n\t\t\t\t// syncSidebarState skipped while loading was in flight.\n\t\t\t\tthis.syncSidebarState()\n\t\t\t}\n\t\t},\n\n\t\tawaitingObject() {\n\t\t\tthis.syncSidebarState()\n\t\t},\n\n\t\tobjectNotFound() {\n\t\t\tthis.syncSidebarState()\n\t\t},\n\n\t\tcontentWithheld() {\n\t\t\tthis.syncSidebarState()\n\t\t},\n\n\t\t/** Another record asks its own purpose: the dialog opens again for it. */\n\t\taccessPurposeKey() {\n\t\t\tthis.accessPurposeAsking = true\n\t\t\tthis.accessPurposeError = ''\n\t\t},\n\n\t\t// Deep, like `sidebarTabs` / `sidebarProps` below: the manifest editors\n\t\t// mutate `page.config.sidebar` IN PLACE, so the reference CnPageRenderer\n\t\t// forwards never changes. A shallow watcher missed every such edit —\n\t\t// switching the sidebar off in CnEditSidebarModal did nothing until the\n\t\t// page was reloaded. assignSidebarState() only writes keys that actually\n\t\t// changed, so the extra calls cost a comparison.\n\t\tsidebar: {\n\t\t\timmediate: true,\n\t\t\tdeep: true,\n\t\t\thandler() {\n\t\t\t\tthis.syncSidebarState()\n\t\t\t},\n\t\t},\n\n\t\t// Keep the provided object context current so detail-page abstract\n\t\t// widgets (CnObjectListWidget / CnStatWidget) can resolve `@objectId` /\n\t\t// `@object.<field>` tokens against the live object.\n\t\tcurrentObject: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.syncObjectContext()\n\t\t\t\t// Re-publish the object into objectSidebarState so the\n\t\t\t\t// hoisted sidebar's data/metadata tab widgets stay current.\n\t\t\t\tthis.syncSidebarState()\n\t\t\t},\n\t\t},\n\n\t\t// Re-publish once the schema Object resolves (fetched async), so the\n\t\t// hoisted sidebar's `data` tab widget gets its `schema` prop.\n\t\tcurrentSchema(schema) {\n\t\t\tthis.syncSidebarState()\n\t\t\treportBindingProblems(schema, { register: this.register, includeFields: this.includeFields })\n\n\t\t\t// Put the Data widget back if the body materialized before the\n\t\t\t// schema arrived. See materializeAutoBody(): fetchObject and\n\t\t\t// fetchSchema race, `shouldRenderAutoBody` gates on the OBJECT\n\t\t\t// only, and the body materializes once — so losing that race meant\n\t\t\t// the page rendered its title and Related panel with every scalar\n\t\t\t// field permanently missing (shillinq#1073).\n\t\t\t//\n\t\t\t// Guarded by `dataWidgetDeferred` rather than by \"is the data\n\t\t\t// widget absent\", so a body the user has edited is never rewritten\n\t\t\t// underneath them. The flag is cleared by the re-materialize, so\n\t\t\t// this runs at most once per materialized body.\n\t\t\tif (schema && this.dataWidgetDeferred) {\n\t\t\t\tthis.materializeAutoBody()\n\t\t\t}\n\t\t},\n\n\t\t// Materialize the default body grid (Data + Related) the first time the\n\t\t// schema-driven object resolves so the detail body is an adjustable grid.\n\t\tshouldRenderAutoBody: {\n\t\t\timmediate: true,\n\t\t\thandler(active) {\n\t\t\t\tif (active && !this.autoBodyLayout) {\n\t\t\t\t\tthis.materializeAutoBody()\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\n\t\t// A different schema means a fresh default grid — drop the materialized\n\t\t// one so it rebuilds for the new object type.\n\t\tresolvedObjectType() {\n\t\t\tthis.autoBodyLayout = null\n\t\t\tthis.autoBodyWidgets = null\n\t\t\t// Cleared with the body it described. materializeAutoBody() sets it\n\t\t\t// afresh, so this only avoids carrying a stale \"deferred\" across a\n\t\t\t// type change.\n\t\t\tthis.dataWidgetDeferred = false\n\t\t},\n\n\t\ttitle() {\n\t\t\tthis.syncSidebarState()\n\t\t},\n\n\t\tsubtitle() {\n\t\t\tthis.syncSidebarState()\n\t\t},\n\n\t\tobjectType() {\n\t\t\tthis.syncSidebarState()\n\t\t},\n\n\t\t// Schema-driven props feed both the sidebar state (via\n\t\t// resolvedObjectType) and the auto-fetch path. Re-sync + re-fetch\n\t\t// whenever any of the three move so the page stays consistent if\n\t\t// a parent component swaps the active object.\n\t\tregister() {\n\t\t\tthis.syncSidebarState()\n\t\t\tthis.pushAiContext()\n\t\t\tthis.fetchObjectIfNeeded()\n\t\t},\n\n\t\tschema() {\n\t\t\tthis.syncSidebarState()\n\t\t\tthis.pushAiContext()\n\t\t\tthis.fetchObjectIfNeeded()\n\t\t},\n\n\t\tobjectId() {\n\t\t\tthis.syncSidebarState()\n\t\t\tthis.pushAiContext()\n\t\t\tthis.fetchObjectIfNeeded()\n\t\t\tthis.syncObjectContext()\n\t\t},\n\n\t\tsidebarTabs: {\n\t\t\tdeep: true,\n\t\t\thandler() {\n\t\t\t\tthis.syncSidebarState()\n\t\t\t},\n\t\t},\n\n\t\tsidebarProps: {\n\t\t\tdeep: true,\n\t\t\thandler() {\n\t\t\t\tthis.syncSidebarState()\n\t\t\t},\n\t\t},\n\t},\n\n\tcreated() {\n\t\tthis.pushAiContext()\n\t\t// Seeded before the first render, so a widget's first request already\n\t\t// carries the page filter's value.\n\t\tthis.initPageFilters()\n\t\tthis.loadPageFilterOptions()\n\t},\n\n\tmounted() {\n\t\t// Kick the schema-driven fetch once the component has mounted —\n\t\t// `effectiveObjectStore` relies on a live Pinia context, which\n\t\t// is guaranteed by mounted() but not by created().\n\t\tthis.fetchObjectIfNeeded()\n\t\tthis.scheduleCellOverflowAudit()\n\n\t\t// `cn:page:refresh` is the channel a successful write announces itself\n\t\t// on — the manifest `refresh` and `api-call` actions bump it, and so do\n\t\t// app dialogs after a save. Until now nothing on a schema-driven detail\n\t\t// page listened: `useEndpointSource` and CnChartWidget answered it, but\n\t\t// every widget reading the page's OBJECT (`type: \"data\"` above all)\n\t\t// reads the copy this component fetched once, so a page kept showing\n\t\t// values the backend had already changed. Measured on a dossiq draft:\n\t\t// publish returned `{\"published\":true,\"version\":1}`, a re-read showed\n\t\t// `isDraft: false`, and the page still said draft.\n\t\t//\n\t\t// The subscription lives HERE rather than in each widget on purpose.\n\t\t// One page carries several object-bound widgets; if each answered the\n\t\t// channel itself, one write would become one read per widget. This\n\t\t// component owns the object, so one write is one read however many\n\t\t// widgets are bound to it.\n\t\tthis._onPageRefresh = (payload) => this.onPageRefreshBus(payload)\n\t\tsubscribe(PAGE_REFRESH_CHANNEL, this._onPageRefresh)\n\t},\n\n\tupdated() {\n\t\tthis.scheduleCellOverflowAudit()\n\t},\n\n\tbeforeUnmount() {\n\t\tif (this._onPageRefresh) {\n\t\t\tunsubscribe(PAGE_REFRESH_CHANNEL, this._onPageRefresh)\n\t\t\tthis._onPageRefresh = null\n\t\t}\n\t\tif (this.hasExternalSidebar) {\n\t\t\tthis.objectSidebarState.active = false\n\t\t\t// Clear manifest-driven tabs so the next mount starts fresh\n\t\t\t// rather than inheriting the previous page's tab strip.\n\t\t\tthis.objectSidebarState.tabs = undefined\n\t\t}\n\t\t// Reset AI context fields so stale detail context doesn't leak\n\t\tif (this.cnAiContext) {\n\t\t\tthis.cnAiContext.pageKind = 'custom'\n\t\t\tthis.cnAiContext.objectUuid = undefined\n\t\t\tthis.cnAiContext.registerSlug = undefined\n\t\t\tthis.cnAiContext.schemaSlug = undefined\n\t\t}\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Read the record's newest create or update audit entry into\n\t\t * `lastChange`; a failed read leaves it null.\n\t\t *\n\t\t * @spec openspec/changes/header-meta-last-change/specs/detail-page/spec.md#requirement-the-header-meta-line-can-name-who-changed-the-record-last\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadLastChange() {\n\t\t\tconst key = this.lastChangeKey\n\t\t\tthis.lastChange = null\n\t\t\tif (key === '') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([\n\t\t\t\t\timport('@nextcloud/axios'),\n\t\t\t\t\timport('@nextcloud/router'),\n\t\t\t\t])\n\t\t\t\tconst url = generateUrl(\n\t\t\t\t\t'/apps/openregister/api/objects/{register}/{schema}/{id}/audit-trails',\n\t\t\t\t\t{ register: this.register, schema: this.schema, id: this.objectId },\n\t\t\t\t)\n\t\t\t\tconst res = await axios.get(url, { params: { _limit: 1, sort: 'created', order: 'DESC', action: 'create,update' } })\n\t\t\t\tconst rows = (res && res.data && Array.isArray(res.data.results)) ? res.data.results : []\n\t\t\t\tif (key === this.lastChangeKey) {\n\t\t\t\t\tthis.lastChange = rows[0] || null\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// No trail to read: the meta line drops the tokens.\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * A key of the declared trail: the last crumb that sets it wins.\n\t\t *\n\t\t * @spec openspec/changes/detail-breadcrumb-trail/specs/detail-page/spec.md#requirement-a-record-page-breadcrumb-can-name-more-than-one-level\n\t\t * @param {string} key `currentField` or `separator`.\n\t\t * @return {string} Its value, or ''.\n\t\t */\n\t\tbreadcrumbOption(key) {\n\t\t\tconst found = [...this.declaredCrumbs].reverse().find((crumb) => typeof crumb[key] === 'string' && crumb[key] !== '')\n\t\t\treturn found ? found[key] : ''\n\t\t},\n\n\t\t/**\n\t\t * A body widget starts or stops showing its own cards in the side\n\t\t * column. While any does, the generic side cards stand aside.\n\t\t *\n\t\t * @param {string|number} owner Who claims, unique per widget.\n\t\t * @param {boolean} on Whether it shows its cards now.\n\t\t * @return {void}\n\t\t */\n\t\tclaimSideColumn(owner, on) {\n\t\t\tconst rest = this.sideClaims.filter((id) => id !== owner)\n\t\t\tthis.sideClaims = on ? [...rest, owner] : rest\n\t\t},\n\n\t\t/**\n\t\t * A text of the access purpose gate through the app's translate, '' when unset.\n\t\t *\n\t\t * @param {string} key `title` or `description`.\n\t\t * @return {string}\n\t\t */\n\t\taccessPurposeText(key) {\n\t\t\tconst text = this.accessPurpose && this.accessPurpose[key]\n\t\t\treturn typeof text === 'string' && text !== '' ? this.cnTranslate(text) : ''\n\t\t},\n\n\t\t/**\n\t\t * Record the reader's purpose with the app's endpoint, then open the\n\t\t * record and remember it for this browser session. A refusal (or a\n\t\t * failure to record) keeps the record closed and says why.\n\t\t *\n\t\t * @spec openspec/changes/detail-access-purpose/specs/detail-access-purpose/spec.md#requirement-a-detail-page-can-ask-for-a-purpose-before-it-opens\n\t\t * @param {{purpose: string, note: string}} answer The chosen purpose.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync recordAccessPurpose(answer) {\n\t\t\tconst url = String(this.accessPurpose?.url || '').replace(/\\{objectId\\}/g, encodeURIComponent(String(this.objectId || '')))\n\t\t\tif (url === '') {\n\t\t\t\tthis.accessPurposeError = t('nextcloud-vue', 'This page cannot record a purpose, so the record stays closed.')\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.accessPurposeBusy = true\n\t\t\tthis.accessPurposeError = ''\n\t\t\ttry {\n\t\t\t\tconst { default: axios } = await import('@nextcloud/axios')\n\t\t\t\tawait axios.post(prefixUrl(url), { purpose: answer.purpose, note: answer.note })\n\t\t\t\tconst key = this.accessPurposeKey\n\t\t\t\tthis.accessPurposeGranted = [...new Set([...this.accessPurposeGranted, key])]\n\t\t\t\ttry {\n\t\t\t\t\twindow.sessionStorage.setItem(ACCESS_PURPOSE_SESSION_KEY, JSON.stringify(this.accessPurposeGranted))\n\t\t\t\t} catch {\n\t\t\t\t\t// Without session storage the purpose is asked again on the next visit.\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tconst data = error?.response?.data\n\t\t\t\tthis.accessPurposeError = (data && (data.message || data.error)) || t('nextcloud-vue', 'Your purpose could not be recorded, so the record stays closed.')\n\t\t\t} finally {\n\t\t\t\tthis.accessPurposeBusy = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The widget definitions with each `tabs` widget's tabs narrowed and\n\t\t * ordered by `pickedTabIds`; the definitions as given without a pick.\n\t\t * A new definition is returned for a changed widget; the declared one\n\t\t * is never mutated.\n\t\t *\n\t\t * @spec openspec/changes/detail-tabs-from/specs/detail-page/spec.md#requirement-a-record-can-pick-its-tabs-and-their-order\n\t\t * @param {Array<object>} widgets The widget definitions.\n\t\t * @return {Array<object>}\n\t\t */\n\t\tpickTabs(widgets) {\n\t\t\tconst ids = this.pickedTabIds\n\t\t\tif (!ids || !Array.isArray(widgets)) {\n\t\t\t\treturn widgets\n\t\t\t}\n\t\t\tconst idOf = (tab) => String(typeof tab === 'string' ? tab : (tab && (tab.id ?? tab.widgetId)) ?? '')\n\t\t\treturn widgets.map((widget) => {\n\t\t\t\tif (!widget || widget.type !== 'tabs' || !Array.isArray(widget.content?.tabs)) {\n\t\t\t\t\treturn widget\n\t\t\t\t}\n\t\t\t\tconst tabs = ids\n\t\t\t\t\t.map((id) => widget.content.tabs.find((tab) => idOf(tab) === id))\n\t\t\t\t\t.filter(Boolean)\n\t\t\t\treturn tabs.length > 0 ? { ...widget, content: { ...widget.content, tabs } } : widget\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * The tag after an admin-only entry in the board look (\"beheerder\",\n\t\t * DqZaakMeer), or null for any other entry or look. It is drawn by\n\t\t * detail-page.css from the attribute, since the menu renders its\n\t\t * entries' text as plain text.\n\t\t *\n\t\t * @param {{key: string}} group The menu group the entry is in.\n\t\t * @return {string|null} The tag, or null.\n\t\t */\n\t\tadminTagFor(group) {\n\t\t\treturn this.isBoard && group && group.key === 'cn-admin' ? t('nextcloud-vue', 'admin') : null\n\t\t},\n\n\t\t/**\n\t\t * Open the object sidebar through the shared channel.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\topenSidebar() {\n\t\t\tif (this.objectSidebarState) {\n\t\t\t\tthis.objectSidebarState.open = true\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Start or finish arranging the chosen view. Finishing stores the\n\t\t * arrangement of every view the user moved, once.\n\t\t *\n\t\t * @return {void}\n\t\t * @spec openspec/changes/page-view-user-layouts/specs/view-switch-containers/spec.md#requirement-a-user-arranges-each-view-for-themselves\n\t\t */\n\t\ttoggleArrangeView() {\n\t\t\tconst leaving = this.arrangingView\n\t\t\tthis.arrangingView = !this.arrangingView\n\t\t\tif (leaving) {\n\t\t\t\tthis.saveViewUserLayouts()\n\t\t\t}\n\t\t},\n\n\t\t// Expose the shared grid helpers to the template (grid mode + auto-body).\n\t\tcnGridCellStyle,\n\t\thasGridRow,\n\n\t\t/**\n\t\t * Emit a list step. When the control is a link it has navigated\n\t\t * already, so the payload `{ event, to }` tells the host not to push\n\t\t * again (`useListNavigation`'s `goNext`/`goPrevious` honour it).\n\t\t *\n\t\t * @param {string} name `previous-record` or `next-record`.\n\t\t * @param {object|string|null} to The link target, or null.\n\t\t * @param {MouseEvent} event The click event.\n\t\t */\n\t\tonListStep(name, to, event) {\n\t\t\tif (to && this.$router) {\n\t\t\t\tthis.$emit(name, { event, to })\n\t\t\t} else {\n\t\t\t\tthis.$emit(name)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Move focus to the primary action when the skip link is followed.\n\t\t *\n\t\t * The `href` anchor alone moves the DOCUMENT to the element without\n\t\t * moving FOCUS to it in every browser, which is the difference\n\t\t * between a skip link that works and one that scrolls and then hands\n\t\t * the next tab press back to the top of the page.\n\t\t *\n\t\t * @param {Event} event The click event.\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tonSkipToPrimaryAction(event) {\n\t\t\t// The ref covers the header button; the id also finds the button\n\t\t\t// when it sits inside the \"what now\" card.\n\t\t\tconst target = this.$refs.primaryActionButton?.$el\n\t\t\t\t?? this.$refs.primaryActionButton\n\t\t\t\t?? this.$el?.querySelector?.(`[id=\"${this.primaryActionAnchorId}\"]`)\n\t\t\tconst focusable = target?.matches?.('button, a') ? target : target?.querySelector?.('button, a')\n\t\t\tif (focusable?.focus) {\n\t\t\t\tevent.preventDefault()\n\t\t\t\tfocusable.focus()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The primary button was pressed.\n\t\t *\n\t\t * A stage's action is dispatched the way a header action is, through\n\t\t * the `run()` CnActionButtons bound for it, and announced to the host\n\t\t * with its declaration. A link action is left to the browser and\n\t\t * announces nothing. Without a stage action this is the page's\n\t\t * `primaryAction`, announced as before, and when it is an in-app link\n\t\t * a plain click routes there.\n\t\t *\n\t\t * @param {MouseEvent} [event] The click.\n\t\t * @return {void}\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-primary-action-follows-the-stage\n\t\t * @spec openspec/changes/header-action-href/specs/header-action-href/spec.md#requirement-a-header-action-with-href-is-a-link-that-runs-and-emits-nothing\n\t\t */\n\t\tonPrimaryActionClick(event) {\n\t\t\tconst entry = this.stagePrimaryEntry\n\t\t\tif (!entry) {\n\t\t\t\tthis.$emit('primary-action', this.primaryAction)\n\t\t\t\tif (this.primaryActionLink?.to) {\n\t\t\t\t\tfollowLinkClick(event, this.primaryActionLink.to, this.$router)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (entry.href) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.$emit('primary-action', this.headerActionsById[entry.id] || { id: entry.id, label: entry.label })\n\t\t\tthis.onPinnedActionClick(entry)\n\t\t},\n\n\t\t/**\n\t\t * A pinned action's button was pressed: run it, unless it is a link\n\t\t * the browser is already following.\n\t\t *\n\t\t * @param {object} entry The action's menu entry.\n\t\t * @return {void}\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-quick-actions\n\t\t */\n\t\tonPinnedActionClick(entry) {\n\t\t\tif (!entry || entry.href || typeof entry.run !== 'function') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tentry.run()\n\t\t},\n\n\t\t/**\n\t\t * The active tab changed: tell the host, and put it in the address\n\t\t * when the page declares that its tab is part of its address.\n\t\t *\n\t\t * `replace`, never `push`, for the FIRST tab of a visit: an address\n\t\t * that arrived without a tab is being corrected to its canonical form\n\t\t * (ADR-052), and a correction must not become a history entry the\n\t\t * back button walks into. Later changes push, so back and forward\n\t\t * walk the tabs the reader actually visited.\n\t\t *\n\t\t * @param {string} tabId The tab now active.\n\t\t * @param {boolean} [canonicalising] Whether this is the arrival correction rather than a reader's choice.\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tonTabChange(tabId, canonicalising = false) {\n\t\t\tif (typeof tabId !== 'string' || tabId === '') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\t/**\n\t\t\t * @event tab-change Emitted when the active tab changes.\n\t\t\t * @type {string}\n\t\t\t */\n\t\t\tthis.$emit('tab-change', tabId)\n\t\t\tif (!this.tabInAddress || !this.$router || !this.$route) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.$route.query?._tab === tabId) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst to = { name: this.$route.name, params: { ...this.$route.params }, query: { ...this.$route.query, _tab: tabId } }\n\t\t\tconst navigate = canonicalising ? this.$router.replace : this.$router.push\n\t\t\tnavigate.call(this.$router, to).catch(() => {})\n\t\t},\n\n\t\t/**\n\t\t * The tab the address asks for, or null.\n\t\t *\n\t\t * @return {string|null} The tab id.\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\trequestedTabFromAddress() {\n\t\t\tconst tab = this.$route?.query?._tab\n\t\t\treturn typeof tab === 'string' && tab !== '' ? tab : null\n\t\t},\n\n\t\t/**\n\t\t * The sidebar settled on a tab: put it in the address.\n\t\t *\n\t\t * The sidebar reports the tab it actually shows, not the one that was\n\t\t * asked for, so an address naming a tab this reader may not see is\n\t\t * corrected to the one they can. `canonicalising` is true for that\n\t\t * correction and for the tabless arrival, so neither becomes a\n\t\t * history entry the back button walks into (ADR-052).\n\t\t *\n\t\t * @param {string} tabId The tab now showing.\n\t\t * @param {boolean} [canonicalising] Whether this is a correction rather than a reader's choice.\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tonSidebarTabChange(tabId, canonicalising = false) {\n\t\t\tthis.onTabChange(tabId, canonicalising || this.requestedTabFromAddress() === null)\n\t\t},\n\n\t\t/**\n\t\t * Release the lock the current user holds on this record.\n\t\t *\n\t\t * Only reachable from the locked card's Unlock button, which the card\n\t\t * only renders for the viewer's OWN lock — so this never releases\n\t\t * somebody else's. A failure is left to `useObjectLock`, which logs it;\n\t\t * the flag is cleared either way so a failed release does not strand\n\t\t * the button disabled.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync onReleaseLock() {\n\t\t\tif (!this.lockState || this.releasingLock) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.releasingLock = true\n\t\t\ttry {\n\t\t\t\tawait this.lockState.release()\n\t\t\t} finally {\n\t\t\t\tthis.releasingLock = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Re-emit the page-header menu's Refresh to the host.\n\t\t *\n\t\t * @param {{ widgetId: string, title: string }} payload - Action payload.\n\t\t * @param {object} event - Synthetic event (host may preventDefault).\n\t\t */\n\t\tasync onHeaderRefresh(payload, event) {\n\t\t\t/**\n\t\t\t * @event refresh The page-header Refresh action was clicked.\n\t\t\t * @type {{ widgetId: string, title: string }}\n\t\t\t */\n\t\t\tthis.$emit('refresh', payload, event)\n\t\t\t// Schema-driven (manifest) detail pages self-fetch — without this\n\t\t\t// the Refresh action has no host listener to act on and does\n\t\t\t// nothing. Re-fetch the object + schema and spin the action.\n\t\t\tif (!this.selfFetchesRecord) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.internalRefreshing = true\n\t\t\tconst done = this.fetchObjectIfNeeded()\n\t\t\tevent?.waitUntil?.(done)\n\t\t\ttry {\n\t\t\t\tawait done\n\t\t\t} finally {\n\t\t\t\tthis.internalRefreshing = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Look up the records the `resolve` pills point at, through the same\n\t\t * reference-label resolver the index tables use. A failed lookup keeps\n\t\t * the raw value.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/detail-pill-resolve/specs/detail-page/spec.md#requirement-a-header-pill-can-show-a-referenced-record-s-label\n\t\t */\n\t\tasync refreshPillRefLabels() {\n\t\t\tconst targets = this.pillResolveTargets\n\t\t\tconst seq = ++this.pillRefSeq\n\t\t\tif (targets.length === 0) {\n\t\t\t\tthis.pillRefLabels = {}\n\t\t\t\tthis.pillRefLooks = {}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!this._pillRefResolver) {\n\t\t\t\tthis._pillRefResolver = createRefLabelResolver(() => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\treturn this.objectStore || useObjectStore()\n\t\t\t\t\t} catch {\n\t\t\t\t\t\treturn null\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t\tconst labels = {}\n\t\t\tconst looks = {}\n\t\t\tconst field = async (register, schema, id, name) => {\n\t\t\t\tif (!name) {\n\t\t\t\t\treturn null\n\t\t\t\t}\n\t\t\t\tconst found = await this._pillRefResolver.resolve(register, schema, [id], name, { strict: true }).catch(() => ({}))\n\t\t\t\treturn found && typeof found[id] === 'string' ? found[id] : null\n\t\t\t}\n\t\t\tawait Promise.all(targets.map(async ([key, register, schema, labelField, id, colorField, iconField]) => {\n\t\t\t\tconst [found, color, icon] = await Promise.all([\n\t\t\t\t\tthis._pillRefResolver.resolve(register, schema, [id], labelField).catch(() => ({})),\n\t\t\t\t\tfield(register, schema, id, colorField),\n\t\t\t\t\tfield(register, schema, id, iconField),\n\t\t\t\t])\n\t\t\t\tif (found && typeof found[id] === 'string') {\n\t\t\t\t\tlabels[key] = found[id]\n\t\t\t\t}\n\t\t\t\tif (color || icon) {\n\t\t\t\t\tlooks[key] = { color, icon }\n\t\t\t\t}\n\t\t\t}))\n\t\t\tif (seq === this.pillRefSeq) {\n\t\t\t\tthis.pillRefLabels = labels\n\t\t\t\tthis.pillRefLooks = looks\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Read the totals `headerMetaCounts` names: a register list through\n\t\t * one list request, a named source by loading it and counting its\n\t\t * rows. A failed count is null and leaves the line out.\n\t\t *\n\t\t * @spec openspec/changes/detail-header-meta-counts/specs/detail-page/spec.md#requirement-a-record-page-header-line-can-count-a-list\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadHeaderMetaCounts() {\n\t\t\tconst key = this.headerMetaCountKey\n\t\t\tif (key === '') {\n\t\t\t\tthis.headerMetaCountValues = {}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst ctx = { objectId: this.objectId, object: this.resolvedObject || undefined }\n\t\t\tconst values = {}\n\t\t\tawait Promise.all(JSON.parse(key)[0].map(async ([name, from]) => {\n\t\t\t\ttry {\n\t\t\t\t\tif (from.entitySource) {\n\t\t\t\t\t\tconst source = resolveIndexSource(from.entitySource)\n\t\t\t\t\t\tif (!source) {\n\t\t\t\t\t\t\tvalues[name] = null\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t\tawait source.load()\n\t\t\t\t\t\tvalues[name] = source.rows().length\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tvalues[name] = await fetchListTotal(from.register, from.schema, from.filter || {}, ctx)\n\t\t\t\t} catch {\n\t\t\t\t\tvalues[name] = null\n\t\t\t\t}\n\t\t\t}))\n\t\t\tif (key === this.headerMetaCountKey) {\n\t\t\t\tthis.headerMetaCountValues = values\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Count the lists behind the header's count pills (`countFrom`): one\n\t\t * list request each (`_limit=1`, the `total`). A failed count, or a\n\t\t * record without an id yet, shows no pill.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-a-header-pill-can-be-a-constant-or-a-count\n\t\t */\n\t\tasync refreshPillCounts() {\n\t\t\tconst pills = [['type', this.typePill], ['status', this.statusPill]]\n\t\t\t\t.map(([key, config]) => [key, pillCountSource(config)])\n\t\t\t\t.filter(([, source]) => source)\n\t\t\tconst seq = ++this.pillCountSeq\n\t\t\tif (pills.length === 0 || this.objectId === null || this.objectId === undefined || this.objectId === '') {\n\t\t\t\tthis.pillCounts = {}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst ctx = { objectId: this.objectId, object: this.resolvedObject || undefined }\n\t\t\tconst counts = {}\n\t\t\tawait Promise.all(pills.map(async ([key, source]) => {\n\t\t\t\tcounts[key] = await fetchListTotal(source.register, source.schema, source.filter, ctx).catch(() => null)\n\t\t\t}))\n\t\t\tif (seq === this.pillCountSeq) {\n\t\t\t\tthis.pillCounts = counts\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * A `cn:page:refresh` reached the page — re-read the object so every\n\t\t * object-bound widget re-renders from what the backend now holds.\n\t\t *\n\t\t * Deliberately a no-op while `internalRefreshing` is set. The header's\n\t\t * own Refresh item emits `@refresh` FIRST and only then emits on this\n\t\t * channel (see CnActionsMenu.onRefreshClick), and `onHeaderRefresh`\n\t\t * sets the flag synchronously before awaiting its fetch — so without\n\t\t * this guard one click on Refresh would fetch the object twice.\n\t\t *\n\t\t * @param {{ waitUntil?: (promise: Promise<unknown>) => void }} [payload] Bus payload.\n\t\t * @return {void}\n\t\t */\n\t\tonPageRefreshBus(payload) {\n\t\t\tthis.refreshPillCounts()\n\t\t\tif (!this.selfFetchesRecord) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.internalRefreshing) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.internalRefreshing = true\n\t\t\tconst done = Promise.resolve(this.fetchObjectIfNeeded()).finally(() => {\n\t\t\t\tthis.internalRefreshing = false\n\t\t\t})\n\t\t\tpayload?.waitUntil?.(done)\n\t\t},\n\n\t\t/**\n\t\t * Re-emit the page-header menu's Request-a-feature to the host.\n\t\t *\n\t\t * @param {{ widgetId: string, title: string }} payload - Action payload.\n\t\t * @param {object} event - Synthetic event (host may preventDefault).\n\t\t */\n\t\tonHeaderRequestFeature(payload, event) {\n\t\t\t/**\n\t\t\t * @event request-feature The page-header Request-a-feature action\n\t\t\t * was clicked.\n\t\t\t * @type {{ widgetId: string, title: string }}\n\t\t\t */\n\t\t\tthis.$emit('request-feature', payload, event)\n\t\t},\n\n\t\t/**\n\t\t * Open the sidebar leaf a \"Linked apps\" row on the auto-body\n\t\t * CnRelatedObjectsWidget points at. Publishes the requested tab and\n\t\t * opens the external sidebar via `objectSidebarState`, and re-emits\n\t\t * `open-integration` so a host can override the routing.\n\t\t *\n\t\t * @param {string} integrationId - The leaf integration id (tab id).\n\t\t */\n\t\tonAutoBodyOpenIntegration(integrationId) {\n\t\t\tif (this.hasExternalSidebar && this.objectSidebarState) {\n\t\t\t\tthis.objectSidebarState.open = true\n\t\t\t\tthis.objectSidebarState.requestedTab = integrationId\n\t\t\t}\n\t\t\t/**\n\t\t\t * @event open-integration A related-objects \"Linked apps\" row\n\t\t\t * was clicked on the auto-body. Payload is the leaf id.\n\t\t\t * @type {string}\n\t\t\t */\n\t\t\tthis.$emit('open-integration', integrationId)\n\t\t},\n\n\t\t/**\n\t\t * Re-emit a click on an object in the Related widget. The page does not\n\t\t * know the routes; the host (CnPageRenderer on a manifest page) opens\n\t\t * the object's own detail page.\n\t\t *\n\t\t * @param {object} raw The clicked object; `@self` names its register and schema.\n\t\t */\n\t\tonRelatedObjectSelect(raw) {\n\t\t\t/**\n\t\t\t * @event related-object-click An object in the Related widget was\n\t\t\t * clicked. Payload is the raw object; `@self` carries its register,\n\t\t\t * schema and id.\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('related-object-click', raw)\n\t\t},\n\n\t\t/**\n\t\t * Re-fetch the object after the geo widget persists `@self.geo` (so the\n\t\t * rest of the page — metadata, other widgets — reflects the new value),\n\t\t * and re-emit for the host.\n\t\t *\n\t\t * @param {object|null} geo The saved geo value (GeoJSON Point or null).\n\t\t */\n\t\tonGeoSaved(geo) {\n\t\t\tthis.fetchObjectIfNeeded()\n\t\t\t/**\n\t\t\t * @event geo-saved The object's `@self.geo` was saved via the geo\n\t\t\t * widget. Payload is the saved geo value (GeoJSON Point or null).\n\t\t\t * @type {object|null}\n\t\t\t */\n\t\t\tthis.$emit('geo-saved', geo)\n\t\t},\n\n\t\t/**\n\t\t * Re-emit a successful lifecycle transition to the host.\n\t\t *\n\t\t * @param {{ action: string, to: string, object: object }} payload The transition result.\n\t\t */\n\t\tonTransitioned(payload) {\n\t\t\t/**\n\t\t\t * @event transitioned A declarative lifecycle transition succeeded on this\n\t\t\t * page's object. Payload is `{ action, to, object }`.\n\t\t\t * @type {{ action: string, to: string, object: object }}\n\t\t\t */\n\t\t\tthis.$emit('transitioned', payload)\n\t\t},\n\n\t\t/**\n\t\t * Tick or untick one checklist item through the record source. The\n\t\t * source replaces its record with the answer; a refusal is shown with\n\t\t * the server's message and leaves the item as it was.\n\t\t *\n\t\t * @param {string} itemId The item id.\n\t\t * @param {boolean} checked The new state.\n\t\t * @return {Promise<boolean>} Whether the write was taken.\n\t\t * @spec openspec/changes/record-checklist-widget/specs/record-checklist-widget/spec.md#requirement-a-checklist-widget-ticks-items-through-the-record-source\n\t\t */\n\t\tasync checkSourceItem(itemId, checked) {\n\t\t\tconst source = this.recordSource\n\t\t\tconst objectId = this.objectId\n\t\t\tif (!this.recordCanCheckItems || !objectId) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tconst outcome = await source.checkItem(String(objectId), String(itemId), checked === true)\n\t\t\tif (!outcome || outcome.ok !== true) {\n\t\t\t\tconst { showError } = await import('@nextcloud/dialogs')\n\t\t\t\tshowError(String(outcome?.message || '').trim() || t('nextcloud-vue', 'This could not be done. Try again.'))\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn true\n\t\t},\n\n\t\t/**\n\t\t * Run a record-source verb on the record in view. The source keeps its\n\t\t * record in step with the answer, so the header, pills and widgets\n\t\t * redraw from it. A refusal is shown with the server's own message,\n\t\t * which names the verb and the reason.\n\t\t *\n\t\t * @param {{id: string, label: string, successMessage?: string}} verb The verb.\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/record-source-verbs/specs/record-source-verbs/spec.md#requirement-a-record-source-offers-header-verbs\n\t\t */\n\t\tasync runSourceVerb(verb) {\n\t\t\tconst source = this.recordSource\n\t\t\tconst objectId = this.objectId\n\t\t\tif (!source || !objectId || this.sourceVerbBusy) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (verb.form && verb.form.schema) {\n\t\t\t\tthis.sourceVerbForm = verb\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.sourceVerbBusy = true\n\t\t\ttry {\n\t\t\t\tconst url = this.verbEndpoints && typeof this.verbEndpoints[verb.id] === 'string' ? this.verbEndpoints[verb.id] : undefined\n\t\t\t\tconst outcome = await source.runVerb(String(objectId), verb.id, url ? { url } : {})\n\t\t\t\tconst { showError, showSuccess } = await import('@nextcloud/dialogs')\n\t\t\t\tif (!outcome || outcome.ok !== true) {\n\t\t\t\t\tshowError(String(outcome?.message || '').trim() || t('nextcloud-vue', 'This could not be done. Try again.'))\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif (verb.successMessage) {\n\t\t\t\t\tshowSuccess(verb.successMessage)\n\t\t\t\t}\n\t\t\t\tthis.$emit('transitioned', { action: verb.id, to: String(outcome.record?.state ?? ''), object: outcome.record || null })\n\t\t\t} finally {\n\t\t\t\tthis.sourceVerbBusy = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Save the open record-source verb form through the source: only the\n\t\t * form's own fields are sent. The form shows the server's refusal and\n\t\t * stays open; on success it closes and the page redraws from the\n\t\t * record the source kept.\n\t\t *\n\t\t * @param {object} formData The form's values.\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/task-edit-action/specs/record-source-verbs/spec.md#requirement-a-record-source-verb-can-ask-for-values-first\n\t\t */\n\t\tasync onSourceVerbFormConfirm(formData) {\n\t\t\tconst verb = this.sourceVerbForm\n\t\t\tconst source = this.recordSource\n\t\t\tconst dialog = this.$refs.sourceVerbFormDialog\n\t\t\tif (!verb || !source || !this.objectId) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst keys = Object.keys(verb.form.schema.properties || {})\n\t\t\tconst body = Object.fromEntries(keys.filter((key) => formData && key in formData).map((key) => [key, formData[key]]))\n\t\t\tconst outcome = await source.runVerb(String(this.objectId), verb.id, { body })\n\t\t\tif (!outcome || outcome.ok !== true) {\n\t\t\t\tdialog?.setResult({ error: String(outcome?.message || '').trim() || t('nextcloud-vue', 'This could not be done. Try again.') })\n\t\t\t\treturn\n\t\t\t}\n\t\t\tdialog?.setResult({ success: true })\n\t\t\tthis.sourceVerbForm = null\n\t\t\tif (verb.successMessage) {\n\t\t\t\tconst { showSuccess } = await import('@nextcloud/dialogs')\n\t\t\t\tshowSuccess(verb.successMessage)\n\t\t\t}\n\t\t\tthis.$emit('transitioned', { action: verb.id, to: String(outcome.record?.state ?? ''), object: outcome.record || null })\n\t\t},\n\n\t\t/**\n\t\t * Re-fetch the page's object after a lifecycle transition so the new state\n\t\t * (and any other server-side mutations the guard applied) render.\n\t\t */\n\t\tonLifecycleReload() {\n\t\t\tthis.fetchObjectIfNeeded()\n\t\t},\n\n\t\t/**\n\t\t * Open the relation-link modal for a configured `relationLinks` entry.\n\t\t *\n\t\t * @param {object} link The relation-link descriptor.\n\t\t */\n\t\topenRelationLink(link) {\n\t\t\tthis.activeRelationLink = link\n\t\t},\n\n\t\t/**\n\t\t * A relation-link modal saved a patched FK — re-fetch the object and\n\t\t * re-emit so the host can react.\n\t\t *\n\t\t * @param {object} saved The updated object.\n\t\t */\n\t\tonRelationLinked(saved) {\n\t\t\tthis.fetchObjectIfNeeded()\n\t\t\t/**\n\t\t\t * @event relation-linked A relation-link action patched a foreign key on\n\t\t\t * this page's object. Payload is the updated object.\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('relation-linked', saved)\n\t\t},\n\n\t\t/**\n\t\t * Re-emit a related-collection row click to the host.\n\t\t *\n\t\t * @param {{ collection: object, row: object, index: number }} payload The click payload.\n\t\t */\n\t\tonRelatedRowClick(payload) {\n\t\t\t/**\n\t\t\t * @event related-row-click A row in a `relatedCollections` section was\n\t\t\t * clicked. Payload is `{ collection, row, index }`.\n\t\t\t * @type {{ collection: object, row: object, index: number }}\n\t\t\t */\n\t\t\tthis.$emit('related-row-click', payload)\n\t\t},\n\n\t\t/**\n\t\t * Replace the page's object in the store cache, for a section that saved\n\t\t * it and holds the saved version. An object with another id is ignored.\n\t\t *\n\t\t * @param {object} object The saved object.\n\t\t */\n\t\tsetCurrentObject(object) {\n\t\t\tconst store = this.effectiveObjectStore\n\t\t\tconst type = this.resolvedObjectType\n\t\t\tif (!store || !type || !this.objectId || !object) {\n\t\t\t\t// eslint-disable-next-line no-console -- tells the integrating section its save is not shown\n\t\t\t\tconsole.warn('[CnDetailPage] setObject ignored: the page has no object store, type or id')\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst id = object.id ?? object['@self']?.id\n\t\t\tif (id !== undefined && id !== null && String(id) !== String(this.objectId)) {\n\t\t\t\t// eslint-disable-next-line no-console -- tells the integrating section its save is not shown\n\t\t\t\tconsole.warn(`[CnDetailPage] setObject ignored: object ${id} is not the page's object ${this.objectId}`)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tstore.objects = {\n\t\t\t\t...store.objects,\n\t\t\t\t[type]: { ...(store.objects?.[type] || {}), [this.objectId]: object },\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Schema-driven fetch entry point — no-op outside the\n\t\t * `register`+`schema`+`objectId` mode. Registers the type on\n\t\t * the store with the canonical 4-arg signature (matches what\n\t\t * `CnIndexPage` does), then fetches the object and its schema\n\t\t * in parallel. Errors land on `this._fetchError` (exposed via\n\t\t * the `error` template gate); the loading flag is intentionally\n\t\t * left to the store's own per-type `loading[type]` so it\n\t\t * cooperates with parallel fetches from sibling components.\n\t\t *\n\t\t * Called from `mounted()` and from the `register` / `schema` /\n\t\t * `objectId` watchers — every prop change re-runs in one\n\t\t * place so the request lifecycle stays predictable.\n\t\t */\n\t\t/**\n\t\t * `PUT .../read-state`, once per register, schema and object per page load.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/record-unread-markers/tasks.md#task-2\n\t\t */\n\t\tasync sendReadState() {\n\t\t\tconst key = `${this.register}/${this.schema}/${this.objectId}`\n\t\t\tif (this.readStateSentFor === key) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.readStateSentFor = key\n\t\t\tconst result = await setReadState(this.register, this.schema, String(this.objectId), true)\n\t\t\tif (result.ok) {\n\t\t\t\tpatchStoredSelf(this.register, this.schema, String(this.objectId), { unread: false })\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Mark as unread: `DELETE .../read-state`, then `marked-unread`.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/record-unread-markers/tasks.md#task-2\n\t\t */\n\t\tasync onMarkUnread() {\n\t\t\t// Fix the key first, so the unread marker coming back does not mark it read again.\n\t\t\tthis.readStateSentFor = `${this.register}/${this.schema}/${this.objectId}`\n\t\t\tconst result = await setReadState(this.register, this.schema, String(this.objectId), false)\n\t\t\tif (!result.ok) {\n\t\t\t\timport('@nextcloud/dialogs').then(({ showError }) => showError(result.message || t('nextcloud-vue', 'Could not mark this record as unread.'))).catch(() => {})\n\t\t\t\treturn\n\t\t\t}\n\t\t\tpatchStoredSelf(this.register, this.schema, String(this.objectId), { unread: true })\n\t\t\t/** @event marked-unread Emitted after the record was marked unread; the page stays open unless `markUnreadNavigatesBack`. */\n\t\t\tthis.$emit('marked-unread')\n\t\t\tif (this.markUnreadNavigatesBack && this.$router && typeof this.$router.back === 'function') {\n\t\t\t\tthis.$router.back()\n\t\t\t}\n\t\t},\n\n\t\tasync fetchObjectIfNeeded() {\n\t\t\tif (this.recordSource) {\n\t\t\t\tawait this.fetchSourceRecord()\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// Create archetype: no object to fetch, but the create form needs\n\t\t\t// the schema — register the type and fetch its schema, then stop.\n\t\t\tif (!this.hasSchemaDrivenFetch) {\n\t\t\t\tthis.objectFetchPending = false\n\t\t\t\tif (this.isCreateMode) {\n\t\t\t\t\tawait this.fetchSchemaForCreate()\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst store = this.effectiveObjectStore\n\t\t\tif (!store) {\n\t\t\t\tthis.objectFetchPending = false\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst type = this.resolvedObjectType\n\t\t\t// (slug, schemaId, registerId, slugs) — same shape as the\n\t\t\t// CnIndexPage / CnLogsPage fix. Passing the slug strings into\n\t\t\t// the positional id slots is intentional (OR's REST accepts\n\t\t\t// either numeric ids or kebab slugs there), and the 4th-arg\n\t\t\t// hints feed the live-updates transport.\n\t\t\t// Register the type ONCE. `registerObjectType` resets the type's\n\t\t\t// cache (`objects[type] = {}`, `schemas[type] = null`), so calling\n\t\t\t// it on every refresh blanks the object + schema until the\n\t\t\t// re-fetch lands — the content visibly disappears mid-refresh.\n\t\t\t// Skip re-registration when the type is already registered so a\n\t\t\t// refresh re-fetches and replaces the data in place.\n\t\t\tif (typeof store.registerObjectType === 'function'\n\t\t\t\t&& !store.objectTypeRegistry?.[type]) {\n\t\t\t\tstore.registerObjectType(\n\t\t\t\t\ttype,\n\t\t\t\t\tthis.schema,\n\t\t\t\t\tthis.register,\n\t\t\t\t\t{ registerSlug: this.register, schemaSlug: this.schema },\n\t\t\t\t)\n\t\t\t}\n\t\t\tconst objectId = this.objectId\n\t\t\tconst fetchesObject = typeof store.fetchObject === 'function'\n\t\t\tthis.objectFetchPending = fetchesObject\n\t\t\tif (fetchesObject) {\n\t\t\t\tthis.objectNotFound = false\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst tasks = []\n\t\t\t\tif (fetchesObject) {\n\t\t\t\t\ttasks.push(store.fetchObject(type, objectId, { extend: this.readExtend }))\n\t\t\t\t}\n\t\t\t\tif (typeof store.fetchSchema === 'function') {\n\t\t\t\t\ttasks.push(store.fetchSchema(type))\n\t\t\t\t}\n\t\t\t\tawait Promise.all(tasks)\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console -- the failure is already handled; the console is the only channel a host app can read the detail on\n\t\t\t\tconsole.error('[CnDetailPage] schema-driven fetch failed:', err)\n\t\t\t} finally {\n\t\t\t\t// A newer fetch for another id owns the flags now.\n\t\t\t\tif (fetchesObject && objectId === this.objectId) {\n\t\t\t\t\tthis.objectNotFound = !store.objects?.[type]?.[objectId]\n\t\t\t\t\t\t&& store.getError?.(type)?.status === 404\n\t\t\t\t\tthis.objectFetchPending = false\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Read the record through the record source. A refusal (403) or a\n\t\t * missing record (404) shows the not-found state; which records the\n\t\t * viewer may read is the server's decision.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync fetchSourceRecord() {\n\t\t\tconst source = this.recordSource\n\t\t\tconst objectId = this.objectId\n\t\t\tif (!objectId) {\n\t\t\t\tthis.objectFetchPending = false\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.objectFetchPending = true\n\t\t\tthis.objectNotFound = false\n\t\t\ttry {\n\t\t\t\tawait source.loadRecord(String(objectId))\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console -- the failure is already handled; the console is the only channel a host app can read the detail on\n\t\t\t\tconsole.error('[CnDetailPage] record source fetch failed:', err)\n\t\t\t} finally {\n\t\t\t\t// A newer fetch for another id owns the flags now.\n\t\t\t\tif (objectId === this.objectId) {\n\t\t\t\t\tconst status = typeof source.recordError === 'function' ? source.recordError(String(objectId)) : null\n\t\t\t\t\tthis.objectNotFound = !this.currentObject && (status === 403 || status === 404)\n\t\t\t\t\tthis.objectFetchPending = false\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Whether a grid-layout item resolves to an integration-typed\n\t\t * widget — `def.type === 'integration'` with a string\n\t\t * `integrationId`. Used by the grid template to render the\n\t\t * registry widget as the slot fallback.\n\t\t *\n\t\t * @param {object} item Layout item\n\t\t * @return {boolean} true when the matching widget def is an integration widget\n\t\t */\n\t\t/**\n\t\t * Register the type and fetch ONLY its schema, for the create archetype\n\t\t * (no object id to fetch). Populates `currentSchema` so the inline create\n\t\t * form can auto-generate its fields.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync fetchSchemaForCreate() {\n\t\t\tconst store = this.effectiveObjectStore\n\t\t\tif (!store) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst type = this.resolvedObjectType\n\t\t\tif (typeof store.registerObjectType === 'function'\n\t\t\t\t&& !store.objectTypeRegistry?.[type]) {\n\t\t\t\tstore.registerObjectType(\n\t\t\t\t\ttype,\n\t\t\t\t\tthis.schema,\n\t\t\t\t\tthis.register,\n\t\t\t\t\t{ registerSlug: this.register, schemaSlug: this.schema },\n\t\t\t\t)\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tif (typeof store.fetchSchema === 'function') {\n\t\t\t\t\tawait store.fetchSchema(type)\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\t// eslint-disable-next-line no-console -- the failure is already handled; the console is the only channel a host app can read the detail on\n\t\t\t\tconsole.error('[CnDetailPage] create-mode schema fetch failed:', err)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Persist the create form: POST the new object to OpenRegister, then\n\t\t * navigate to the created object's detail route (`createRoute` with its\n\t\t * id) — or back in history when no route is configured.\n\t\t *\n\t\t * Returns the outcome as well as applying it. The built-in dialog reads\n\t\t * the outcome off its own ref (`createFormDialog.setResult`), which a\n\t\t * `#form-dialog` replacement does not have — the returned result is how\n\t\t * a replacement learns whether to unlock its own dialog. Nothing on the\n\t\t * default path consumes the return value.\n\t\t *\n\t\t * @param {object} formData The confirmed form values.\n\t\t * @return {Promise<{success?: boolean, data?: object, error?: string}>}\n\t\t */\n\t\tasync onCreateFormConfirm(formData) {\n\t\t\ttry {\n\t\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([\n\t\t\t\t\timport('@nextcloud/axios'),\n\t\t\t\t\timport('@nextcloud/router'),\n\t\t\t\t])\n\t\t\t\tconst url = generateUrl(\n\t\t\t\t\t'/apps/openregister/api/objects/{register}/{schema}',\n\t\t\t\t\t{ register: this.register, schema: this.schema },\n\t\t\t\t)\n\t\t\t\tconst res = await axios.post(url, formData)\n\t\t\t\tconst created = (res && res.data) ? res.data : {}\n\t\t\t\tconst newId = (created['@self'] && created['@self'].id) || created.id\n\t\t\t\tif (this.$refs.createFormDialog) {\n\t\t\t\t\tthis.$refs.createFormDialog.setResult({ success: true })\n\t\t\t\t}\n\t\t\t\t/**\n\t\t\t\t * @event created Emitted after a successful create-form save.\n\t\t\t\t * @type {object}\n\t\t\t\t */\n\t\t\t\tthis.$emit('created', created)\n\t\t\t\tif (this.createRoute && newId && this.$router) {\n\t\t\t\t\tthis.$router.push({ name: this.createRoute, params: { id: String(newId) } }).catch(() => {})\n\t\t\t\t} else if (this.$router) {\n\t\t\t\t\tthis.$router.back()\n\t\t\t\t}\n\t\t\t\treturn { success: true, data: created }\n\t\t\t} catch (e) {\n\t\t\t\tconst error = (e && e.message) || 'error'\n\t\t\t\tif (this.$refs.createFormDialog) {\n\t\t\t\t\tthis.$refs.createFormDialog.setResult({ error })\n\t\t\t\t}\n\t\t\t\treturn { error }\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * `confirm` for the `#form-dialog` slot: the page's own save path,\n\t\t * handed to a replacement dialog so a create or edit made there behaves\n\t\t * exactly like one made in the built-in dialog — same duplicate-id\n\t\t * refusal, same store-then-axios fallback, same `created` / `edited`\n\t\t * events, same lifecycle reload.\n\t\t *\n\t\t * Bound as a PROP rather than left as an `@confirm` listener, because\n\t\t * CnPageRenderer mounts a manifest-declared replacement with\n\t\t * `v-bind=\"slotProps\"` — props only. A listener is unreachable from a\n\t\t * manifest even in principle, which is what made the same slot on\n\t\t * CnIndexPage decorative until openconnector#1150.\n\t\t *\n\t\t * Dispatches on `isCreateMode`, the same flag that decides which of the\n\t\t * two built-in dialogs renders — they are mutually exclusive, since\n\t\t * create mode is by definition a page with no object id.\n\t\t *\n\t\t * @param {object} formData The submitted form values.\n\t\t * @return {Promise<{success?: boolean, data?: object, error?: string}>} The save outcome, for the replacement's own `setResult`.\n\t\t */\n\t\tonFormDialogConfirm(formData) {\n\t\t\treturn this.isCreateMode\n\t\t\t\t? this.onCreateFormConfirm(formData)\n\t\t\t\t: this.onEditFormConfirm(formData)\n\t\t},\n\n\t\t/**\n\t\t * `close` for the `#form-dialog` slot. Dispatches the same way\n\t\t * `onFormDialogConfirm` does, so a dismissed create still navigates\n\t\t * back rather than stranding the user on an id-less page.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonFormDialogClose() {\n\t\t\tif (this.isCreateMode) {\n\t\t\t\tthis.onCreateFormClose()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.closeEditForm()\n\t\t},\n\n\t\t/**\n\t\t * Open the record edit form.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\topenEditForm() {\n\t\t\t// 🔴 OPENING TAKES THE LOCK. Two people editing one record is not\n\t\t\t// resolved by whoever presses Save last: the first person's work is\n\t\t\t// gone and neither of them is told. The button is already withdrawn\n\t\t\t// while somebody else holds the lock (`canEditRecord`), so this is\n\t\t\t// the backstop for the seconds between the last read and the click.\n\t\t\t//\n\t\t\t// 🔑 THE FORM OPENS SYNCHRONOUSLY, AND THE ACQUIRE RUNS BESIDE IT.\n\t\t\t// Awaiting the round trip here would make `openEditForm()` async,\n\t\t\t// and every consumer that calls it and then asserts or renders on\n\t\t\t// the next tick would see a form that is not open yet: measured,\n\t\t\t// 10 of the 17 `CnDetailPageFormDialogSlot` tests went red on\n\t\t\t// exactly that. A conflict closes the form again and names the\n\t\t\t// holder, which is the same outcome one tick later.\n\t\t\tthis.editLockRefusal = ''\n\t\t\tthis.editFormOpen = true\n\t\t\tthis.acquireEditLock()\n\t\t},\n\n\t\t/**\n\t\t * Take the lock for the edit that just opened.\n\t\t *\n\t\t * A refusal CLOSES the form and shows the holder, because a form left\n\t\t * open over somebody else's lock is a form that will be filled in and\n\t\t * then refused. Any other failure leaves the form open: the server\n\t\t * re-checks on the write, so a lock that could not be taken costs an\n\t\t * optimistic edit rather than an editor who cannot work at all.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tacquireEditLock() {\n\t\t\tif (!this.lockState) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.lockState.acquire().catch((e) => {\n\t\t\t\tif (e?.name !== 'LockConflictError') {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tthis.editLockRefusal = e.message\n\t\t\t\tthis.editFormOpen = false\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Close the record edit form without saving.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tcloseEditForm() {\n\t\t\tthis.editFormOpen = false\n\t\t\tthis.releaseEditLock()\n\t\t},\n\n\t\t/**\n\t\t * Hand back the lock this page took for an edit.\n\t\t *\n\t\t * Fire and forget and never throws: a release that fails must not stop\n\t\t * a form from closing, and the lock has a server-side TTL behind it.\n\t\t * `release()` is a no-op when the viewer does not hold the lock, so\n\t\t * calling it on a page that opened no form takes nothing from anybody.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\treleaseEditLock() {\n\t\t\tif (!this.lockState || !this.lockState.lockedByMe.value) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.lockState.release().catch(() => {})\n\t\t},\n\n\t\t/**\n\t\t * Persist an edit of the record this page is showing.\n\t\t *\n\t\t * Saves through the object store when one is available so the page's own\n\t\t * subscription re-renders with the new values; the store is also what\n\t\t * every other surface reads from, so a direct HTTP write would leave\n\t\t * them showing the pre-edit record until something else refetched.\n\t\t * Falls back to a PUT for consumers with no Pinia.\n\t\t *\n\t\t * Returns the outcome as well as applying it, for the same reason\n\t\t * `onCreateFormConfirm` does: a `#form-dialog` replacement has no ref\n\t\t * on the built-in dialog and would otherwise never learn whether the\n\t\t * save landed. Nothing on the default path consumes the return value.\n\t\t *\n\t\t * @param {object} formData The submitted form values.\n\t\t * @return {Promise<{success?: boolean, data?: object, error?: string}>}\n\t\t */\n\t\tasync onEditFormConfirm(formData) {\n\t\t\t// `saveObject` picks PUT over POST on the presence of `id`, and an\n\t\t\t// OpenRegister object carries its id in `@self`, not at the top\n\t\t\t// level. Without this the edit would CREATE a duplicate record.\n\t\t\tconst dialog = this.$refs.editFormDialog\n\t\t\tif (!this.objectId) {\n\t\t\t\t// REFUSE rather than save. An empty id does not error — it makes\n\t\t\t\t// saveObject POST, which silently CREATES A DUPLICATE and leaves\n\t\t\t\t// the record the user was editing untouched. That presents as\n\t\t\t\t// \"my change did not save\", with a new orphan row behind it, and\n\t\t\t\t// nothing anywhere says so.\n\t\t\t\tconst error = t('nextcloud-vue', 'Cannot save: this record has no id, so the edit would create a duplicate instead of updating it.')\n\t\t\t\tif (dialog) {\n\t\t\t\t\tdialog.setResult({ error })\n\t\t\t\t}\n\t\t\t\treturn { error }\n\t\t\t}\n\t\t\tconst payload = { ...formData, id: this.objectId }\n\t\t\ttry {\n\t\t\t\tconst store = this.effectiveObjectStore\n\t\t\t\tlet saved = null\n\t\t\t\tif (store && typeof store.saveObject === 'function' && this.resolvedObjectType) {\n\t\t\t\t\t// Only the keys the form changed are written (a PATCH), so keys\n\t\t\t\t\t// the edit form does not show keep their stored value.\n\t\t\t\t\tsaved = await saveEdited(store, this.resolvedObjectType, this.currentObject || null, payload)\n\t\t\t\t\tif (!saved) {\n\t\t\t\t\t\tconst err = store.getError?.(this.resolvedObjectType)\n\t\t\t\t\t\tconst error = (err && err.message) || t('nextcloud-vue', 'Save failed')\n\t\t\t\t\t\tif (dialog) {\n\t\t\t\t\t\t\tdialog.setResult({ error })\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn { error }\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([\n\t\t\t\t\t\timport('@nextcloud/axios'),\n\t\t\t\t\t\timport('@nextcloud/router'),\n\t\t\t\t\t])\n\t\t\t\t\tconst url = generateUrl(\n\t\t\t\t\t\t'/apps/openregister/api/objects/{register}/{schema}/{id}',\n\t\t\t\t\t\t{ register: this.register, schema: this.schema, id: this.objectId },\n\t\t\t\t\t)\n\t\t\t\t\tconst res = await axios.put(url, payload)\n\t\t\t\t\tsaved = (res && res.data) ? res.data : payload\n\t\t\t\t}\n\t\t\t\tif (dialog) {\n\t\t\t\t\tdialog.setResult({ success: true })\n\t\t\t\t}\n\t\t\t\tthis.editFormOpen = false\n\t\t\t\tthis.releaseEditLock()\n\t\t\t\t/**\n\t\t\t\t * @event edited Emitted after the record edit form saves successfully.\n\t\t\t\t * @type {object} The saved record.\n\t\t\t\t */\n\t\t\t\tthis.$emit('edited', saved)\n\t\t\t\tthis.onLifecycleReload()\n\t\t\t\treturn { success: true, data: saved }\n\t\t\t} catch (e) {\n\t\t\t\tconst error = (e && e.message) || t('nextcloud-vue', 'Save failed')\n\t\t\t\tif (dialog) {\n\t\t\t\t\tdialog.setResult({ error })\n\t\t\t\t}\n\t\t\t\treturn { error }\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The create form was dismissed without saving — navigate back so the\n\t\t * user is not stranded on an id-less page.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tonCreateFormClose() {\n\t\t\t/**\n\t\t\t * @event create-cancel Emitted when the create form is dismissed.\n\t\t\t */\n\t\t\tthis.$emit('create-cancel')\n\t\t\tif (this.$router) {\n\t\t\t\tthis.$router.back()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Whether a grid item is a schema-driven `data` widget — rendered via\n\t\t * CnObjectDataWidget with the page's loaded object + the def's overrides.\n\t\t *\n\t\t * @param {object} item Layout item\n\t\t * @return {boolean} true when the matching widget def is `type: 'data'`.\n\t\t */\n\t\tisDataWidget(item) {\n\t\t\treturn isDataWidgetDef(this.findWidget(item))\n\t\t},\n\n\t\t/**\n\t\t * Whether a grid item is the schema-driven `related` widget — rendered via\n\t\t * CnRelatedObjectsWidget with the page's loaded object. One of the two\n\t\t * default body widgets.\n\t\t *\n\t\t * @param {object} item Layout item\n\t\t * @return {boolean} true when the matching widget def is `type: 'related'`.\n\t\t */\n\t\tisRelatedWidget(item) {\n\t\t\treturn isRelatedWidgetDef(this.findWidget(item))\n\t\t},\n\n\t\t/**\n\t\t * Whether a grid item is the `object-geo` widget — rendered via\n\t\t * CnObjectGeoWidget with the page's loaded object so the user can view /\n\t\t * edit the object's `@self.geo` on a map.\n\t\t *\n\t\t * @param {object} item Layout item\n\t\t * @return {boolean} true when the matching widget def is `type: 'object-geo'`.\n\t\t */\n\t\tisGeoWidget(item) {\n\t\t\treturn isGeoWidgetDef(this.findWidget(item))\n\t\t},\n\n\t\t/**\n\t\t * Whether a grid item is a content-only catalog widget (a bare table\n\t\t * renderer with no chrome of its own). These get the titled\n\t\t * CnWidgetWrapper card in the grid (ADR-062); self-chromed catalog\n\t\t * widgets (stat / chart / gauge / tile / …) render bare.\n\t\t *\n\t\t * @param {object} item Layout item.\n\t\t * @return {boolean} true when the widget def's type is content-only.\n\t\t */\n\t\tisContentOnlyWidget(item) {\n\t\t\treturn isContentOnlyWidgetDef(this.findWidget(item))\n\t\t},\n\n\t\t/**\n\t\t * Whether a grid item is a registry \"card\" widget — a single self-\n\t\t * contained KPI / gauge / delta tile (registry entry `card: true`).\n\t\t * These render bare tile content, so on a detail page they need the\n\t\t * same titled CnWidgetWrapper card chrome the dashboard gives them\n\t\t * (ADR-062: a lone stat must not read as uncarded floating text —\n\t\t * pipelinq contracts / POS).\n\t\t *\n\t\t * @param {object} item Layout item.\n\t\t * @return {boolean} true when the widget def's registry entry is a card.\n\t\t */\n\t\tisCardWidget(item) {\n\t\t\treturn isCardWidgetDef(this.findWidget(item))\n\t\t},\n\n\t\t/**\n\t\t * Debounced dev-mode audit: warn when a grid widget's rendered content\n\t\t * overflows its fixed cell (ADR-062 — the cell is the budget; overflow\n\t\t * is a design bug, never a scroll surface). No-op in production builds.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tscheduleCellOverflowAudit() {\n\t\t\tif (process.env.NODE_ENV === 'production') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tclearTimeout(this._cellAuditTimer)\n\t\t\tthis._cellAuditTimer = setTimeout(() => this.auditCellOverflow(), 800)\n\t\t},\n\n\t\t/**\n\t\t * Measure every grid cell and console.warn the widget ids whose content\n\t\t * is taller than the cell. Dev aid only — called via\n\t\t * `scheduleCellOverflowAudit`.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tauditCellOverflow() {\n\t\t\tif (!this.$el || !this.$el.querySelectorAll) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.$el.querySelectorAll('.grid-stack-item').forEach((cell) => {\n\t\t\t\tconst content = cell.querySelector('.grid-stack-item-content')\n\t\t\t\tif (!content) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif (content.scrollHeight > content.clientHeight + 8) {\n\t\t\t\t\tconst inner = cell.querySelector('.cn-detail-page__grid-item')\n\t\t\t\t\tconst label = (inner && inner.getAttribute('aria-labelledby')) || cell.getAttribute('gs-id') || ''\n\t\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\t\tconsole.warn(`[CnDetailPage] widget cell ${label || '(unlabelled)'} content overflows its gridHeight `\n\t\t\t\t\t\t+ `(${content.scrollHeight}px in ${content.clientHeight}px) — enlarge the cell or scope the widget's content (ADR-062).`)\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Whether to render the grid section `<h3>` title above a widget. Only for\n\t\t * consumer-supplied `#widget-<id>` slots (whose content may be bare) — the\n\t\t * built-in widget renderers (data / related / integration / catalog) draw\n\t\t * their own titled card header, so a grid heading would show the title\n\t\t * twice. Honours `item.showTitle === false` to opt out entirely.\n\t\t *\n\t\t * @param {object} item Layout item.\n\t\t * @return {boolean} true when the grid heading should render.\n\t\t */\n\t\tshowGridTitle(item) {\n\t\t\tif (item.showTitle === false || !this.findWidget(item)) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn Boolean(this.$slots[`widget-${item.widgetId}`] || this.$slots[`widget-${item.widgetId}`])\n\t\t},\n\n\t\t/**\n\t\t * The grid section heading for a consumer slot widget — the widget\n\t\t * definition's manifest `title` run through the host translate\n\t\t * function. Returns the raw value untouched when there is none.\n\t\t *\n\t\t * @param {object} item Layout item.\n\t\t * @return {string|undefined} The heading text.\n\t\t */\n\t\tgridTitle(item) {\n\t\t\tconst title = this.findWidget(item)?.title\n\t\t\treturn title ? this.effectiveTranslate(title) : title\n\t\t},\n\n\t\t/**\n\t\t * Whether a card widget (stat / gauge / delta) shows the wrapper header.\n\t\t *\n\t\t * These renderers draw their own label from `content.label`, so a header\n\t\t * carrying the same manifest title printed it twice. `showTitle: false`\n\t\t * on the layout item switches it off, the same key and the same meaning\n\t\t * as on the grid `<h3>` (`showGridTitle`).\n\t\t *\n\t\t * @param {object} item Layout item.\n\t\t * @return {boolean} true when the wrapper header renders.\n\t\t */\n\t\tshowCardTitle(item) {\n\t\t\tif (item.showTitle === false) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tconst def = this.findWidget(item)\n\t\t\treturn def?.title !== undefined || widgetContentOf(def).title !== undefined\n\t\t},\n\n\t\t/**\n\t\t * Resolve the widget definition for a layout item. Overrides the\n\t\t * gridLayout mixin's `findWidget` (which only reads the `widgets` prop) so\n\t\t * the materialized default auto-body widgets resolve too. Searches the\n\t\t * effective `bodyGridWidgets`.\n\t\t *\n\t\t * @param {{ widgetId: string }} item Layout item.\n\t\t * @return {object|undefined} The matching widget definition.\n\t\t */\n\t\t/**\n\t\t * A widget definition with its `titleWhen` resolved against the record.\n\t\t *\n\t\t * @param {object|null|undefined} def The definition.\n\t\t * @return {object|null|undefined} The definition to hand on.\n\t\t * @spec openspec/changes/widget-title-when/specs/widget-title-when/spec.md#requirement-a-widget-title-can-depend-on-the-record\n\t\t */\n\t\ttitled(def) {\n\t\t\treturn withTitleWhen(def, this.titleData)\n\t\t},\n\n\t\tfindWidget(item) {\n\t\t\treturn this.bodyGridWidgets.find((w) => w.id === item.widgetId)\n\t\t\t\t|| this.findViewWidget(item.widgetId)\n\t\t\t\t|| undefined\n\t\t},\n\n\t\t/**\n\t\t * Read the records `visibilityContext` names for the loaded record. A\n\t\t * late answer for a record the page has left is dropped.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-page-can-load-records-a-condition-names\n\t\t */\n\t\tasync refreshVisibilityRows() {\n\t\t\tconst key = this.visibilityRefsKey\n\t\t\tconst refs = this.visibilityRefs\n\t\t\tconst seq = ++this.visibilityRowsSeq\n\t\t\tif (refs.length === 0) {\n\t\t\t\t// Write only on a change, so a page without `visibilityContext`\n\t\t\t\t// never touches the state its layout could read.\n\t\t\t\tif (Object.keys(this.visibilityRows).length > 0) {\n\t\t\t\t\tthis.visibilityRows = {}\n\t\t\t\t}\n\t\t\t\tif (this.visibilityRowsFor !== key) {\n\t\t\t\t\tthis.visibilityRowsFor = key\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!this._visibilityRowCache) {\n\t\t\t\tthis._visibilityRowCache = new Map()\n\t\t\t}\n\t\t\tconst rows = await fetchVisibilityRows(refs, this._visibilityRowCache)\n\t\t\tif (seq !== this.visibilityRowsSeq) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.visibilityRows = rows\n\t\t\tthis.visibilityRowsFor = key\n\t\t},\n\n\t\t/**\n\t\t * Ask the endpoint and source `visibleWhen` conditions of the grid\n\t\t * widgets, once per record. Each is fail-safe: an error hides the widget.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t * @spec openspec/changes/detail-widgets-visible-when/specs/detail-widgets-visible-when/spec.md#requirement-a-detail-grid-widget-shows-only-while-its-visiblewhen-holds-for-the-record\n\t\t */\n\t\tasync refreshWidgetConditions() {\n\t\t\tconst seq = ++this.widgetConditionSeq\n\t\t\tif (this.remoteWidgetConditionsKey === '') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst [, entries] = JSON.parse(this.remoteWidgetConditionsKey)\n\t\t\tconst ctx = { objectId: this.objectId, object: this.widgetVisibilityData || undefined }\n\t\t\tconst answers = {}\n\t\t\tawait Promise.all(entries.map(async ([id, cond]) => {\n\t\t\t\tanswers[id] = await evaluateVisibleWhen(cond, ctx)\n\t\t\t}))\n\t\t\tif (seq === this.widgetConditionSeq) {\n\t\t\t\tthis.widgetConditionAnswers = answers\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Drop the items that draw nothing, then close the rows they freed —\n\t\t * `float: true` leaves the item below pinned under a gap nobody owns.\n\t\t * Skipped when nothing was dropped, so an authored gap survives.\n\t\t *\n\t\t * @param {Array<object>} items The authored layout items.\n\t\t *\n\t\t * @return {Array<object>} What the grid should render.\n\t\t */\n\t\tcloseDroppedRows(items) {\n\t\t\tconst drawable = items.filter((item) => this.layoutItemCanDraw(item))\n\t\t\tif (drawable.length === items.length) {\n\t\t\t\treturn drawable\n\t\t\t}\n\n\t\t\treturn compactLayoutRows(drawable)\n\t\t},\n\n\t\t/**\n\t\t * Whether a side entry belongs beside the open body tab: an entry with\n\t\t * `tabs` (tab ids) shows only while one of them is open; an entry with\n\t\t * `notTabs` stands aside while one of those is open (a tab that draws\n\t\t * the side column as its own, DqZaakAssistent); an entry with neither\n\t\t * shows beside every tab. Edit mode shows them all.\n\t\t *\n\t\t * @spec openspec/changes/detail-side-cards-per-tab/specs/detail-page/spec.md#requirement-a-side-card-can-follow-the-open-tab\n\t\t * @spec openspec/changes/side-card-not-on-tabs/specs/detail-page/spec.md#requirement-a-side-card-can-stand-aside-for-a-tab\n\t\t * @param {object} widget The side entry's definition.\n\t\t * @return {boolean}\n\t\t */\n\t\tsideWidgetOnTab(widget) {\n\t\t\treturn this.editingBody || onTab(widget, this.activeBodyTab)\n\t\t},\n\n\t\t/**\n\t\t * Whether a side column widget shows: always in edit mode or without a\n\t\t * `visibleWhen`, else while it holds for the record and the records\n\t\t * `visibilityContext` names, read by the same `detailWidgetVisible` the\n\t\t * body grid uses (local field conditions, `@context.<name>.*`, and the\n\t\t * answers of endpoint and source conditions).\n\t\t *\n\t\t * @param {object} widget A side column widget definition.\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/detail-side-visible-when/specs/detail-page/spec.md#requirement-a-side-column-widget-shows-only-while-its-visiblewhen-holds\n\t\t */\n\t\tsideWidgetVisible(widget) {\n\t\t\tif (this.editingBody || widget.visibleWhen === undefined || widget.visibleWhen === null) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn detailWidgetVisible(widget, this.widgetVisibilityData, {\n\t\t\t\tpending: this.visibilityRowsPending,\n\t\t\t\tanswers: this.widgetConditionAnswers,\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Whether a layout item has anything to draw, so a grid cell is worth\n\t\t * spending on it.\n\t\t *\n\t\t * A widget whose definition carries `visibleWhen` draws only while the\n\t\t * condition holds for the record (see `detailWidgetVisible`).\n\t\t *\n\t\t * Beyond that, TWO widget types are judged, and no others.\n\t\t *\n\t\t * `integration`, in the case where nothing can ever appear: a leaf from\n\t\t * another app that is not registered, with no `requiredApp` to fall back\n\t\t * on. A cross-app feature is normally optional — humaniq's hours leaf on a\n\t\t * dossiq case — and without this the host still renders its wrapper, so the\n\t\t * absent app costs a bordered, full-height ghost card in the grid. An app\n\t\t * nobody installed should cost nothing.\n\t\t *\n\t\t * A widget that declares `requiredApp` is KEPT: the host answers a missing\n\t\t * app with a set-up state, which is content, and hiding it would turn an\n\t\t * actionable \"install humaniq\" into silence.\n\t\t *\n\t\t * `presence`, whenever nobody else is on the record. A runtime fact that\n\t\t * flips both ways, so the host holds the subscription (see setup) and the\n\t\t * row returns the moment somebody arrives.\n\t\t *\n\t\t * Deliberately NOT generalised to \"anything that resolves to nothing\".\n\t\t * A `custom` widget whose `#widget-<id>` slot is missing also draws an\n\t\t * empty host, but that is a WIRING FAULT rather than an absent optional\n\t\t * feature, and quietly dropping it would hide the bug instead of showing\n\t\t * it — CnPageRenderer's split pane forwards no slots, which is exactly\n\t\t * that fault and wants fixing, not concealing.\n\t\t *\n\t\t * The widget the header holds (`headerWidget`) draws nothing here\n\t\t * either: it is already drawn in the header.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-detail-header-can-be-a-card-that-holds-a-widget\n\t\t * @param {object} item A layout item.\n\t\t * @return {boolean} False only when the item provably draws nothing.\n\t\t */\n\t\tlayoutItemCanDraw(item) {\n\t\t\t// In edit mode nothing is hidden: an author has to see the placement to\n\t\t\t// move or delete it, and a widget that vanished from the editor while\n\t\t\t// staying in the manifest would be unmanageable.\n\t\t\tif (this.editingBody) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\t// The widget the header holds (`headerWidget`) is drawn there, so\n\t\t\t// its grid cell would be the same widget twice.\n\t\t\tif (this.headerWidgetDef !== null && item.widgetId === this.headerWidgetDef.id) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tconst widget = this.findWidget(item)\n\t\t\tif (!widget) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\t// The widget's own `visibleWhen`, read against the record and the\n\t\t\t// records `visibilityContext` names (a case's case type). Without\n\t\t\t// one the widget shows, as it always did, and the layout takes no\n\t\t\t// dependency on the record: a page without conditions re-lays out\n\t\t\t// exactly as often as before.\n\t\t\tif (widget.visibleWhen !== undefined && widget.visibleWhen !== null\n\t\t\t\t&& !detailWidgetVisible(widget, this.widgetVisibilityData, {\n\t\t\t\t\tpending: this.visibilityRowsPending,\n\t\t\t\t\tanswers: this.widgetConditionAnswers,\n\t\t\t\t})) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\t// The board look shows the activity as the History tab. When a tabs\n\t\t\t// widget already holds it, it is not also a body section.\n\t\t\tif (this.isBoard && isActivityWidget(widget) && this.tabbedWidgetIds.has(widget.id)) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\t// Presence is empty almost always, and no attribute can reclaim its\n\t\t\t// row: GridStack floors `sizeToContent` at the item's `gs-min-h`.\n\t\t\tif (widget.type === 'presence') {\n\t\t\t\treturn (this.presentOthers || []).length > 0\n\t\t\t}\n\t\t\tif (widget.type !== 'integration') {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tconst requiredApp = widget.requiredApp || (widget.content || {}).requiredApp || ''\n\t\t\tif (requiredApp !== '' && !isAppInstalled(requiredApp)) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tif (typeof this.getRegistryProvider !== 'function') {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn !!this.getRegistryProvider(widget.integrationId)\n\t\t},\n\n\t\t/**\n\t\t * Build the default body grid for the schema-driven auto-body: a full-width\n\t\t * Data widget with the Related-objects widget beneath it. Materialized once\n\t\t * (lazily) so the geometry is a mutable, reactive array the user can drag /\n\t\t * resize in edit mode. Both widgets carry `showTitle: false` because they\n\t\t * render their own card chrome.\n\t\t */\n\t\tmaterializeAutoBody() {\n\t\t\t// One source of truth (shared with the Buildiq edit button's\n\t\t\t// \"eject\" on edit) so the in-memory default and the manifest-ejected\n\t\t\t// default are identical. The data widget's content carries the page's\n\t\t\t// register/schema so its per-property editor can resolve the schema\n\t\t\t// (empty on legacy objectType-only mounts → the editor falls back to\n\t\t\t// the injected `cnObjectContext`).\n\t\t\tconst grid = defaultDetailGrid({\n\t\t\t\tregister: this.register || '',\n\t\t\t\tschema: this.schema || '',\n\t\t\t\tshowRelated: this.showRelatedObjects,\n\t\t\t})\n\t\t\t// Drop the Data widget when there's no schema to render (the helper\n\t\t\t// always includes it; the auto-body only shows it with a schema).\n\t\t\t//\n\t\t\t// This can happen because the schema HAS NOT ARRIVED YET, not\n\t\t\t// because there is none: `schemaDrivenFetch` fires fetchObject and\n\t\t\t// fetchSchema in parallel, and this body materializes as soon as\n\t\t\t// `shouldRenderAutoBody` turns true — which tests `currentObject`\n\t\t\t// and NOT `currentSchema`. When the object wins that race the Data\n\t\t\t// widget is filtered out, and because the body materializes only\n\t\t\t// once (`if (active && !this.autoBodyLayout)`) and resets only when\n\t\t\t// `resolvedObjectType` changes, the schema landing a moment later\n\t\t\t// never puts it back.\n\t\t\t//\n\t\t\t// The symptom is a detail page showing its title and Related panel\n\t\t\t// with EVERY declared scalar field missing, intermittently, on\n\t\t\t// identical code — shillinq#1073, where the object fetch returned\n\t\t\t// 200 carrying the values the page then did not render.\n\t\t\t//\n\t\t\t// `dataWidgetDeferred` records that this filtering was provisional\n\t\t\t// so the watcher below can re-materialize exactly once, when the\n\t\t\t// schema arrives. It is deliberately NOT a plain \"re-materialize if\n\t\t\t// the data widget is absent\": a body the user has edited must not\n\t\t\t// have the widget silently restored underneath them.\n\t\t\tthis.dataWidgetDeferred = !this.currentSchema\n\t\t\tif (!this.currentSchema) {\n\t\t\t\tgrid.widgets = grid.widgets.filter((w) => w.widgetId !== 'data')\n\t\t\t\tgrid.layout = grid.layout.filter((l) => l.widgetId !== 'data')\n\t\t\t}\n\t\t\tthis.autoBodyWidgets = grid.widgets\n\t\t\tthis.autoBodyLayout = grid.layout\n\t\t},\n\n\t\t/**\n\t\t * Persist a drag/resize from the body grid. For the default auto-body the\n\t\t * new geometry is kept locally so the move sticks; for an explicit `layout`\n\t\t * prop the change is emitted upward (`layout-change` + `update:layout`) for\n\t\t * the manifest renderer / host to persist.\n\t\t *\n\t\t * @param {Array} updated The new layout array from CnDashboardGrid.\n\t\t *\n\t\t * @event layout-change Emitted with the updated body-grid layout. The\n\t\t * sibling `update:layout` event is also emitted so `:layout.sync`\n\t\t * consumers stay in sync.\n\t\t */\n\t\tonBodyLayoutChange(updated) {\n\t\t\tif (this.hasGridLayout) {\n\t\t\t\t// Config-backed grid (manifest editor, ADR-041): the `layout` prop is\n\t\t\t\t// the SAME array the working manifest holds (the route-sentinel\n\t\t\t\t// resolver is reference-preserving), so write the new geometry back\n\t\t\t\t// onto its items in place — Save page then persists the resize.\n\t\t\t\tfor (const u of updated) {\n\t\t\t\t\tconst item = this.layout.find((l) => String(l.id) === String(u.id))\n\t\t\t\t\tif (item) {\n\t\t\t\t\t\titem.gridX = u.gridX\n\t\t\t\t\t\titem.gridY = u.gridY\n\t\t\t\t\t\titem.gridWidth = u.gridWidth\n\t\t\t\t\t\titem.gridHeight = u.gridHeight\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// In-memory default grid — keep the new geometry locally so the move\n\t\t\t\t// sticks for the session (un-customised page, nothing to persist).\n\t\t\t\tthis.autoBodyLayout = updated\n\t\t\t}\n\t\t\t/**\n\t\t\t * @event layout-change Emitted when a body-grid widget is dragged or\n\t\t\t * resized. Payload is the updated layout array.\n\t\t\t * @type {Array}\n\t\t\t */\n\t\t\tthis.$emit('layout-change', updated)\n\t\t\t// Description goes ABOVE `@event`, not inline after it:\n\t\t\t// vue-docgen-api's event-name splitter stops at the first `:`, so\n\t\t\t// `@event update:layout <description>` is read as one long event\n\t\t\t// NAME and the generated docs show an empty description.\n\t\t\t/**\n\t\t\t * Sibling of `layout-change` so `v-model:layout` consumers stay in\n\t\t\t * sync.\n\t\t\t *\n\t\t\t * @event update:layout\n\t\t\t * @type {Array}\n\t\t\t */\n\t\t\tthis.$emit('update:layout', updated)\n\t\t},\n\n\t\t/**\n\t\t * The registered config FORM for a grid item's widget type, or null.\n\t\t * Used to gate the per-widget configure cog (only configurable widgets\n\t\t * show one) in Buildiq edit mode.\n\t\t *\n\t\t * @param {object} item Layout item\n\t\t * @return {object|null} The form component, or null.\n\t\t */\n\t\tregistryFormFor(item) {\n\t\t\tconst def = this.findWidget(item)\n\t\t\tif (!def || !def.type) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst entry = getWidgetTypeEntry(def.type)\n\t\t\treturn (entry && entry.form) || null\n\t\t},\n\n\t\t/**\n\t\t * Whether a grid item shows the configure cog: the page is in edit\n\t\t * mode and the item resolves to a widget definition. Not gated on a\n\t\t * registered form: the style editor modal handles a type without one\n\t\t * (title, icon, style, delete), and the delete is what a custom widget\n\t\t * or an integration leaf needs the cog for.\n\t\t *\n\t\t * @param {object} item Layout item.\n\t\t * @return {boolean} true when the cog renders.\n\t\t */\n\t\tshowsCog(item) {\n\t\t\treturn this.editingBody === true && Boolean(this.findWidget(item))\n\t\t},\n\n\t\t/**\n\t\t * The registered RENDERER for a grid item's content-driven catalog\n\t\t * widget type (stat / chart / delta / gauge / object-list / …), or null.\n\t\t * Integration widgets resolve separately; the `data` renderer needs\n\t\t * object context the slot supplies, so it is excluded from this generic\n\t\t * fallback.\n\t\t *\n\t\t * @param {object} item Layout item\n\t\t * @return {object|null} The renderer component, or null.\n\t\t */\n\t\tregistryRendererFor(item) {\n\t\t\treturn resolveRegistryRenderer(this.findWidget(item), this.cnRegistry)\n\t\t},\n\n\t\t/**\n\t\t * The stored content/config blob for a grid item's catalog widget\n\t\t * (passed to its renderer).\n\t\t *\n\t\t * @param {object} item Layout item\n\t\t * @return {object} The widget def's `content`, or an empty object.\n\t\t */\n\t\twidgetContentFor(item) {\n\t\t\treturn widgetContentOf(this.findWidget(item))\n\t\t},\n\n\t\t/**\n\t\t * Resolve the effective title for a grid widget.\n\t\t *\n\t\t * A title-owning widget type (registry `ownsTitle`, e.g. `related`) keeps\n\t\t * its editable title in `content.title` — its config sub-form owns the\n\t\t * title field and the generic chrome title input is hidden. For those,\n\t\t * `content.title` MUST win over the chrome `def.title`, otherwise a\n\t\t * non-empty seed title (e.g. the default detail grid seeds the related\n\t\t * widget with `title: 'Related'`) permanently shadows the user's edit and\n\t\t * the title can never be changed. For every other type the chrome\n\t\t * `def.title` is authoritative, falling back to `content.title`. Empty\n\t\t * resolves to `undefined` so the widget renders its own default label.\n\t\t *\n\t\t * @param {object} item Layout item.\n\t\t * @return {string|undefined} The title to render, or undefined for the default.\n\t\t */\n\t\twidgetDisplayTitle(item) {\n\t\t\treturn widgetTitleOf(this.findWidget(item))\n\t\t},\n\n\t\t/**\n\t\t * Open the per-widget style/config editor for a grid item (cog click).\n\t\t *\n\t\t * @param {object} item Layout item to configure.\n\t\t */\n\t\tconfigureWidget(item) {\n\t\t\tthis.configWidgetId = item.widgetId\n\t\t\tthis.showWidgetConfig = true\n\t\t},\n\n\t\t/**\n\t\t * Persist the editor's changes onto the matching widget definition IN\n\t\t * PLACE (so an injected in-place manifest editor picks them up), close\n\t\t * the modal, and emit `widget-config-change` for consumers that persist\n\t\t * the page config themselves.\n\t\t *\n\t\t * @param {object} edited The widget object mutated by the editor.\n\t\t *\n\t\t * @event widget-config-change Emitted after a widget's config is saved.\n\t\t * @type {object}\n\t\t */\n\t\tonWidgetConfigSave(edited) {\n\t\t\tconst def = this.bodyGridWidgets.find((w) => w.id === this.configWidgetId)\n\t\t\tif (def) {\n\t\t\t\tif (edited.title !== undefined) {\n\t\t\t\t\tdef.title = edited.title\n\t\t\t\t}\n\t\t\t\tif (edited.content !== undefined) {\n\t\t\t\t\tdef.content = edited.content\n\t\t\t\t}\n\t\t\t\tdef.styleConfig = edited.styleConfig || {}\n\t\t\t}\n\t\t\tthis.showWidgetConfig = false\n\t\t\t/**\n\t\t\t * @event widget-config-change Emitted after a grid widget's config is\n\t\t\t * saved via the cog editor, or after the widget is removed (payload\n\t\t\t * null). Consumers persist the updated page/widget config.\n\t\t\t * @type {object|null}\n\t\t\t */\n\t\t\tthis.$emit('widget-config-change', def || null)\n\t\t},\n\n\t\t/**\n\t\t * Delete from inside the editor — drop the widget def + its layout\n\t\t * placement and close.\n\t\t *\n\t\t * @param {object} _w The widget the editor wants deleted (unused; routed\n\t\t * by `configWidgetId`).\n\t\t *\n\t\t * @event widget-config-change Emitted after the widget is removed.\n\t\t * @type {null}\n\t\t */\n\t\tonWidgetConfigDelete(_w) {\n\t\t\tconst id = this.configWidgetId\n\t\t\t// Drop from whichever pair backs the active grid: the explicit\n\t\t\t// `layout`/`widgets` props (manifest grid page) or the materialized\n\t\t\t// default auto-body arrays.\n\t\t\tconst widgetArr = this.hasGridLayout ? this.widgets : this.autoBodyWidgets\n\t\t\tconst layoutArr = this.hasGridLayout ? this.layout : this.autoBodyLayout\n\t\t\tif (Array.isArray(widgetArr)) {\n\t\t\t\tconst wIdx = widgetArr.findIndex((w) => w.id === id)\n\t\t\t\tif (wIdx !== -1) {\n\t\t\t\t\twidgetArr.splice(wIdx, 1)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (Array.isArray(layoutArr)) {\n\t\t\t\tconst lIdx = layoutArr.findIndex((l) => l.widgetId === id)\n\t\t\t\tif (lIdx !== -1) {\n\t\t\t\t\tlayoutArr.splice(lIdx, 1)\n\t\t\t\t}\n\t\t\t}\n\t\t\tthis.showWidgetConfig = false\n\t\t\tthis.$emit('widget-config-change', null)\n\t\t},\n\n\t\t/**\n\t\t * Push pageKind + objectUuid + register/schema context into the\n\t\t * reactive cnAiContext holder so the AI Chat Companion knows\n\t\t * what object the user is viewing.\n\t\t */\n\t\tpushAiContext() {\n\t\t\tif (!this.cnAiContext) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst resolved = this.resolvedSidebar || {}\n\t\t\tthis.cnAiContext.pageKind = 'detail'\n\t\t\tthis.cnAiContext.objectUuid = this.objectId ? String(this.objectId) : undefined\n\t\t\tthis.cnAiContext.registerSlug = resolved.register || this.register || this.sidebarProps?.register || undefined\n\t\t\tthis.cnAiContext.schemaSlug = resolved.schema || this.schema || this.resolvedObjectType || this.sidebarProps?.schema || undefined\n\t\t},\n\n\t\t/**\n\t\t * Refresh the provided `cnObjectContext` ref so detail-page abstract\n\t\t * widgets can resolve `@objectId` / `@object.<field>` filter tokens\n\t\t * against the current object, and hand back an object they saved\n\t\t * (`setObject`). No-op when setup() didn't expose the ref\n\t\t * (standalone / read-only mounts).\n\t\t */\n\t\tsyncObjectContext() {\n\t\t\tif (!this.cnObjectContextRef) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst resolved = this.resolvedSidebar || {}\n\t\t\tthis.cnObjectContextRef.value = {\n\t\t\t\tobjectId: this.objectId !== undefined && this.objectId !== null ? String(this.objectId) : null,\n\t\t\t\tobject: this.currentObject || null,\n\t\t\t\tregister: resolved.register || this.register || this.sidebarProps?.register || '',\n\t\t\t\tschema: resolved.schema || this.schema || this.resolvedObjectType || this.sidebarProps?.schema || '',\n\t\t\t\tcontext: this.visibilityRowsPending ? null : (this.visibilityRows || null),\n\t\t\t\tsetObject: (object) => this.setCurrentObject(object),\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Pushes the resolved sidebar config into the\n\t\t * `objectSidebarState` provide/inject channel for the host\n\t\t * App's mounted `<CnObjectSidebar>` to consume.\n\t\t *\n\t\t * Object form fields take precedence over `sidebarProps` for\n\t\t * any field they declare; `sidebarProps` continues to fill\n\t\t * fields the Object form omits, preserving back-compat.\n\t\t *\n\t\t * Suppression (`show: false` or `enabled: false`) clears\n\t\t * `tabs` so a hidden detail page does not leak prior tab\n\t\t * state to the next mount.\n\t\t *\n\t\t * @spec openspec/changes/case-page-and-list-as-a-place/specs/index-page/spec.md\n\t\t */\n\t\tsyncSidebarState() {\n\t\t\tif (!this.hasExternalSidebar) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.warnIfDeprecatedSidebarShape()\n\t\t\t// During a background refresh (content stays in place — see\n\t\t\t// `hasLoadedOnce`), a transient loading-driven `enabled: false`\n\t\t\t// must NOT tear down the sidebar. Hosts commonly bind\n\t\t\t// `:sidebar=\"{ enabled: !loading }\"`, so without this guard every\n\t\t\t// refresh unmounts the host's CnObjectSidebar and re-fetches all\n\t\t\t// its sub-resources (files/notes/tags/tasks/audit). Skip the sync\n\t\t\t// while refreshing; the `loading` watcher re-syncs once it settles.\n\t\t\tif (this.loading && this.hasLoadedOnce) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst r = this.resolvedSidebar\n\t\t\t// A record held back for its access purpose shows nothing in the sidebar either.\n\t\t\tconst hasRecord = !this.objectNotFound && !this.awaitingObject && !this.contentWithheld\n\t\t\tif (this.sidebarActive && hasRecord && this.resolvedObjectType && this.objectId) {\n\t\t\t\tconst merged = this.mergeSidebarSources(r)\n\t\t\t\t// Seed `open` only on the inactive→active edge (first activation\n\t\t\t\t// of this object). Subsequent syncs must NOT clobber it, otherwise\n\t\t\t\t// the user's close/toggle would be undone on the next reactive\n\t\t\t\t// change. The shared channel owns `open` after seeding.\n\t\t\t\t//\n\t\t\t\t// `sidebarSeeded` used to be written here and never read, so\n\t\t\t\t// `open` went into assignSidebarState() on EVERY sync and the\n\t\t\t\t// paragraph above described behaviour the code did not have. A\n\t\t\t\t// detail page emits several syncs while it hydrates (the object\n\t\t\t\t// resolves, then the schema), and each one reset `open` to the\n\t\t\t\t// prop default — reclosing a sidebar the user had just opened.\n\t\t\t\t// Downstream that made openbuild's e2e race the UI: the tab was\n\t\t\t\t// clicked and its panel was hidden again before the assertion\n\t\t\t\t// ran (openbuild#268 — \"resolves 30×, hidden\").\n\t\t\t\tconst seedOpen = (this.sidebarSeeded === false)\n\t\t\t\tif (seedOpen) {\n\t\t\t\t\tthis.sidebarSeeded = true\n\t\t\t\t}\n\t\t\t\t// Manifest-driven open-enum tabs (forwarded to the host\n\t\t\t\t// app's mounted CnObjectSidebar via inject). When the\n\t\t\t\t// top-level `sidebarTabs` prop is non-empty it provides\n\t\t\t\t// the tabs (manifest pattern); otherwise the merged\n\t\t\t\t// `sidebar.tabs` / `sidebarProps.tabs` legacy paths win.\n\t\t\t\t// Falls back to `undefined` so the host's CnObjectSidebar\n\t\t\t\t// renders its built-in tab set.\n\t\t\t\tconst tabs = (this.sidebarTabs && this.sidebarTabs.length > 0)\n\t\t\t\t\t? this.sidebarTabs\n\t\t\t\t\t: merged.tabs\n\t\t\t\tthis.assignSidebarState({\n\t\t\t\t\tactive: true,\n\t\t\t\t\t// Omitted after the seeding sync — assignSidebarState()\n\t\t\t\t\t// only writes the keys it is given, so leaving `open` out\n\t\t\t\t\t// preserves whatever the shared channel currently holds.\n\t\t\t\t\t...(seedOpen ? { open: this.sidebarOpen } : {}),\n\t\t\t\t\tobjectType: this.resolvedObjectType,\n\t\t\t\t\tobjectId: this.objectId,\n\t\t\t\t\t// The loaded object, so coordinate-blind sidebar-tab widgets\n\t\t\t\t\t// (the `data` / `metadata` built-ins) receive it as `objectData`\n\t\t\t\t\t// — the hoisted sidebar is a CnAppRoot sibling, not a\n\t\t\t\t\t// CnDetailPage child, so it can't inject our object context.\n\t\t\t\t\tobject: this.resolvedObject || null,\n\t\t\t\t\t// The resolved schema OBJECT (not the slug), which the `data`\n\t\t\t\t\t// built-in (CnObjectDataWidget) needs alongside `objectData`.\n\t\t\t\t\tschemaObject: this.currentSchema || null,\n\t\t\t\t\ttitle: merged.title || this.title || '',\n\t\t\t\t\tsubtitle: merged.subtitle || this.subtitle || '',\n\t\t\t\t\tregister: merged.register || this.register || '',\n\t\t\t\t\tschema: merged.schema || this.schema || '',\n\t\t\t\t\thiddenTabs: merged.hiddenTabs || [],\n\t\t\t\t\ttabs,\n\t\t\t\t\t// ADR-019. `config.sidebar.useRegistry` reaches\n\t\t\t\t\t// `resolvedSidebar` intact — the Object branch returns\n\t\t\t\t\t// `{show, enabled, ...cfg}` — and survives\n\t\t\t\t\t// mergeSidebarSources(), which spreads `resolved` before\n\t\t\t\t\t// its own field list. It was simply never published here,\n\t\t\t\t\t// so the host's CnObjectSidebar always saw the prop default\n\t\t\t\t\t// `false` and rendered its BACKWARDS-COMPATIBLE branch: the\n\t\t\t\t\t// five hard-coded built-in tabs (files, notes, tags, tasks,\n\t\t\t\t\t// audit-trail) instead of one tab per registered provider.\n\t\t\t\t\t//\n\t\t\t\t\t// The declaration therefore did nothing at all. Measured on\n\t\t\t\t\t// decidiq run 32702211376, whose MeetingIntegrations page\n\t\t\t\t\t// sets `config.sidebar.useRegistry: true`:\n\t\t\t\t\t//\n\t\t\t\t\t// JS registry carries every expected provider passed\n\t\t\t\t\t// every component leaf has a tab + widget passed\n\t\t\t\t\t// OCS caps and JS registry agree passed\n\t\t\t\t\t// providers the server reports available 10\n\t\t\t\t\t// tabs the sidebar rendered 5\n\t\t\t\t\t//\n\t\t\t\t\t// Five, exactly, because that is the fallback branch. Every\n\t\t\t\t\t// app whose manifest opts a page into the registry sidebar\n\t\t\t\t\t// has been getting the built-ins.\n\t\t\t\t\t//\n\t\t\t\t\t// `excludeIntegrations` rides the same channel and was\n\t\t\t\t\t// dropped for the same reason; without it a page cannot\n\t\t\t\t\t// suppress a provider it does not want.\n\t\t\t\t\tuseRegistry: merged.useRegistry === true,\n\t\t\t\t\texcludeIntegrations: merged.excludeIntegrations || [],\n\t\t\t\t\t// The tab the address asks for, and the way back. The\n\t\t\t\t\t// sidebar lives outside this component's tree (the host\n\t\t\t\t\t// mounts it at NcContent level), so the address and the\n\t\t\t\t\t// tab strip can only meet on this shared channel.\n\t\t\t\t\t...(this.tabInAddress\n\t\t\t\t\t\t? { requestedTab: this.requestedTabFromAddress(), onTabChange: this.onSidebarTabChange }\n\t\t\t\t\t\t: {}),\n\t\t\t\t})\n\t\t\t} else {\n\t\t\t\tthis.assignSidebarState({ active: false, tabs: undefined })\n\t\t\t\t// Re-arm the seed so the next activation re-applies `sidebarOpen`.\n\t\t\t\tthis.sidebarSeeded = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Apply fields onto the shared `objectSidebarState` only when they\n\t\t * actually change. Writing the same logical value — notably a fresh\n\t\t * `[]` for `hiddenTabs` when none is configured — would otherwise put\n\t\t * a new array reference on the reactive object every call and trigger\n\t\t * a host re-render. That churn fed an infinite render loop: the host\n\t\t * App re-renders on the new `hiddenTabs` ref → its `<router-view>`\n\t\t * re-renders the routed detail page → the page's inline `:sidebar`\n\t\t * prop re-fires this sync → repeat. Arrays are compared shallowly so\n\t\t * an equivalent array is treated as unchanged.\n\t\t *\n\t\t * @param {object} fields Partial `objectSidebarState` to apply.\n\t\t */\n\t\tassignSidebarState(fields) {\n\t\t\tconst state = this.objectSidebarState\n\t\t\tfor (const key of Object.keys(fields)) {\n\t\t\t\tconst next = fields[key]\n\t\t\t\tconst cur = state[key]\n\t\t\t\tif (Array.isArray(next) && Array.isArray(cur)\n\t\t\t\t\t&& next.length === cur.length\n\t\t\t\t\t&& next.every((v, i) => v === cur[i])) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif (cur !== next) {\n\t\t\t\t\tstate[key] = next\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Merge the Object-form `sidebar` fields with the legacy\n\t\t * `sidebarProps` fields. Object form wins on conflict; first\n\t\t * conflict per instance fires a one-shot console.warn naming\n\t\t * the conflicting fields.\n\t\t *\n\t\t * @param {object} resolved Pre-normalised sidebar config object.\n\t\t * @return {object} Merged config — Object form fields override\n\t\t * sidebarProps fields where both are set.\n\t\t */\n\t\tmergeSidebarSources(resolved) {\n\t\t\tconst objectForm = (this.sidebar && typeof this.sidebar === 'object') ? this.sidebar : null\n\t\t\tconst props = this.sidebarProps || {}\n\t\t\tconst merged = {\n\t\t\t\ttitle: objectForm?.title ?? props.title,\n\t\t\t\tsubtitle: objectForm?.subtitle ?? props.subtitle,\n\t\t\t\tregister: objectForm?.register ?? props.register,\n\t\t\t\tschema: objectForm?.schema ?? props.schema,\n\t\t\t\thiddenTabs: objectForm?.hiddenTabs ?? props.hiddenTabs,\n\t\t\t\ttabs: objectForm?.tabs ?? props.tabs,\n\t\t\t}\n\t\t\tif (objectForm && !this.__sidebarConflictWarned) {\n\t\t\t\tconst overlap = ['title', 'subtitle', 'register', 'schema', 'hiddenTabs', 'tabs']\n\t\t\t\t\t.filter((field) => objectForm[field] !== undefined && props[field] !== undefined)\n\t\t\t\tif (overlap.length > 0) {\n\t\t\t\t\t// eslint-disable-next-line no-console -- a deliberate warning to the developer integrating this component\n\t\t\t\t\tconsole.warn(`[CnDetailPage] :sidebar (Object) and :sidebarProps both set ${overlap.join(', ')}; the :sidebar values win. Move all fields to :sidebar to silence this warning.`)\n\t\t\t\t\tthis.__sidebarConflictWarned = true\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Re-anchor `resolved` for the contract — callers expect\n\t\t\t// the returned object to reflect the normalised shape.\n\t\t\treturn { ...resolved, ...merged }\n\t\t},\n\n\t\t/**\n\t\t * Log a one-shot deprecation warning when the legacy Boolean\n\t\t * form of the `sidebar` prop is observed. Tracked via a\n\t\t * non-reactive instance flag so subsequent renders/toggles\n\t\t * don't spam the console.\n\t\t */\n\t\twarnIfDeprecatedSidebarShape() {\n\t\t\tif (typeof this.sidebar !== 'boolean') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.__sidebarBooleanWarned) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.__sidebarBooleanWarned = true\n\n\t\t\t// eslint-disable-next-line no-console -- a deliberate warning to the developer integrating this component\n\t\t\tconsole.warn('[CnDetailPage] :sidebar=Boolean is deprecated; pass an Object — see docs/components/cn-detail-page.md for the new shape.')\n\t\t},\n\t},\n}\n</script>\n\n<!-- Styles in css/detail-page.css -->\n"],"names":["_createElementBlock","_normalizeClass","_renderSlot","_createElementVNode","_openBlock","_Fragment","_renderList","_createBlock","_createVNode","_toDisplayString","_resolveDynamicComponent","_createTextVNode","_normalizeStyle","_withCtx"],"mappings":"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"}