@conduction/nextcloud-vue 2.83.0 → 2.85.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.
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue.js +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js +131 -3
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +54 -18
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue.js +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue.js +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +232 -4
- package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue3.js +95 -51
- package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
- package/dist/esm/components/CnBodySections/CnBodySections.vue.js +1 -1
- package/dist/esm/components/CnBodySections/CnBodySections.vue2.js +22 -1
- package/dist/esm/components/CnBodySections/CnBodySections.vue2.js.map +1 -1
- package/dist/esm/components/CnBodySections/CnBodySections.vue3.js.map +1 -1
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue.js +1 -1
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js +47 -4
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue.js +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js +239 -3
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js +5 -5
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +50 -0
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +22 -12
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue.js +1 -1
- package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue2.js +1 -1
- package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +226 -8
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +3 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +18 -5
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +38 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +45 -5
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue.js +1 -1
- package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue2.js +1 -1
- package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +548 -77
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +191 -82
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/useSelfFetchList.js +11 -2
- package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
- package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue2.js +29 -6
- package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue.js +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +126 -2
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js +24 -2
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +106 -3
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue.js +1 -1
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js +67 -5
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js.map +1 -1
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js +181 -124
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js.map +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue.js +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js +135 -2
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js.map +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js +95 -43
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js.map +1 -1
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue.js +1 -1
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js +5 -1
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js.map +1 -1
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue3.js.map +1 -1
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue.js +1 -1
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js +72 -5
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js +33 -8
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue.js +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +135 -11
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +6 -2
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue.js +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js +17 -2
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +39 -6
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue.js +1 -1
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js +140 -7
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue.js +1 -1
- package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue2.js +37 -9
- package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue2.js.map +1 -1
- package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js +6 -5
- package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js.map +1 -1
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue.js +1 -1
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js +16 -0
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js.map +1 -1
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js.map +1 -1
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue.js +1 -1
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js +2 -1
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue2.js +53 -1
- package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue3.js +3 -2
- package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue.js +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +135 -0
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue.js +1 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js +19 -0
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js +37 -19
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js.map +1 -1
- package/dist/esm/composables/useListView.js +21 -0
- package/dist/esm/composables/useListView.js.map +1 -1
- package/dist/esm/dialogs/CnTransitionInputDialog.vue.js +1 -1
- package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js +68 -6
- package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js.map +1 -1
- package/dist/esm/dialogs/CnTransitionInputDialog.vue3.js +133 -105
- package/dist/esm/dialogs/CnTransitionInputDialog.vue3.js.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +38 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +78 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/nextcloud-vue.css +1030 -533
- package/dist/esm/store/useObjectStore.js +16 -2
- package/dist/esm/store/useObjectStore.js.map +1 -1
- package/dist/esm/utils/boardColumns.js +8 -1
- package/dist/esm/utils/boardColumns.js.map +1 -1
- package/dist/esm/utils/boardReferenceColumns.js +278 -0
- package/dist/esm/utils/boardReferenceColumns.js.map +1 -0
- package/dist/esm/utils/buildManifest.js +41 -4
- package/dist/esm/utils/buildManifest.js.map +1 -1
- package/dist/esm/utils/detailWidgetVisibility.js +149 -0
- package/dist/esm/utils/detailWidgetVisibility.js.map +1 -0
- package/dist/esm/utils/formatMetric.js +39 -3
- package/dist/esm/utils/formatMetric.js.map +1 -1
- package/dist/esm/utils/gridStack.js +40 -1
- package/dist/esm/utils/gridStack.js.map +1 -1
- package/dist/esm/utils/humaniseKey.js +41 -0
- package/dist/esm/utils/humaniseKey.js.map +1 -0
- package/dist/esm/utils/notificationRuleLabel.js +6 -23
- package/dist/esm/utils/notificationRuleLabel.js.map +1 -1
- package/dist/esm/utils/resolveManifestTokens.js +1 -0
- package/dist/esm/utils/resolveManifestTokens.js.map +1 -1
- package/dist/esm/utils/sentinelTokens.js +9 -0
- package/dist/esm/utils/sentinelTokens.js.map +1 -1
- package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/esm/utils/widgetDispatch.js +8 -2
- package/dist/esm/utils/widgetDispatch.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +17159 -13422
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +1030 -533
- package/l10n/en.json +38 -1
- package/l10n/nl.json +78 -1
- package/package.json +1 -1
- package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +160 -8
- package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +1 -1
- package/src/components/CnBoardView/CnBoardView.vue +262 -6
- package/src/components/CnBodySections/CnBodySections.vue +22 -1
- package/src/components/CnCountdownWidget/CnCountdownWidget.vue +47 -4
- package/src/components/CnDashboardGrid/CnDashboardGrid.vue +244 -8
- package/src/components/CnDashboardPage/CnDashboardPage.vue +62 -2
- package/src/components/CnDeltaWidget/CnDeltaWidget.vue +1 -1
- package/src/components/CnDetailPage/CnDetailPage.md +3 -1
- package/src/components/CnDetailPage/CnDetailPage.vue +226 -8
- package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +3 -1
- package/src/components/CnFactsWidget/CnFactsWidget.vue +18 -5
- package/src/components/CnFieldListWidget/CnFieldListWidget.vue +63 -1
- package/src/components/CnGaugeWidget/CnGaugeWidget.vue +1 -1
- package/src/components/CnIndexPage/CnIndexPage.vue +586 -25
- package/src/components/CnIndexPage/useSelfFetchList.js +10 -1
- package/src/components/CnInteractionFormWidget/CnInteractionFormWidget.vue +29 -6
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.md +1 -1
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +154 -5
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +106 -3
- package/src/components/CnRegisterMapping/CnRegisterMapping.md +5 -0
- package/src/components/CnRegisterMapping/CnRegisterMapping.vue +231 -96
- package/src/components/CnReportsPage/CnReportsPage.vue +179 -1
- package/src/components/CnSettingsPage/CnSettingsPage.vue +5 -1
- package/src/components/CnStagesWidget/CnStagesWidget.vue +103 -6
- package/src/components/CnStatWidget/CnStatWidget.vue +137 -12
- package/src/components/CnStatsBlockWidget/CnStatsBlockWidget.vue +34 -3
- package/src/components/CnTabsWidget/CnTabsWidget.vue +39 -6
- package/src/components/CnToggleTableWidget/CnToggleTableWidget.vue +140 -7
- package/src/components/CnVersionInfoCard/CnVersionInfoCard.md +2 -1
- package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +42 -13
- package/src/components/CnWalkthrough/CnWalkthrough.vue +16 -0
- package/src/components/CnWeekStripWidget/CnWeekStripWidget.vue +2 -1
- package/src/components/CnWidgetCardGrid/CnWidgetCardGrid.vue +55 -2
- package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +135 -0
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.md +10 -0
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +44 -1
- package/src/composables/useListView.js +21 -0
- package/src/css/board.css +65 -4
- package/src/css/dialog.css +4 -2
- package/src/css/form-field.css +14 -0
- package/src/css/form-widget.css +11 -0
- package/src/css/index-page.css +49 -0
- package/src/css/kpi-card.css +13 -0
- package/src/css/look-board-reports.css +58 -0
- package/src/css/look-board.css +108 -7
- package/src/dialogs/CnTransitionInputDialog.vue +87 -7
- package/src/index.js +2 -0
- package/src/schemas/app-manifest-v2.schema.json +137 -6
- package/src/store/useObjectStore.js +16 -2
- package/src/utils/boardColumns.js +8 -1
- package/src/utils/boardReferenceColumns.js +274 -0
- package/src/utils/buildManifest.js +40 -3
- package/src/utils/detailWidgetVisibility.js +145 -0
- package/src/utils/formatMetric.js +38 -2
- package/src/utils/gridStack.js +39 -0
- package/src/utils/humaniseKey.js +38 -0
- package/src/utils/notificationRuleLabel.js +5 -23
- package/src/utils/resolveManifestTokens.js +1 -0
- package/src/utils/sentinelTokens.js +9 -0
- package/src/utils/validateManifestV2.compiled.js +3 -3
- package/src/utils/widgetDispatch.js +8 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnWidgetWrapper.vue3.js","sources":["../../../../src/components/CnWidgetWrapper/CnWidgetWrapper.vue?vue&type=template&id=64db0f38&lang.js"],"sourcesContent":["<!--\n CnWidgetWrapper — Container shell around a dashboard widget.\n\n Provides header (icon + title), scrollable content area, and optional\n footer with action buttons. Applies style configuration for borders,\n backgrounds, and padding. The built-in overflow Actions menu (Refresh /\n Documentation / Request a feature) is delegated to the shared\n CnActionsMenu so widgets and page surfaces stay in lockstep.\n-->\n<template>\n\t<div\n\t\tclass=\"cn-widget-wrapper\"\n\t\t:class=\"{\n\t\t\t'cn-widget-wrapper--borderless': noBorder,\n\t\t\t'cn-widget-wrapper--flush': noPadding,\n\t\t\t'cn-widget-wrapper--chromeless': chromeless,\n\t\t\t'cn-widget-wrapper--nc-dashboard': chrome === 'nc-dashboard',\n\t\t\t'cn-widget-wrapper--high-contrast': highContrast,\n\t\t}\"\n\t\t:data-cn-high-contrast=\"highContrast ? 'true' : undefined\"\n\t\t:style=\"wrapperStyles\">\n\t\t<!-- Header.\n\t\t Rendered when a title earns it, or when controls that are actually\n\t\t on screen need a home. Three conditions rather than one, and each of\n\t\t the three is a bug this component has already shipped: gating on the\n\t\t title alone took the Save button for an inline edit away along with\n\t\t the doubled title; reading `showActions`, which defaults to true,\n\t\t handed a header to every headerless KPI tile; and reading the\n\t\t `actions` slot's mere PRESENCE gave a chromeless panel a 59px band\n\t\t holding nothing, because `CnObjectDataWidget` provides that template\n\t\t always and fills it only while an edit is unsaved.\n\t\t `headerIsWorthIt()` carries the reasoning. -->\n\t\t<div v-if=\"headerIsWorthIt()\"\n\t\t\tclass=\"cn-widget-wrapper__header\"\n\t\t\t:class=\"{ 'cn-widget-wrapper__header--actions-only': !titleVisible }\"\n\t\t\t:style=\"[headerStyles, titleIconStyle]\">\n\t\t\t<!-- Title icon — left: rendered before the title group. Moved\n\t\t\t INSIDE header-left so the header's `space-between` cannot pull\n\t\t\t it away from the title it belongs to; header-left's `gap` is\n\t\t\t what now separates icon from title (they used to touch). -->\n\t\t\t<div v-if=\"titleVisible\" class=\"cn-widget-wrapper__header-left\">\n\t\t\t\t<div v-if=\"$slots['title-icon'] && titleIconPosition === 'left'\"\n\t\t\t\t\tclass=\"cn-widget-wrapper__title-icon\">\n\t\t\t\t\t<slot name=\"title-icon\" />\n\t\t\t\t</div>\n\t\t\t\t<img\n\t\t\t\t\tv-if=\"iconUrl\"\n\t\t\t\t\t:src=\"iconUrl\"\n\t\t\t\t\t:alt=\"resolvedTitle\"\n\t\t\t\t\tclass=\"cn-widget-wrapper__icon\">\n\t\t\t\t<span\n\t\t\t\t\tv-else-if=\"iconClass\"\n\t\t\t\t\t:class=\"iconClass\"\n\t\t\t\t\tclass=\"cn-widget-wrapper__icon\" />\n\t\t\t\t<h3 :id=\"titleId\" class=\"cn-widget-wrapper__title\">\n\t\t\t\t\t{{ resolvedTitle }}\n\t\t\t\t</h3>\n\t\t\t\t<!-- @slot title-meta Rendered inside the header's left group,\n\t\t\t\t after the title and before the spacer. Use for small\n\t\t\t\t contextual chips (e.g. a date-range badge on chart\n\t\t\t\t widgets). Kept inside header-left so the chip sits next\n\t\t\t\t to the title rather than floating right next to the\n\t\t\t\t actions menu. -->\n\t\t\t\t<div v-if=\"$slots['title-meta']\" class=\"cn-widget-wrapper__title-meta\">\n\t\t\t\t\t<slot name=\"title-meta\" />\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<!-- `showActions` gates the overflow MENU, not the slot beside it.\n\t\t\t It used to gate both, so a caller who switched the menu off\n\t\t\t also lost the Save button for an inline edit, silently. -->\n\t\t\t<div v-if=\"actionsAreVisible()\" class=\"cn-widget-wrapper__actions\">\n\t\t\t\t<!-- One text link in the header (`headerLink`): \"All deadlines\",\n\t\t\t\t \"To the board\". A route is a router link, an href a plain\n\t\t\t\t anchor. Sits before the slot and the overflow menu. -->\n\t\t\t\t<component\n\t\t\t\t\t:is=\"headerLinkTag\"\n\t\t\t\t\tv-if=\"headerLinkTag\"\n\t\t\t\t\tclass=\"cn-widget-wrapper__header-link\"\n\t\t\t\t\tdata-testid=\"cn-widget-wrapper-header-link\"\n\t\t\t\t\tv-bind=\"headerLinkAttrs\">\n\t\t\t\t\t{{ headerLinkLabel }}\n\t\t\t\t</component>\n\t\t\t\t<!-- @slot actions Custom action buttons rendered before the\n\t\t\t\t built-in overflow menu. -->\n\t\t\t\t<slot name=\"actions\" />\n\t\t\t\t<CnActionsMenu\n\t\t\t\t\tv-if=\"showActions\"\n\t\t\t\t\t:showRefresh=\"effectiveShowRefresh\"\n\t\t\t\t\t:showRequestFeature=\"effectiveShowRequestFeature\"\n\t\t\t\t\t:showReportBug=\"showReportBug\"\n\t\t\t\t\t:showDocumentation=\"showDocumentation\"\n\t\t\t\t\t:documentationUrl=\"documentationUrl\"\n\t\t\t\t\t:docsAnchor=\"docsAnchor\"\n\t\t\t\t\t:reportBugUrl=\"reportBugUrl\"\n\t\t\t\t\t:documentationLabel=\"documentationLabel\"\n\t\t\t\t\t:refreshLabel=\"refreshLabel\"\n\t\t\t\t\t:requestFeatureLabel=\"requestFeatureLabel\"\n\t\t\t\t\t:actionsMenuLabel=\"actionsMenuLabel\"\n\t\t\t\t\t:refreshing=\"refreshing\"\n\t\t\t\t\t:widgetId=\"resolvedWidgetId\"\n\t\t\t\t\t:title=\"displayTitle\"\n\t\t\t\t\t:surface=\"`widget:${resolvedWidgetId}`\"\n\t\t\t\t\t:specRef=\"specRef\"\n\t\t\t\t\trefreshChannel=\"cn:widget:refresh\"\n\t\t\t\t\ttestidBase=\"cn-widget-wrapper\"\n\t\t\t\t\t@refresh=\"onActionsRefresh\"\n\t\t\t\t\t@requestFeature=\"onActionsRequestFeature\">\n\t\t\t\t\t<!-- @slot action-items Additional NcActionButton-family\n\t\t\t\t\t items rendered inside the overflow menu, after the\n\t\t\t\t\t built-in Refresh / Documentation / Request-a-feature\n\t\t\t\t\t group. -->\n\t\t\t\t\t<template v-if=\"hasActionItemsSlot\" #action-items>\n\t\t\t\t\t\t<slot name=\"action-items\" />\n\t\t\t\t\t</template>\n\t\t\t\t</CnActionsMenu>\n\t\t\t</div>\n\t\t\t<!-- Title icon — right: rendered after actions, far right -->\n\t\t\t<div v-if=\"$slots['title-icon'] && titleIconPosition === 'right'\"\n\t\t\t\tclass=\"cn-widget-wrapper__title-icon\">\n\t\t\t\t<slot name=\"title-icon\" />\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- Headerless meta (e.g. a KPI date chip on a flush card). When the\n\t\t header is hidden but a `title-meta` chip is provided, float it in the\n\t\t top-right corner over the content instead of dropping it — so a\n\t\t compact flush KPI tile can carry a date-range chip without growing a\n\t\t full header bar. -->\n\t\t<div\n\t\t\tv-if=\"!titleVisible && $slots['title-meta']\"\n\t\t\tclass=\"cn-widget-wrapper__floating-meta\">\n\t\t\t<slot name=\"title-meta\" />\n\t\t</div>\n\n\t\t<!-- Content -->\n\t\t<!--\n\t\t The content area is `overflow: auto`, so it can become a scrollable\n\t\t region. WCAG 2.1.1 (Keyboard) requires scrollable regions to be\n\t\t keyboard-focusable so keyboard-only users can scroll them, and 4.1.2\n\t\t requires an accessible name. We expose it as a labelled region and\n\t\t give it tabindex=\"0\". When the header (and its title) is hidden we\n\t\t fall back to an explicit aria-label from the widget title so the\n\t\t region is never anonymous.\n\t\t-->\n\t\t<div\n\t\t\tclass=\"cn-widget-wrapper__content\"\n\t\t\ttabindex=\"0\"\n\t\t\trole=\"region\"\n\t\t\t:aria-labelledby=\"titleVisible ? titleId : null\"\n\t\t\t:aria-label=\"titleVisible ? null : resolvedTitle\">\n\t\t\t<slot />\n\t\t</div>\n\n\t\t<!-- Footer -->\n\t\t<div v-if=\"$slots.footer || (buttons && buttons.length > 0)\" class=\"cn-widget-wrapper__footer\">\n\t\t\t<slot name=\"footer\">\n\t\t\t\t<a\n\t\t\t\t\tv-for=\"button in buttons\"\n\t\t\t\t\t:key=\"button.link\"\n\t\t\t\t\t:href=\"button.link\"\n\t\t\t\t\tclass=\"cn-widget-wrapper__footer-link\">\n\t\t\t\t\t{{ button.text }}\n\t\t\t\t</a>\n\t\t\t</slot>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { slotRenders } from '../../utils/slotContent.js'\nimport { CnActionsMenu } from '../CnActionsMenu/index.js'\n\n/**\n * CnWidgetWrapper — Widget container with header, content, and footer.\n *\n * ```vue\n * <CnWidgetWrapper title=\"My Cases\" :icon-url=\"casesIconUrl\">\n * <MyCasesChart :data=\"chartData\" />\n * </CnWidgetWrapper>\n * ```\n *\n * With NC widget object\n * ```vue\n * <CnWidgetWrapper\n * :title=\"widget.title\"\n * :icon-url=\"widget.iconUrl\"\n * :icon-class=\"widget.iconClass\"\n * :documentation-url=\"widget.documentationUrl\"\n * :buttons=\"widget.buttons\">\n * <CnWidgetRenderer :widget=\"widget\" />\n * </CnWidgetWrapper>\n * ```\n */\n/**\n * Semantic variant → CSS colour token for the widget header icon. Tokens\n * only — the nldesign app themes by overriding the Nextcloud variables, so a\n * literal hex here would ignore government theming entirely (ADR-062 / NL\n * Design System).\n *\n * @type {Record<string, string>}\n */\nconst TITLE_ICON_VARIANT_COLORS = {\n\tprimary: 'var(--color-primary-element)',\n\tsuccess: 'var(--color-success)',\n\twarning: 'var(--color-warning)',\n\terror: 'var(--color-error)',\n\tinfo: 'var(--color-info, var(--color-primary-element))',\n\tneutral: 'var(--color-text-maxcontrast)',\n}\n\nexport default {\n\tname: 'CnWidgetWrapper',\n\n\tcomponents: {\n\t\tCnActionsMenu,\n\t},\n\n\tinject: {\n\t\t/**\n\t\t * Host translate function provided by CnAppRoot as\n\t\t * `cnTranslate: this.translate` (bound to the host app's id). The\n\t\t * manifest-authored widget title is run through it so the card\n\t\t * heading follows the user's language. Defaults to an identity\n\t\t * function so an untranslated key renders as itself.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/** Widget title */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Widget'),\n\t\t},\n\n\t\t/** Whether to show the header with title */\n\t\tshowTitle: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Chrome variant for the wrapper card.\n\t\t * - `'default'` — the library's own card chrome (opaque background,\n\t\t * 1px border, compact header).\n\t\t * - `'nc-dashboard'` — matches the native Nextcloud Dashboard panel\n\t\t * exactly using the same design tokens: translucent blurred\n\t\t * background (`--color-main-background-blur` + `--filter-background-blur`),\n\t\t * `--border-radius-container-large` corners, no border/shadow, a 16px\n\t\t * header with a 20px/700 title and 32px leading icon, and content\n\t\t * inset 16px on the sides + bottom. `styleConfig` overrides still\n\t\t * layer on top so a user can customise any token.\n\t\t *\n\t\t * @type {'default'|'nc-dashboard'}\n\t\t */\n\t\tchrome: {\n\t\t\ttype: String,\n\t\t\tdefault: 'default',\n\t\t\tvalidator: (v) => ['default', 'nc-dashboard'].includes(v),\n\t\t},\n\n\t\t/**\n\t\t * Remove border and background — makes the wrapper transparent.\n\t\t * Useful for widgets that are self-contained cards (e.g. CnStatsBlock).\n\t\t */\n\t\tborderless: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Remove content padding — allows content to go edge-to-edge.\n\t\t * Useful for list-style widgets where items should span the full width.\n\t\t */\n\t\tflush: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Draw NO card of this widget's own: no border, no background, no\n\t\t * content padding, no title row and no header divider. For a surface\n\t\t * that already supplies the card and the heading — a tab panel being\n\t\t * the case that matters, where a nested card repeats both.\n\t\t *\n\t\t * The wrapper ELEMENT still renders. That is the contract, not an\n\t\t * oversight: `CnObjectDataWidget` measures its overflow against\n\t\t * `.cn-widget-wrapper__content` (`closest()` plus a ResizeObserver on\n\t\t * that node), `src/css/table.css` gives a table inside it its\n\t\t * edge-to-edge treatment, and `src/css/detail-page.css` and\n\t\t * `dashboard.css` size their content areas through it. Dropping the\n\t\t * node would take all of that out silently — `closest()` returning\n\t\t * null reads as \"nothing overflows\", never as an error.\n\t\t *\n\t\t * Controls in the `actions` slot are NOT chrome and keep their header:\n\t\t * the Save button that commits an inline edit lives there, and a panel\n\t\t * that hides it is silently unsaveable.\n\t\t *\n\t\t * `chromeless` is what an integration provider already asks for through\n\t\t * `CnDetailWidgetHost` (`bareWidget`), so this is the same word for the\n\t\t * same thing on every path into the wrapper.\n\t\t */\n\t\tchromeless: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Icon URL (image) */\n\t\ticonUrl: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Icon CSS class (e.g., Nextcloud icon class) */\n\t\ticonClass: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Position of the title-icon slot in the header.\n\t\t * 'left' places it before the title; 'right' places it after the actions.\n\t\t */\n\t\ttitleIconPosition: {\n\t\t\ttype: String,\n\t\t\tdefault: 'right',\n\t\t\tvalidator: (v) => ['left', 'right'].includes(v),\n\t\t},\n\n\t\t/**\n\t\t * Explicit CSS colour for the header icon. Overrides `titleIconVariant`.\n\t\t * Must be a CSS custom property or a theme token — never a literal hex\n\t\t * (NL Design System: the nldesign app re-themes by overriding the\n\t\t * Nextcloud variables, and a hardcoded colour ignores that).\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\ttitleIconColor: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Semantic colour for the header icon. Every widget's icon is\n\t\t * coloured — `primary` (the theme colour) is the default, and a\n\t\t * widget whose subject already carries a semantic meaning names it\n\t\t * here so the icon says so at a glance (a Depublished list is\n\t\t * `error`, a Concepts list `warning`, a Published list `success`).\n\t\t *\n\t\t * @type {'primary'|'success'|'warning'|'error'|'info'|'neutral'}\n\t\t */\n\t\ttitleIconVariant: {\n\t\t\ttype: String,\n\t\t\tdefault: 'primary',\n\t\t\tvalidator: (v) => ['primary', 'success', 'warning', 'error', 'info', 'neutral'].includes(v),\n\t\t},\n\n\t\t/** Footer action buttons: [{ text, link }] */\n\t\tbuttons: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * One text link in the header's actions group, before the overflow\n\t\t * menu: `{ label, route?, params?, query?, href? }`. A `route` is a\n\t\t * named router link (needs a router), an `href` a plain anchor. The\n\t\t * label goes through the host translate function. Null (the\n\t\t * default) draws nothing. Manifest: `headerLink` on the widget\n\t\t * definition or its layout entry.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-widget-header-carries-a-text-link\n\t\t * @type {{label: string, route?: string, params?: object, query?: object, href?: string}|null}\n\t\t */\n\t\theaderLink: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Whether the header's overflow action menu (Refresh / Documentation /\n\t\t * Request-a-feature + any `#action-items`) renders. Shown by default;\n\t\t * set `false` for compact surfaces — e.g. a KPI tile whose only header\n\t\t * affordance is a date chip — to drop the menu and free header width.\n\t\t */\n\t\tshowActions: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Style configuration for the wrapper.\n\t\t *\n\t\t * @type {{ backgroundColor: string, borderStyle: string, borderWidth: number, borderColor: string, borderRadius: number, padding: { top: number, right: number, bottom: number, left: number } }}\n\t\t */\n\t\tstyleConfig: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Hide the built-in Refresh item from the overflow action menu.\n\t\t * The Refresh item is shown by default — set this when the widget\n\t\t * has no refreshable data source (e.g. a static tile). Alias for\n\t\t * the inverse `:show-refresh=\"false\"`; either form opts out.\n\t\t */\n\t\thideRefresh: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Hide the built-in Request-a-feature item from the overflow\n\t\t * action menu. Shown by default; set when the consuming app has\n\t\t * no public issue tracker to link out to. Alias for the inverse\n\t\t * `:show-request-feature=\"false\"`; either form opts out.\n\t\t */\n\t\thideRequestFeature: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Whether to show the built-in Refresh item. Tri-state:\n\t\t * - `true` / `false` — force the action on or off.\n\t\t * - `null` (the default) — **auto**: show the action only when a\n\t\t * parent has attached an `@refresh` listener (i.e. something will\n\t\t * actually handle the refresh). This keeps widgets that can't\n\t\t * refresh — e.g. a prop-driven `CnObjectDataWidget` — from showing\n\t\t * a dead button. Widgets that refresh themselves via the\n\t\t * `cn:widget:refresh` event bus (with no `@refresh` listener)\n\t\t * should set `:show-refresh=\"true\"` explicitly.\n\t\t *\n\t\t * `hideRefresh` (or `:show-refresh=\"false\"`) always wins.\n\t\t *\n\t\t * @type {boolean|null}\n\t\t */\n\t\tshowRefresh: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Inverse of `hideRequestFeature`. Defaults to `true` so the\n\t\t * action renders. Set `:show-request-feature=\"false\"` to hide it.\n\t\t * Either flag hides the action.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tshowRequestFeature: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Whether the built-in \"Report a bug\" item renders. On by default —\n\t\t * the trio Request a feature / Report a bug / Documentation is the\n\t\t * contract for every widget; this exists for the rare surface that\n\t\t * must suppress one deliberately.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tshowReportBug: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Whether the built-in \"Documentation\" item renders. On by default,\n\t\t * for the same reason as `showReportBug`. The item's target is\n\t\t * resolved by the shared menu (see `docsAnchor`), so leaving it on\n\t\t * costs the host nothing.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tshowDocumentation: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Explicit documentation link for this widget, opened in a new tab.\n\t\t * Usually unnecessary: leave it empty and set `docsAnchor` instead,\n\t\t * so the link is built from the app-wide documentation base and\n\t\t * lands on this widget's own section.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tdocumentationUrl: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Optional pre-translated label for the Documentation action.\n\t\t * Defaults to the lib's translation of \"Documentation\".\n\t\t */\n\t\tdocumentationLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Documentation'),\n\t\t},\n\n\t\t/**\n\t\t * Widget id for the built-in default Refresh / Request-a-feature\n\t\t * handlers (B2). Forwarded as the `surface: \"widget:<id>\"` value\n\t\t * on the Actions menu (the English headline fallback for its forge\n\t\t * deep-links) AND as the `widgetId` field on the\n\t\t * `cn:widget:refresh` event-bus payload. When unset, the wrapper\n\t\t * falls back to a slugified `displayTitle`, which works but is\n\t\t * less stable than an explicit id.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\twidgetId: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * This widget's own section in the app's documentation, appended to\n\t\t * the app-wide documentation base URL (provided by CnAppRoot) to build\n\t\t * the Actions menu's Documentation deep-link. A bare slug becomes a\n\t\t * `#fragment`. Supply it per widget type — without it the item still\n\t\t * renders, but lands on the docs homepage rather than this widget.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tdocsAnchor: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Explicit \"Report a bug\" target for the Actions menu. Empty (the\n\t\t * default) builds a new-issue deep-link on the app's own forge.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\treportBugUrl: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Declares that this widget wants a high contrast treatment. The flag\n\t\t * reaches the theme as `data-cn-high-contrast` and a `--high-contrast`\n\t\t * class; the component picks no colour of its own (ADR-003). A theme\n\t\t * that says nothing about the flag renders the widget unchanged.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\thighContrast: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Optional `specRef` slug, forwarded to the Actions menu. Accepted\n\t\t * for backward compatibility with the removed in-product suggestion\n\t\t * modal; the forge issue form asks for its own context.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tspecRef: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Whether a refresh is currently in flight. When bound by the host\n\t\t * (e.g. `:refreshing=\"loading\"` around its refetch), the Refresh\n\t\t * item is disabled and shows a loading spinner for exactly as long\n\t\t * as this stays true — so the spinner reflects the real refresh time.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\trefreshing: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Optional pre-translated label for the Refresh action. Defaults\n\t\t * to the lib's translation of \"Refresh\" so callers usually don't\n\t\t * need to set this.\n\t\t */\n\t\trefreshLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Refresh'),\n\t\t},\n\n\t\t/**\n\t\t * Optional pre-translated label for the Request-a-feature action.\n\t\t * Defaults to the lib's translation of \"Request a feature\".\n\t\t */\n\t\trequestFeatureLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Request a feature'),\n\t\t},\n\n\t\t/**\n\t\t * Pre-translated aria-label / tooltip for the overflow menu\n\t\t * trigger. Defaults to \"Actions\".\n\t\t */\n\t\tactionsMenuLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Actions'),\n\t\t},\n\n\t\t/**\n\t\t * Translate function. Falls back to the injected `cnTranslate`,\n\t\t * which itself defaults to an identity function.\n\t\t *\n\t\t * @type {((key: string) => string)|null}\n\t\t */\n\t\ttranslate: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\temits: ['refresh', 'request-feature'],\n\n\tcomputed: {\n\t\tdisplayTitle() {\n\t\t\treturn this.title || 'Widget'\n\t\t},\n\n\t\t/**\n\t\t * The header's icon colour, published as ONE custom property on the\n\t\t * header rather than an inline `color` repeated on each icon element.\n\t\t * Everything under the header — the icon-class span, the `title-icon`\n\t\t * slot's content, an SVG inside it — inherits from that single\n\t\t * declaration, so the icon and any decoration around it cannot end up\n\t\t * different colours.\n\t\t *\n\t\t * `titleIconColor` wins when the host set one; otherwise the\n\t\t * `titleIconVariant` token does, which is why EVERY widget icon is\n\t\t * coloured and not just the ones an app remembered to configure. Every\n\t\t * value is a CSS variable, so the nldesign app's overrides re-theme\n\t\t * them for free (NL Design System).\n\t\t *\n\t\t * @return {object} A style object declaring `--cn-widget-icon-color`.\n\t\t */\n\t\ttitleIconStyle() {\n\t\t\treturn {\n\t\t\t\t'--cn-widget-icon-color': this.titleIconColor\n\t\t\t\t\t|| TITLE_ICON_VARIANT_COLORS[this.titleIconVariant]\n\t\t\t\t\t|| TITLE_ICON_VARIANT_COLORS.primary,\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Effective translate function: the explicit `translate` prop when\n\t\t * given, else the injected `cnTranslate` (identity by default).\n\t\t *\n\t\t * @return {(key: string) => string}\n\t\t */\n\t\teffectiveTranslate() {\n\t\t\treturn this.translate ?? this.cnTranslate\n\t\t},\n\n\t\t/**\n\t\t * The header link's tag: `router-link` for a route (when a router is\n\t\t * mounted), `a` for an href, '' when there is no usable link.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-widget-header-carries-a-text-link\n\t\t * @return {string}\n\t\t */\n\t\theaderLinkTag() {\n\t\t\tconst link = this.headerLink\n\t\t\tif (!link || typeof link.label !== 'string' || link.label === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tif (link.route && this.$router) {\n\t\t\t\treturn 'router-link'\n\t\t\t}\n\t\t\tif (typeof link.href === 'string' && link.href !== '') {\n\t\t\t\treturn 'a'\n\t\t\t}\n\t\t\treturn ''\n\t\t},\n\n\t\t/**\n\t\t * The header link's attributes for its tag.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-widget-header-carries-a-text-link\n\t\t * @return {object}\n\t\t */\n\t\theaderLinkAttrs() {\n\t\t\tconst link = this.headerLink || {}\n\t\t\tif (this.headerLinkTag === 'router-link') {\n\t\t\t\tconst to = { name: link.route }\n\t\t\t\tif (link.params && typeof link.params === 'object') {\n\t\t\t\t\tto.params = link.params\n\t\t\t\t}\n\t\t\t\tif (link.query && typeof link.query === 'object') {\n\t\t\t\t\tto.query = link.query\n\t\t\t\t}\n\t\t\t\treturn { to }\n\t\t\t}\n\t\t\tif (this.headerLinkTag === 'a') {\n\t\t\t\treturn { href: link.href }\n\t\t\t}\n\t\t\treturn {}\n\t\t},\n\n\t\t/**\n\t\t * The header link's text, through the host translate function.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-widget-header-carries-a-text-link\n\t\t * @return {string}\n\t\t */\n\t\theaderLinkLabel() {\n\t\t\tconst label = this.headerLink && this.headerLink.label\n\t\t\treturn label ? this.effectiveTranslate(label) : ''\n\t\t},\n\n\t\t/**\n\t\t * The RENDERED widget title — `displayTitle` run through the host\n\t\t * translate function. Kept separate from `displayTitle` on purpose:\n\t\t * `resolvedWidgetId` slugifies the raw title, so identifiers (DOM\n\t\t * ids, refresh channels, action payloads) stay locale-independent\n\t\t * while only the visible/AT-facing text localises.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedTitle() {\n\t\t\treturn this.title ? this.effectiveTranslate(this.title) : this.displayTitle\n\t\t},\n\n\t\t/**\n\t\t * Stable DOM id for the header title `<h3>`, used as the\n\t\t * `aria-labelledby` target of the scrollable content region so the\n\t\t * region gets an accessible name from the widget title (WCAG 4.1.2).\n\t\t * Derived from `resolvedWidgetId` so it is stable and unique per\n\t\t * widget instance on the page.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\ttitleId() {\n\t\t\treturn `cn-widget-wrapper-title-${this.resolvedWidgetId}`\n\t\t},\n\n\t\t/**\n\t\t * Effective Refresh visibility. `hideRefresh: true` (or\n\t\t * `:show-refresh=\"false\"`) always hides it. When `showRefresh` is\n\t\t * left unset (`null`), auto-detect: show the action only when a\n\t\t * parent attached an `@refresh` listener — otherwise the refresh\n\t\t * would do nothing. `hideRefresh` remains a back-compat alias.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\teffectiveShowRefresh() {\n\t\t\tif (this.hideRefresh) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif (this.showRefresh !== null) {\n\t\t\t\treturn this.showRefresh\n\t\t\t}\n\t\t\t// `$.vnode.props`, not `$attrs`: `refresh` is a declared emit, and\n\t\t\t// Vue keeps declared emits out of `$attrs`.\n\t\t\treturn Boolean(this.$.vnode.props?.onRefresh)\n\t\t},\n\n\t\t/**\n\t\t * Effective Request-a-feature visibility — same OR-of-opt-outs\n\t\t * pattern as `effectiveShowRefresh`.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\teffectiveShowRequestFeature() {\n\t\t\treturn this.showRequestFeature && !this.hideRequestFeature\n\t\t},\n\n\t\t/**\n\t\t * Stable id used in event-bus payloads and the\n\t\t * `surface: \"widget:<id>\"` field on the auto-mounted feature\n\t\t * modal. Prefers the explicit `widgetId` prop; falls back to a\n\t\t * slugified `displayTitle` so widgets without an explicit id\n\t\t * still get a usable identifier.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedWidgetId() {\n\t\t\tif (this.widgetId) {\n\t\t\t\treturn this.widgetId\n\t\t\t}\n\t\t\treturn this.displayTitle.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')\n\t\t},\n\n\t\t/**\n\t\t * Whether the caller supplied an `action-items` slot. Forwarded\n\t\t * conditionally so the shared CnActionsMenu doesn't treat an\n\t\t * always-present pass-through template as content (which would\n\t\t * keep the overflow menu visible even when every built-in is\n\t\t * opted out).\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\thasActionItemsSlot() {\n\t\t\treturn Boolean(this.$slots['action-items']) || Boolean(this.$slots && this.$slots['action-items'])\n\t\t},\n\n\t\t/**\n\t\t * Whether the caller put controls in the `actions` slot.\n\t\t *\n\t\t * Gates the header together with `showTitle`, so a surface that wants no\n\t\t * title still keeps controls that have nowhere else to go — the Save\n\t\t * button `CnObjectDataWidget` renders there being the case that matters.\n\t\t *\n\t\t * Deliberately NOT `showActions`: that prop defaults to true and governs\n\t\t * the overflow menu, so reading it here would give a header to every\n\t\t * headerless KPI tile that never had one.\n\t\t *\n\t\t * @return {boolean} true when the slot is filled.\n\t\t */\n\t\thasActionsSlot() {\n\t\t\treturn Boolean(this.$slots.actions)\n\t\t},\n\n\t\t/**\n\t\t * Whether the card outline is drawn. `chromeless` implies `borderless`\n\t\t * so a caller names the intent once instead of remembering the three\n\t\t * props it decomposes into.\n\t\t *\n\t\t * @return {boolean} true when border and background are suppressed.\n\t\t */\n\t\tnoBorder() {\n\t\t\treturn this.borderless || this.chromeless\n\t\t},\n\n\t\t/**\n\t\t * Whether the content area goes edge to edge. `chromeless` implies\n\t\t * `flush` for the same reason as `noBorder`.\n\t\t *\n\t\t * @return {boolean} true when content padding is suppressed.\n\t\t */\n\t\tnoPadding() {\n\t\t\treturn this.flush || this.chromeless\n\t\t},\n\n\t\t/**\n\t\t * Whether the title row renders. `chromeless` overrides `showTitle`,\n\t\t * which defaults to true: a surface that already names the widget must\n\t\t * not have to also remember to turn the title off.\n\t\t *\n\t\t * Read everywhere the template used to read `showTitle` directly,\n\t\t * including the two ARIA bindings on the content region — a hidden\n\t\t * title cannot be the region's `aria-labelledby` target.\n\t\t *\n\t\t * @return {boolean} true when the header's title group renders.\n\t\t */\n\t\ttitleVisible() {\n\t\t\treturn this.showTitle && !this.chromeless\n\t\t},\n\n\t\twrapperStyles() {\n\t\t\tconst styles = {}\n\n\t\t\tif (this.styleConfig.backgroundColor) {\n\t\t\t\tstyles.backgroundColor = this.styleConfig.backgroundColor\n\t\t\t}\n\n\t\t\tif (this.styleConfig.borderStyle && this.styleConfig.borderStyle !== 'none') {\n\t\t\t\tstyles.border = `${this.styleConfig.borderWidth || 1}px ${this.styleConfig.borderStyle} ${this.styleConfig.borderColor || 'var(--color-border)'}`\n\t\t\t}\n\n\t\t\tif (this.styleConfig.borderRadius !== undefined) {\n\t\t\t\tstyles.borderRadius = `${this.styleConfig.borderRadius}px`\n\t\t\t}\n\n\t\t\tif (this.styleConfig.padding) {\n\t\t\t\tconst p = this.styleConfig.padding\n\t\t\t\tstyles.padding = `${p.top || 0}px ${p.right || 0}px ${p.bottom || 0}px ${p.left || 0}px`\n\t\t\t}\n\n\t\t\treturn styles\n\t\t},\n\n\t\t/**\n\t\t * Inline styles for the header bar, derived from\n\t\t * `styleConfig.headerStyle.{backgroundColor, textColor}`. Lets a host\n\t\t * give individual widgets a custom header colour without a per-app CSS\n\t\t * workaround. Empty object when no header style is configured.\n\t\t *\n\t\t * @return {object} the header style bindings.\n\t\t */\n\t\theaderStyles() {\n\t\t\tconst hs = this.styleConfig.headerStyle\n\t\t\tif (!hs || typeof hs !== 'object') {\n\t\t\t\treturn {}\n\t\t\t}\n\t\t\tconst styles = {}\n\t\t\tif (hs.backgroundColor) {\n\t\t\t\tstyles.backgroundColor = hs.backgroundColor\n\t\t\t}\n\t\t\tif (hs.textColor) {\n\t\t\t\tstyles.color = hs.textColor\n\t\t\t}\n\t\t\treturn styles\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Whether the `actions` slot renders anything a reader can see.\n\t\t *\n\t\t * `CnObjectDataWidget` always PROVIDES the template and fills it only\n\t\t * while an edit is unsaved, so the slot has to be called and its vnodes\n\t\t * inspected. Reading `$slots.actions` as a boolean counts the template\n\t\t * itself as content, which is what gave a titleless tab panel a 59px\n\t\t * band holding nothing but a second Actions menu.\n\t\t *\n\t\t * A method rather than a computed on purpose. The answer changes when the\n\t\t * PARENT's state changes, and a computed reading `$slots` does not\n\t\t * re-track that; a method re-runs on every render, and a slot whose\n\t\t * content depends on parent state already forces this component to\n\t\t * re-render.\n\t\t *\n\t\t * @return {boolean} True when the slot yields visible content.\n\t\t */\n\t\tactionsSlotRenders() {\n\t\t\treturn slotRenders(this.$slots.actions)\n\t\t},\n\n\t\t/**\n\t\t * Whether the header band is worth the vertical space it costs.\n\t\t *\n\t\t * A VISIBLE title always earns it, which `chromeless` rules out. Otherwise\n\t\t * it is earned by controls that are actually on screen: content in the\n\t\t * `actions` slot, or the overflow menu when a caller both asked for it and\n\t\t * provided the slot it sits beside.\n\t\t *\n\t\t * That last clause looks redundant and is not. `showActions` defaults to\n\t\t * TRUE, so reading it alone hands a header to every headerless KPI tile\n\t\t * in the fleet, which is what the floating title-meta test caught when\n\t\t * this was first written the short way.\n\t\t *\n\t\t * @return {boolean} True when the header should render.\n\t\t */\n\t\theaderIsWorthIt() {\n\t\t\tif (this.titleVisible) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tif (this.actionsSlotRenders()) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn this.showActions && this.hasActionsSlot\n\t\t},\n\n\t\t/**\n\t\t * Whether the actions group holds anything.\n\t\t *\n\t\t * The overflow menu and the slot beside it are separate conditions now.\n\t\t * `showActions` used to gate both, so a caller who only wanted the menu\n\t\t * gone also lost the button that commits an inline edit.\n\t\t *\n\t\t * @return {boolean} True when the group would hold a visible control.\n\t\t */\n\t\tactionsAreVisible() {\n\t\t\treturn this.showActions || this.actionsSlotRenders() || Boolean(this.headerLinkTag)\n\t\t},\n\n\t\t/**\n\t\t * Re-emit the shared CnActionsMenu `@refresh` to the host, passing\n\t\t * the synthetic event through unchanged so a host listener can\n\t\t * still `preventDefault()` the built-in default (event-bus emit on\n\t\t * `cn:widget:refresh`).\n\t\t *\n\t\t * @param {{ widgetId: string, title: string }} payload Action payload.\n\t\t * @param {{ defaultPrevented: boolean, preventDefault: () => void }} ev Synthetic event.\n\t\t * @return {void}\n\t\t */\n\t\tonActionsRefresh(payload, ev) {\n\t\t\t/**\n\t\t\t * @event refresh User clicked the Refresh item in the overflow\n\t\t\t * action menu. Payload: `{ widgetId, title }`. Handlers may call\n\t\t\t * the second arg's `preventDefault()` to suppress the built-in\n\t\t\t * default (event-bus emit on `cn:widget:refresh`).\n\t\t\t * @type {{ widgetId: string, title: string }}\n\t\t\t */\n\t\t\tthis.$emit('refresh', payload, ev)\n\t\t},\n\n\t\t/**\n\t\t * Re-emit the shared CnActionsMenu `@request-feature` to the host,\n\t\t * passing the synthetic event through so a host listener can\n\t\t * `preventDefault()` the built-in default (opening the forge's\n\t\t * feature-request issue form).\n\t\t *\n\t\t * @param {{ widgetId: string, title: string }} payload Action payload.\n\t\t * @param {{ defaultPrevented: boolean, preventDefault: () => void }} ev Synthetic event.\n\t\t * @return {void}\n\t\t */\n\t\tonActionsRequestFeature(payload, ev) {\n\t\t\t/**\n\t\t\t * @event request-feature User clicked the Request a feature\n\t\t\t * item. Payload: `{ widgetId, title }`. Handlers may call the\n\t\t\t * second arg's `preventDefault()` to suppress the built-in\n\t\t\t * default (opening the forge's feature-request issue form).\n\t\t\t * @type {{ widgetId: string, title: string }}\n\t\t\t */\n\t\t\tthis.$emit('request-feature', payload, ev)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-widget-wrapper {\n\tposition: relative;\n\theight: 100%;\n\tdisplay: flex;\n\tflex-direction: column;\n\tbackground: var(--color-main-background);\n\tborder: 1px solid var(--color-border);\n\t/* NC design system: every content card is rounded — same token as\n\t CnDetailCard so detail pages read as ONE card family (ADR-062). */\n\tborder-radius: var(--border-radius-large, 8px);\n\toverflow: hidden;\n}\n\n/* Headerless date chip — floated over the top-right of a flush card. */\n.cn-widget-wrapper__floating-meta {\n\tposition: absolute;\n\ttop: 8px;\n\tinset-inline-end: 10px;\n\tz-index: 2;\n}\n\n.cn-widget-wrapper__content {\n\tflex: 1;\n\toverflow: auto;\n\tmin-height: 0;\n\tpadding: 16px;\n}\n\n/* A TABLE GOES EDGE TO EDGE. ALWAYS.\n A table drawn inside 16px of padding renders a border inside a border — two\n rectangles a few pixels apart, which reads as a mistake because it is one.\n The wrapper already has a `flush` prop that zeroes this padding, but a prop\n is something an app has to remember on every table it ever adds, and the\n apps did not: the padding was visible in pipelinq's table widgets.\n So it is structural instead. `:has()` keys on the table component's own root\n class, which means a table is flush by construction and no caller can\n reintroduce the gap by forgetting a prop. */\n.cn-widget-wrapper__content:has(> .cn-table-container),\n.cn-widget-wrapper__content:has(> .cn-widget-object-table) {\n\tpadding: 0;\n}\n\n/* Keyboard focus ring for the now-focusable scrollable content region. */\n.cn-widget-wrapper__content:focus-visible {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: -2px;\n}\n\n.cn-widget-wrapper--borderless {\n\tborder: none;\n\tbackground: transparent;\n}\n\n.cn-widget-wrapper--borderless .cn-widget-wrapper__content {\n\tpadding: 0;\n}\n\n.cn-widget-wrapper--flush .cn-widget-wrapper__content {\n\tpadding: 0;\n}\n\n/* A form or a search list keeps its padding, the reverse of the table rule\n above. A dashboard renders every widget `flush`, and without this their\n fields touch the card's edges. Borderless and chromeless cards, and the\n Nextcloud dashboard variant, keep their own padding. */\n.cn-widget-wrapper--flush:not(.cn-widget-wrapper--borderless, .cn-widget-wrapper--nc-dashboard) > .cn-widget-wrapper__content:has(> .cn-form-widget),\n.cn-widget-wrapper--flush:not(.cn-widget-wrapper--borderless, .cn-widget-wrapper--nc-dashboard) > .cn-widget-wrapper__content:has(> .cn-kb-search-widget) {\n\tpadding: 16px;\n}\n\n/* `chromeless` — border and background are already gone through borderless,\n and the content padding through flush. What is left on the root is the\n corner radius, which still clips: with `overflow: hidden` above, a child\n that paints its own background (a table header) gets rounded corners inside\n a square panel, which is the last visible piece of a card that is supposed\n to be absent. `CnDetailCard.cn-detail-card--chromeless` zeroes the same\n thing for the same reason. */\n.cn-widget-wrapper--chromeless {\n\tborder-radius: 0;\n}\n\n/*\n * `chrome=\"nc-dashboard\"` — reproduce the native Nextcloud Dashboard panel\n * (apps/dashboard) exactly, using the same design tokens so an un-customised\n * widget is pixel-identical to a core dashboard panel. `styleConfig` inline\n * overrides (wrapperStyles / headerStyles) still win over these class rules,\n * so users can override any token. Combined selector raises specificity above\n * the base `.cn-widget-wrapper` rules it supersedes.\n */\n.cn-widget-wrapper__header {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n\t/* The header is `space-between`, so without an explicit gap a right-hand\n\t title-icon sits flush against whatever precedes it. The left-hand icon's\n\t spacing comes from header-left's own gap. */\n\tgap: 8px;\n\tpadding: 12px 16px;\n\tborder-bottom: 1px solid var(--color-border);\n\tflex-shrink: 0;\n}\n\n/* Actions with no title beside them. `header-left` is not rendered at all in\n this mode, and the header is `space-between`, so a single child would sit\n flush LEFT where every other surface puts its actions on the right. */\n.cn-widget-wrapper__header--actions-only {\n\tjustify-content: flex-end;\n}\n\n/* `chromeless` — the surface around this widget already drew the card, so\n nothing here may draw a second one. Border, background and content padding\n are already gone (chromeless implies borderless + flush); what is left is\n the header, which still renders when there are controls to hold. Its bottom\n rule is the card's own divider and reads as a second card edge inside the\n panel, and its 16px side inset leaves those controls hanging off the grid\n that now starts at the panel's edge. Both go.\n\n Placed AFTER the rules it overrides, which is what stylelint's\n no-descending-specificity asks for. The cascade did not need it (this\n selector is more specific either way), a reader does: an override written\n above the thing it overrides has to be read twice. */\n.cn-widget-wrapper--chromeless .cn-widget-wrapper__header {\n\tborder-bottom: none;\n\tpadding-inline: 0;\n}\n\n.cn-widget-wrapper__header-left {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\tmin-width: 0;\n\t/* Grow so the title hugs the (optional) title-icon on the left instead\n\t of centering between icon and actions (header is space-between). */\n\tflex: 1 1 auto;\n}\n\n.cn-widget-wrapper__icon {\n\twidth: 24px;\n\theight: 24px;\n\tflex-shrink: 0;\n\tcolor: var(--cn-widget-icon-color, var(--color-primary-element));\n}\n\n.cn-widget-wrapper__title {\n\tfont-weight: 600;\n\tfont-size: 14px;\n\tmargin: 0;\n\twhite-space: nowrap;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n}\n\n.cn-widget-wrapper.cn-widget-wrapper--nc-dashboard {\n\tbackground: var(--color-main-background-blur, var(--color-main-background));\n\t-webkit-backdrop-filter: var(--filter-background-blur, none);\n\tbackdrop-filter: var(--filter-background-blur, none);\n\tborder: none;\n\tborder-radius: var(--border-radius-container-large, 16px);\n}\n\n.cn-widget-wrapper--nc-dashboard .cn-widget-wrapper__header {\n\tpadding: 16px;\n\tborder-bottom: none;\n}\n\n.cn-widget-wrapper--nc-dashboard .cn-widget-wrapper__header-left {\n\tgap: 16px;\n}\n\n.cn-widget-wrapper--nc-dashboard .cn-widget-wrapper__icon {\n\twidth: 32px;\n\theight: 32px;\n\tbackground-size: 32px;\n}\n\n.cn-widget-wrapper--nc-dashboard .cn-widget-wrapper__title {\n\tfont-size: 20px;\n\tfont-weight: 700;\n\tline-height: 24px;\n}\n\n.cn-widget-wrapper--nc-dashboard .cn-widget-wrapper__content {\n\tpadding: 0 16px 16px;\n}\n\n.cn-widget-wrapper__title-meta {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 4px;\n\tfont-size: 12px;\n\tcolor: var(--color-text-maxcontrast);\n\twhite-space: nowrap;\n\tflex-shrink: 0;\n}\n\n.cn-widget-wrapper__actions {\n\tdisplay: flex;\n\tgap: 4px;\n\tflex-shrink: 0;\n}\n\n/* The header's text link (`headerLink`). Theme hooks: --cn-widget-header-link-size\n and --cn-widget-header-link-weight. */\n.cn-widget-wrapper__header-link {\n\talign-self: center;\n\tpadding: 0 calc(1 * var(--default-grid-baseline));\n\tcolor: var(--color-primary-element);\n\tfont-size: var(--cn-widget-header-link-size, 14px);\n\tfont-weight: var(--cn-widget-header-link-weight, 600);\n\ttext-decoration: none;\n\twhite-space: nowrap;\n}\n\n.cn-widget-wrapper__header-link:hover,\n.cn-widget-wrapper__header-link:focus-visible {\n\ttext-decoration: underline;\n}\n\n.cn-widget-wrapper__header-link:focus-visible {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 2px;\n\tborder-radius: var(--border-radius-small, 4px);\n}\n\n/* Every widget's header icon is coloured — `--cn-widget-icon-color` is set on\n the header from titleIconStyle, and defaults to the theme colour if a\n wrapper is ever rendered without it. Material-design-icon components paint\n with `fill: currentColor`, so this reaches them; the :deep rule below makes\n it reach a raw <svg> in the slot too. */\n.cn-widget-wrapper__title-icon {\n\tdisplay: flex;\n\talign-items: center;\n\tflex-shrink: 0;\n\t/* COLOURED BY DEFAULT. `--cn-widget-icon-color` is set on the header from\n\t titleIconStyle; the theme colour is the fallback for a wrapper rendered\n\t without it. Before this, the prop defaulted to null and the icon inherited\n\t body text colour, which is why every widget header looked the same. */\n\tcolor: var(--cn-widget-icon-color, var(--color-primary-element));\n\n\t/* SPACE BETWEEN THE ICON AND THE TITLE.\n\t This element is a sibling of `__header-left` inside a space-between\n\t header, so it never received that element's `gap: 8px` — the icon sat\n\t flush against the title text. Matching the header-left gap rather than\n\t inventing a value keeps the two icon positions spaced identically. */\n\tmargin-inline-end: 8px;\n}\n\n/* Right-positioned title icons sit AFTER the actions, so the margin belongs on\n the other side or it pushes the icon off the header's right edge. */\n.cn-widget-wrapper__title-icon:last-child {\n\tmargin-inline-end: 0;\n\tmargin-inline-start: 8px;\n}\n\n/* Material-design-icon components paint with `fill: currentColor`, so the\n colour above reaches them; this makes it reach a raw <svg> in the slot too. */\n.cn-widget-wrapper__title-icon :deep(svg) {\n\tfill: currentcolor;\n}\n\n.cn-widget-wrapper__footer {\n\tdisplay: flex;\n\tjustify-content: flex-end;\n\tgap: 8px;\n\tpadding: 8px 16px;\n\tborder-top: 1px solid var(--color-border);\n\tflex-shrink: 0;\n}\n\n.cn-widget-wrapper__footer-link {\n\tfont-size: 13px;\n\tcolor: var(--color-primary-element);\n\ttext-decoration: none;\n}\n\n.cn-widget-wrapper__footer-link:hover {\n\ttext-decoration: underline;\n}\n</style>\n"],"names":["_normalizeClass","_normalizeStyle","_openBlock","_createElementBlock","_renderSlot","_createElementVNode","_resolveDynamicComponent","_createBlock"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CnWidgetWrapper.vue3.js","sources":["../../../../src/components/CnWidgetWrapper/CnWidgetWrapper.vue?vue&type=template&id=b19b1aa6&lang.js"],"sourcesContent":["<!--\n CnWidgetWrapper — Container shell around a dashboard widget.\n\n Provides header (icon + title), scrollable content area, and optional\n footer with action buttons. Applies style configuration for borders,\n backgrounds, and padding. The built-in overflow Actions menu (Refresh /\n Documentation / Request a feature) is delegated to the shared\n CnActionsMenu so widgets and page surfaces stay in lockstep.\n-->\n<template>\n\t<div\n\t\tclass=\"cn-widget-wrapper\"\n\t\t:class=\"{\n\t\t\t'cn-widget-wrapper--borderless': noBorder,\n\t\t\t'cn-widget-wrapper--flush': noPadding,\n\t\t\t'cn-widget-wrapper--chromeless': chromeless,\n\t\t\t'cn-widget-wrapper--nc-dashboard': chrome === 'nc-dashboard',\n\t\t\t'cn-widget-wrapper--high-contrast': highContrast,\n\t\t}\"\n\t\t:data-cn-high-contrast=\"highContrast ? 'true' : undefined\"\n\t\t:style=\"wrapperStyles\">\n\t\t<!-- Header.\n\t\t Rendered when a title earns it, or when controls that are actually\n\t\t on screen need a home. Three conditions rather than one, and each of\n\t\t the three is a bug this component has already shipped: gating on the\n\t\t title alone took the Save button for an inline edit away along with\n\t\t the doubled title; reading `showActions`, which defaults to true,\n\t\t handed a header to every headerless KPI tile; and reading the\n\t\t `actions` slot's mere PRESENCE gave a chromeless panel a 59px band\n\t\t holding nothing, because `CnObjectDataWidget` provides that template\n\t\t always and fills it only while an edit is unsaved.\n\t\t `headerIsWorthIt()` carries the reasoning. -->\n\t\t<div v-if=\"headerIsWorthIt()\"\n\t\t\tclass=\"cn-widget-wrapper__header\"\n\t\t\t:class=\"{ 'cn-widget-wrapper__header--actions-only': !titleVisible }\"\n\t\t\t:style=\"[headerStyles, titleIconStyle]\">\n\t\t\t<!-- Title icon — left: rendered before the title group. Moved\n\t\t\t INSIDE header-left so the header's `space-between` cannot pull\n\t\t\t it away from the title it belongs to; header-left's `gap` is\n\t\t\t what now separates icon from title (they used to touch). -->\n\t\t\t<div v-if=\"titleVisible\" class=\"cn-widget-wrapper__header-left\">\n\t\t\t\t<div v-if=\"$slots['title-icon'] && titleIconPosition === 'left'\"\n\t\t\t\t\tclass=\"cn-widget-wrapper__title-icon\">\n\t\t\t\t\t<slot name=\"title-icon\" />\n\t\t\t\t</div>\n\t\t\t\t<img\n\t\t\t\t\tv-if=\"iconUrl\"\n\t\t\t\t\t:src=\"iconUrl\"\n\t\t\t\t\t:alt=\"resolvedTitle\"\n\t\t\t\t\tclass=\"cn-widget-wrapper__icon\">\n\t\t\t\t<span\n\t\t\t\t\tv-else-if=\"iconClass\"\n\t\t\t\t\t:class=\"iconClass\"\n\t\t\t\t\tclass=\"cn-widget-wrapper__icon\" />\n\t\t\t\t<!-- An eyebrow (`eyebrow`) is a muted line above the title:\n\t\t\t\t \"Saldo per week\" above \"Prognose 13 weken\". -->\n\t\t\t\t<div v-if=\"resolvedEyebrow\" class=\"cn-widget-wrapper__title-stack\">\n\t\t\t\t\t<span class=\"cn-widget-wrapper__eyebrow\" data-testid=\"cn-widget-wrapper-eyebrow\">{{ resolvedEyebrow }}</span>\n\t\t\t\t\t<h3 :id=\"titleId\" class=\"cn-widget-wrapper__title\">\n\t\t\t\t\t\t{{ resolvedTitle }}\n\t\t\t\t\t</h3>\n\t\t\t\t</div>\n\t\t\t\t<h3 v-else :id=\"titleId\" class=\"cn-widget-wrapper__title\">\n\t\t\t\t\t{{ resolvedTitle }}\n\t\t\t\t</h3>\n\t\t\t\t<!-- @slot title-meta Rendered inside the header's left group,\n\t\t\t\t after the title and before the spacer. Use for small\n\t\t\t\t contextual chips (e.g. a date-range badge on chart\n\t\t\t\t widgets). Kept inside header-left so the chip sits next\n\t\t\t\t to the title rather than floating right next to the\n\t\t\t\t actions menu. -->\n\t\t\t\t<div v-if=\"$slots['title-meta']\" class=\"cn-widget-wrapper__title-meta\">\n\t\t\t\t\t<slot name=\"title-meta\" />\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<!-- `showActions` gates the overflow MENU, not the slot beside it.\n\t\t\t It used to gate both, so a caller who switched the menu off\n\t\t\t also lost the Save button for an inline edit, silently. -->\n\t\t\t<div v-if=\"actionsAreVisible()\" class=\"cn-widget-wrapper__actions\">\n\t\t\t\t<!-- One text link in the header (`headerLink`): \"All deadlines\",\n\t\t\t\t \"To the board\". A route is a router link, an href a plain\n\t\t\t\t anchor. Sits before the slot and the overflow menu. -->\n\t\t\t\t<component\n\t\t\t\t\t:is=\"headerLinkTag\"\n\t\t\t\t\tv-if=\"headerLinkTag\"\n\t\t\t\t\tclass=\"cn-widget-wrapper__header-link\"\n\t\t\t\t\tdata-testid=\"cn-widget-wrapper-header-link\"\n\t\t\t\t\tv-bind=\"headerLinkAttrs\">\n\t\t\t\t\t{{ headerLinkLabel }}\n\t\t\t\t</component>\n\t\t\t\t<!-- @slot actions Custom action buttons rendered before the\n\t\t\t\t built-in overflow menu. -->\n\t\t\t\t<slot name=\"actions\" />\n\t\t\t\t<CnActionsMenu\n\t\t\t\t\tv-if=\"showActions\"\n\t\t\t\t\t:showRefresh=\"effectiveShowRefresh\"\n\t\t\t\t\t:showRequestFeature=\"effectiveShowRequestFeature\"\n\t\t\t\t\t:showReportBug=\"showReportBug\"\n\t\t\t\t\t:showDocumentation=\"showDocumentation\"\n\t\t\t\t\t:documentationUrl=\"documentationUrl\"\n\t\t\t\t\t:docsAnchor=\"docsAnchor\"\n\t\t\t\t\t:reportBugUrl=\"reportBugUrl\"\n\t\t\t\t\t:documentationLabel=\"documentationLabel\"\n\t\t\t\t\t:refreshLabel=\"refreshLabel\"\n\t\t\t\t\t:requestFeatureLabel=\"requestFeatureLabel\"\n\t\t\t\t\t:actionsMenuLabel=\"actionsMenuLabel\"\n\t\t\t\t\t:refreshing=\"refreshing\"\n\t\t\t\t\t:widgetId=\"resolvedWidgetId\"\n\t\t\t\t\t:title=\"displayTitle\"\n\t\t\t\t\t:surface=\"`widget:${resolvedWidgetId}`\"\n\t\t\t\t\t:specRef=\"specRef\"\n\t\t\t\t\trefreshChannel=\"cn:widget:refresh\"\n\t\t\t\t\ttestidBase=\"cn-widget-wrapper\"\n\t\t\t\t\t@refresh=\"onActionsRefresh\"\n\t\t\t\t\t@requestFeature=\"onActionsRequestFeature\">\n\t\t\t\t\t<!-- @slot action-items Additional NcActionButton-family\n\t\t\t\t\t items rendered inside the overflow menu, after the\n\t\t\t\t\t built-in Refresh / Documentation / Request-a-feature\n\t\t\t\t\t group. -->\n\t\t\t\t\t<template v-if=\"hasActionItemsSlot\" #action-items>\n\t\t\t\t\t\t<slot name=\"action-items\" />\n\t\t\t\t\t</template>\n\t\t\t\t</CnActionsMenu>\n\t\t\t</div>\n\t\t\t<!-- Title icon — right: rendered after actions, far right -->\n\t\t\t<div v-if=\"$slots['title-icon'] && titleIconPosition === 'right'\"\n\t\t\t\tclass=\"cn-widget-wrapper__title-icon\">\n\t\t\t\t<slot name=\"title-icon\" />\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- Headerless meta (e.g. a KPI date chip on a flush card). When the\n\t\t header is hidden but a `title-meta` chip is provided, float it in the\n\t\t top-right corner over the content instead of dropping it — so a\n\t\t compact flush KPI tile can carry a date-range chip without growing a\n\t\t full header bar. -->\n\t\t<div\n\t\t\tv-if=\"!titleVisible && $slots['title-meta']\"\n\t\t\tclass=\"cn-widget-wrapper__floating-meta\">\n\t\t\t<slot name=\"title-meta\" />\n\t\t</div>\n\n\t\t<!-- Content -->\n\t\t<!--\n\t\t The content area is `overflow: auto`, so it can become a scrollable\n\t\t region. WCAG 2.1.1 (Keyboard) requires scrollable regions to be\n\t\t keyboard-focusable so keyboard-only users can scroll them, and 4.1.2\n\t\t requires an accessible name. We expose it as a labelled region and\n\t\t give it tabindex=\"0\". When the header (and its title) is hidden we\n\t\t fall back to an explicit aria-label from the widget title so the\n\t\t region is never anonymous.\n\t\t-->\n\t\t<div\n\t\t\tclass=\"cn-widget-wrapper__content\"\n\t\t\ttabindex=\"0\"\n\t\t\trole=\"region\"\n\t\t\t:aria-labelledby=\"titleVisible ? titleId : null\"\n\t\t\t:aria-label=\"titleVisible ? null : resolvedTitle\">\n\t\t\t<slot />\n\t\t</div>\n\n\t\t<!-- Footer -->\n\t\t<div v-if=\"$slots.footer || (buttons && buttons.length > 0)\" class=\"cn-widget-wrapper__footer\">\n\t\t\t<slot name=\"footer\">\n\t\t\t\t<a\n\t\t\t\t\tv-for=\"button in buttons\"\n\t\t\t\t\t:key=\"button.link\"\n\t\t\t\t\t:href=\"button.link\"\n\t\t\t\t\tclass=\"cn-widget-wrapper__footer-link\">\n\t\t\t\t\t{{ button.text }}\n\t\t\t\t</a>\n\t\t\t</slot>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { slotRenders } from '../../utils/slotContent.js'\nimport { CnActionsMenu } from '../CnActionsMenu/index.js'\n\n/**\n * CnWidgetWrapper — Widget container with header, content, and footer.\n *\n * ```vue\n * <CnWidgetWrapper title=\"My Cases\" :icon-url=\"casesIconUrl\">\n * <MyCasesChart :data=\"chartData\" />\n * </CnWidgetWrapper>\n * ```\n *\n * With NC widget object\n * ```vue\n * <CnWidgetWrapper\n * :title=\"widget.title\"\n * :icon-url=\"widget.iconUrl\"\n * :icon-class=\"widget.iconClass\"\n * :documentation-url=\"widget.documentationUrl\"\n * :buttons=\"widget.buttons\">\n * <CnWidgetRenderer :widget=\"widget\" />\n * </CnWidgetWrapper>\n * ```\n */\n/**\n * Semantic variant → CSS colour token for the widget header icon. Tokens\n * only — the nldesign app themes by overriding the Nextcloud variables, so a\n * literal hex here would ignore government theming entirely (ADR-062 / NL\n * Design System).\n *\n * @type {Record<string, string>}\n */\nconst TITLE_ICON_VARIANT_COLORS = {\n\tprimary: 'var(--color-primary-element)',\n\tsuccess: 'var(--color-success)',\n\twarning: 'var(--color-warning)',\n\terror: 'var(--color-error)',\n\tinfo: 'var(--color-info, var(--color-primary-element))',\n\tneutral: 'var(--color-text-maxcontrast)',\n}\n\nexport default {\n\tname: 'CnWidgetWrapper',\n\n\tcomponents: {\n\t\tCnActionsMenu,\n\t},\n\n\tinject: {\n\t\t/**\n\t\t * Host translate function provided by CnAppRoot as\n\t\t * `cnTranslate: this.translate` (bound to the host app's id). The\n\t\t * manifest-authored widget title is run through it so the card\n\t\t * heading follows the user's language. Defaults to an identity\n\t\t * function so an untranslated key renders as itself.\n\t\t */\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/** Widget title */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Widget'),\n\t\t},\n\n\t\t/**\n\t\t * A short muted line above the title (\"Saldo per week\" above\n\t\t * \"Prognose 13 weken\"), through the host translate. Empty draws none.\n\t\t */\n\t\teyebrow: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Whether to show the header with title */\n\t\tshowTitle: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Chrome variant for the wrapper card.\n\t\t * - `'default'` — the library's own card chrome (opaque background,\n\t\t * 1px border, compact header).\n\t\t * - `'nc-dashboard'` — matches the native Nextcloud Dashboard panel\n\t\t * exactly using the same design tokens: translucent blurred\n\t\t * background (`--color-main-background-blur` + `--filter-background-blur`),\n\t\t * `--border-radius-container-large` corners, no border/shadow, a 16px\n\t\t * header with a 20px/700 title and 32px leading icon, and content\n\t\t * inset 16px on the sides + bottom. `styleConfig` overrides still\n\t\t * layer on top so a user can customise any token.\n\t\t *\n\t\t * @type {'default'|'nc-dashboard'}\n\t\t */\n\t\tchrome: {\n\t\t\ttype: String,\n\t\t\tdefault: 'default',\n\t\t\tvalidator: (v) => ['default', 'nc-dashboard'].includes(v),\n\t\t},\n\n\t\t/**\n\t\t * Remove border and background — makes the wrapper transparent.\n\t\t * Useful for widgets that are self-contained cards (e.g. CnStatsBlock).\n\t\t */\n\t\tborderless: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Remove content padding — allows content to go edge-to-edge.\n\t\t * Useful for list-style widgets where items should span the full width.\n\t\t */\n\t\tflush: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Draw NO card of this widget's own: no border, no background, no\n\t\t * content padding, no title row and no header divider. For a surface\n\t\t * that already supplies the card and the heading — a tab panel being\n\t\t * the case that matters, where a nested card repeats both.\n\t\t *\n\t\t * The wrapper ELEMENT still renders. That is the contract, not an\n\t\t * oversight: `CnObjectDataWidget` measures its overflow against\n\t\t * `.cn-widget-wrapper__content` (`closest()` plus a ResizeObserver on\n\t\t * that node), `src/css/table.css` gives a table inside it its\n\t\t * edge-to-edge treatment, and `src/css/detail-page.css` and\n\t\t * `dashboard.css` size their content areas through it. Dropping the\n\t\t * node would take all of that out silently — `closest()` returning\n\t\t * null reads as \"nothing overflows\", never as an error.\n\t\t *\n\t\t * Controls in the `actions` slot are NOT chrome and keep their header:\n\t\t * the Save button that commits an inline edit lives there, and a panel\n\t\t * that hides it is silently unsaveable.\n\t\t *\n\t\t * `chromeless` is what an integration provider already asks for through\n\t\t * `CnDetailWidgetHost` (`bareWidget`), so this is the same word for the\n\t\t * same thing on every path into the wrapper.\n\t\t */\n\t\tchromeless: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Icon URL (image) */\n\t\ticonUrl: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Icon CSS class (e.g., Nextcloud icon class) */\n\t\ticonClass: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Position of the title-icon slot in the header.\n\t\t * 'left' places it before the title; 'right' places it after the actions.\n\t\t */\n\t\ttitleIconPosition: {\n\t\t\ttype: String,\n\t\t\tdefault: 'right',\n\t\t\tvalidator: (v) => ['left', 'right'].includes(v),\n\t\t},\n\n\t\t/**\n\t\t * Explicit CSS colour for the header icon. Overrides `titleIconVariant`.\n\t\t * Must be a CSS custom property or a theme token — never a literal hex\n\t\t * (NL Design System: the nldesign app re-themes by overriding the\n\t\t * Nextcloud variables, and a hardcoded colour ignores that).\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\ttitleIconColor: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Semantic colour for the header icon. Every widget's icon is\n\t\t * coloured — `primary` (the theme colour) is the default, and a\n\t\t * widget whose subject already carries a semantic meaning names it\n\t\t * here so the icon says so at a glance (a Depublished list is\n\t\t * `error`, a Concepts list `warning`, a Published list `success`).\n\t\t *\n\t\t * @type {'primary'|'success'|'warning'|'error'|'info'|'neutral'}\n\t\t */\n\t\ttitleIconVariant: {\n\t\t\ttype: String,\n\t\t\tdefault: 'primary',\n\t\t\tvalidator: (v) => ['primary', 'success', 'warning', 'error', 'info', 'neutral'].includes(v),\n\t\t},\n\n\t\t/** Footer action buttons: [{ text, link }] */\n\t\tbuttons: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * One text link in the header's actions group, before the overflow\n\t\t * menu: `{ label, route?, params?, query?, href? }`. A `route` is a\n\t\t * named router link (needs a router), an `href` a plain anchor. The\n\t\t * label goes through the host translate function. Null (the\n\t\t * default) draws nothing. Manifest: `headerLink` on the widget\n\t\t * definition or its layout entry.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-widget-header-carries-a-text-link\n\t\t * @type {{label: string, route?: string, params?: object, query?: object, href?: string}|null}\n\t\t */\n\t\theaderLink: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Whether the header's overflow action menu (Refresh / Documentation /\n\t\t * Request-a-feature + any `#action-items`) renders. Shown by default;\n\t\t * set `false` for compact surfaces — e.g. a KPI tile whose only header\n\t\t * affordance is a date chip — to drop the menu and free header width.\n\t\t */\n\t\tshowActions: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Style configuration for the wrapper.\n\t\t *\n\t\t * @type {{ backgroundColor: string, borderStyle: string, borderWidth: number, borderColor: string, borderRadius: number, padding: { top: number, right: number, bottom: number, left: number } }}\n\t\t */\n\t\tstyleConfig: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Hide the built-in Refresh item from the overflow action menu.\n\t\t * The Refresh item is shown by default — set this when the widget\n\t\t * has no refreshable data source (e.g. a static tile). Alias for\n\t\t * the inverse `:show-refresh=\"false\"`; either form opts out.\n\t\t */\n\t\thideRefresh: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Hide the built-in Request-a-feature item from the overflow\n\t\t * action menu. Shown by default; set when the consuming app has\n\t\t * no public issue tracker to link out to. Alias for the inverse\n\t\t * `:show-request-feature=\"false\"`; either form opts out.\n\t\t */\n\t\thideRequestFeature: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Whether to show the built-in Refresh item. Tri-state:\n\t\t * - `true` / `false` — force the action on or off.\n\t\t * - `null` (the default) — **auto**: show the action only when a\n\t\t * parent has attached an `@refresh` listener (i.e. something will\n\t\t * actually handle the refresh). This keeps widgets that can't\n\t\t * refresh — e.g. a prop-driven `CnObjectDataWidget` — from showing\n\t\t * a dead button. Widgets that refresh themselves via the\n\t\t * `cn:widget:refresh` event bus (with no `@refresh` listener)\n\t\t * should set `:show-refresh=\"true\"` explicitly.\n\t\t *\n\t\t * `hideRefresh` (or `:show-refresh=\"false\"`) always wins.\n\t\t *\n\t\t * @type {boolean|null}\n\t\t */\n\t\tshowRefresh: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Inverse of `hideRequestFeature`. Defaults to `true` so the\n\t\t * action renders. Set `:show-request-feature=\"false\"` to hide it.\n\t\t * Either flag hides the action.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tshowRequestFeature: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Whether the built-in \"Report a bug\" item renders. On by default —\n\t\t * the trio Request a feature / Report a bug / Documentation is the\n\t\t * contract for every widget; this exists for the rare surface that\n\t\t * must suppress one deliberately.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tshowReportBug: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Whether the built-in \"Documentation\" item renders. On by default,\n\t\t * for the same reason as `showReportBug`. The item's target is\n\t\t * resolved by the shared menu (see `docsAnchor`), so leaving it on\n\t\t * costs the host nothing.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\tshowDocumentation: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Explicit documentation link for this widget, opened in a new tab.\n\t\t * Usually unnecessary: leave it empty and set `docsAnchor` instead,\n\t\t * so the link is built from the app-wide documentation base and\n\t\t * lands on this widget's own section.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tdocumentationUrl: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Optional pre-translated label for the Documentation action.\n\t\t * Defaults to the lib's translation of \"Documentation\".\n\t\t */\n\t\tdocumentationLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Documentation'),\n\t\t},\n\n\t\t/**\n\t\t * Widget id for the built-in default Refresh / Request-a-feature\n\t\t * handlers (B2). Forwarded as the `surface: \"widget:<id>\"` value\n\t\t * on the Actions menu (the English headline fallback for its forge\n\t\t * deep-links) AND as the `widgetId` field on the\n\t\t * `cn:widget:refresh` event-bus payload. When unset, the wrapper\n\t\t * falls back to a slugified `displayTitle`, which works but is\n\t\t * less stable than an explicit id.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\twidgetId: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * This widget's own section in the app's documentation, appended to\n\t\t * the app-wide documentation base URL (provided by CnAppRoot) to build\n\t\t * the Actions menu's Documentation deep-link. A bare slug becomes a\n\t\t * `#fragment`. Supply it per widget type — without it the item still\n\t\t * renders, but lands on the docs homepage rather than this widget.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tdocsAnchor: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Explicit \"Report a bug\" target for the Actions menu. Empty (the\n\t\t * default) builds a new-issue deep-link on the app's own forge.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\treportBugUrl: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Declares that this widget wants a high contrast treatment. The flag\n\t\t * reaches the theme as `data-cn-high-contrast` and a `--high-contrast`\n\t\t * class; the component picks no colour of its own (ADR-003). A theme\n\t\t * that says nothing about the flag renders the widget unchanged.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\thighContrast: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Optional `specRef` slug, forwarded to the Actions menu. Accepted\n\t\t * for backward compatibility with the removed in-product suggestion\n\t\t * modal; the forge issue form asks for its own context.\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tspecRef: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Whether a refresh is currently in flight. When bound by the host\n\t\t * (e.g. `:refreshing=\"loading\"` around its refetch), the Refresh\n\t\t * item is disabled and shows a loading spinner for exactly as long\n\t\t * as this stays true — so the spinner reflects the real refresh time.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\trefreshing: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/**\n\t\t * Optional pre-translated label for the Refresh action. Defaults\n\t\t * to the lib's translation of \"Refresh\" so callers usually don't\n\t\t * need to set this.\n\t\t */\n\t\trefreshLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Refresh'),\n\t\t},\n\n\t\t/**\n\t\t * Optional pre-translated label for the Request-a-feature action.\n\t\t * Defaults to the lib's translation of \"Request a feature\".\n\t\t */\n\t\trequestFeatureLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Request a feature'),\n\t\t},\n\n\t\t/**\n\t\t * Pre-translated aria-label / tooltip for the overflow menu\n\t\t * trigger. Defaults to \"Actions\".\n\t\t */\n\t\tactionsMenuLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Actions'),\n\t\t},\n\n\t\t/**\n\t\t * Translate function. Falls back to the injected `cnTranslate`,\n\t\t * which itself defaults to an identity function.\n\t\t *\n\t\t * @type {((key: string) => string)|null}\n\t\t */\n\t\ttranslate: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\temits: ['refresh', 'request-feature'],\n\n\tcomputed: {\n\t\tdisplayTitle() {\n\t\t\treturn this.title || 'Widget'\n\t\t},\n\n\t\t/**\n\t\t * The header's icon colour, published as ONE custom property on the\n\t\t * header rather than an inline `color` repeated on each icon element.\n\t\t * Everything under the header — the icon-class span, the `title-icon`\n\t\t * slot's content, an SVG inside it — inherits from that single\n\t\t * declaration, so the icon and any decoration around it cannot end up\n\t\t * different colours.\n\t\t *\n\t\t * `titleIconColor` wins when the host set one; otherwise the\n\t\t * `titleIconVariant` token does, which is why EVERY widget icon is\n\t\t * coloured and not just the ones an app remembered to configure. Every\n\t\t * value is a CSS variable, so the nldesign app's overrides re-theme\n\t\t * them for free (NL Design System).\n\t\t *\n\t\t * @return {object} A style object declaring `--cn-widget-icon-color`.\n\t\t */\n\t\ttitleIconStyle() {\n\t\t\treturn {\n\t\t\t\t'--cn-widget-icon-color': this.titleIconColor\n\t\t\t\t\t|| TITLE_ICON_VARIANT_COLORS[this.titleIconVariant]\n\t\t\t\t\t|| TITLE_ICON_VARIANT_COLORS.primary,\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Effective translate function: the explicit `translate` prop when\n\t\t * given, else the injected `cnTranslate` (identity by default).\n\t\t *\n\t\t * @return {(key: string) => string}\n\t\t */\n\t\teffectiveTranslate() {\n\t\t\treturn this.translate ?? this.cnTranslate\n\t\t},\n\n\t\t/**\n\t\t * The header link's tag: `router-link` for a route (when a router is\n\t\t * mounted), `a` for an href, '' when there is no usable link.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-widget-header-carries-a-text-link\n\t\t * @return {string}\n\t\t */\n\t\theaderLinkTag() {\n\t\t\tconst link = this.headerLink\n\t\t\tif (!link || typeof link.label !== 'string' || link.label === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tif (link.route && this.$router) {\n\t\t\t\treturn 'router-link'\n\t\t\t}\n\t\t\tif (typeof link.href === 'string' && link.href !== '') {\n\t\t\t\treturn 'a'\n\t\t\t}\n\t\t\treturn ''\n\t\t},\n\n\t\t/**\n\t\t * The header link's attributes for its tag.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-widget-header-carries-a-text-link\n\t\t * @return {object}\n\t\t */\n\t\theaderLinkAttrs() {\n\t\t\tconst link = this.headerLink || {}\n\t\t\tif (this.headerLinkTag === 'router-link') {\n\t\t\t\tconst to = { name: link.route }\n\t\t\t\tif (link.params && typeof link.params === 'object') {\n\t\t\t\t\tto.params = link.params\n\t\t\t\t}\n\t\t\t\tif (link.query && typeof link.query === 'object') {\n\t\t\t\t\tto.query = link.query\n\t\t\t\t}\n\t\t\t\treturn { to }\n\t\t\t}\n\t\t\tif (this.headerLinkTag === 'a') {\n\t\t\t\treturn { href: link.href }\n\t\t\t}\n\t\t\treturn {}\n\t\t},\n\n\t\t/**\n\t\t * The header link's text, through the host translate function.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-widget-header-carries-a-text-link\n\t\t * @return {string}\n\t\t */\n\t\theaderLinkLabel() {\n\t\t\tconst label = this.headerLink && this.headerLink.label\n\t\t\treturn label ? this.effectiveTranslate(label) : ''\n\t\t},\n\n\t\t/**\n\t\t * The eyebrow through the host translate, or ''.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/widget-title-eyebrow/specs/dashboard-page/spec.md#requirement-a-widget-card-can-carry-a-line-above-its-title\n\t\t */\n\t\tresolvedEyebrow() {\n\t\t\treturn this.eyebrow ? this.effectiveTranslate(this.eyebrow) : ''\n\t\t},\n\n\t\t/**\n\t\t * The RENDERED widget title — `displayTitle` run through the host\n\t\t * translate function. Kept separate from `displayTitle` on purpose:\n\t\t * `resolvedWidgetId` slugifies the raw title, so identifiers (DOM\n\t\t * ids, refresh channels, action payloads) stay locale-independent\n\t\t * while only the visible/AT-facing text localises.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedTitle() {\n\t\t\treturn this.title ? this.effectiveTranslate(this.title) : this.displayTitle\n\t\t},\n\n\t\t/**\n\t\t * Stable DOM id for the header title `<h3>`, used as the\n\t\t * `aria-labelledby` target of the scrollable content region so the\n\t\t * region gets an accessible name from the widget title (WCAG 4.1.2).\n\t\t * Derived from `resolvedWidgetId` so it is stable and unique per\n\t\t * widget instance on the page.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\ttitleId() {\n\t\t\treturn `cn-widget-wrapper-title-${this.resolvedWidgetId}`\n\t\t},\n\n\t\t/**\n\t\t * Effective Refresh visibility. `hideRefresh: true` (or\n\t\t * `:show-refresh=\"false\"`) always hides it. When `showRefresh` is\n\t\t * left unset (`null`), auto-detect: show the action only when a\n\t\t * parent attached an `@refresh` listener — otherwise the refresh\n\t\t * would do nothing. `hideRefresh` remains a back-compat alias.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\teffectiveShowRefresh() {\n\t\t\tif (this.hideRefresh) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif (this.showRefresh !== null) {\n\t\t\t\treturn this.showRefresh\n\t\t\t}\n\t\t\t// `$.vnode.props`, not `$attrs`: `refresh` is a declared emit, and\n\t\t\t// Vue keeps declared emits out of `$attrs`.\n\t\t\treturn Boolean(this.$.vnode.props?.onRefresh)\n\t\t},\n\n\t\t/**\n\t\t * Effective Request-a-feature visibility — same OR-of-opt-outs\n\t\t * pattern as `effectiveShowRefresh`.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\teffectiveShowRequestFeature() {\n\t\t\treturn this.showRequestFeature && !this.hideRequestFeature\n\t\t},\n\n\t\t/**\n\t\t * Stable id used in event-bus payloads and the\n\t\t * `surface: \"widget:<id>\"` field on the auto-mounted feature\n\t\t * modal. Prefers the explicit `widgetId` prop; falls back to a\n\t\t * slugified `displayTitle` so widgets without an explicit id\n\t\t * still get a usable identifier.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tresolvedWidgetId() {\n\t\t\tif (this.widgetId) {\n\t\t\t\treturn this.widgetId\n\t\t\t}\n\t\t\treturn this.displayTitle.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')\n\t\t},\n\n\t\t/**\n\t\t * Whether the caller supplied an `action-items` slot. Forwarded\n\t\t * conditionally so the shared CnActionsMenu doesn't treat an\n\t\t * always-present pass-through template as content (which would\n\t\t * keep the overflow menu visible even when every built-in is\n\t\t * opted out).\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\thasActionItemsSlot() {\n\t\t\treturn Boolean(this.$slots['action-items']) || Boolean(this.$slots && this.$slots['action-items'])\n\t\t},\n\n\t\t/**\n\t\t * Whether the caller put controls in the `actions` slot.\n\t\t *\n\t\t * Gates the header together with `showTitle`, so a surface that wants no\n\t\t * title still keeps controls that have nowhere else to go — the Save\n\t\t * button `CnObjectDataWidget` renders there being the case that matters.\n\t\t *\n\t\t * Deliberately NOT `showActions`: that prop defaults to true and governs\n\t\t * the overflow menu, so reading it here would give a header to every\n\t\t * headerless KPI tile that never had one.\n\t\t *\n\t\t * @return {boolean} true when the slot is filled.\n\t\t */\n\t\thasActionsSlot() {\n\t\t\treturn Boolean(this.$slots.actions)\n\t\t},\n\n\t\t/**\n\t\t * Whether the card outline is drawn. `chromeless` implies `borderless`\n\t\t * so a caller names the intent once instead of remembering the three\n\t\t * props it decomposes into.\n\t\t *\n\t\t * @return {boolean} true when border and background are suppressed.\n\t\t */\n\t\tnoBorder() {\n\t\t\treturn this.borderless || this.chromeless\n\t\t},\n\n\t\t/**\n\t\t * Whether the content area goes edge to edge. `chromeless` implies\n\t\t * `flush` for the same reason as `noBorder`.\n\t\t *\n\t\t * @return {boolean} true when content padding is suppressed.\n\t\t */\n\t\tnoPadding() {\n\t\t\treturn this.flush || this.chromeless\n\t\t},\n\n\t\t/**\n\t\t * Whether the title row renders. `chromeless` overrides `showTitle`,\n\t\t * which defaults to true: a surface that already names the widget must\n\t\t * not have to also remember to turn the title off.\n\t\t *\n\t\t * Read everywhere the template used to read `showTitle` directly,\n\t\t * including the two ARIA bindings on the content region — a hidden\n\t\t * title cannot be the region's `aria-labelledby` target.\n\t\t *\n\t\t * @return {boolean} true when the header's title group renders.\n\t\t */\n\t\ttitleVisible() {\n\t\t\treturn this.showTitle && !this.chromeless\n\t\t},\n\n\t\twrapperStyles() {\n\t\t\tconst styles = {}\n\n\t\t\tif (this.styleConfig.backgroundColor) {\n\t\t\t\tstyles.backgroundColor = this.styleConfig.backgroundColor\n\t\t\t}\n\n\t\t\tif (this.styleConfig.borderStyle && this.styleConfig.borderStyle !== 'none') {\n\t\t\t\tstyles.border = `${this.styleConfig.borderWidth || 1}px ${this.styleConfig.borderStyle} ${this.styleConfig.borderColor || 'var(--color-border)'}`\n\t\t\t}\n\n\t\t\tif (this.styleConfig.borderRadius !== undefined) {\n\t\t\t\tstyles.borderRadius = `${this.styleConfig.borderRadius}px`\n\t\t\t}\n\n\t\t\tif (this.styleConfig.padding) {\n\t\t\t\tconst p = this.styleConfig.padding\n\t\t\t\tstyles.padding = `${p.top || 0}px ${p.right || 0}px ${p.bottom || 0}px ${p.left || 0}px`\n\t\t\t}\n\n\t\t\treturn styles\n\t\t},\n\n\t\t/**\n\t\t * Inline styles for the header bar, derived from\n\t\t * `styleConfig.headerStyle.{backgroundColor, textColor}`. Lets a host\n\t\t * give individual widgets a custom header colour without a per-app CSS\n\t\t * workaround. Empty object when no header style is configured.\n\t\t *\n\t\t * @return {object} the header style bindings.\n\t\t */\n\t\theaderStyles() {\n\t\t\tconst hs = this.styleConfig.headerStyle\n\t\t\tif (!hs || typeof hs !== 'object') {\n\t\t\t\treturn {}\n\t\t\t}\n\t\t\tconst styles = {}\n\t\t\tif (hs.backgroundColor) {\n\t\t\t\tstyles.backgroundColor = hs.backgroundColor\n\t\t\t}\n\t\t\tif (hs.textColor) {\n\t\t\t\tstyles.color = hs.textColor\n\t\t\t}\n\t\t\treturn styles\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Whether the `actions` slot renders anything a reader can see.\n\t\t *\n\t\t * `CnObjectDataWidget` always PROVIDES the template and fills it only\n\t\t * while an edit is unsaved, so the slot has to be called and its vnodes\n\t\t * inspected. Reading `$slots.actions` as a boolean counts the template\n\t\t * itself as content, which is what gave a titleless tab panel a 59px\n\t\t * band holding nothing but a second Actions menu.\n\t\t *\n\t\t * A method rather than a computed on purpose. The answer changes when the\n\t\t * PARENT's state changes, and a computed reading `$slots` does not\n\t\t * re-track that; a method re-runs on every render, and a slot whose\n\t\t * content depends on parent state already forces this component to\n\t\t * re-render.\n\t\t *\n\t\t * @return {boolean} True when the slot yields visible content.\n\t\t */\n\t\tactionsSlotRenders() {\n\t\t\treturn slotRenders(this.$slots.actions)\n\t\t},\n\n\t\t/**\n\t\t * Whether the header band is worth the vertical space it costs.\n\t\t *\n\t\t * A VISIBLE title always earns it, which `chromeless` rules out. Otherwise\n\t\t * it is earned by controls that are actually on screen: content in the\n\t\t * `actions` slot, or the overflow menu when a caller both asked for it and\n\t\t * provided the slot it sits beside.\n\t\t *\n\t\t * That last clause looks redundant and is not. `showActions` defaults to\n\t\t * TRUE, so reading it alone hands a header to every headerless KPI tile\n\t\t * in the fleet, which is what the floating title-meta test caught when\n\t\t * this was first written the short way.\n\t\t *\n\t\t * @return {boolean} True when the header should render.\n\t\t */\n\t\theaderIsWorthIt() {\n\t\t\tif (this.titleVisible) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tif (this.actionsSlotRenders()) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn this.showActions && this.hasActionsSlot\n\t\t},\n\n\t\t/**\n\t\t * Whether the actions group holds anything.\n\t\t *\n\t\t * The overflow menu and the slot beside it are separate conditions now.\n\t\t * `showActions` used to gate both, so a caller who only wanted the menu\n\t\t * gone also lost the button that commits an inline edit.\n\t\t *\n\t\t * @return {boolean} True when the group would hold a visible control.\n\t\t */\n\t\tactionsAreVisible() {\n\t\t\treturn this.showActions || this.actionsSlotRenders() || Boolean(this.headerLinkTag)\n\t\t},\n\n\t\t/**\n\t\t * Re-emit the shared CnActionsMenu `@refresh` to the host, passing\n\t\t * the synthetic event through unchanged so a host listener can\n\t\t * still `preventDefault()` the built-in default (event-bus emit on\n\t\t * `cn:widget:refresh`).\n\t\t *\n\t\t * @param {{ widgetId: string, title: string }} payload Action payload.\n\t\t * @param {{ defaultPrevented: boolean, preventDefault: () => void }} ev Synthetic event.\n\t\t * @return {void}\n\t\t */\n\t\tonActionsRefresh(payload, ev) {\n\t\t\t/**\n\t\t\t * @event refresh User clicked the Refresh item in the overflow\n\t\t\t * action menu. Payload: `{ widgetId, title }`. Handlers may call\n\t\t\t * the second arg's `preventDefault()` to suppress the built-in\n\t\t\t * default (event-bus emit on `cn:widget:refresh`).\n\t\t\t * @type {{ widgetId: string, title: string }}\n\t\t\t */\n\t\t\tthis.$emit('refresh', payload, ev)\n\t\t},\n\n\t\t/**\n\t\t * Re-emit the shared CnActionsMenu `@request-feature` to the host,\n\t\t * passing the synthetic event through so a host listener can\n\t\t * `preventDefault()` the built-in default (opening the forge's\n\t\t * feature-request issue form).\n\t\t *\n\t\t * @param {{ widgetId: string, title: string }} payload Action payload.\n\t\t * @param {{ defaultPrevented: boolean, preventDefault: () => void }} ev Synthetic event.\n\t\t * @return {void}\n\t\t */\n\t\tonActionsRequestFeature(payload, ev) {\n\t\t\t/**\n\t\t\t * @event request-feature User clicked the Request a feature\n\t\t\t * item. Payload: `{ widgetId, title }`. Handlers may call the\n\t\t\t * second arg's `preventDefault()` to suppress the built-in\n\t\t\t * default (opening the forge's feature-request issue form).\n\t\t\t * @type {{ widgetId: string, title: string }}\n\t\t\t */\n\t\t\tthis.$emit('request-feature', payload, ev)\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-widget-wrapper {\n\tposition: relative;\n\theight: 100%;\n\tdisplay: flex;\n\tflex-direction: column;\n\tbackground: var(--color-main-background);\n\tborder: 1px solid var(--color-border);\n\t/* NC design system: every content card is rounded — same token as\n\t CnDetailCard so detail pages read as ONE card family (ADR-062). */\n\tborder-radius: var(--border-radius-large, 8px);\n\toverflow: hidden;\n}\n\n/* Headerless date chip — floated over the top-right of a flush card. */\n.cn-widget-wrapper__floating-meta {\n\tposition: absolute;\n\ttop: 8px;\n\tinset-inline-end: 10px;\n\tz-index: 2;\n}\n\n.cn-widget-wrapper__content {\n\tflex: 1;\n\toverflow: auto;\n\tmin-height: 0;\n\tpadding: 16px;\n}\n\n/* A TABLE GOES EDGE TO EDGE. ALWAYS.\n A table drawn inside 16px of padding renders a border inside a border — two\n rectangles a few pixels apart, which reads as a mistake because it is one.\n The wrapper already has a `flush` prop that zeroes this padding, but a prop\n is something an app has to remember on every table it ever adds, and the\n apps did not: the padding was visible in pipelinq's table widgets.\n So it is structural instead. `:has()` keys on the table component's own root\n class, which means a table is flush by construction and no caller can\n reintroduce the gap by forgetting a prop. */\n.cn-widget-wrapper__content:has(> .cn-table-container),\n.cn-widget-wrapper__content:has(> .cn-widget-object-table) {\n\tpadding: 0;\n}\n\n/* Keyboard focus ring for the now-focusable scrollable content region. */\n.cn-widget-wrapper__content:focus-visible {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: -2px;\n}\n\n.cn-widget-wrapper--borderless {\n\tborder: none;\n\tbackground: transparent;\n}\n\n.cn-widget-wrapper--borderless .cn-widget-wrapper__content {\n\tpadding: 0;\n}\n\n.cn-widget-wrapper--flush .cn-widget-wrapper__content {\n\tpadding: 0;\n}\n\n/* A form or a search list keeps its padding, the reverse of the table rule\n above. A dashboard renders every widget `flush`, and without this their\n fields touch the card's edges. Borderless and chromeless cards, and the\n Nextcloud dashboard variant, keep their own padding. */\n.cn-widget-wrapper--flush:not(.cn-widget-wrapper--borderless, .cn-widget-wrapper--nc-dashboard) > .cn-widget-wrapper__content:has(> .cn-form-widget),\n.cn-widget-wrapper--flush:not(.cn-widget-wrapper--borderless, .cn-widget-wrapper--nc-dashboard) > .cn-widget-wrapper__content:has(> .cn-kb-search-widget) {\n\tpadding: 16px;\n}\n\n/* `chromeless` — border and background are already gone through borderless,\n and the content padding through flush. What is left on the root is the\n corner radius, which still clips: with `overflow: hidden` above, a child\n that paints its own background (a table header) gets rounded corners inside\n a square panel, which is the last visible piece of a card that is supposed\n to be absent. `CnDetailCard.cn-detail-card--chromeless` zeroes the same\n thing for the same reason. */\n.cn-widget-wrapper--chromeless {\n\tborder-radius: 0;\n}\n\n/*\n * `chrome=\"nc-dashboard\"` — reproduce the native Nextcloud Dashboard panel\n * (apps/dashboard) exactly, using the same design tokens so an un-customised\n * widget is pixel-identical to a core dashboard panel. `styleConfig` inline\n * overrides (wrapperStyles / headerStyles) still win over these class rules,\n * so users can override any token. Combined selector raises specificity above\n * the base `.cn-widget-wrapper` rules it supersedes.\n */\n.cn-widget-wrapper__header {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n\t/* The header is `space-between`, so without an explicit gap a right-hand\n\t title-icon sits flush against whatever precedes it. The left-hand icon's\n\t spacing comes from header-left's own gap. */\n\tgap: 8px;\n\tpadding: 12px 16px;\n\tborder-bottom: 1px solid var(--color-border);\n\tflex-shrink: 0;\n}\n\n/* Actions with no title beside them. `header-left` is not rendered at all in\n this mode, and the header is `space-between`, so a single child would sit\n flush LEFT where every other surface puts its actions on the right. */\n.cn-widget-wrapper__header--actions-only {\n\tjustify-content: flex-end;\n}\n\n/* `chromeless` — the surface around this widget already drew the card, so\n nothing here may draw a second one. Border, background and content padding\n are already gone (chromeless implies borderless + flush); what is left is\n the header, which still renders when there are controls to hold. Its bottom\n rule is the card's own divider and reads as a second card edge inside the\n panel, and its 16px side inset leaves those controls hanging off the grid\n that now starts at the panel's edge. Both go.\n\n Placed AFTER the rules it overrides, which is what stylelint's\n no-descending-specificity asks for. The cascade did not need it (this\n selector is more specific either way), a reader does: an override written\n above the thing it overrides has to be read twice. */\n.cn-widget-wrapper--chromeless .cn-widget-wrapper__header {\n\tborder-bottom: none;\n\tpadding-inline: 0;\n}\n\n.cn-widget-wrapper__header-left {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 8px;\n\tmin-width: 0;\n\t/* Grow so the title hugs the (optional) title-icon on the left instead\n\t of centering between icon and actions (header is space-between). */\n\tflex: 1 1 auto;\n}\n\n.cn-widget-wrapper__icon {\n\twidth: 24px;\n\theight: 24px;\n\tflex-shrink: 0;\n\tcolor: var(--cn-widget-icon-color, var(--color-primary-element));\n}\n\n.cn-widget-wrapper__title-stack {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 2px;\n\tmin-width: 0;\n}\n\n.cn-widget-wrapper__eyebrow {\n\tfont-size: 13px;\n\tline-height: 1.3;\n\tcolor: var(--color-text-maxcontrast);\n\twhite-space: nowrap;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n}\n\n.cn-widget-wrapper__title {\n\tfont-weight: 600;\n\tfont-size: 14px;\n\tmargin: 0;\n\twhite-space: nowrap;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n}\n\n.cn-widget-wrapper.cn-widget-wrapper--nc-dashboard {\n\tbackground: var(--color-main-background-blur, var(--color-main-background));\n\t-webkit-backdrop-filter: var(--filter-background-blur, none);\n\tbackdrop-filter: var(--filter-background-blur, none);\n\tborder: none;\n\tborder-radius: var(--border-radius-container-large, 16px);\n}\n\n.cn-widget-wrapper--nc-dashboard .cn-widget-wrapper__header {\n\tpadding: 16px;\n\tborder-bottom: none;\n}\n\n.cn-widget-wrapper--nc-dashboard .cn-widget-wrapper__header-left {\n\tgap: 16px;\n}\n\n.cn-widget-wrapper--nc-dashboard .cn-widget-wrapper__icon {\n\twidth: 32px;\n\theight: 32px;\n\tbackground-size: 32px;\n}\n\n.cn-widget-wrapper--nc-dashboard .cn-widget-wrapper__title {\n\tfont-size: 20px;\n\tfont-weight: 700;\n\tline-height: 24px;\n}\n\n.cn-widget-wrapper--nc-dashboard .cn-widget-wrapper__content {\n\tpadding: 0 16px 16px;\n}\n\n.cn-widget-wrapper__title-meta {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 4px;\n\tfont-size: 12px;\n\tcolor: var(--color-text-maxcontrast);\n\twhite-space: nowrap;\n\tflex-shrink: 0;\n}\n\n.cn-widget-wrapper__actions {\n\tdisplay: flex;\n\tgap: 4px;\n\tflex-shrink: 0;\n}\n\n/* The header's text link (`headerLink`). Theme hooks: --cn-widget-header-link-size\n and --cn-widget-header-link-weight. */\n.cn-widget-wrapper__header-link {\n\talign-self: center;\n\tpadding: 0 calc(1 * var(--default-grid-baseline));\n\tcolor: var(--color-primary-element);\n\tfont-size: var(--cn-widget-header-link-size, 14px);\n\tfont-weight: var(--cn-widget-header-link-weight, 600);\n\ttext-decoration: none;\n\twhite-space: nowrap;\n}\n\n.cn-widget-wrapper__header-link:hover,\n.cn-widget-wrapper__header-link:focus-visible {\n\ttext-decoration: underline;\n}\n\n.cn-widget-wrapper__header-link:focus-visible {\n\toutline: 2px solid var(--color-primary-element);\n\toutline-offset: 2px;\n\tborder-radius: var(--border-radius-small, 4px);\n}\n\n/* Every widget's header icon is coloured — `--cn-widget-icon-color` is set on\n the header from titleIconStyle, and defaults to the theme colour if a\n wrapper is ever rendered without it. Material-design-icon components paint\n with `fill: currentColor`, so this reaches them; the :deep rule below makes\n it reach a raw <svg> in the slot too. */\n.cn-widget-wrapper__title-icon {\n\tdisplay: flex;\n\talign-items: center;\n\tflex-shrink: 0;\n\t/* COLOURED BY DEFAULT. `--cn-widget-icon-color` is set on the header from\n\t titleIconStyle; the theme colour is the fallback for a wrapper rendered\n\t without it. Before this, the prop defaulted to null and the icon inherited\n\t body text colour, which is why every widget header looked the same. */\n\tcolor: var(--cn-widget-icon-color, var(--color-primary-element));\n\n\t/* SPACE BETWEEN THE ICON AND THE TITLE.\n\t This element is a sibling of `__header-left` inside a space-between\n\t header, so it never received that element's `gap: 8px` — the icon sat\n\t flush against the title text. Matching the header-left gap rather than\n\t inventing a value keeps the two icon positions spaced identically. */\n\tmargin-inline-end: 8px;\n}\n\n/* Right-positioned title icons sit AFTER the actions, so the margin belongs on\n the other side or it pushes the icon off the header's right edge. */\n.cn-widget-wrapper__title-icon:last-child {\n\tmargin-inline-end: 0;\n\tmargin-inline-start: 8px;\n}\n\n/* Material-design-icon components paint with `fill: currentColor`, so the\n colour above reaches them; this makes it reach a raw <svg> in the slot too. */\n.cn-widget-wrapper__title-icon :deep(svg) {\n\tfill: currentcolor;\n}\n\n.cn-widget-wrapper__footer {\n\tdisplay: flex;\n\tjustify-content: flex-end;\n\tgap: 8px;\n\tpadding: 8px 16px;\n\tborder-top: 1px solid var(--color-border);\n\tflex-shrink: 0;\n}\n\n.cn-widget-wrapper__footer-link {\n\tfont-size: 13px;\n\tcolor: var(--color-primary-element);\n\ttext-decoration: none;\n}\n\n.cn-widget-wrapper__footer-link:hover {\n\ttext-decoration: underline;\n}\n</style>\n"],"names":["_normalizeClass","_normalizeStyle","_openBlock","_createElementBlock","_renderSlot","_createElementVNode","_toDisplayString","_resolveDynamicComponent","_createBlock"],"mappings":"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"}
|
|
@@ -291,6 +291,26 @@ function useListView(objectTypeOrOptions, options) {
|
|
|
291
291
|
refresh(page);
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
+
/**
|
|
295
|
+
* Add the next page after the rows already loaded, for a view that shows
|
|
296
|
+
* every row at once (the board). Does nothing on the last page. A refresh
|
|
297
|
+
* started while the page is in flight drops it.
|
|
298
|
+
*
|
|
299
|
+
* @return {Promise<void>}
|
|
300
|
+
*/
|
|
301
|
+
async function loadMore() {
|
|
302
|
+
const current = pagination.value;
|
|
303
|
+
const next = Number(current.page || 1) + 1;
|
|
304
|
+
if (next > Number(current.pages || 1)) {
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
const seq = refreshSeq;
|
|
308
|
+
await objectStore.fetchCollection(getType(), buildParams(next), {
|
|
309
|
+
append: true,
|
|
310
|
+
isCurrent: () => seq === refreshSeq,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
294
314
|
/**
|
|
295
315
|
* Handle page-size change. Resets to page 1.
|
|
296
316
|
*
|
|
@@ -402,6 +422,7 @@ function useListView(objectTypeOrOptions, options) {
|
|
|
402
422
|
onPageSizeChange,
|
|
403
423
|
// Explicit fetch
|
|
404
424
|
refresh,
|
|
425
|
+
loadMore,
|
|
405
426
|
// The query the list sends (search, sort, facet filters, fixed filters),
|
|
406
427
|
// built from the current state, for an export that must follow the list.
|
|
407
428
|
buildParams,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useListView.js","sources":["../../../src/composables/useListView.js"],"sourcesContent":["import { computed, isRef, onBeforeUnmount, onMounted, ref, watch } from 'vue'\nimport { useObjectStore } from '../store/index.js'\nimport { facetFilterParams } from '../utils/facets.js'\n\n/**\n * Composable for managing list view state with full objectStore integration.\n *\n * When called with an `objectType` string, connects to the objectStore and handles\n * schema loading, collection fetching, sidebar wiring, and all event handlers\n * automatically. Everything a `CnIndexPage`-based list view needs is returned\n * directly — no additional computed properties or methods required in the component.\n *\n * Backward-compatible: existing `useListView(options)` and `useListView()` calls\n * continue to work without modification.\n *\n * @param {string|Function|object} [objectTypeOrOptions] Object type slug (new API), a getter or ref returning one (the list follows it when it changes), or a legacy options object\n * @param {object} [options] Options (new API only)\n * @param {object|null} [options.objectStore] Custom object store instance (from createObjectStore). When provided, uses this store instead of the default useObjectStore(). Required when the app uses createObjectStore with a custom store ID.\n * @param {object|null} [options.sidebarState] Sidebar state object from `inject('sidebarState')`. When provided, the composable wires and unwires the sidebar automatically on mount/unmount.\n * @param {number} [options.defaultPageSize] Default `_limit` sent to the API\n * @param {number} [options.debounceMs] Search debounce in milliseconds\n * @param {object} [options.defaultSort] Default sort applied on mount e.g. `{ key: 'createdAt', order: 'desc' }`\n * @param {Array<{key: string, order: 'asc'|'desc'}>} [options.defaultSortKeys] Default MULTI-column sort applied on mount (ordered priority list). Takes precedence over `defaultSort` when both are given; used e.g. to restore a sort persisted in the route query.\n * @param {Array<string>|null} [options.defaultVisibleColumns] Initial visible-column key set. Seeds the sidebar's Columns tab with the curated default (e.g. a manifest `columns` list) so toggles add/remove from it. Omit (or `null`) for schema-driven tables where every column starts visible.\n * @param {object} [options.defaultActiveFilters] Initial facet-filter map, e.g. to restore filters persisted in the route query on mount.\n * @param {string} [options.defaultSearchTerm] Initial search term, e.g. to restore `_search` from the route query on mount.\n * @param {object|(() => object)} [options.fixedFilters] A filter map (or getter returning one) merged into every fetch AFTER the user's facet filters, so the fixed entries always win. Used e.g. by `CnIndexPage` to apply a route-param-scoped `pages[].config.filter`. Default `{}` — omitting it is behaviourally identical to before.\n * @param {Array<string>|(() => Array<string>)} [options.extend] OpenRegister `_extend[]` values (or a getter returning them) forwarded on every fetch, e.g. `['calculations']` so a schema's `materialise: false` calculations are evaluated and can be shown as columns. Default `[]` — omitting it sends no `_extend` at all, exactly as before.\n * @return {object} Reactive state and event handlers\n *\n * @example\n * // New API — minimal\n * const { schema, objects, loading, pagination,\n * onSearch, onSort, onFilterChange, onPageChange, refresh } = useListView('client')\n *\n * @example\n * // New API — with sidebar wiring\n * const list = useListView('client', {\n * sidebarState: inject('sidebarState', null),\n * })\n *\n * @example\n * // Legacy API — still works\n * const { searchTerm, filters, onSearchInput, toggleSort } = useListView({\n * objectType: 'client',\n * fetchFn: (type, params) => objectStore.fetchCollection(type, params),\n * })\n */\nexport function useListView(objectTypeOrOptions, options) {\n\t// Backward compat: if first arg is an object or absent, delegate to legacy implementation\n\tif (!objectTypeOrOptions || (typeof objectTypeOrOptions === 'object' && !isRef(objectTypeOrOptions))) {\n\t\treturn useLegacyListView(objectTypeOrOptions || {})\n\t}\n\n\t// ── New API ──────────────────────────────────────────────────────────\n\t// A string, a ref or a getter: a getter lets the page switch object type\n\t// (a folder carrying its own schema) without re-creating the list.\n\tconst typeSource = objectTypeOrOptions\n\tconst isDynamicType = typeof typeSource !== 'string'\n\tconst getType = () => (isRef(typeSource) ? typeSource.value : (typeof typeSource === 'function' ? typeSource() : typeSource))\n\tconst opts = options || {}\n\tconst sidebarState = opts.sidebarState || null\n\n\tconst objectStore = opts.objectStore || useObjectStore()\n\n\t// ── State refs ───────────────────────────────────────────────────────\n\tconst schema = ref(null)\n\tconst searchTerm = ref(opts.defaultSearchTerm || '')\n\t// `sortKeys` is the source of truth (ordered multi-column priority list);\n\t// `sortKey`/`sortOrder` are kept as a primary-entry mirror so every\n\t// existing reader of the pre-multi-sort refs keeps working unchanged.\n\tconst initialSortKeys = Array.isArray(opts.defaultSortKeys) && opts.defaultSortKeys.length > 0\n\t\t? opts.defaultSortKeys\n\t\t: (opts.defaultSort?.key ? [{ key: opts.defaultSort.key, order: opts.defaultSort.order || 'asc' }] : [])\n\tconst sortKeys = ref(initialSortKeys)\n\tconst sortKey = ref(initialSortKeys[0]?.key || null)\n\tconst sortOrder = ref(initialSortKeys[0]?.order || 'asc')\n\tconst activeFilters = ref((opts.defaultActiveFilters && typeof opts.defaultActiveFilters === 'object') ? opts.defaultActiveFilters : {})\n\t// Seed the visible-column set to the configured columns so the sidebar's\n\t// Columns tab reflects the curated default and toggles add/remove from it.\n\t// `null` (no seed) means \"all columns visible\" (schema-driven tables).\n\tconst visibleColumns = ref(opts.defaultVisibleColumns ?? null)\n\tconst pageSize = ref(opts.defaultPageSize || 20)\n\n\t// ── Computed refs from the store ─────────────────────────────────────\n\tconst objects = computed(() => objectStore.collections[getType()] || [])\n\t// True from creation until the mount sequence below has issued its first\n\t// fetch. The store's own flag only goes up once `fetchCollection` runs, and\n\t// `onMounted` awaits `fetchSchema()` first — so for the length of that round\n\t// trip the list was \"not loading\" with zero rows, and every consumer\n\t// rendered its empty state (\"No log entries to show\") before any data had\n\t// been asked for. Folded into `loading` rather than exposed separately so\n\t// existing consumers get the fix without touching their templates.\n\tconst bootstrapping = ref(true)\n\tconst loading = computed(() => bootstrapping.value || objectStore.loading[getType()] || false)\n\tconst pagination = computed(() => objectStore.pagination[getType()] || { total: 0, page: 1, pages: 1, limit: 20 })\n\t// Facets are computed by the platform over the whole query rather than the\n\t// loaded page, so anything that needs the complete set of values for a\n\t// field (a folder pane grouping by it, say) reads them here rather than\n\t// deriving them from `objects`, which is only the current page.\n\tconst facets = computed(() => objectStore.facets[getType()] || {})\n\t// Personal-lens reports of the latest response (`{ recent: { available, reason } }`).\n\t// @spec openspec/changes/lens-says-why-it-is-empty/specs/personal-lens-availability/spec.md#requirement-a-list-response-carries-the-report-of-the-lenses-it-was-asked-for\n\tconst lenses = computed(() => (objectStore.lenses && objectStore.lenses[getType()]) || {})\n\t// Outcome of the latest `refresh()`: the store's error for it, or null once\n\t// it succeeded or a later fetch stored new rows. The store keeps the previous\n\t// rows on a failed fetch, so this is how a consumer tells stale rows from\n\t// current ones.\n\tconst error = ref(null)\n\tlet refreshSeq = 0\n\tlet pendingRefreshes = 0\n\n\tlet searchTimeout = null\n\n\t// ── Param construction ───────────────────────────────────────────────\n\n\t/**\n\t * Resolve `opts.fixedFilters` to a plain map. Accepts a plain object OR a\n\t * function/getter returning one (so callers can derive it from reactive\n\t * sources — e.g. route params — and have it re-read on every fetch).\n\t *\n\t * @return {object} The fixed-filter map (may be empty).\n\t */\n\tfunction resolveFixedFilters() {\n\t\tconst f = typeof opts.fixedFilters === 'function' ? opts.fixedFilters() : opts.fixedFilters\n\t\treturn (f && typeof f === 'object') ? f : {}\n\t}\n\n\t/**\n\t * Resolve `opts.extend` to a list of non-empty strings.\n\t *\n\t * Accepts an array or a getter returning one, so a caller can re-scope it\n\t * between fetches the way `fixedFilters` allows.\n\t *\n\t * @return {Array<string>} The `_extend[]` values, possibly empty.\n\t */\n\tfunction resolveExtend() {\n\t\tconst e = typeof opts.extend === 'function' ? opts.extend() : opts.extend\n\t\tif (!Array.isArray(e)) {\n\t\t\treturn []\n\t\t}\n\t\treturn e.filter((v) => typeof v === 'string' && v !== '')\n\t}\n\n\t/**\n\t * Build API fetch params from current reactive state.\n\t *\n\t * @param {number} page Page number to request\n\t * @return {object} Params object ready to pass to fetchCollection\n\t */\n\tfunction buildParams(page) {\n\t\tconst params = { _limit: pageSize.value, _page: page }\n\n\t\tif (searchTerm.value) {\n\t\t\tparams._search = searchTerm.value\n\t\t}\n\n\t\tif (sortKeys.value.length > 0) {\n\t\t\t// Object key insertion order = sort priority order, matching\n\t\t\t// OpenRegister's `_order` format exactly (`ObjectsController::\n\t\t\t// normalizeOrderParameter` / `MagicMapper`'s ordering loop). A\n\t\t\t// single active key produces byte-identical output to before.\n\t\t\tparams._order = Object.fromEntries(sortKeys.value.map((k) => [k.key, k.order]))\n\t\t}\n\n\t\tfor (const [key, values] of Object.entries(activeFilters.value)) {\n\t\t\t// Single-value arrays are unwrapped to scalar params; the\n\t\t\t// missing-value option becomes `<key>_isnull=true`.\n\t\t\tObject.assign(params, facetFilterParams(key, values))\n\t\t}\n\n\t\t// Fixed filters (e.g. a route-param-scoped `pages[].config.filter`) are\n\t\t// merged LAST so they always win over a colliding facet `activeFilter`.\n\t\t// Repeated `_extend[]`, serialized by the shared query builder. Set\n\t\t// before the fixed filters so an explicit `_extend` in a filter map\n\t\t// still wins, matching every other key's precedence here.\n\t\tconst extend = resolveExtend()\n\t\tif (extend.length > 0) {\n\t\t\tparams._extend = extend\n\t\t}\n\n\t\tfor (const [key, value] of Object.entries(resolveFixedFilters())) {\n\t\t\tif (value !== undefined && value !== null && value !== '') {\n\t\t\t\tparams[key] = value\n\t\t\t}\n\t\t}\n\n\t\treturn params\n\t}\n\n\t// ── Fetch ────────────────────────────────────────────────────────────\n\n\t/**\n\t * Fetch the collection using current state params and update sidebar facet data.\n\t * Only the most recently started refresh sets `error`, so an older response\n\t * settling late cannot override a newer one's outcome.\n\t *\n\t * @param {number} [page] Page to fetch\n\t * @return {Promise<void>}\n\t */\n\tasync function refresh(page = 1) {\n\t\tconst seq = ++refreshSeq\n\t\t// This call's own result. `errors[getType()]` is shared by every call\n\t\t// for the type and is not cleared on success, so it cannot say which\n\t\t// request failed; it is only the fallback for a store that ignores\n\t\t// `options.outcome`.\n\t\tconst outcome = {}\n\t\tpendingRefreshes++\n\t\ttry {\n\t\t\tawait objectStore.fetchCollection(getType(), buildParams(page), { outcome })\n\t\t} catch (e) {\n\t\t\tif (seq === refreshSeq) {\n\t\t\t\terror.value = e\n\t\t\t}\n\t\t\tthrow e\n\t\t} finally {\n\t\t\tpendingRefreshes--\n\t\t}\n\t\tif (seq === refreshSeq) {\n\t\t\terror.value = ('error' in outcome) ? outcome.error : (objectStore.errors?.[getType()] || null)\n\t\t}\n\t}\n\n\t// A fetch outside `refresh()` (e.g. the live-updates refetch) that stores\n\t// new rows recovers the list from an earlier failure. Rows written while\n\t// a refresh is pending are left to that refresh, which decides `error`.\n\twatch(\n\t\t() => objectStore.collections[getType()],\n\t\t() => {\n\t\t\tif (pendingRefreshes === 0) {\n\t\t\t\terror.value = null\n\t\t\t}\n\t\t},\n\t\t{ flush: 'sync' },\n\t)\n\n\t// ── Event handlers ───────────────────────────────────────────────────\n\n\t/**\n\t * Handle search input. Debounced by `options.debounceMs` (default 300 ms).\n\t *\n\t * @param {string} value New search string\n\t */\n\tfunction onSearch(value) {\n\t\tsearchTerm.value = value\n\t\tclearTimeout(searchTimeout)\n\t\tsearchTimeout = setTimeout(() => refresh(1), opts.debounceMs || 300)\n\t}\n\n\t/**\n\t * Handle sort change. Updates sort state and triggers refresh.\n\t *\n\t * Accepts CnDataTable's extended `sort` event payload: `keys` (the full\n\t * ordered multi-column list) is used when present; a legacy caller\n\t * passing only `{ key, order }` (no `keys`) still works exactly as\n\t * before — it's normalised to a single-entry `sortKeys` list.\n\t *\n\t * @param {{key: string|null, order: string|null, keys?: Array<{key: string, order: string}>}} sort New sort definition.\n\t */\n\tfunction onSort({ key, order, keys }) {\n\t\tconst nextKeys = Array.isArray(keys) ? keys : (key ? [{ key, order: order || 'asc' }] : [])\n\t\tsortKeys.value = nextKeys\n\t\tsortKey.value = nextKeys[0]?.key ?? null\n\t\tsortOrder.value = nextKeys[0]?.order ?? 'asc'\n\t\trefresh(1)\n\t}\n\n\t/**\n\t * Handle filter change for a single key. Empty arrays remove the key.\n\t *\n\t * @param {string} key Filter key (maps to API param name)\n\t * @param {Array} values Selected filter values\n\t */\n\tfunction onFilterChange(key, values) {\n\t\tif (!values || values.length === 0) {\n\t\t\tconst updated = { ...activeFilters.value }\n\t\t\tdelete updated[key]\n\t\t\tactiveFilters.value = updated\n\t\t} else {\n\t\t\tactiveFilters.value = { ...activeFilters.value, [key]: values }\n\t\t}\n\t\trefresh(1)\n\t}\n\n\t/**\n\t * Handle page navigation.\n\t *\n\t * @param {number} page Page number to navigate to\n\t */\n\tfunction onPageChange(page) {\n\t\trefresh(page)\n\t}\n\n\t/**\n\t * Handle page-size change. Resets to page 1.\n\t *\n\t * @param {number} size New page size\n\t */\n\tfunction onPageSizeChange(size) {\n\t\tpageSize.value = size\n\t\trefresh(1)\n\t}\n\n\t// ── Sidebar wiring ───────────────────────────────────────────────────\n\n\tfunction setupSidebar() {\n\t\tif (!sidebarState) {\n\t\t\treturn\n\t\t}\n\t\tsidebarState.active = true\n\t\tsidebarState.schema = schema.value\n\t\tsidebarState.searchValue = searchTerm.value\n\t\tsidebarState.activeFilters = {}\n\t\tsidebarState.onSearch = onSearch\n\t\tsidebarState.onColumnsChange = (cols) => {\n\t\t\tvisibleColumns.value = cols\n\t\t}\n\t\tsidebarState.onFilterChange = ({ key, values }) => onFilterChange(key, values)\n\t}\n\n\tfunction teardownSidebar() {\n\t\tif (!sidebarState) {\n\t\t\treturn\n\t\t}\n\t\tsidebarState.active = false\n\t\tsidebarState.schema = null\n\t\tsidebarState.activeFilters = {}\n\t\tsidebarState.facetData = {}\n\t\tsidebarState.onSearch = null\n\t\tsidebarState.onColumnsChange = null\n\t\tsidebarState.onFilterChange = null\n\t}\n\n\t// Push facet data to sidebar after each store update\n\tif (sidebarState) {\n\t\twatch(\n\t\t\t() => objectStore.facets[getType()],\n\t\t\t(facets) => {\n\t\t\t\tsidebarState.facetData = facets || {}\n\t\t\t},\n\t\t)\n\t}\n\n\t// A dynamic object type that changes starts that type's list from page 1\n\t// with its own schema.\n\tif (isDynamicType) {\n\t\twatch(getType, async (next, prev) => {\n\t\t\tif (!next || next === prev) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tschema.value = null\n\t\t\tschema.value = await objectStore.fetchSchema(next)\n\t\t\tawait refresh(1)\n\t\t})\n\t}\n\n\t// ── Lifecycle ────────────────────────────────────────────────────────\n\n\tonMounted(async () => {\n\t\ttry {\n\t\t\tschema.value = await objectStore.fetchSchema(getType())\n\t\t\tif (sidebarState) {\n\t\t\t\tsetupSidebar()\n\t\t\t}\n\t\t\tawait refresh(1)\n\t\t} finally {\n\t\t\t// Hand the loading state back to the store either way: a schema\n\t\t\t// fetch that rejects must not leave the list spinning forever.\n\t\t\tbootstrapping.value = false\n\t\t}\n\t})\n\n\tonBeforeUnmount(() => {\n\t\tclearTimeout(searchTimeout)\n\t\tteardownSidebar()\n\t})\n\n\t// ── Return value ─────────────────────────────────────────────────────\n\n\treturn {\n\t\t// Store-derived\n\t\tschema,\n\t\tobjects,\n\t\tloading,\n\t\tpagination,\n\t\tfacets,\n\t\tlenses,\n\t\terror,\n\t\t// Local state\n\t\tsearchTerm,\n\t\tsortKey,\n\t\tsortOrder,\n\t\tsortKeys,\n\t\tactiveFilters,\n\t\tvisibleColumns,\n\t\tpageSize,\n\t\t// Event handlers\n\t\tonSearch,\n\t\tonSort,\n\t\tonFilterChange,\n\t\tonPageChange,\n\t\tonPageSizeChange,\n\t\t// Explicit fetch\n\t\trefresh,\n\t\t// The query the list sends (search, sort, facet filters, fixed filters),\n\t\t// built from the current state, for an export that must follow the list.\n\t\tbuildParams,\n\t}\n}\n\n// ── Legacy implementation ─────────────────────────────────────────────────────\n\n/**\n * Legacy `useListView(options)` implementation.\n * Preserved verbatim for backward compatibility.\n *\n * @param {object} options Legacy options object\n * @param {string} [options.objectType] The registered object type slug\n * @param {(type: string, params: object) => Promise<Array<object>>} [options.fetchFn] Function to call: (type, params) => Promise<Array>\n * @param {number} [options.debounceMs] Search debounce in milliseconds\n * @param {number} [options.pageSize] Default page size\n * @param {object} [options.defaultSort] Default sort: { key: string, order: 'asc'|'desc' }\n * @return {object} Reactive state and methods\n */\nfunction useLegacyListView(options) {\n\tconst searchTerm = ref('')\n\tconst filters = ref({})\n\tconst sortKey = ref(options.defaultSort?.key || null)\n\tconst sortOrder = ref(options.defaultSort?.order || 'asc')\n\tconst currentPage = ref(1)\n\tconst pageSize = ref(options.pageSize || 20)\n\n\tlet searchTimeout = null\n\n\tfunction buildFetchParams() {\n\t\tconst params = {\n\t\t\t_limit: pageSize.value,\n\t\t\t_page: currentPage.value,\n\t\t}\n\n\t\tif (searchTerm.value) {\n\t\t\tparams._search = searchTerm.value\n\t\t}\n\n\t\tif (sortKey.value) {\n\t\t\tparams._order = { [sortKey.value]: sortOrder.value }\n\t\t}\n\n\t\tfor (const [key, value] of Object.entries(filters.value)) {\n\t\t\tif (value !== null && value !== '' && value !== false) {\n\t\t\t\tparams[key] = value\n\t\t\t}\n\t\t}\n\n\t\treturn params\n\t}\n\n\tasync function fetchData(page) {\n\t\tif (page !== undefined) {\n\t\t\tcurrentPage.value = page\n\t\t}\n\t\tconst params = buildFetchParams()\n\t\tif (options.fetchFn) {\n\t\t\treturn options.fetchFn(options.objectType, params)\n\t\t}\n\t}\n\n\tfunction onSearchInput(value) {\n\t\tsearchTerm.value = value\n\t\tclearTimeout(searchTimeout)\n\t\tsearchTimeout = setTimeout(() => fetchData(1), options.debounceMs || 300)\n\t}\n\n\tfunction toggleSort(key) {\n\t\tif (sortKey.value === key) {\n\t\t\tif (sortOrder.value === 'asc') {\n\t\t\t\tsortOrder.value = 'desc'\n\t\t\t} else {\n\t\t\t\tsortKey.value = null\n\t\t\t\tsortOrder.value = 'asc'\n\t\t\t}\n\t\t} else {\n\t\t\tsortKey.value = key\n\t\t\tsortOrder.value = 'asc'\n\t\t}\n\t\tfetchData(1)\n\t}\n\n\tfunction setFilter(key, value) {\n\t\tfilters.value = { ...filters.value, [key]: value }\n\t\tfetchData(1)\n\t}\n\n\tfunction clearAllFilters() {\n\t\tsearchTerm.value = ''\n\t\tfilters.value = {}\n\t\tsortKey.value = options.defaultSort?.key || null\n\t\tsortOrder.value = options.defaultSort?.order || 'asc'\n\t\tfetchData(1)\n\t}\n\n\tfunction goToPage(page) {\n\t\tcurrentPage.value = page\n\t\tfetchData()\n\t}\n\n\tonBeforeUnmount(() => clearTimeout(searchTimeout))\n\n\treturn {\n\t\tsearchTerm,\n\t\tfilters,\n\t\tsortKey,\n\t\tsortOrder,\n\t\tcurrentPage,\n\t\tpageSize,\n\t\tonSearchInput,\n\t\ttoggleSort,\n\t\tsetFilter,\n\t\tclearAllFilters,\n\t\tgoToPage,\n\t\tfetch: fetchData,\n\t\tbuildFetchParams,\n\t}\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useListView.js","sources":["../../../src/composables/useListView.js"],"sourcesContent":["import { computed, isRef, onBeforeUnmount, onMounted, ref, watch } from 'vue'\nimport { useObjectStore } from '../store/index.js'\nimport { facetFilterParams } from '../utils/facets.js'\n\n/**\n * Composable for managing list view state with full objectStore integration.\n *\n * When called with an `objectType` string, connects to the objectStore and handles\n * schema loading, collection fetching, sidebar wiring, and all event handlers\n * automatically. Everything a `CnIndexPage`-based list view needs is returned\n * directly — no additional computed properties or methods required in the component.\n *\n * Backward-compatible: existing `useListView(options)` and `useListView()` calls\n * continue to work without modification.\n *\n * @param {string|Function|object} [objectTypeOrOptions] Object type slug (new API), a getter or ref returning one (the list follows it when it changes), or a legacy options object\n * @param {object} [options] Options (new API only)\n * @param {object|null} [options.objectStore] Custom object store instance (from createObjectStore). When provided, uses this store instead of the default useObjectStore(). Required when the app uses createObjectStore with a custom store ID.\n * @param {object|null} [options.sidebarState] Sidebar state object from `inject('sidebarState')`. When provided, the composable wires and unwires the sidebar automatically on mount/unmount.\n * @param {number} [options.defaultPageSize] Default `_limit` sent to the API\n * @param {number} [options.debounceMs] Search debounce in milliseconds\n * @param {object} [options.defaultSort] Default sort applied on mount e.g. `{ key: 'createdAt', order: 'desc' }`\n * @param {Array<{key: string, order: 'asc'|'desc'}>} [options.defaultSortKeys] Default MULTI-column sort applied on mount (ordered priority list). Takes precedence over `defaultSort` when both are given; used e.g. to restore a sort persisted in the route query.\n * @param {Array<string>|null} [options.defaultVisibleColumns] Initial visible-column key set. Seeds the sidebar's Columns tab with the curated default (e.g. a manifest `columns` list) so toggles add/remove from it. Omit (or `null`) for schema-driven tables where every column starts visible.\n * @param {object} [options.defaultActiveFilters] Initial facet-filter map, e.g. to restore filters persisted in the route query on mount.\n * @param {string} [options.defaultSearchTerm] Initial search term, e.g. to restore `_search` from the route query on mount.\n * @param {object|(() => object)} [options.fixedFilters] A filter map (or getter returning one) merged into every fetch AFTER the user's facet filters, so the fixed entries always win. Used e.g. by `CnIndexPage` to apply a route-param-scoped `pages[].config.filter`. Default `{}` — omitting it is behaviourally identical to before.\n * @param {Array<string>|(() => Array<string>)} [options.extend] OpenRegister `_extend[]` values (or a getter returning them) forwarded on every fetch, e.g. `['calculations']` so a schema's `materialise: false` calculations are evaluated and can be shown as columns. Default `[]` — omitting it sends no `_extend` at all, exactly as before.\n * @return {object} Reactive state and event handlers\n *\n * @example\n * // New API — minimal\n * const { schema, objects, loading, pagination,\n * onSearch, onSort, onFilterChange, onPageChange, refresh } = useListView('client')\n *\n * @example\n * // New API — with sidebar wiring\n * const list = useListView('client', {\n * sidebarState: inject('sidebarState', null),\n * })\n *\n * @example\n * // Legacy API — still works\n * const { searchTerm, filters, onSearchInput, toggleSort } = useListView({\n * objectType: 'client',\n * fetchFn: (type, params) => objectStore.fetchCollection(type, params),\n * })\n */\nexport function useListView(objectTypeOrOptions, options) {\n\t// Backward compat: if first arg is an object or absent, delegate to legacy implementation\n\tif (!objectTypeOrOptions || (typeof objectTypeOrOptions === 'object' && !isRef(objectTypeOrOptions))) {\n\t\treturn useLegacyListView(objectTypeOrOptions || {})\n\t}\n\n\t// ── New API ──────────────────────────────────────────────────────────\n\t// A string, a ref or a getter: a getter lets the page switch object type\n\t// (a folder carrying its own schema) without re-creating the list.\n\tconst typeSource = objectTypeOrOptions\n\tconst isDynamicType = typeof typeSource !== 'string'\n\tconst getType = () => (isRef(typeSource) ? typeSource.value : (typeof typeSource === 'function' ? typeSource() : typeSource))\n\tconst opts = options || {}\n\tconst sidebarState = opts.sidebarState || null\n\n\tconst objectStore = opts.objectStore || useObjectStore()\n\n\t// ── State refs ───────────────────────────────────────────────────────\n\tconst schema = ref(null)\n\tconst searchTerm = ref(opts.defaultSearchTerm || '')\n\t// `sortKeys` is the source of truth (ordered multi-column priority list);\n\t// `sortKey`/`sortOrder` are kept as a primary-entry mirror so every\n\t// existing reader of the pre-multi-sort refs keeps working unchanged.\n\tconst initialSortKeys = Array.isArray(opts.defaultSortKeys) && opts.defaultSortKeys.length > 0\n\t\t? opts.defaultSortKeys\n\t\t: (opts.defaultSort?.key ? [{ key: opts.defaultSort.key, order: opts.defaultSort.order || 'asc' }] : [])\n\tconst sortKeys = ref(initialSortKeys)\n\tconst sortKey = ref(initialSortKeys[0]?.key || null)\n\tconst sortOrder = ref(initialSortKeys[0]?.order || 'asc')\n\tconst activeFilters = ref((opts.defaultActiveFilters && typeof opts.defaultActiveFilters === 'object') ? opts.defaultActiveFilters : {})\n\t// Seed the visible-column set to the configured columns so the sidebar's\n\t// Columns tab reflects the curated default and toggles add/remove from it.\n\t// `null` (no seed) means \"all columns visible\" (schema-driven tables).\n\tconst visibleColumns = ref(opts.defaultVisibleColumns ?? null)\n\tconst pageSize = ref(opts.defaultPageSize || 20)\n\n\t// ── Computed refs from the store ─────────────────────────────────────\n\tconst objects = computed(() => objectStore.collections[getType()] || [])\n\t// True from creation until the mount sequence below has issued its first\n\t// fetch. The store's own flag only goes up once `fetchCollection` runs, and\n\t// `onMounted` awaits `fetchSchema()` first — so for the length of that round\n\t// trip the list was \"not loading\" with zero rows, and every consumer\n\t// rendered its empty state (\"No log entries to show\") before any data had\n\t// been asked for. Folded into `loading` rather than exposed separately so\n\t// existing consumers get the fix without touching their templates.\n\tconst bootstrapping = ref(true)\n\tconst loading = computed(() => bootstrapping.value || objectStore.loading[getType()] || false)\n\tconst pagination = computed(() => objectStore.pagination[getType()] || { total: 0, page: 1, pages: 1, limit: 20 })\n\t// Facets are computed by the platform over the whole query rather than the\n\t// loaded page, so anything that needs the complete set of values for a\n\t// field (a folder pane grouping by it, say) reads them here rather than\n\t// deriving them from `objects`, which is only the current page.\n\tconst facets = computed(() => objectStore.facets[getType()] || {})\n\t// Personal-lens reports of the latest response (`{ recent: { available, reason } }`).\n\t// @spec openspec/changes/lens-says-why-it-is-empty/specs/personal-lens-availability/spec.md#requirement-a-list-response-carries-the-report-of-the-lenses-it-was-asked-for\n\tconst lenses = computed(() => (objectStore.lenses && objectStore.lenses[getType()]) || {})\n\t// Outcome of the latest `refresh()`: the store's error for it, or null once\n\t// it succeeded or a later fetch stored new rows. The store keeps the previous\n\t// rows on a failed fetch, so this is how a consumer tells stale rows from\n\t// current ones.\n\tconst error = ref(null)\n\tlet refreshSeq = 0\n\tlet pendingRefreshes = 0\n\n\tlet searchTimeout = null\n\n\t// ── Param construction ───────────────────────────────────────────────\n\n\t/**\n\t * Resolve `opts.fixedFilters` to a plain map. Accepts a plain object OR a\n\t * function/getter returning one (so callers can derive it from reactive\n\t * sources — e.g. route params — and have it re-read on every fetch).\n\t *\n\t * @return {object} The fixed-filter map (may be empty).\n\t */\n\tfunction resolveFixedFilters() {\n\t\tconst f = typeof opts.fixedFilters === 'function' ? opts.fixedFilters() : opts.fixedFilters\n\t\treturn (f && typeof f === 'object') ? f : {}\n\t}\n\n\t/**\n\t * Resolve `opts.extend` to a list of non-empty strings.\n\t *\n\t * Accepts an array or a getter returning one, so a caller can re-scope it\n\t * between fetches the way `fixedFilters` allows.\n\t *\n\t * @return {Array<string>} The `_extend[]` values, possibly empty.\n\t */\n\tfunction resolveExtend() {\n\t\tconst e = typeof opts.extend === 'function' ? opts.extend() : opts.extend\n\t\tif (!Array.isArray(e)) {\n\t\t\treturn []\n\t\t}\n\t\treturn e.filter((v) => typeof v === 'string' && v !== '')\n\t}\n\n\t/**\n\t * Build API fetch params from current reactive state.\n\t *\n\t * @param {number} page Page number to request\n\t * @return {object} Params object ready to pass to fetchCollection\n\t */\n\tfunction buildParams(page) {\n\t\tconst params = { _limit: pageSize.value, _page: page }\n\n\t\tif (searchTerm.value) {\n\t\t\tparams._search = searchTerm.value\n\t\t}\n\n\t\tif (sortKeys.value.length > 0) {\n\t\t\t// Object key insertion order = sort priority order, matching\n\t\t\t// OpenRegister's `_order` format exactly (`ObjectsController::\n\t\t\t// normalizeOrderParameter` / `MagicMapper`'s ordering loop). A\n\t\t\t// single active key produces byte-identical output to before.\n\t\t\tparams._order = Object.fromEntries(sortKeys.value.map((k) => [k.key, k.order]))\n\t\t}\n\n\t\tfor (const [key, values] of Object.entries(activeFilters.value)) {\n\t\t\t// Single-value arrays are unwrapped to scalar params; the\n\t\t\t// missing-value option becomes `<key>_isnull=true`.\n\t\t\tObject.assign(params, facetFilterParams(key, values))\n\t\t}\n\n\t\t// Fixed filters (e.g. a route-param-scoped `pages[].config.filter`) are\n\t\t// merged LAST so they always win over a colliding facet `activeFilter`.\n\t\t// Repeated `_extend[]`, serialized by the shared query builder. Set\n\t\t// before the fixed filters so an explicit `_extend` in a filter map\n\t\t// still wins, matching every other key's precedence here.\n\t\tconst extend = resolveExtend()\n\t\tif (extend.length > 0) {\n\t\t\tparams._extend = extend\n\t\t}\n\n\t\tfor (const [key, value] of Object.entries(resolveFixedFilters())) {\n\t\t\tif (value !== undefined && value !== null && value !== '') {\n\t\t\t\tparams[key] = value\n\t\t\t}\n\t\t}\n\n\t\treturn params\n\t}\n\n\t// ── Fetch ────────────────────────────────────────────────────────────\n\n\t/**\n\t * Fetch the collection using current state params and update sidebar facet data.\n\t * Only the most recently started refresh sets `error`, so an older response\n\t * settling late cannot override a newer one's outcome.\n\t *\n\t * @param {number} [page] Page to fetch\n\t * @return {Promise<void>}\n\t */\n\tasync function refresh(page = 1) {\n\t\tconst seq = ++refreshSeq\n\t\t// This call's own result. `errors[getType()]` is shared by every call\n\t\t// for the type and is not cleared on success, so it cannot say which\n\t\t// request failed; it is only the fallback for a store that ignores\n\t\t// `options.outcome`.\n\t\tconst outcome = {}\n\t\tpendingRefreshes++\n\t\ttry {\n\t\t\tawait objectStore.fetchCollection(getType(), buildParams(page), { outcome })\n\t\t} catch (e) {\n\t\t\tif (seq === refreshSeq) {\n\t\t\t\terror.value = e\n\t\t\t}\n\t\t\tthrow e\n\t\t} finally {\n\t\t\tpendingRefreshes--\n\t\t}\n\t\tif (seq === refreshSeq) {\n\t\t\terror.value = ('error' in outcome) ? outcome.error : (objectStore.errors?.[getType()] || null)\n\t\t}\n\t}\n\n\t// A fetch outside `refresh()` (e.g. the live-updates refetch) that stores\n\t// new rows recovers the list from an earlier failure. Rows written while\n\t// a refresh is pending are left to that refresh, which decides `error`.\n\twatch(\n\t\t() => objectStore.collections[getType()],\n\t\t() => {\n\t\t\tif (pendingRefreshes === 0) {\n\t\t\t\terror.value = null\n\t\t\t}\n\t\t},\n\t\t{ flush: 'sync' },\n\t)\n\n\t// ── Event handlers ───────────────────────────────────────────────────\n\n\t/**\n\t * Handle search input. Debounced by `options.debounceMs` (default 300 ms).\n\t *\n\t * @param {string} value New search string\n\t */\n\tfunction onSearch(value) {\n\t\tsearchTerm.value = value\n\t\tclearTimeout(searchTimeout)\n\t\tsearchTimeout = setTimeout(() => refresh(1), opts.debounceMs || 300)\n\t}\n\n\t/**\n\t * Handle sort change. Updates sort state and triggers refresh.\n\t *\n\t * Accepts CnDataTable's extended `sort` event payload: `keys` (the full\n\t * ordered multi-column list) is used when present; a legacy caller\n\t * passing only `{ key, order }` (no `keys`) still works exactly as\n\t * before — it's normalised to a single-entry `sortKeys` list.\n\t *\n\t * @param {{key: string|null, order: string|null, keys?: Array<{key: string, order: string}>}} sort New sort definition.\n\t */\n\tfunction onSort({ key, order, keys }) {\n\t\tconst nextKeys = Array.isArray(keys) ? keys : (key ? [{ key, order: order || 'asc' }] : [])\n\t\tsortKeys.value = nextKeys\n\t\tsortKey.value = nextKeys[0]?.key ?? null\n\t\tsortOrder.value = nextKeys[0]?.order ?? 'asc'\n\t\trefresh(1)\n\t}\n\n\t/**\n\t * Handle filter change for a single key. Empty arrays remove the key.\n\t *\n\t * @param {string} key Filter key (maps to API param name)\n\t * @param {Array} values Selected filter values\n\t */\n\tfunction onFilterChange(key, values) {\n\t\tif (!values || values.length === 0) {\n\t\t\tconst updated = { ...activeFilters.value }\n\t\t\tdelete updated[key]\n\t\t\tactiveFilters.value = updated\n\t\t} else {\n\t\t\tactiveFilters.value = { ...activeFilters.value, [key]: values }\n\t\t}\n\t\trefresh(1)\n\t}\n\n\t/**\n\t * Handle page navigation.\n\t *\n\t * @param {number} page Page number to navigate to\n\t */\n\tfunction onPageChange(page) {\n\t\trefresh(page)\n\t}\n\n\t/**\n\t * Add the next page after the rows already loaded, for a view that shows\n\t * every row at once (the board). Does nothing on the last page. A refresh\n\t * started while the page is in flight drops it.\n\t *\n\t * @return {Promise<void>}\n\t */\n\tasync function loadMore() {\n\t\tconst current = pagination.value\n\t\tconst next = Number(current.page || 1) + 1\n\t\tif (next > Number(current.pages || 1)) {\n\t\t\treturn\n\t\t}\n\t\tconst seq = refreshSeq\n\t\tawait objectStore.fetchCollection(getType(), buildParams(next), {\n\t\t\tappend: true,\n\t\t\tisCurrent: () => seq === refreshSeq,\n\t\t})\n\t}\n\n\t/**\n\t * Handle page-size change. Resets to page 1.\n\t *\n\t * @param {number} size New page size\n\t */\n\tfunction onPageSizeChange(size) {\n\t\tpageSize.value = size\n\t\trefresh(1)\n\t}\n\n\t// ── Sidebar wiring ───────────────────────────────────────────────────\n\n\tfunction setupSidebar() {\n\t\tif (!sidebarState) {\n\t\t\treturn\n\t\t}\n\t\tsidebarState.active = true\n\t\tsidebarState.schema = schema.value\n\t\tsidebarState.searchValue = searchTerm.value\n\t\tsidebarState.activeFilters = {}\n\t\tsidebarState.onSearch = onSearch\n\t\tsidebarState.onColumnsChange = (cols) => {\n\t\t\tvisibleColumns.value = cols\n\t\t}\n\t\tsidebarState.onFilterChange = ({ key, values }) => onFilterChange(key, values)\n\t}\n\n\tfunction teardownSidebar() {\n\t\tif (!sidebarState) {\n\t\t\treturn\n\t\t}\n\t\tsidebarState.active = false\n\t\tsidebarState.schema = null\n\t\tsidebarState.activeFilters = {}\n\t\tsidebarState.facetData = {}\n\t\tsidebarState.onSearch = null\n\t\tsidebarState.onColumnsChange = null\n\t\tsidebarState.onFilterChange = null\n\t}\n\n\t// Push facet data to sidebar after each store update\n\tif (sidebarState) {\n\t\twatch(\n\t\t\t() => objectStore.facets[getType()],\n\t\t\t(facets) => {\n\t\t\t\tsidebarState.facetData = facets || {}\n\t\t\t},\n\t\t)\n\t}\n\n\t// A dynamic object type that changes starts that type's list from page 1\n\t// with its own schema.\n\tif (isDynamicType) {\n\t\twatch(getType, async (next, prev) => {\n\t\t\tif (!next || next === prev) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tschema.value = null\n\t\t\tschema.value = await objectStore.fetchSchema(next)\n\t\t\tawait refresh(1)\n\t\t})\n\t}\n\n\t// ── Lifecycle ────────────────────────────────────────────────────────\n\n\tonMounted(async () => {\n\t\ttry {\n\t\t\tschema.value = await objectStore.fetchSchema(getType())\n\t\t\tif (sidebarState) {\n\t\t\t\tsetupSidebar()\n\t\t\t}\n\t\t\tawait refresh(1)\n\t\t} finally {\n\t\t\t// Hand the loading state back to the store either way: a schema\n\t\t\t// fetch that rejects must not leave the list spinning forever.\n\t\t\tbootstrapping.value = false\n\t\t}\n\t})\n\n\tonBeforeUnmount(() => {\n\t\tclearTimeout(searchTimeout)\n\t\tteardownSidebar()\n\t})\n\n\t// ── Return value ─────────────────────────────────────────────────────\n\n\treturn {\n\t\t// Store-derived\n\t\tschema,\n\t\tobjects,\n\t\tloading,\n\t\tpagination,\n\t\tfacets,\n\t\tlenses,\n\t\terror,\n\t\t// Local state\n\t\tsearchTerm,\n\t\tsortKey,\n\t\tsortOrder,\n\t\tsortKeys,\n\t\tactiveFilters,\n\t\tvisibleColumns,\n\t\tpageSize,\n\t\t// Event handlers\n\t\tonSearch,\n\t\tonSort,\n\t\tonFilterChange,\n\t\tonPageChange,\n\t\tonPageSizeChange,\n\t\t// Explicit fetch\n\t\trefresh,\n\t\tloadMore,\n\t\t// The query the list sends (search, sort, facet filters, fixed filters),\n\t\t// built from the current state, for an export that must follow the list.\n\t\tbuildParams,\n\t}\n}\n\n// ── Legacy implementation ─────────────────────────────────────────────────────\n\n/**\n * Legacy `useListView(options)` implementation.\n * Preserved verbatim for backward compatibility.\n *\n * @param {object} options Legacy options object\n * @param {string} [options.objectType] The registered object type slug\n * @param {(type: string, params: object) => Promise<Array<object>>} [options.fetchFn] Function to call: (type, params) => Promise<Array>\n * @param {number} [options.debounceMs] Search debounce in milliseconds\n * @param {number} [options.pageSize] Default page size\n * @param {object} [options.defaultSort] Default sort: { key: string, order: 'asc'|'desc' }\n * @return {object} Reactive state and methods\n */\nfunction useLegacyListView(options) {\n\tconst searchTerm = ref('')\n\tconst filters = ref({})\n\tconst sortKey = ref(options.defaultSort?.key || null)\n\tconst sortOrder = ref(options.defaultSort?.order || 'asc')\n\tconst currentPage = ref(1)\n\tconst pageSize = ref(options.pageSize || 20)\n\n\tlet searchTimeout = null\n\n\tfunction buildFetchParams() {\n\t\tconst params = {\n\t\t\t_limit: pageSize.value,\n\t\t\t_page: currentPage.value,\n\t\t}\n\n\t\tif (searchTerm.value) {\n\t\t\tparams._search = searchTerm.value\n\t\t}\n\n\t\tif (sortKey.value) {\n\t\t\tparams._order = { [sortKey.value]: sortOrder.value }\n\t\t}\n\n\t\tfor (const [key, value] of Object.entries(filters.value)) {\n\t\t\tif (value !== null && value !== '' && value !== false) {\n\t\t\t\tparams[key] = value\n\t\t\t}\n\t\t}\n\n\t\treturn params\n\t}\n\n\tasync function fetchData(page) {\n\t\tif (page !== undefined) {\n\t\t\tcurrentPage.value = page\n\t\t}\n\t\tconst params = buildFetchParams()\n\t\tif (options.fetchFn) {\n\t\t\treturn options.fetchFn(options.objectType, params)\n\t\t}\n\t}\n\n\tfunction onSearchInput(value) {\n\t\tsearchTerm.value = value\n\t\tclearTimeout(searchTimeout)\n\t\tsearchTimeout = setTimeout(() => fetchData(1), options.debounceMs || 300)\n\t}\n\n\tfunction toggleSort(key) {\n\t\tif (sortKey.value === key) {\n\t\t\tif (sortOrder.value === 'asc') {\n\t\t\t\tsortOrder.value = 'desc'\n\t\t\t} else {\n\t\t\t\tsortKey.value = null\n\t\t\t\tsortOrder.value = 'asc'\n\t\t\t}\n\t\t} else {\n\t\t\tsortKey.value = key\n\t\t\tsortOrder.value = 'asc'\n\t\t}\n\t\tfetchData(1)\n\t}\n\n\tfunction setFilter(key, value) {\n\t\tfilters.value = { ...filters.value, [key]: value }\n\t\tfetchData(1)\n\t}\n\n\tfunction clearAllFilters() {\n\t\tsearchTerm.value = ''\n\t\tfilters.value = {}\n\t\tsortKey.value = options.defaultSort?.key || null\n\t\tsortOrder.value = options.defaultSort?.order || 'asc'\n\t\tfetchData(1)\n\t}\n\n\tfunction goToPage(page) {\n\t\tcurrentPage.value = page\n\t\tfetchData()\n\t}\n\n\tonBeforeUnmount(() => clearTimeout(searchTimeout))\n\n\treturn {\n\t\tsearchTerm,\n\t\tfilters,\n\t\tsortKey,\n\t\tsortOrder,\n\t\tcurrentPage,\n\t\tpageSize,\n\t\tonSearchInput,\n\t\ttoggleSort,\n\t\tsetFilter,\n\t\tclearAllFilters,\n\t\tgoToPage,\n\t\tfetch: fetchData,\n\t\tbuildFetchParams,\n\t}\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -2,7 +2,7 @@ import script from './CnTransitionInputDialog.vue2.js';
|
|
|
2
2
|
import { render } from './CnTransitionInputDialog.vue3.js';
|
|
3
3
|
|
|
4
4
|
script.render = render;
|
|
5
|
-
script.__scopeId = "data-v-
|
|
5
|
+
script.__scopeId = "data-v-457532e4";
|
|
6
6
|
const __sfc_main = script;
|
|
7
7
|
|
|
8
8
|
export { __sfc_main as default };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { translate } from '@nextcloud/l10n';
|
|
2
|
-
import { NcButton, NcTextField, NcTextArea, NcCheckboxRadioSwitch } from '@nextcloud/vue';
|
|
2
|
+
import { NcButton, NcLoadingIcon, NcTextField, NcTextArea, NcCheckboxRadioSwitch, NcSelect } from '@nextcloud/vue';
|
|
3
3
|
import __sfc_main$1 from '../components/CnDialog/CnDialog.vue.js';
|
|
4
4
|
import __sfc_main from '../components/CnResourceSelect/CnResourceSelect.vue.js';
|
|
5
5
|
import { dialogBoardMixin } from '../mixins/dialogBoard.js';
|
|
@@ -22,7 +22,13 @@ import { fieldsFromSchema } from '../utils/schema.js';
|
|
|
22
22
|
* from the property `title`, booleans render as a switch, numbers as a number
|
|
23
23
|
* field, and long text (`maxLength > 255` or a `textarea`/`markdown` format)
|
|
24
24
|
* as a textarea. Anything else — including a field the schema does not
|
|
25
|
-
* declare — falls back to a plain labelled text input.
|
|
25
|
+
* declare — falls back to a plain labelled text input.
|
|
26
|
+
*
|
|
27
|
+
* An input may also say how it is asked for, which matters when the choices
|
|
28
|
+
* depend on the record rather than on its schema: `label` names the field,
|
|
29
|
+
* and `options: [{ value, label }]` turns it into a picker that shows each
|
|
30
|
+
* `label` and sends the chosen `value`. An input that declares neither is
|
|
31
|
+
* rendered from the schema as before. Deliberately minimal:
|
|
26
32
|
* text, textarea, number and checkbox cover transition inputs; this is not a
|
|
27
33
|
* form engine (use `CnFormDialog` for full object forms).
|
|
28
34
|
*
|
|
@@ -44,9 +50,11 @@ var script = {
|
|
|
44
50
|
CnResourceSelect: __sfc_main,
|
|
45
51
|
CnDialog: __sfc_main$1,
|
|
46
52
|
NcButton,
|
|
53
|
+
NcLoadingIcon,
|
|
47
54
|
NcTextField,
|
|
48
55
|
NcTextArea,
|
|
49
56
|
NcCheckboxRadioSwitch,
|
|
57
|
+
NcSelect,
|
|
50
58
|
},
|
|
51
59
|
|
|
52
60
|
mixins: [dialogBoardMixin],
|
|
@@ -57,7 +65,11 @@ var script = {
|
|
|
57
65
|
* `x-openregister-lifecycle.transitions.<action>.inputs` (server-derived
|
|
58
66
|
* or config-declared); `label` doubles as dialog title + confirm label.
|
|
59
67
|
*
|
|
60
|
-
*
|
|
68
|
+
* An input may carry a `label` for its field and `options` to pick
|
|
69
|
+
* from, each `{ value, label }`; the picker shows the label and sends
|
|
70
|
+
* the value.
|
|
71
|
+
*
|
|
72
|
+
* @type {{action?: string, label?: string, inputs?: Array<{field: string, required?: boolean, label?: string, options?: Array<{value: string, label?: string}>}>}}
|
|
61
73
|
*/
|
|
62
74
|
transition: {
|
|
63
75
|
type: Object,
|
|
@@ -154,6 +166,8 @@ var script = {
|
|
|
154
166
|
required: input.required === true,
|
|
155
167
|
picker: input.picker && typeof input.picker === 'object' ? input.picker : null,
|
|
156
168
|
fields: Array.isArray(input.fields) ? input.fields.filter((f) => typeof f === 'string') : null,
|
|
169
|
+
label: typeof input.label === 'string' && input.label.trim() !== '' ? input.label.trim() : null,
|
|
170
|
+
options: this.declaredOptions(input.options),
|
|
157
171
|
}))
|
|
158
172
|
},
|
|
159
173
|
|
|
@@ -163,26 +177,40 @@ var script = {
|
|
|
163
177
|
* widget from the type/format); the widget is then clamped to the minimal
|
|
164
178
|
* set this dialog renders (text / textarea / number / checkbox). Fields
|
|
165
179
|
* the schema does not declare fall back to a plain text input.
|
|
180
|
+
*
|
|
181
|
+
* An input that declares `options` is a picker whatever the schema
|
|
182
|
+
* says, because its choices are the ones this transition offers on
|
|
183
|
+
* this record. A declared `label` names the field over the schema's.
|
|
166
184
|
*/
|
|
167
185
|
fields() {
|
|
168
186
|
const keys = this.inputs.map((input) => input.field);
|
|
169
187
|
const resolved = fieldsFromSchema(this.schema, { include: keys, includeReadOnly: true });
|
|
170
188
|
return this.inputs.map((input) => {
|
|
171
189
|
const field = resolved.find((f) => f.key === input.field);
|
|
190
|
+
if (input.options) {
|
|
191
|
+
return {
|
|
192
|
+
key: input.field,
|
|
193
|
+
label: input.label || (field && field.label) || input.field,
|
|
194
|
+
description: (field && field.description) || '',
|
|
195
|
+
widget: 'select',
|
|
196
|
+
options: input.options,
|
|
197
|
+
required: input.required,
|
|
198
|
+
}
|
|
199
|
+
}
|
|
172
200
|
const prop = (this.schema && this.schema.properties && this.schema.properties[input.field]) || null;
|
|
173
201
|
if (prop && prop.type === 'object' && prop.properties && typeof prop.properties === 'object') {
|
|
174
202
|
return this.objectField(input, prop)
|
|
175
203
|
}
|
|
176
204
|
if (!field) {
|
|
177
|
-
return { key: input.field, label: input.field, description: '', widget: 'text', required: input.required }
|
|
205
|
+
return { key: input.field, label: input.label || input.field, description: '', widget: 'text', required: input.required }
|
|
178
206
|
}
|
|
179
207
|
if (field.reference && field.reference.multiple !== true) {
|
|
180
208
|
const register = field.reference.register || this.register;
|
|
181
209
|
if (register !== '') {
|
|
182
|
-
return { ...field, widget: 'reference', referenceRegister: register, picker: input.picker, required: input.required }
|
|
210
|
+
return { ...field, label: input.label || field.label, widget: 'reference', referenceRegister: register, picker: input.picker, required: input.required }
|
|
183
211
|
}
|
|
184
212
|
}
|
|
185
|
-
return { ...field, widget: this.clampWidget(field.widget), required: input.required }
|
|
213
|
+
return { ...field, label: input.label || field.label, widget: this.clampWidget(field.widget), required: input.required }
|
|
186
214
|
})
|
|
187
215
|
},
|
|
188
216
|
|
|
@@ -296,6 +324,40 @@ var script = {
|
|
|
296
324
|
}
|
|
297
325
|
},
|
|
298
326
|
|
|
327
|
+
/**
|
|
328
|
+
* The choices an input declares, as `[{ value, label }]`, or null when
|
|
329
|
+
* it declares none. An option without a value is dropped, because it
|
|
330
|
+
* cannot be sent; one without a label shows its value.
|
|
331
|
+
*
|
|
332
|
+
* @param {unknown} options The input's declared `options`.
|
|
333
|
+
* @return {Array<{value: string, label: string}>|null}
|
|
334
|
+
*/
|
|
335
|
+
declaredOptions(options) {
|
|
336
|
+
if (!Array.isArray(options)) {
|
|
337
|
+
return null
|
|
338
|
+
}
|
|
339
|
+
const usable = options
|
|
340
|
+
.filter((option) => option && typeof option === 'object'
|
|
341
|
+
&& (typeof option.value === 'string' || typeof option.value === 'number')
|
|
342
|
+
&& String(option.value).trim() !== '')
|
|
343
|
+
.map((option) => ({
|
|
344
|
+
value: String(option.value),
|
|
345
|
+
label: typeof option.label === 'string' && option.label.trim() !== '' ? option.label.trim() : String(option.value),
|
|
346
|
+
}));
|
|
347
|
+
return usable.length > 0 ? usable : null
|
|
348
|
+
},
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* The option a picker field currently holds, or null.
|
|
352
|
+
*
|
|
353
|
+
* @param {object} field The select field descriptor.
|
|
354
|
+
* @return {{value: string, label: string}|null}
|
|
355
|
+
*/
|
|
356
|
+
selectedOption(field) {
|
|
357
|
+
const value = this.values[field.key];
|
|
358
|
+
return field.options.find((option) => option.value === String(value ?? '')) || null
|
|
359
|
+
},
|
|
360
|
+
|
|
299
361
|
/**
|
|
300
362
|
* Ids a reference picker must not offer: the current record, when the
|
|
301
363
|
* input's `picker.excludeSelf` says so.
|