@conduction/nextcloud-vue 2.73.0 → 2.73.1
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/CnAppRoot/CnAppRoot.vue2.js +29 -0
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +18 -0
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +9 -6
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +16 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +5 -4
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +16 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +6 -6
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue2.js +53 -5
- package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue2.js.map +1 -1
- package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue3.js +1 -1
- package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue3.js.map +1 -1
- package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue2.js +133 -0
- package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue3.js +22 -14
- package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +15 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +3 -2
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetGrid/CnWidgetGrid.vue2.js +15 -1
- package/dist/esm/components/CnWidgetGrid/CnWidgetGrid.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetGrid/CnWidgetGrid.vue3.js +2 -2
- package/dist/esm/components/CnWidgetGrid/CnWidgetGrid.vue3.js.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/utils/notificationRuleLabel.js +132 -0
- package/dist/esm/utils/notificationRuleLabel.js.map +1 -0
- package/dist/nextcloud-vue.cjs.js +473 -43
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/package.json +1 -1
- package/src/components/CnAppRoot/CnAppRoot.md +1 -0
- package/src/components/CnAppRoot/CnAppRoot.vue +29 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +24 -3
- package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +22 -4
- package/src/components/CnIndexPage/CnIndexPage.vue +17 -2
- package/src/components/CnNotificationPreferences/CnNotificationPreferences.vue +54 -6
- package/src/components/CnPageRenderer/CnPageRenderer.vue +147 -6
- package/src/components/CnTabsWidget/CnTabsWidget.vue +17 -2
- package/src/components/CnWidgetGrid/CnWidgetGrid.vue +19 -3
- package/src/index.js +2 -0
- package/src/utils/notificationRuleLabel.js +129 -0
- package/webpack/index.d.ts +16 -0
- package/webpack/index.js +70 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnTabsWidget.vue2.js","sources":["../../../../src/components/CnTabsWidget/CnTabsWidget.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-tabs-widget\">\n\t\t<CnTabs\n\t\t\t:aria-label=\"stripLabel\"\n\t\t\t:moreLabel=\"moreLabel\"\n\t\t\t:variant=\"stripVariant\"\n\t\t\tclass=\"cn-tabs-widget__tabs\"\n\t\t\t@update:activeIndex=\"onTabChange\">\n\t\t\t<!-- One Actions menu for the whole widget, bound to whichever child\n\t\t\t is showing. This is the point of the component: six tabbed\n\t\t\t widgets used to mean six card headers stacked down the page. -->\n\t\t\t<template #nav-end>\n\t\t\t\t<CnActionsMenu\n\t\t\t\t\t:showRefresh=\"showRefresh\"\n\t\t\t\t\t:showRequestFeature=\"showRequestFeature\"\n\t\t\t\t\t:showDocumentation=\"showDocumentation\"\n\t\t\t\t\t:documentationUrl=\"documentationUrl\"\n\t\t\t\t\t:widgetId=\"activeWidgetId\"\n\t\t\t\t\t:title=\"activeTitle\"\n\t\t\t\t\t:surface=\"`widget:${activeWidgetId}`\"\n\t\t\t\t\trefreshChannel=\"cn:widget:refresh\"\n\t\t\t\t\ttestidBase=\"cn-tabs-widget\">\n\t\t\t\t\t<!-- The open panel's own items, below the built-in trio.\n\t\t\t\t\t A panel draws no header, so these would otherwise have\n\t\t\t\t\t nowhere to go: the data widget's Metadata and full edit\n\t\t\t\t\t dialog vanished outright when the panel stopped drawing\n\t\t\t\t\t a card. They are rendered here rather than rebuilt here\n\t\t\t\t\t because `run` closes over the widget that published it,\n\t\t\t\t\t which is what keeps the dialog's per-widget config and\n\t\t\t\t\t the widget's own save path. See utils/panelActions.js.\n\n\t\t\t\t\t Only the ACTIVE panel's items. `lazy` keeps a visited\n\t\t\t\t\t tab mounted, so several panels publish at once and an\n\t\t\t\t\t unfiltered list would offer actions for a sheet nobody\n\t\t\t\t\t is looking at. -->\n\t\t\t\t\t<template v-if=\"activePanelActions.length\" #action-items>\n\t\t\t\t\t\t<NcActionButton\n\t\t\t\t\t\t\tv-for=\"action in activePanelActions\"\n\t\t\t\t\t\t\t:key=\"action.key\"\n\t\t\t\t\t\t\t:closeAfterClick=\"true\"\n\t\t\t\t\t\t\t@click=\"action.run()\">\n\t\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t\t<CnIcon :name=\"action.icon\" :size=\"20\" />\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t{{ action.label }}\n\t\t\t\t\t\t</NcActionButton>\n\t\t\t\t\t</template>\n\t\t\t\t</CnActionsMenu>\n\t\t\t</template>\n\n\t\t\t<CnTab\n\t\t\t\tv-for=\"(entry, index) in resolvedTabs\"\n\t\t\t\t:key=\"entry.key\"\n\t\t\t\t:active=\"index === activeIndex\"\n\t\t\t\t:title=\"entry.label\"\n\t\t\t\t:count=\"entry.count\"\n\t\t\t\t:overflow=\"entry.overflow\"\n\t\t\t\tlazy\n\t\t\t\t@click=\"activeIndex = index\">\n\t\t\t\t<template #title>\n\t\t\t\t\t<span class=\"cn-tabs-widget__title\">\n\t\t\t\t\t\t<CnIcon\n\t\t\t\t\t\t\tv-if=\"entry.icon\"\n\t\t\t\t\t\t\t:name=\"entry.icon\"\n\t\t\t\t\t\t\t:size=\"18\"\n\t\t\t\t\t\t\tclass=\"cn-tabs-widget__title-icon\" />\n\t\t\t\t\t\t{{ entry.label }}\n\t\t\t\t\t</span>\n\t\t\t\t</template>\n\n\t\t\t\t<CnDetailWidgetHost\n\t\t\t\t\tv-if=\"entry.widget\"\n\t\t\t\t\t:widget=\"entry.widget\"\n\t\t\t\t\tchrome=\"bare\"\n\t\t\t\t\t:objectId=\"objectId\"\n\t\t\t\t\t:object=\"objectData\"\n\t\t\t\t\t:objectType=\"objectType\"\n\t\t\t\t\t:schemaObject=\"schemaObject\"\n\t\t\t\t\t:register=\"register\"\n\t\t\t\t\t:schema=\"schema\"\n\t\t\t\t\t:store=\"store\"\n\t\t\t\t\t:surface=\"surface\"\n\t\t\t\t\t:integrationContext=\"integrationContext\"\n\t\t\t\t\t:cnRegistry=\"cnRegistry\"\n\t\t\t\t\t@geoSaved=\"onGeoSaved\"\n\t\t\t\t\t@openIntegration=\"onOpenIntegration\" />\n\t\t\t\t<NcEmptyContent v-else :name=\"missingLabel(entry)\" />\n\t\t\t</CnTab>\n\t\t</CnTabs>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcActionButton, NcEmptyContent } from '@nextcloud/vue'\nimport CnDetailWidgetHost from '../CnDetailWidgetHost/CnDetailWidgetHost.vue'\nimport CnIcon from '../CnIcon/CnIcon.vue'\nimport CnTab from '../CnTabs/CnTab.vue'\nimport CnTabs from '../CnTabs/CnTabs.vue'\nimport { resolveTabCount } from '../../utils/detailActionModel.js'\nimport { PANEL_ACTION_SINK } from '../../utils/panelActions.js'\nimport { widgetTitleOf } from '../../utils/widgetDispatch.js'\nimport { CnActionsMenu } from '../CnActionsMenu/index.js'\n\n/**\n * CnTabsWidget — a widget that holds other widgets, one per tab.\n *\n * A detail page that carries notes, files, related records, sub-records, mail\n * and appointments renders six cards, each with its own header and its own\n * Actions menu, stacked down the page. All six say roughly the same thing about\n * one record, and only one of them is being read at a time. This widget puts\n * them behind a tab strip instead.\n *\n * ## What the tabs take over\n *\n * The strip owns the title and the card. A child renders through\n * `CnDetailWidgetHost` with `chrome=\"bare\"`, so it draws no header and no\n * border of its own, and its content fills the panel exactly as it would have\n * filled its card. The tab label is what the widget's card header used to say.\n *\n * The Actions menu moves OUT of the panels and into the bar, beside the strip\n * rather than inside it, and rebinds to whichever child is showing. So there is\n * one menu, and it always acts on what you are looking at.\n *\n * ## Configuring it\n *\n * `content.tabs[]` names the children and their labels, so a deployment can\n * relabel or reorder tabs, or drop one, without touching code:\n *\n * ```js\n * content: {\n * tabs: [\n * { widgetId: 'case-notes', label: 'Notes', icon: 'NoteTextOutline' },\n * { widgetId: 'case-files', label: 'Files and attachments' },\n * ],\n * }\n * ```\n *\n * `label` and `icon` are optional and fall back to the child widget's own\n * title and icon, so the common case is a list of `widgetId`s.\n *\n * ## Panels are lazy, and stay mounted\n *\n * Each panel is a `CnTab` with `lazy`, so a child only mounts when its tab is\n * first opened. Six eager panels that each fetch on `mounted()` would fire six\n * requests on page load to answer five questions nobody asked. Once opened, a\n * panel stays mounted, so switching back never refetches.\n *\n * ## A named tab whose widget is missing\n *\n * The tab still renders, and its panel says which widget id did not resolve.\n * Dropping the tab would be worse: `content.tabs[]` is hand-authored config,\n * and a typo that silently removes a tab is a typo nobody finds.\n */\nexport default {\n\tname: 'CnTabsWidget',\n\n\tcomponents: {\n\t\tCnActionsMenu,\n\t\tCnDetailWidgetHost,\n\t\tCnIcon,\n\t\tCnTab,\n\t\tCnTabs,\n\t\tNcActionButton,\n\t\tNcEmptyContent,\n\t},\n\n\t/**\n\t * Offer the panels a way to put their own menu items in the strip's menu.\n\t *\n\t * Re-provided one level down by `CnDetailWidgetHost`, which knows the id to\n\t * key by; see utils/panelActions.js for why the items travel up instead of\n\t * being rebuilt here.\n\t *\n\t * @return {object} The provided sink.\n\t */\n\tprovide() {\n\t\treturn {\n\t\t\t[PANEL_ACTION_SINK]: {\n\t\t\t\t/**\n\t\t\t\t * Publish a panel's items, under one SOURCE.\n\t\t\t\t *\n\t\t\t\t * A panel has two possible publishers: the host, for an action\n\t\t\t\t * the host itself would have drawn (the catalog Add), and the\n\t\t\t\t * widget inside it, for its own menu items. Keyed by source so\n\t\t\t\t * the two coexist; a single slot per widget meant whichever\n\t\t\t\t * published last silently replaced the other.\n\t\t\t\t *\n\t\t\t\t * Replaces the map rather than mutating it so the computed that\n\t\t\t\t * reads it re-evaluates on any change.\n\t\t\t\t *\n\t\t\t\t * @param {string} id The publishing widget's id.\n\t\t\t\t * @param {object[]} items Its PanelAction descriptors.\n\t\t\t\t * @param {string} [source] Who is publishing: `host` or `widget`.\n\t\t\t\t * @return {void}\n\t\t\t\t */\n\t\t\t\tset: (id, items, source = 'widget') => {\n\t\t\t\t\t// `activePanelActions` spreads what is stored, so a\n\t\t\t\t\t// non-array becomes one menu item per character.\n\t\t\t\t\tif (!id || !Array.isArray(items)) {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tconst forId = { ...(this.panelActionsByWidget[id] || {}), [source]: items }\n\t\t\t\t\tthis.panelActionsByWidget = { ...this.panelActionsByWidget, [id]: forId }\n\t\t\t\t},\n\n\t\t\t\t/**\n\t\t\t\t * Withdraw one source's items, on unmount or when that\n\t\t\t\t * publisher's own menu comes back. The other source's items\n\t\t\t\t * stay.\n\t\t\t\t *\n\t\t\t\t * @param {string} id The publishing widget's id.\n\t\t\t\t * @param {string} [source] Who is withdrawing.\n\t\t\t\t * @return {void}\n\t\t\t\t */\n\t\t\t\tclear: (id, source = 'widget') => {\n\t\t\t\t\tconst forId = this.panelActionsByWidget[id]\n\t\t\t\t\tif (!forId || !(source in forId)) {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tconst { [source]: _removed, ...keptSources } = forId\n\t\t\t\t\tif (Object.keys(keptSources).length) {\n\t\t\t\t\t\tthis.panelActionsByWidget = { ...this.panelActionsByWidget, [id]: keptSources }\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tconst { [id]: _gone, ...rest } = this.panelActionsByWidget\n\t\t\t\t\tthis.panelActionsByWidget = rest\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget's config: `{ tabs, ariaLabel }`.\n\t\t *\n\t\t * `tabs[]` entries are `{ widgetId, label?, icon?, count?, countField?, overflow? }`.\n\t\t * `label` and `icon` fall back to the referenced widget's own title and\n\t\t * icon. `count` is a number shown after the label; `countField` reads\n\t\t * it off the record instead (a list counts its items). `overflow: true`\n\t\t * lists the tab under \"More\".\n\t\t *\n\t\t * `maxVisibleTabs` caps the strip: later tabs go under \"More\".\n\t\t * `hideEmpty: true` moves a tab whose count is 0 there too.\n\t\t * `moreLabel` names that menu. `variant: \"segmented\"` draws the strip\n\t\t * as a pill switch (CnTabs' segmented variant); `line` is the default.\n\t\t *\n\t\t * @type {{ tabs?: Array<{widgetId: string, label?: string, icon?: string, count?: number, countField?: string, overflow?: boolean}>, ariaLabel?: string, maxVisibleTabs?: number, hideEmpty?: boolean, moreLabel?: string, variant?: ('line'|'segmented') }}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Every widget definition available on the surface, for `content.tabs[]`\n\t\t * to reference by id.\n\t\t *\n\t\t * The surface passes its whole list rather than the resolved children\n\t\t * because a tab may name a widget that does not exist, and this component\n\t\t * has to be able to say so.\n\t\t *\n\t\t * @type {object[]}\n\t\t */\n\t\tavailableWidgets: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** The bound record's id. */\n\t\tobjectId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The loaded record, or null while it is still being fetched. */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The resolved object-type slug. */\n\t\tobjectType: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The resolved JSON Schema object, needed by a `data` child. */\n\t\tschemaObject: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** OpenRegister register slug of the surface. */\n\t\tregister: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** OpenRegister schema slug of the surface. */\n\t\tschema: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The effective object store. */\n\t\tstore: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Rendering surface forwarded to integration children (AD-19). */\n\t\tsurface: {\n\t\t\ttype: String,\n\t\t\tdefault: 'detail-page',\n\t\t},\n\n\t\t/** Object context forwarded to integration children. */\n\t\tintegrationContext: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The consumer's component registry, for custom child widget types. */\n\t\tcnRegistry: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** Show the Refresh entry in the hoisted Actions menu. */\n\t\tshowRefresh: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Show the Request-a-feature entry in the hoisted Actions menu. */\n\t\tshowRequestFeature: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Show the Documentation entry in the hoisted Actions menu. */\n\t\tshowDocumentation: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Documentation URL for the hoisted Actions menu. */\n\t\tdocumentationUrl: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\temits: ['geo-saved', 'open-integration'],\n\n\tdata() {\n\t\treturn {\n\t\t\tactiveIndex: 0,\n\t\t\t// Items published by the panels, keyed by widget id. Keyed rather\n\t\t\t// than a flat list because `lazy` keeps a visited tab mounted, so\n\t\t\t// more than one panel publishes at a time.\n\t\t\tpanelActionsByWidget: {},\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The open panel's own menu items, or an empty list.\n\t\t *\n\t\t * @return {object[]} PanelAction descriptors for the active tab.\n\t\t */\n\t\tactivePanelActions() {\n\t\t\tconst forId = this.panelActionsByWidget[this.activeWidgetId]\n\t\t\tif (!forId) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\t// Host first, then the widget's own: the catalog Add is about the\n\t\t\t// panel as a whole, the widget's items about what is in it.\n\t\t\treturn [...(forId.host || []), ...(forId.widget || [])]\n\t\t},\n\n\t\t/**\n\t\t * The configured tabs, each paired with the widget definition it names.\n\t\t *\n\t\t * A tab goes under the strip's \"More\" menu when it says `overflow`, when\n\t\t * `content.hideEmpty` is set and its count is 0, or when\n\t\t * `content.maxVisibleTabs` places are already taken. `count` is a\n\t\t * literal, or read off the record through `countField`.\n\t\t *\n\t\t * @return {object[]} `{ key, widgetId, label, icon, widget, count, overflow }` per tab.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-tab-counts-and-overflow\n\t\t */\n\t\tresolvedTabs() {\n\t\t\tconst tabs = Array.isArray(this.content?.tabs) ? this.content.tabs : []\n\t\t\tconst max = Number(this.content?.maxVisibleTabs)\n\t\t\tconst hideEmpty = this.content?.hideEmpty === true\n\t\t\t// How many tabs have taken a place in the strip so far. A tab that\n\t\t\t// is already going under \"More\" does not use one up.\n\t\t\tlet placed = 0\n\t\t\treturn tabs.map((tab, index) => {\n\t\t\t\tconst widgetId = typeof tab === 'string' ? tab : tab?.widgetId\n\t\t\t\tconst widget = this.availableWidgets.find((w) => w && w.id === widgetId) || null\n\t\t\t\tconst count = resolveTabCount(typeof tab === 'object' ? tab : null, this.objectData)\n\t\t\t\tlet overflow = (tab && tab.overflow === true) || (hideEmpty && count === 0)\n\t\t\t\tif (!overflow) {\n\t\t\t\t\tif (Number.isFinite(max) && max > 0 && placed >= max) {\n\t\t\t\t\t\toverflow = true\n\t\t\t\t\t} else {\n\t\t\t\t\t\tplaced += 1\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn {\n\t\t\t\t\tkey: `${widgetId || 'tab'}-${index}`,\n\t\t\t\t\twidgetId,\n\t\t\t\t\tlabel: (tab && tab.label) || widgetTitleOf(widget) || widgetId || '',\n\t\t\t\t\ticon: (tab && tab.icon) || widget?.icon || '',\n\t\t\t\t\twidget,\n\t\t\t\t\tcount,\n\t\t\t\t\toverflow,\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Name of the menu that holds the tabs that are not in the strip.\n\t\t *\n\t\t * @return {string|undefined} The configured label, or undefined for the default.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-tab-counts-and-overflow\n\t\t */\n\t\tmoreLabel() {\n\t\t\tconst label = this.content?.moreLabel\n\t\t\treturn typeof label === 'string' && label !== '' ? label : undefined\n\t\t},\n\n\t\t/**\n\t\t * The tab currently showing.\n\t\t *\n\t\t * @return {object|null} The resolved tab, or null when there are none.\n\t\t */\n\t\tactiveTab() {\n\t\t\treturn this.resolvedTabs[this.activeIndex] || null\n\t\t},\n\n\t\t/**\n\t\t * The active child's id, so the hoisted Refresh reaches THAT child's\n\t\t * fetch over `cn:widget:refresh` rather than a sibling's.\n\t\t *\n\t\t * @return {string} The active widget id.\n\t\t */\n\t\tactiveWidgetId() {\n\t\t\treturn this.activeTab?.widgetId || ''\n\t\t},\n\n\t\t/**\n\t\t * The active child's label, so the Actions menu names what it acts on.\n\t\t *\n\t\t * @return {string} The active tab's label.\n\t\t */\n\t\tactiveTitle() {\n\t\t\treturn this.activeTab?.label || ''\n\t\t},\n\n\t\t/**\n\t\t * The strip's look: `segmented` when the content asks for it, else\n\t\t * CnTabs' default `line`. An unknown value falls back to `line`.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-tabs-widget-renders-a-segmented-strip\n\t\t * @return {('line'|'segmented')} The CnTabs variant.\n\t\t */\n\t\tstripVariant() {\n\t\t\treturn this.content?.variant === 'segmented' ? 'segmented' : 'line'\n\t\t},\n\n\t\t/**\n\t\t * Accessible name for the tab strip.\n\t\t *\n\t\t * @return {string} The aria-label.\n\t\t */\n\t\tstripLabel() {\n\t\t\treturn this.content?.ariaLabel || t('nextcloud-vue', 'Details')\n\t\t},\n\t},\n\n\t/**\n\t * Open on the first tab that is in the strip. Tab 0 is the default, and\n\t * when tab 0 sits under \"More\" the strip would open on a tab the author\n\t * chose to tuck away.\n\t *\n\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-tab-counts-and-overflow\n\t */\n\tcreated() {\n\t\tconst first = this.resolvedTabs.findIndex((tab) => !tab.overflow)\n\t\tif (first > 0) {\n\t\t\tthis.activeIndex = first\n\t\t}\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Re-emit a geo child's save so the surface can reload the record.\n\t\t *\n\t\t * @param {object} geo The saved geometry.\n\t\t * @return {void}\n\t\t */\n\t\tonGeoSaved(geo) {\n\t\t\t/**\n\t\t\t * @event geo-saved Re-emitted from a geo child that saved a geometry.\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('geo-saved', geo)\n\t\t},\n\n\t\t/**\n\t\t * Re-emit a related child's request to open an integration.\n\t\t *\n\t\t * @param {string} integrationId The integration to open.\n\t\t * @return {void}\n\t\t */\n\t\tonOpenIntegration(integrationId) {\n\t\t\t/**\n\t\t\t * @event open-integration Re-emitted from a related child asking to open an integration.\n\t\t\t * @type {string}\n\t\t\t */\n\t\t\tthis.$emit('open-integration', integrationId)\n\t\t},\n\n\t\t/**\n\t\t * Follow the strip's own selection, so keyboard navigation moves the\n\t\t * hoisted Actions menu too.\n\t\t *\n\t\t * @param {number} index The newly selected tab index.\n\t\t * @return {void}\n\t\t */\n\t\tonTabChange(index) {\n\t\t\tif (typeof index === 'number' && index >= 0) {\n\t\t\t\tthis.activeIndex = index\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Empty-state text for a tab whose widget id resolves to nothing.\n\t\t *\n\t\t * @param {object} entry The resolved tab.\n\t\t * @return {string} The message.\n\t\t */\n\t\tmissingLabel(entry) {\n\t\t\treturn t('nextcloud-vue', 'No widget found for \"{id}\"', { id: entry.widgetId || '' })\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n/* The panel is the card; the strip is not inside it. There is no title bar\n above the tabs either: the open tab names the panel, so a title row would\n say the same thing twice and cost a row of height on a card that is mostly\n content.\n\n The card chrome used to wrap the strip as well, which drew a border and a\n pair of rounded corners ABOVE the tabs and boxed them in. Folder tabs are\n drawn as the edge of the sheet they open, so a second edge around them\n reads as a header the widget does not have. Border, radius and background\n now live on the panel below, and the strip sits bare on the page. */\n/* The class is doubled on purpose, and the four properties below are RESET\n rather than simply omitted.\n\n Nextcloud serves every enabled app's assets on every page, each app bundles\n this library's compiled CSS, and the Vue scope id is derived from the file\n PATH, so `data-v-1dbd6122` is byte-identical across library versions. An app\n still on an older release therefore ships a rule with exactly this selector\n and the OLD declarations, and it lands on the pages of an app already on the\n new one. Measured on a dossiq case page: three stale copies of the pre-fix\n rule, one from hermiq's `companion.css` (its companion bundle loads on every\n page by design) and two inline from other apps' bundles. Same specificity,\n so source order decided, and the card border came back around the strip.\n\n Dropping a declaration cannot beat a rule that sets it, so `border` and the\n rest are explicitly zeroed. Doubling the class takes this selector to\n (0,3,0) against the stale (0,2,0), which wins on specificity rather than on\n `!important` or on load order this library does not control. The same\n technique, for the same reason, is used on `.cn-tabs__nav .cn-tabs__nav-item`\n in CnTabs to beat Nextcloud's own button margin. */\n.cn-tabs-widget.cn-tabs-widget {\n\tbackground-color: transparent;\n\tborder: 0;\n\tborder-radius: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\theight: 100%;\n\tmin-height: 0;\n\toverflow: visible;\n\tpadding: 0;\n}\n\n/* No inset, so the first tab starts at the panel's own left edge. The 8px\n here was there to clear the card's rounded top corner; that corner is gone\n from the strip now, and the inset it existed for left the first tab floating\n 8px inside the sheet it opens.\n\n The bar's own bottom rule is switched off and redrawn as a pseudo-element\n that stops one corner radius short of the right edge. CnTabs draws its rule\n across the bar's whole width, which is the sheet's top edge everywhere\n except at the far right, where the panel's top-right corner curves away\n below it: the rule ran straight on to the widget's edge and read as a\n border sticking out past a rounded corner (Ruben, 2026-09-13). Ending it\n where the curve begins makes the rule and the curve one continuous edge. */\n.cn-tabs-widget__tabs :deep(.cn-tabs__bar) {\n\tborder-bottom: none;\n\tpadding: 0;\n\tposition: relative;\n}\n\n.cn-tabs-widget__tabs :deep(.cn-tabs__bar::after) {\n\tborder-bottom: 1px solid var(--color-border);\n\tcontent: '';\n\tinset-block-end: 0;\n\tinset-inline: 0 var(--border-radius-large);\n\tpointer-events: none;\n\tposition: absolute;\n}\n\n/* The open tab erases the slice of the rule directly above the panel with its\n own background-coloured bottom edge, which only works if it paints ABOVE the\n pseudo-element: a positioned pseudo-element otherwise paints over every\n in-flow sibling, rule included. */\n.cn-tabs-widget__tabs :deep(.cn-tabs__nav-item--active) {\n\tposition: relative;\n\tz-index: 1;\n}\n\n.cn-tabs-widget__tabs {\n\tdisplay: flex;\n\tflex-direction: column;\n\t/* Grow to the widget's height, so the panel fills the grid cell the\n\t layout gave it. Without this the strip plus the open panel took only\n\t the height of the panel's content, and a cell of nine rows ended in\n\t 130px of nothing under a sheet that stopped short of its own card. */\n\tflex: 1 1 auto;\n\tmin-height: 0;\n}\n\n/* The panel area is the scroll region, so the strip stays put while a long\n child scrolls under it.\n\n Padded on all four sides. The top was once zeroed to protect the join with\n the open tab, but padding sits inside the painted box — only a margin would\n have opened that gap, and a zero top put every panel's content against the\n strip. */\n.cn-tabs-widget__tabs :deep(.cn-tabs__content) {\n\tbackground-color: var(--color-main-background);\n\t/* Three sides only: the bar's rule (redrawn above, stopping at the corner)\n\t is this sheet's top edge, and the open tab erases the slice of it\n\t directly above the panel so the two read as one surface. A border-top\n\t here would put a second line under that tab which the tab cannot paint\n\t over. */\n\tborder: 1px solid var(--color-border);\n\tborder-top: none;\n\t/* Three rounded corners. The top-left stays square because the first tab\n\t is drawn joined to the panel there, and a curve under it would curl the\n\t sheet away from that tab. The top-right has no tab above it: the strip\n\t ends where the last tab ends and the Actions menu floats free, so a\n\t square corner there read as a sheet cut off, not as a tab's edge. */\n\tborder-radius: 0 var(--border-radius-large) var(--border-radius-large) var(--border-radius-large);\n\tflex: 1 1 auto;\n\tmin-height: 0;\n\toverflow: auto;\n\tpadding: 12px;\n}\n\n.cn-tabs-widget__tabs :deep(.cn-tab) {\n\theight: 100%;\n}\n\n.cn-tabs-widget__title {\n\talign-items: center;\n\tdisplay: inline-flex;\n\tgap: 6px;\n}\n\n.cn-tabs-widget__title-icon {\n\tflex: 0 0 auto;\n}\n</style>\n"],"names":["CnActionsMenu","CnDetailWidgetHost","CnIcon","CnTab","CnTabs","t"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CnTabsWidget.vue2.js","sources":["../../../../src/components/CnTabsWidget/CnTabsWidget.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-tabs-widget\">\n\t\t<CnTabs\n\t\t\t:aria-label=\"stripLabel\"\n\t\t\t:moreLabel=\"moreLabel\"\n\t\t\t:variant=\"stripVariant\"\n\t\t\tclass=\"cn-tabs-widget__tabs\"\n\t\t\t@update:activeIndex=\"onTabChange\">\n\t\t\t<!-- One Actions menu for the whole widget, bound to whichever child\n\t\t\t is showing. This is the point of the component: six tabbed\n\t\t\t widgets used to mean six card headers stacked down the page. -->\n\t\t\t<template #nav-end>\n\t\t\t\t<CnActionsMenu\n\t\t\t\t\t:showRefresh=\"showRefresh\"\n\t\t\t\t\t:showRequestFeature=\"showRequestFeature\"\n\t\t\t\t\t:showDocumentation=\"showDocumentation\"\n\t\t\t\t\t:documentationUrl=\"documentationUrl\"\n\t\t\t\t\t:widgetId=\"activeWidgetId\"\n\t\t\t\t\t:title=\"activeTitle\"\n\t\t\t\t\t:surface=\"`widget:${activeWidgetId}`\"\n\t\t\t\t\trefreshChannel=\"cn:widget:refresh\"\n\t\t\t\t\ttestidBase=\"cn-tabs-widget\">\n\t\t\t\t\t<!-- The open panel's own items, below the built-in trio.\n\t\t\t\t\t A panel draws no header, so these would otherwise have\n\t\t\t\t\t nowhere to go: the data widget's Metadata and full edit\n\t\t\t\t\t dialog vanished outright when the panel stopped drawing\n\t\t\t\t\t a card. They are rendered here rather than rebuilt here\n\t\t\t\t\t because `run` closes over the widget that published it,\n\t\t\t\t\t which is what keeps the dialog's per-widget config and\n\t\t\t\t\t the widget's own save path. See utils/panelActions.js.\n\n\t\t\t\t\t Only the ACTIVE panel's items. `lazy` keeps a visited\n\t\t\t\t\t tab mounted, so several panels publish at once and an\n\t\t\t\t\t unfiltered list would offer actions for a sheet nobody\n\t\t\t\t\t is looking at. -->\n\t\t\t\t\t<template v-if=\"activePanelActions.length\" #action-items>\n\t\t\t\t\t\t<NcActionButton\n\t\t\t\t\t\t\tv-for=\"action in activePanelActions\"\n\t\t\t\t\t\t\t:key=\"action.key\"\n\t\t\t\t\t\t\t:closeAfterClick=\"true\"\n\t\t\t\t\t\t\t@click=\"action.run()\">\n\t\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t\t<CnIcon :name=\"action.icon\" :size=\"20\" />\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t{{ action.label }}\n\t\t\t\t\t\t</NcActionButton>\n\t\t\t\t\t</template>\n\t\t\t\t</CnActionsMenu>\n\t\t\t</template>\n\n\t\t\t<CnTab\n\t\t\t\tv-for=\"(entry, index) in resolvedTabs\"\n\t\t\t\t:key=\"entry.key\"\n\t\t\t\t:active=\"index === activeIndex\"\n\t\t\t\t:title=\"entry.label\"\n\t\t\t\t:count=\"entry.count\"\n\t\t\t\t:overflow=\"entry.overflow\"\n\t\t\t\tlazy\n\t\t\t\t@click=\"activeIndex = index\">\n\t\t\t\t<template #title>\n\t\t\t\t\t<span class=\"cn-tabs-widget__title\">\n\t\t\t\t\t\t<CnIcon\n\t\t\t\t\t\t\tv-if=\"entry.icon\"\n\t\t\t\t\t\t\t:name=\"entry.icon\"\n\t\t\t\t\t\t\t:size=\"18\"\n\t\t\t\t\t\t\tclass=\"cn-tabs-widget__title-icon\" />\n\t\t\t\t\t\t{{ entry.label }}\n\t\t\t\t\t</span>\n\t\t\t\t</template>\n\n\t\t\t\t<CnDetailWidgetHost\n\t\t\t\t\tv-if=\"entry.widget\"\n\t\t\t\t\t:widget=\"entry.widget\"\n\t\t\t\t\tchrome=\"bare\"\n\t\t\t\t\t:objectId=\"objectId\"\n\t\t\t\t\t:object=\"objectData\"\n\t\t\t\t\t:objectType=\"objectType\"\n\t\t\t\t\t:schemaObject=\"schemaObject\"\n\t\t\t\t\t:register=\"register\"\n\t\t\t\t\t:schema=\"schema\"\n\t\t\t\t\t:store=\"store\"\n\t\t\t\t\t:surface=\"surface\"\n\t\t\t\t\t:integrationContext=\"integrationContext\"\n\t\t\t\t\t:cnRegistry=\"cnRegistry\"\n\t\t\t\t\t@geoSaved=\"onGeoSaved\"\n\t\t\t\t\t@openIntegration=\"onOpenIntegration\"\n\t\t\t\t\t@selectObject=\"onSelectObject\" />\n\t\t\t\t<NcEmptyContent v-else :name=\"missingLabel(entry)\" />\n\t\t\t</CnTab>\n\t\t</CnTabs>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcActionButton, NcEmptyContent } from '@nextcloud/vue'\nimport CnDetailWidgetHost from '../CnDetailWidgetHost/CnDetailWidgetHost.vue'\nimport CnIcon from '../CnIcon/CnIcon.vue'\nimport CnTab from '../CnTabs/CnTab.vue'\nimport CnTabs from '../CnTabs/CnTabs.vue'\nimport { resolveTabCount } from '../../utils/detailActionModel.js'\nimport { PANEL_ACTION_SINK } from '../../utils/panelActions.js'\nimport { widgetTitleOf } from '../../utils/widgetDispatch.js'\nimport { CnActionsMenu } from '../CnActionsMenu/index.js'\n\n/**\n * CnTabsWidget — a widget that holds other widgets, one per tab.\n *\n * A detail page that carries notes, files, related records, sub-records, mail\n * and appointments renders six cards, each with its own header and its own\n * Actions menu, stacked down the page. All six say roughly the same thing about\n * one record, and only one of them is being read at a time. This widget puts\n * them behind a tab strip instead.\n *\n * ## What the tabs take over\n *\n * The strip owns the title and the card. A child renders through\n * `CnDetailWidgetHost` with `chrome=\"bare\"`, so it draws no header and no\n * border of its own, and its content fills the panel exactly as it would have\n * filled its card. The tab label is what the widget's card header used to say.\n *\n * The Actions menu moves OUT of the panels and into the bar, beside the strip\n * rather than inside it, and rebinds to whichever child is showing. So there is\n * one menu, and it always acts on what you are looking at.\n *\n * ## Configuring it\n *\n * `content.tabs[]` names the children and their labels, so a deployment can\n * relabel or reorder tabs, or drop one, without touching code:\n *\n * ```js\n * content: {\n * tabs: [\n * { widgetId: 'case-notes', label: 'Notes', icon: 'NoteTextOutline' },\n * { widgetId: 'case-files', label: 'Files and attachments' },\n * ],\n * }\n * ```\n *\n * `label` and `icon` are optional and fall back to the child widget's own\n * title and icon, so the common case is a list of `widgetId`s.\n *\n * ## Panels are lazy, and stay mounted\n *\n * Each panel is a `CnTab` with `lazy`, so a child only mounts when its tab is\n * first opened. Six eager panels that each fetch on `mounted()` would fire six\n * requests on page load to answer five questions nobody asked. Once opened, a\n * panel stays mounted, so switching back never refetches.\n *\n * ## A named tab whose widget is missing\n *\n * The tab still renders, and its panel says which widget id did not resolve.\n * Dropping the tab would be worse: `content.tabs[]` is hand-authored config,\n * and a typo that silently removes a tab is a typo nobody finds.\n */\nexport default {\n\tname: 'CnTabsWidget',\n\n\tcomponents: {\n\t\tCnActionsMenu,\n\t\tCnDetailWidgetHost,\n\t\tCnIcon,\n\t\tCnTab,\n\t\tCnTabs,\n\t\tNcActionButton,\n\t\tNcEmptyContent,\n\t},\n\n\t/**\n\t * Offer the panels a way to put their own menu items in the strip's menu.\n\t *\n\t * Re-provided one level down by `CnDetailWidgetHost`, which knows the id to\n\t * key by; see utils/panelActions.js for why the items travel up instead of\n\t * being rebuilt here.\n\t *\n\t * @return {object} The provided sink.\n\t */\n\tprovide() {\n\t\treturn {\n\t\t\t[PANEL_ACTION_SINK]: {\n\t\t\t\t/**\n\t\t\t\t * Publish a panel's items, under one SOURCE.\n\t\t\t\t *\n\t\t\t\t * A panel has two possible publishers: the host, for an action\n\t\t\t\t * the host itself would have drawn (the catalog Add), and the\n\t\t\t\t * widget inside it, for its own menu items. Keyed by source so\n\t\t\t\t * the two coexist; a single slot per widget meant whichever\n\t\t\t\t * published last silently replaced the other.\n\t\t\t\t *\n\t\t\t\t * Replaces the map rather than mutating it so the computed that\n\t\t\t\t * reads it re-evaluates on any change.\n\t\t\t\t *\n\t\t\t\t * @param {string} id The publishing widget's id.\n\t\t\t\t * @param {object[]} items Its PanelAction descriptors.\n\t\t\t\t * @param {string} [source] Who is publishing: `host` or `widget`.\n\t\t\t\t * @return {void}\n\t\t\t\t */\n\t\t\t\tset: (id, items, source = 'widget') => {\n\t\t\t\t\t// `activePanelActions` spreads what is stored, so a\n\t\t\t\t\t// non-array becomes one menu item per character.\n\t\t\t\t\tif (!id || !Array.isArray(items)) {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tconst forId = { ...(this.panelActionsByWidget[id] || {}), [source]: items }\n\t\t\t\t\tthis.panelActionsByWidget = { ...this.panelActionsByWidget, [id]: forId }\n\t\t\t\t},\n\n\t\t\t\t/**\n\t\t\t\t * Withdraw one source's items, on unmount or when that\n\t\t\t\t * publisher's own menu comes back. The other source's items\n\t\t\t\t * stay.\n\t\t\t\t *\n\t\t\t\t * @param {string} id The publishing widget's id.\n\t\t\t\t * @param {string} [source] Who is withdrawing.\n\t\t\t\t * @return {void}\n\t\t\t\t */\n\t\t\t\tclear: (id, source = 'widget') => {\n\t\t\t\t\tconst forId = this.panelActionsByWidget[id]\n\t\t\t\t\tif (!forId || !(source in forId)) {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tconst { [source]: _removed, ...keptSources } = forId\n\t\t\t\t\tif (Object.keys(keptSources).length) {\n\t\t\t\t\t\tthis.panelActionsByWidget = { ...this.panelActionsByWidget, [id]: keptSources }\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tconst { [id]: _gone, ...rest } = this.panelActionsByWidget\n\t\t\t\t\tthis.panelActionsByWidget = rest\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget's config: `{ tabs, ariaLabel }`.\n\t\t *\n\t\t * `tabs[]` entries are `{ widgetId, label?, icon?, count?, countField?, overflow? }`.\n\t\t * `label` and `icon` fall back to the referenced widget's own title and\n\t\t * icon. `count` is a number shown after the label; `countField` reads\n\t\t * it off the record instead (a list counts its items). `overflow: true`\n\t\t * lists the tab under \"More\".\n\t\t *\n\t\t * `maxVisibleTabs` caps the strip: later tabs go under \"More\".\n\t\t * `hideEmpty: true` moves a tab whose count is 0 there too.\n\t\t * `moreLabel` names that menu. `variant: \"segmented\"` draws the strip\n\t\t * as a pill switch (CnTabs' segmented variant); `line` is the default.\n\t\t *\n\t\t * @type {{ tabs?: Array<{widgetId: string, label?: string, icon?: string, count?: number, countField?: string, overflow?: boolean}>, ariaLabel?: string, maxVisibleTabs?: number, hideEmpty?: boolean, moreLabel?: string, variant?: ('line'|'segmented') }}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Every widget definition available on the surface, for `content.tabs[]`\n\t\t * to reference by id.\n\t\t *\n\t\t * The surface passes its whole list rather than the resolved children\n\t\t * because a tab may name a widget that does not exist, and this component\n\t\t * has to be able to say so.\n\t\t *\n\t\t * @type {object[]}\n\t\t */\n\t\tavailableWidgets: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** The bound record's id. */\n\t\tobjectId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The loaded record, or null while it is still being fetched. */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The resolved object-type slug. */\n\t\tobjectType: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The resolved JSON Schema object, needed by a `data` child. */\n\t\tschemaObject: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** OpenRegister register slug of the surface. */\n\t\tregister: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** OpenRegister schema slug of the surface. */\n\t\tschema: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The effective object store. */\n\t\tstore: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Rendering surface forwarded to integration children (AD-19). */\n\t\tsurface: {\n\t\t\ttype: String,\n\t\t\tdefault: 'detail-page',\n\t\t},\n\n\t\t/** Object context forwarded to integration children. */\n\t\tintegrationContext: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The consumer's component registry, for custom child widget types. */\n\t\tcnRegistry: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** Show the Refresh entry in the hoisted Actions menu. */\n\t\tshowRefresh: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Show the Request-a-feature entry in the hoisted Actions menu. */\n\t\tshowRequestFeature: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Show the Documentation entry in the hoisted Actions menu. */\n\t\tshowDocumentation: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Documentation URL for the hoisted Actions menu. */\n\t\tdocumentationUrl: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\temits: ['geo-saved', 'open-integration', 'select-object'],\n\n\tdata() {\n\t\treturn {\n\t\t\tactiveIndex: 0,\n\t\t\t// Items published by the panels, keyed by widget id. Keyed rather\n\t\t\t// than a flat list because `lazy` keeps a visited tab mounted, so\n\t\t\t// more than one panel publishes at a time.\n\t\t\tpanelActionsByWidget: {},\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The open panel's own menu items, or an empty list.\n\t\t *\n\t\t * @return {object[]} PanelAction descriptors for the active tab.\n\t\t */\n\t\tactivePanelActions() {\n\t\t\tconst forId = this.panelActionsByWidget[this.activeWidgetId]\n\t\t\tif (!forId) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\t// Host first, then the widget's own: the catalog Add is about the\n\t\t\t// panel as a whole, the widget's items about what is in it.\n\t\t\treturn [...(forId.host || []), ...(forId.widget || [])]\n\t\t},\n\n\t\t/**\n\t\t * The configured tabs, each paired with the widget definition it names.\n\t\t *\n\t\t * A tab goes under the strip's \"More\" menu when it says `overflow`, when\n\t\t * `content.hideEmpty` is set and its count is 0, or when\n\t\t * `content.maxVisibleTabs` places are already taken. `count` is a\n\t\t * literal, or read off the record through `countField`.\n\t\t *\n\t\t * @return {object[]} `{ key, widgetId, label, icon, widget, count, overflow }` per tab.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-tab-counts-and-overflow\n\t\t */\n\t\tresolvedTabs() {\n\t\t\tconst tabs = Array.isArray(this.content?.tabs) ? this.content.tabs : []\n\t\t\tconst max = Number(this.content?.maxVisibleTabs)\n\t\t\tconst hideEmpty = this.content?.hideEmpty === true\n\t\t\t// How many tabs have taken a place in the strip so far. A tab that\n\t\t\t// is already going under \"More\" does not use one up.\n\t\t\tlet placed = 0\n\t\t\treturn tabs.map((tab, index) => {\n\t\t\t\tconst widgetId = typeof tab === 'string' ? tab : tab?.widgetId\n\t\t\t\tconst widget = this.availableWidgets.find((w) => w && w.id === widgetId) || null\n\t\t\t\tconst count = resolveTabCount(typeof tab === 'object' ? tab : null, this.objectData)\n\t\t\t\tlet overflow = (tab && tab.overflow === true) || (hideEmpty && count === 0)\n\t\t\t\tif (!overflow) {\n\t\t\t\t\tif (Number.isFinite(max) && max > 0 && placed >= max) {\n\t\t\t\t\t\toverflow = true\n\t\t\t\t\t} else {\n\t\t\t\t\t\tplaced += 1\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn {\n\t\t\t\t\tkey: `${widgetId || 'tab'}-${index}`,\n\t\t\t\t\twidgetId,\n\t\t\t\t\tlabel: (tab && tab.label) || widgetTitleOf(widget) || widgetId || '',\n\t\t\t\t\ticon: (tab && tab.icon) || widget?.icon || '',\n\t\t\t\t\twidget,\n\t\t\t\t\tcount,\n\t\t\t\t\toverflow,\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Name of the menu that holds the tabs that are not in the strip.\n\t\t *\n\t\t * @return {string|undefined} The configured label, or undefined for the default.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-tab-counts-and-overflow\n\t\t */\n\t\tmoreLabel() {\n\t\t\tconst label = this.content?.moreLabel\n\t\t\treturn typeof label === 'string' && label !== '' ? label : undefined\n\t\t},\n\n\t\t/**\n\t\t * The tab currently showing.\n\t\t *\n\t\t * @return {object|null} The resolved tab, or null when there are none.\n\t\t */\n\t\tactiveTab() {\n\t\t\treturn this.resolvedTabs[this.activeIndex] || null\n\t\t},\n\n\t\t/**\n\t\t * The active child's id, so the hoisted Refresh reaches THAT child's\n\t\t * fetch over `cn:widget:refresh` rather than a sibling's.\n\t\t *\n\t\t * @return {string} The active widget id.\n\t\t */\n\t\tactiveWidgetId() {\n\t\t\treturn this.activeTab?.widgetId || ''\n\t\t},\n\n\t\t/**\n\t\t * The active child's label, so the Actions menu names what it acts on.\n\t\t *\n\t\t * @return {string} The active tab's label.\n\t\t */\n\t\tactiveTitle() {\n\t\t\treturn this.activeTab?.label || ''\n\t\t},\n\n\t\t/**\n\t\t * The strip's look: `segmented` when the content asks for it, else\n\t\t * CnTabs' default `line`. An unknown value falls back to `line`.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-tabs-widget-renders-a-segmented-strip\n\t\t * @return {('line'|'segmented')} The CnTabs variant.\n\t\t */\n\t\tstripVariant() {\n\t\t\treturn this.content?.variant === 'segmented' ? 'segmented' : 'line'\n\t\t},\n\n\t\t/**\n\t\t * Accessible name for the tab strip.\n\t\t *\n\t\t * @return {string} The aria-label.\n\t\t */\n\t\tstripLabel() {\n\t\t\treturn this.content?.ariaLabel || t('nextcloud-vue', 'Details')\n\t\t},\n\t},\n\n\t/**\n\t * Open on the first tab that is in the strip. Tab 0 is the default, and\n\t * when tab 0 sits under \"More\" the strip would open on a tab the author\n\t * chose to tuck away.\n\t *\n\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-tab-counts-and-overflow\n\t */\n\tcreated() {\n\t\tconst first = this.resolvedTabs.findIndex((tab) => !tab.overflow)\n\t\tif (first > 0) {\n\t\t\tthis.activeIndex = first\n\t\t}\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Re-emit a geo child's save so the surface can reload the record.\n\t\t *\n\t\t * @param {object} geo The saved geometry.\n\t\t * @return {void}\n\t\t */\n\t\tonGeoSaved(geo) {\n\t\t\t/**\n\t\t\t * @event geo-saved Re-emitted from a geo child that saved a geometry.\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('geo-saved', geo)\n\t\t},\n\n\t\t/**\n\t\t * Re-emit a related child's request to open an integration.\n\t\t *\n\t\t * @param {string} integrationId The integration to open.\n\t\t * @return {void}\n\t\t */\n\t\tonOpenIntegration(integrationId) {\n\t\t\t/**\n\t\t\t * @event open-integration Re-emitted from a related child asking to open an integration.\n\t\t\t * @type {string}\n\t\t\t */\n\t\t\tthis.$emit('open-integration', integrationId)\n\t\t},\n\n\t\t/**\n\t\t * Re-emit a related child's object click; the page opens the object.\n\t\t *\n\t\t * @param {object} raw The clicked object.\n\t\t * @return {void}\n\t\t */\n\t\tonSelectObject(raw) {\n\t\t\t/**\n\t\t\t * @event select-object Re-emitted from a related child whose object row was clicked. Payload is the raw object.\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('select-object', raw)\n\t\t},\n\n\t\t/**\n\t\t * Follow the strip's own selection, so keyboard navigation moves the\n\t\t * hoisted Actions menu too.\n\t\t *\n\t\t * @param {number} index The newly selected tab index.\n\t\t * @return {void}\n\t\t */\n\t\tonTabChange(index) {\n\t\t\tif (typeof index === 'number' && index >= 0) {\n\t\t\t\tthis.activeIndex = index\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Empty-state text for a tab whose widget id resolves to nothing.\n\t\t *\n\t\t * @param {object} entry The resolved tab.\n\t\t * @return {string} The message.\n\t\t */\n\t\tmissingLabel(entry) {\n\t\t\treturn t('nextcloud-vue', 'No widget found for \"{id}\"', { id: entry.widgetId || '' })\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n/* The panel is the card; the strip is not inside it. There is no title bar\n above the tabs either: the open tab names the panel, so a title row would\n say the same thing twice and cost a row of height on a card that is mostly\n content.\n\n The card chrome used to wrap the strip as well, which drew a border and a\n pair of rounded corners ABOVE the tabs and boxed them in. Folder tabs are\n drawn as the edge of the sheet they open, so a second edge around them\n reads as a header the widget does not have. Border, radius and background\n now live on the panel below, and the strip sits bare on the page. */\n/* The class is doubled on purpose, and the four properties below are RESET\n rather than simply omitted.\n\n Nextcloud serves every enabled app's assets on every page, each app bundles\n this library's compiled CSS, and the Vue scope id is derived from the file\n PATH, so `data-v-1dbd6122` is byte-identical across library versions. An app\n still on an older release therefore ships a rule with exactly this selector\n and the OLD declarations, and it lands on the pages of an app already on the\n new one. Measured on a dossiq case page: three stale copies of the pre-fix\n rule, one from hermiq's `companion.css` (its companion bundle loads on every\n page by design) and two inline from other apps' bundles. Same specificity,\n so source order decided, and the card border came back around the strip.\n\n Dropping a declaration cannot beat a rule that sets it, so `border` and the\n rest are explicitly zeroed. Doubling the class takes this selector to\n (0,3,0) against the stale (0,2,0), which wins on specificity rather than on\n `!important` or on load order this library does not control. The same\n technique, for the same reason, is used on `.cn-tabs__nav .cn-tabs__nav-item`\n in CnTabs to beat Nextcloud's own button margin. */\n.cn-tabs-widget.cn-tabs-widget {\n\tbackground-color: transparent;\n\tborder: 0;\n\tborder-radius: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\theight: 100%;\n\tmin-height: 0;\n\toverflow: visible;\n\tpadding: 0;\n}\n\n/* No inset, so the first tab starts at the panel's own left edge. The 8px\n here was there to clear the card's rounded top corner; that corner is gone\n from the strip now, and the inset it existed for left the first tab floating\n 8px inside the sheet it opens.\n\n The bar's own bottom rule is switched off and redrawn as a pseudo-element\n that stops one corner radius short of the right edge. CnTabs draws its rule\n across the bar's whole width, which is the sheet's top edge everywhere\n except at the far right, where the panel's top-right corner curves away\n below it: the rule ran straight on to the widget's edge and read as a\n border sticking out past a rounded corner (Ruben, 2026-09-13). Ending it\n where the curve begins makes the rule and the curve one continuous edge. */\n.cn-tabs-widget__tabs :deep(.cn-tabs__bar) {\n\tborder-bottom: none;\n\tpadding: 0;\n\tposition: relative;\n}\n\n.cn-tabs-widget__tabs :deep(.cn-tabs__bar::after) {\n\tborder-bottom: 1px solid var(--color-border);\n\tcontent: '';\n\tinset-block-end: 0;\n\tinset-inline: 0 var(--border-radius-large);\n\tpointer-events: none;\n\tposition: absolute;\n}\n\n/* The open tab erases the slice of the rule directly above the panel with its\n own background-coloured bottom edge, which only works if it paints ABOVE the\n pseudo-element: a positioned pseudo-element otherwise paints over every\n in-flow sibling, rule included. */\n.cn-tabs-widget__tabs :deep(.cn-tabs__nav-item--active) {\n\tposition: relative;\n\tz-index: 1;\n}\n\n.cn-tabs-widget__tabs {\n\tdisplay: flex;\n\tflex-direction: column;\n\t/* Grow to the widget's height, so the panel fills the grid cell the\n\t layout gave it. Without this the strip plus the open panel took only\n\t the height of the panel's content, and a cell of nine rows ended in\n\t 130px of nothing under a sheet that stopped short of its own card. */\n\tflex: 1 1 auto;\n\tmin-height: 0;\n}\n\n/* The panel area is the scroll region, so the strip stays put while a long\n child scrolls under it.\n\n Padded on all four sides. The top was once zeroed to protect the join with\n the open tab, but padding sits inside the painted box — only a margin would\n have opened that gap, and a zero top put every panel's content against the\n strip. */\n.cn-tabs-widget__tabs :deep(.cn-tabs__content) {\n\tbackground-color: var(--color-main-background);\n\t/* Three sides only: the bar's rule (redrawn above, stopping at the corner)\n\t is this sheet's top edge, and the open tab erases the slice of it\n\t directly above the panel so the two read as one surface. A border-top\n\t here would put a second line under that tab which the tab cannot paint\n\t over. */\n\tborder: 1px solid var(--color-border);\n\tborder-top: none;\n\t/* Three rounded corners. The top-left stays square because the first tab\n\t is drawn joined to the panel there, and a curve under it would curl the\n\t sheet away from that tab. The top-right has no tab above it: the strip\n\t ends where the last tab ends and the Actions menu floats free, so a\n\t square corner there read as a sheet cut off, not as a tab's edge. */\n\tborder-radius: 0 var(--border-radius-large) var(--border-radius-large) var(--border-radius-large);\n\tflex: 1 1 auto;\n\tmin-height: 0;\n\toverflow: auto;\n\tpadding: 12px;\n}\n\n.cn-tabs-widget__tabs :deep(.cn-tab) {\n\theight: 100%;\n}\n\n.cn-tabs-widget__title {\n\talign-items: center;\n\tdisplay: inline-flex;\n\tgap: 6px;\n}\n\n.cn-tabs-widget__title-icon {\n\tflex: 0 0 auto;\n}\n</style>\n"],"names":["CnActionsMenu","CnDetailWidgetHost","CnIcon","CnTab","CnTabs","t"],"mappings":"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"}
|
|
@@ -101,8 +101,9 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
|
|
|
101
101
|
integrationContext: $props.integrationContext,
|
|
102
102
|
cnRegistry: $props.cnRegistry,
|
|
103
103
|
onGeoSaved: $options.onGeoSaved,
|
|
104
|
-
onOpenIntegration: $options.onOpenIntegration
|
|
105
|
-
|
|
104
|
+
onOpenIntegration: $options.onOpenIntegration,
|
|
105
|
+
onSelectObject: $options.onSelectObject
|
|
106
|
+
}, null, 8 /* PROPS */, ["widget", "objectId", "object", "objectType", "schemaObject", "register", "schema", "store", "surface", "integrationContext", "cnRegistry", "onGeoSaved", "onOpenIntegration", "onSelectObject"]))
|
|
106
107
|
: (openBlock(), createBlock(_component_NcEmptyContent, {
|
|
107
108
|
key: 1,
|
|
108
109
|
name: $options.missingLabel(entry)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnTabsWidget.vue3.js","sources":["../../../../src/components/CnTabsWidget/CnTabsWidget.vue?vue&type=template&id=1dbd6122&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-tabs-widget\">\n\t\t<CnTabs\n\t\t\t:aria-label=\"stripLabel\"\n\t\t\t:moreLabel=\"moreLabel\"\n\t\t\t:variant=\"stripVariant\"\n\t\t\tclass=\"cn-tabs-widget__tabs\"\n\t\t\t@update:activeIndex=\"onTabChange\">\n\t\t\t<!-- One Actions menu for the whole widget, bound to whichever child\n\t\t\t is showing. This is the point of the component: six tabbed\n\t\t\t widgets used to mean six card headers stacked down the page. -->\n\t\t\t<template #nav-end>\n\t\t\t\t<CnActionsMenu\n\t\t\t\t\t:showRefresh=\"showRefresh\"\n\t\t\t\t\t:showRequestFeature=\"showRequestFeature\"\n\t\t\t\t\t:showDocumentation=\"showDocumentation\"\n\t\t\t\t\t:documentationUrl=\"documentationUrl\"\n\t\t\t\t\t:widgetId=\"activeWidgetId\"\n\t\t\t\t\t:title=\"activeTitle\"\n\t\t\t\t\t:surface=\"`widget:${activeWidgetId}`\"\n\t\t\t\t\trefreshChannel=\"cn:widget:refresh\"\n\t\t\t\t\ttestidBase=\"cn-tabs-widget\">\n\t\t\t\t\t<!-- The open panel's own items, below the built-in trio.\n\t\t\t\t\t A panel draws no header, so these would otherwise have\n\t\t\t\t\t nowhere to go: the data widget's Metadata and full edit\n\t\t\t\t\t dialog vanished outright when the panel stopped drawing\n\t\t\t\t\t a card. They are rendered here rather than rebuilt here\n\t\t\t\t\t because `run` closes over the widget that published it,\n\t\t\t\t\t which is what keeps the dialog's per-widget config and\n\t\t\t\t\t the widget's own save path. See utils/panelActions.js.\n\n\t\t\t\t\t Only the ACTIVE panel's items. `lazy` keeps a visited\n\t\t\t\t\t tab mounted, so several panels publish at once and an\n\t\t\t\t\t unfiltered list would offer actions for a sheet nobody\n\t\t\t\t\t is looking at. -->\n\t\t\t\t\t<template v-if=\"activePanelActions.length\" #action-items>\n\t\t\t\t\t\t<NcActionButton\n\t\t\t\t\t\t\tv-for=\"action in activePanelActions\"\n\t\t\t\t\t\t\t:key=\"action.key\"\n\t\t\t\t\t\t\t:closeAfterClick=\"true\"\n\t\t\t\t\t\t\t@click=\"action.run()\">\n\t\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t\t<CnIcon :name=\"action.icon\" :size=\"20\" />\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t{{ action.label }}\n\t\t\t\t\t\t</NcActionButton>\n\t\t\t\t\t</template>\n\t\t\t\t</CnActionsMenu>\n\t\t\t</template>\n\n\t\t\t<CnTab\n\t\t\t\tv-for=\"(entry, index) in resolvedTabs\"\n\t\t\t\t:key=\"entry.key\"\n\t\t\t\t:active=\"index === activeIndex\"\n\t\t\t\t:title=\"entry.label\"\n\t\t\t\t:count=\"entry.count\"\n\t\t\t\t:overflow=\"entry.overflow\"\n\t\t\t\tlazy\n\t\t\t\t@click=\"activeIndex = index\">\n\t\t\t\t<template #title>\n\t\t\t\t\t<span class=\"cn-tabs-widget__title\">\n\t\t\t\t\t\t<CnIcon\n\t\t\t\t\t\t\tv-if=\"entry.icon\"\n\t\t\t\t\t\t\t:name=\"entry.icon\"\n\t\t\t\t\t\t\t:size=\"18\"\n\t\t\t\t\t\t\tclass=\"cn-tabs-widget__title-icon\" />\n\t\t\t\t\t\t{{ entry.label }}\n\t\t\t\t\t</span>\n\t\t\t\t</template>\n\n\t\t\t\t<CnDetailWidgetHost\n\t\t\t\t\tv-if=\"entry.widget\"\n\t\t\t\t\t:widget=\"entry.widget\"\n\t\t\t\t\tchrome=\"bare\"\n\t\t\t\t\t:objectId=\"objectId\"\n\t\t\t\t\t:object=\"objectData\"\n\t\t\t\t\t:objectType=\"objectType\"\n\t\t\t\t\t:schemaObject=\"schemaObject\"\n\t\t\t\t\t:register=\"register\"\n\t\t\t\t\t:schema=\"schema\"\n\t\t\t\t\t:store=\"store\"\n\t\t\t\t\t:surface=\"surface\"\n\t\t\t\t\t:integrationContext=\"integrationContext\"\n\t\t\t\t\t:cnRegistry=\"cnRegistry\"\n\t\t\t\t\t@geoSaved=\"onGeoSaved\"\n\t\t\t\t\t@openIntegration=\"onOpenIntegration\" />\n\t\t\t\t<NcEmptyContent v-else :name=\"missingLabel(entry)\" />\n\t\t\t</CnTab>\n\t\t</CnTabs>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcActionButton, NcEmptyContent } from '@nextcloud/vue'\nimport CnDetailWidgetHost from '../CnDetailWidgetHost/CnDetailWidgetHost.vue'\nimport CnIcon from '../CnIcon/CnIcon.vue'\nimport CnTab from '../CnTabs/CnTab.vue'\nimport CnTabs from '../CnTabs/CnTabs.vue'\nimport { resolveTabCount } from '../../utils/detailActionModel.js'\nimport { PANEL_ACTION_SINK } from '../../utils/panelActions.js'\nimport { widgetTitleOf } from '../../utils/widgetDispatch.js'\nimport { CnActionsMenu } from '../CnActionsMenu/index.js'\n\n/**\n * CnTabsWidget — a widget that holds other widgets, one per tab.\n *\n * A detail page that carries notes, files, related records, sub-records, mail\n * and appointments renders six cards, each with its own header and its own\n * Actions menu, stacked down the page. All six say roughly the same thing about\n * one record, and only one of them is being read at a time. This widget puts\n * them behind a tab strip instead.\n *\n * ## What the tabs take over\n *\n * The strip owns the title and the card. A child renders through\n * `CnDetailWidgetHost` with `chrome=\"bare\"`, so it draws no header and no\n * border of its own, and its content fills the panel exactly as it would have\n * filled its card. The tab label is what the widget's card header used to say.\n *\n * The Actions menu moves OUT of the panels and into the bar, beside the strip\n * rather than inside it, and rebinds to whichever child is showing. So there is\n * one menu, and it always acts on what you are looking at.\n *\n * ## Configuring it\n *\n * `content.tabs[]` names the children and their labels, so a deployment can\n * relabel or reorder tabs, or drop one, without touching code:\n *\n * ```js\n * content: {\n * tabs: [\n * { widgetId: 'case-notes', label: 'Notes', icon: 'NoteTextOutline' },\n * { widgetId: 'case-files', label: 'Files and attachments' },\n * ],\n * }\n * ```\n *\n * `label` and `icon` are optional and fall back to the child widget's own\n * title and icon, so the common case is a list of `widgetId`s.\n *\n * ## Panels are lazy, and stay mounted\n *\n * Each panel is a `CnTab` with `lazy`, so a child only mounts when its tab is\n * first opened. Six eager panels that each fetch on `mounted()` would fire six\n * requests on page load to answer five questions nobody asked. Once opened, a\n * panel stays mounted, so switching back never refetches.\n *\n * ## A named tab whose widget is missing\n *\n * The tab still renders, and its panel says which widget id did not resolve.\n * Dropping the tab would be worse: `content.tabs[]` is hand-authored config,\n * and a typo that silently removes a tab is a typo nobody finds.\n */\nexport default {\n\tname: 'CnTabsWidget',\n\n\tcomponents: {\n\t\tCnActionsMenu,\n\t\tCnDetailWidgetHost,\n\t\tCnIcon,\n\t\tCnTab,\n\t\tCnTabs,\n\t\tNcActionButton,\n\t\tNcEmptyContent,\n\t},\n\n\t/**\n\t * Offer the panels a way to put their own menu items in the strip's menu.\n\t *\n\t * Re-provided one level down by `CnDetailWidgetHost`, which knows the id to\n\t * key by; see utils/panelActions.js for why the items travel up instead of\n\t * being rebuilt here.\n\t *\n\t * @return {object} The provided sink.\n\t */\n\tprovide() {\n\t\treturn {\n\t\t\t[PANEL_ACTION_SINK]: {\n\t\t\t\t/**\n\t\t\t\t * Publish a panel's items, under one SOURCE.\n\t\t\t\t *\n\t\t\t\t * A panel has two possible publishers: the host, for an action\n\t\t\t\t * the host itself would have drawn (the catalog Add), and the\n\t\t\t\t * widget inside it, for its own menu items. Keyed by source so\n\t\t\t\t * the two coexist; a single slot per widget meant whichever\n\t\t\t\t * published last silently replaced the other.\n\t\t\t\t *\n\t\t\t\t * Replaces the map rather than mutating it so the computed that\n\t\t\t\t * reads it re-evaluates on any change.\n\t\t\t\t *\n\t\t\t\t * @param {string} id The publishing widget's id.\n\t\t\t\t * @param {object[]} items Its PanelAction descriptors.\n\t\t\t\t * @param {string} [source] Who is publishing: `host` or `widget`.\n\t\t\t\t * @return {void}\n\t\t\t\t */\n\t\t\t\tset: (id, items, source = 'widget') => {\n\t\t\t\t\t// `activePanelActions` spreads what is stored, so a\n\t\t\t\t\t// non-array becomes one menu item per character.\n\t\t\t\t\tif (!id || !Array.isArray(items)) {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tconst forId = { ...(this.panelActionsByWidget[id] || {}), [source]: items }\n\t\t\t\t\tthis.panelActionsByWidget = { ...this.panelActionsByWidget, [id]: forId }\n\t\t\t\t},\n\n\t\t\t\t/**\n\t\t\t\t * Withdraw one source's items, on unmount or when that\n\t\t\t\t * publisher's own menu comes back. The other source's items\n\t\t\t\t * stay.\n\t\t\t\t *\n\t\t\t\t * @param {string} id The publishing widget's id.\n\t\t\t\t * @param {string} [source] Who is withdrawing.\n\t\t\t\t * @return {void}\n\t\t\t\t */\n\t\t\t\tclear: (id, source = 'widget') => {\n\t\t\t\t\tconst forId = this.panelActionsByWidget[id]\n\t\t\t\t\tif (!forId || !(source in forId)) {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tconst { [source]: _removed, ...keptSources } = forId\n\t\t\t\t\tif (Object.keys(keptSources).length) {\n\t\t\t\t\t\tthis.panelActionsByWidget = { ...this.panelActionsByWidget, [id]: keptSources }\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tconst { [id]: _gone, ...rest } = this.panelActionsByWidget\n\t\t\t\t\tthis.panelActionsByWidget = rest\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget's config: `{ tabs, ariaLabel }`.\n\t\t *\n\t\t * `tabs[]` entries are `{ widgetId, label?, icon?, count?, countField?, overflow? }`.\n\t\t * `label` and `icon` fall back to the referenced widget's own title and\n\t\t * icon. `count` is a number shown after the label; `countField` reads\n\t\t * it off the record instead (a list counts its items). `overflow: true`\n\t\t * lists the tab under \"More\".\n\t\t *\n\t\t * `maxVisibleTabs` caps the strip: later tabs go under \"More\".\n\t\t * `hideEmpty: true` moves a tab whose count is 0 there too.\n\t\t * `moreLabel` names that menu. `variant: \"segmented\"` draws the strip\n\t\t * as a pill switch (CnTabs' segmented variant); `line` is the default.\n\t\t *\n\t\t * @type {{ tabs?: Array<{widgetId: string, label?: string, icon?: string, count?: number, countField?: string, overflow?: boolean}>, ariaLabel?: string, maxVisibleTabs?: number, hideEmpty?: boolean, moreLabel?: string, variant?: ('line'|'segmented') }}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Every widget definition available on the surface, for `content.tabs[]`\n\t\t * to reference by id.\n\t\t *\n\t\t * The surface passes its whole list rather than the resolved children\n\t\t * because a tab may name a widget that does not exist, and this component\n\t\t * has to be able to say so.\n\t\t *\n\t\t * @type {object[]}\n\t\t */\n\t\tavailableWidgets: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** The bound record's id. */\n\t\tobjectId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The loaded record, or null while it is still being fetched. */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The resolved object-type slug. */\n\t\tobjectType: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The resolved JSON Schema object, needed by a `data` child. */\n\t\tschemaObject: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** OpenRegister register slug of the surface. */\n\t\tregister: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** OpenRegister schema slug of the surface. */\n\t\tschema: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The effective object store. */\n\t\tstore: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Rendering surface forwarded to integration children (AD-19). */\n\t\tsurface: {\n\t\t\ttype: String,\n\t\t\tdefault: 'detail-page',\n\t\t},\n\n\t\t/** Object context forwarded to integration children. */\n\t\tintegrationContext: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The consumer's component registry, for custom child widget types. */\n\t\tcnRegistry: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** Show the Refresh entry in the hoisted Actions menu. */\n\t\tshowRefresh: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Show the Request-a-feature entry in the hoisted Actions menu. */\n\t\tshowRequestFeature: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Show the Documentation entry in the hoisted Actions menu. */\n\t\tshowDocumentation: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Documentation URL for the hoisted Actions menu. */\n\t\tdocumentationUrl: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\temits: ['geo-saved', 'open-integration'],\n\n\tdata() {\n\t\treturn {\n\t\t\tactiveIndex: 0,\n\t\t\t// Items published by the panels, keyed by widget id. Keyed rather\n\t\t\t// than a flat list because `lazy` keeps a visited tab mounted, so\n\t\t\t// more than one panel publishes at a time.\n\t\t\tpanelActionsByWidget: {},\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The open panel's own menu items, or an empty list.\n\t\t *\n\t\t * @return {object[]} PanelAction descriptors for the active tab.\n\t\t */\n\t\tactivePanelActions() {\n\t\t\tconst forId = this.panelActionsByWidget[this.activeWidgetId]\n\t\t\tif (!forId) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\t// Host first, then the widget's own: the catalog Add is about the\n\t\t\t// panel as a whole, the widget's items about what is in it.\n\t\t\treturn [...(forId.host || []), ...(forId.widget || [])]\n\t\t},\n\n\t\t/**\n\t\t * The configured tabs, each paired with the widget definition it names.\n\t\t *\n\t\t * A tab goes under the strip's \"More\" menu when it says `overflow`, when\n\t\t * `content.hideEmpty` is set and its count is 0, or when\n\t\t * `content.maxVisibleTabs` places are already taken. `count` is a\n\t\t * literal, or read off the record through `countField`.\n\t\t *\n\t\t * @return {object[]} `{ key, widgetId, label, icon, widget, count, overflow }` per tab.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-tab-counts-and-overflow\n\t\t */\n\t\tresolvedTabs() {\n\t\t\tconst tabs = Array.isArray(this.content?.tabs) ? this.content.tabs : []\n\t\t\tconst max = Number(this.content?.maxVisibleTabs)\n\t\t\tconst hideEmpty = this.content?.hideEmpty === true\n\t\t\t// How many tabs have taken a place in the strip so far. A tab that\n\t\t\t// is already going under \"More\" does not use one up.\n\t\t\tlet placed = 0\n\t\t\treturn tabs.map((tab, index) => {\n\t\t\t\tconst widgetId = typeof tab === 'string' ? tab : tab?.widgetId\n\t\t\t\tconst widget = this.availableWidgets.find((w) => w && w.id === widgetId) || null\n\t\t\t\tconst count = resolveTabCount(typeof tab === 'object' ? tab : null, this.objectData)\n\t\t\t\tlet overflow = (tab && tab.overflow === true) || (hideEmpty && count === 0)\n\t\t\t\tif (!overflow) {\n\t\t\t\t\tif (Number.isFinite(max) && max > 0 && placed >= max) {\n\t\t\t\t\t\toverflow = true\n\t\t\t\t\t} else {\n\t\t\t\t\t\tplaced += 1\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn {\n\t\t\t\t\tkey: `${widgetId || 'tab'}-${index}`,\n\t\t\t\t\twidgetId,\n\t\t\t\t\tlabel: (tab && tab.label) || widgetTitleOf(widget) || widgetId || '',\n\t\t\t\t\ticon: (tab && tab.icon) || widget?.icon || '',\n\t\t\t\t\twidget,\n\t\t\t\t\tcount,\n\t\t\t\t\toverflow,\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Name of the menu that holds the tabs that are not in the strip.\n\t\t *\n\t\t * @return {string|undefined} The configured label, or undefined for the default.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-tab-counts-and-overflow\n\t\t */\n\t\tmoreLabel() {\n\t\t\tconst label = this.content?.moreLabel\n\t\t\treturn typeof label === 'string' && label !== '' ? label : undefined\n\t\t},\n\n\t\t/**\n\t\t * The tab currently showing.\n\t\t *\n\t\t * @return {object|null} The resolved tab, or null when there are none.\n\t\t */\n\t\tactiveTab() {\n\t\t\treturn this.resolvedTabs[this.activeIndex] || null\n\t\t},\n\n\t\t/**\n\t\t * The active child's id, so the hoisted Refresh reaches THAT child's\n\t\t * fetch over `cn:widget:refresh` rather than a sibling's.\n\t\t *\n\t\t * @return {string} The active widget id.\n\t\t */\n\t\tactiveWidgetId() {\n\t\t\treturn this.activeTab?.widgetId || ''\n\t\t},\n\n\t\t/**\n\t\t * The active child's label, so the Actions menu names what it acts on.\n\t\t *\n\t\t * @return {string} The active tab's label.\n\t\t */\n\t\tactiveTitle() {\n\t\t\treturn this.activeTab?.label || ''\n\t\t},\n\n\t\t/**\n\t\t * The strip's look: `segmented` when the content asks for it, else\n\t\t * CnTabs' default `line`. An unknown value falls back to `line`.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-tabs-widget-renders-a-segmented-strip\n\t\t * @return {('line'|'segmented')} The CnTabs variant.\n\t\t */\n\t\tstripVariant() {\n\t\t\treturn this.content?.variant === 'segmented' ? 'segmented' : 'line'\n\t\t},\n\n\t\t/**\n\t\t * Accessible name for the tab strip.\n\t\t *\n\t\t * @return {string} The aria-label.\n\t\t */\n\t\tstripLabel() {\n\t\t\treturn this.content?.ariaLabel || t('nextcloud-vue', 'Details')\n\t\t},\n\t},\n\n\t/**\n\t * Open on the first tab that is in the strip. Tab 0 is the default, and\n\t * when tab 0 sits under \"More\" the strip would open on a tab the author\n\t * chose to tuck away.\n\t *\n\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-tab-counts-and-overflow\n\t */\n\tcreated() {\n\t\tconst first = this.resolvedTabs.findIndex((tab) => !tab.overflow)\n\t\tif (first > 0) {\n\t\t\tthis.activeIndex = first\n\t\t}\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Re-emit a geo child's save so the surface can reload the record.\n\t\t *\n\t\t * @param {object} geo The saved geometry.\n\t\t * @return {void}\n\t\t */\n\t\tonGeoSaved(geo) {\n\t\t\t/**\n\t\t\t * @event geo-saved Re-emitted from a geo child that saved a geometry.\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('geo-saved', geo)\n\t\t},\n\n\t\t/**\n\t\t * Re-emit a related child's request to open an integration.\n\t\t *\n\t\t * @param {string} integrationId The integration to open.\n\t\t * @return {void}\n\t\t */\n\t\tonOpenIntegration(integrationId) {\n\t\t\t/**\n\t\t\t * @event open-integration Re-emitted from a related child asking to open an integration.\n\t\t\t * @type {string}\n\t\t\t */\n\t\t\tthis.$emit('open-integration', integrationId)\n\t\t},\n\n\t\t/**\n\t\t * Follow the strip's own selection, so keyboard navigation moves the\n\t\t * hoisted Actions menu too.\n\t\t *\n\t\t * @param {number} index The newly selected tab index.\n\t\t * @return {void}\n\t\t */\n\t\tonTabChange(index) {\n\t\t\tif (typeof index === 'number' && index >= 0) {\n\t\t\t\tthis.activeIndex = index\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Empty-state text for a tab whose widget id resolves to nothing.\n\t\t *\n\t\t * @param {object} entry The resolved tab.\n\t\t * @return {string} The message.\n\t\t */\n\t\tmissingLabel(entry) {\n\t\t\treturn t('nextcloud-vue', 'No widget found for \"{id}\"', { id: entry.widgetId || '' })\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n/* The panel is the card; the strip is not inside it. There is no title bar\n above the tabs either: the open tab names the panel, so a title row would\n say the same thing twice and cost a row of height on a card that is mostly\n content.\n\n The card chrome used to wrap the strip as well, which drew a border and a\n pair of rounded corners ABOVE the tabs and boxed them in. Folder tabs are\n drawn as the edge of the sheet they open, so a second edge around them\n reads as a header the widget does not have. Border, radius and background\n now live on the panel below, and the strip sits bare on the page. */\n/* The class is doubled on purpose, and the four properties below are RESET\n rather than simply omitted.\n\n Nextcloud serves every enabled app's assets on every page, each app bundles\n this library's compiled CSS, and the Vue scope id is derived from the file\n PATH, so `data-v-1dbd6122` is byte-identical across library versions. An app\n still on an older release therefore ships a rule with exactly this selector\n and the OLD declarations, and it lands on the pages of an app already on the\n new one. Measured on a dossiq case page: three stale copies of the pre-fix\n rule, one from hermiq's `companion.css` (its companion bundle loads on every\n page by design) and two inline from other apps' bundles. Same specificity,\n so source order decided, and the card border came back around the strip.\n\n Dropping a declaration cannot beat a rule that sets it, so `border` and the\n rest are explicitly zeroed. Doubling the class takes this selector to\n (0,3,0) against the stale (0,2,0), which wins on specificity rather than on\n `!important` or on load order this library does not control. The same\n technique, for the same reason, is used on `.cn-tabs__nav .cn-tabs__nav-item`\n in CnTabs to beat Nextcloud's own button margin. */\n.cn-tabs-widget.cn-tabs-widget {\n\tbackground-color: transparent;\n\tborder: 0;\n\tborder-radius: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\theight: 100%;\n\tmin-height: 0;\n\toverflow: visible;\n\tpadding: 0;\n}\n\n/* No inset, so the first tab starts at the panel's own left edge. The 8px\n here was there to clear the card's rounded top corner; that corner is gone\n from the strip now, and the inset it existed for left the first tab floating\n 8px inside the sheet it opens.\n\n The bar's own bottom rule is switched off and redrawn as a pseudo-element\n that stops one corner radius short of the right edge. CnTabs draws its rule\n across the bar's whole width, which is the sheet's top edge everywhere\n except at the far right, where the panel's top-right corner curves away\n below it: the rule ran straight on to the widget's edge and read as a\n border sticking out past a rounded corner (Ruben, 2026-09-13). Ending it\n where the curve begins makes the rule and the curve one continuous edge. */\n.cn-tabs-widget__tabs :deep(.cn-tabs__bar) {\n\tborder-bottom: none;\n\tpadding: 0;\n\tposition: relative;\n}\n\n.cn-tabs-widget__tabs :deep(.cn-tabs__bar::after) {\n\tborder-bottom: 1px solid var(--color-border);\n\tcontent: '';\n\tinset-block-end: 0;\n\tinset-inline: 0 var(--border-radius-large);\n\tpointer-events: none;\n\tposition: absolute;\n}\n\n/* The open tab erases the slice of the rule directly above the panel with its\n own background-coloured bottom edge, which only works if it paints ABOVE the\n pseudo-element: a positioned pseudo-element otherwise paints over every\n in-flow sibling, rule included. */\n.cn-tabs-widget__tabs :deep(.cn-tabs__nav-item--active) {\n\tposition: relative;\n\tz-index: 1;\n}\n\n.cn-tabs-widget__tabs {\n\tdisplay: flex;\n\tflex-direction: column;\n\t/* Grow to the widget's height, so the panel fills the grid cell the\n\t layout gave it. Without this the strip plus the open panel took only\n\t the height of the panel's content, and a cell of nine rows ended in\n\t 130px of nothing under a sheet that stopped short of its own card. */\n\tflex: 1 1 auto;\n\tmin-height: 0;\n}\n\n/* The panel area is the scroll region, so the strip stays put while a long\n child scrolls under it.\n\n Padded on all four sides. The top was once zeroed to protect the join with\n the open tab, but padding sits inside the painted box — only a margin would\n have opened that gap, and a zero top put every panel's content against the\n strip. */\n.cn-tabs-widget__tabs :deep(.cn-tabs__content) {\n\tbackground-color: var(--color-main-background);\n\t/* Three sides only: the bar's rule (redrawn above, stopping at the corner)\n\t is this sheet's top edge, and the open tab erases the slice of it\n\t directly above the panel so the two read as one surface. A border-top\n\t here would put a second line under that tab which the tab cannot paint\n\t over. */\n\tborder: 1px solid var(--color-border);\n\tborder-top: none;\n\t/* Three rounded corners. The top-left stays square because the first tab\n\t is drawn joined to the panel there, and a curve under it would curl the\n\t sheet away from that tab. The top-right has no tab above it: the strip\n\t ends where the last tab ends and the Actions menu floats free, so a\n\t square corner there read as a sheet cut off, not as a tab's edge. */\n\tborder-radius: 0 var(--border-radius-large) var(--border-radius-large) var(--border-radius-large);\n\tflex: 1 1 auto;\n\tmin-height: 0;\n\toverflow: auto;\n\tpadding: 12px;\n}\n\n.cn-tabs-widget__tabs :deep(.cn-tab) {\n\theight: 100%;\n}\n\n.cn-tabs-widget__title {\n\talign-items: center;\n\tdisplay: inline-flex;\n\tgap: 6px;\n}\n\n.cn-tabs-widget__title-icon {\n\tflex: 0 0 auto;\n}\n</style>\n"],"names":["_createVNode","_createElementBlock","_Fragment","_renderList","_createBlock","_openBlock","_createElementVNode","_toDisplayString"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CnTabsWidget.vue3.js","sources":["../../../../src/components/CnTabsWidget/CnTabsWidget.vue?vue&type=template&id=1dbd6122&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div class=\"cn-tabs-widget\">\n\t\t<CnTabs\n\t\t\t:aria-label=\"stripLabel\"\n\t\t\t:moreLabel=\"moreLabel\"\n\t\t\t:variant=\"stripVariant\"\n\t\t\tclass=\"cn-tabs-widget__tabs\"\n\t\t\t@update:activeIndex=\"onTabChange\">\n\t\t\t<!-- One Actions menu for the whole widget, bound to whichever child\n\t\t\t is showing. This is the point of the component: six tabbed\n\t\t\t widgets used to mean six card headers stacked down the page. -->\n\t\t\t<template #nav-end>\n\t\t\t\t<CnActionsMenu\n\t\t\t\t\t:showRefresh=\"showRefresh\"\n\t\t\t\t\t:showRequestFeature=\"showRequestFeature\"\n\t\t\t\t\t:showDocumentation=\"showDocumentation\"\n\t\t\t\t\t:documentationUrl=\"documentationUrl\"\n\t\t\t\t\t:widgetId=\"activeWidgetId\"\n\t\t\t\t\t:title=\"activeTitle\"\n\t\t\t\t\t:surface=\"`widget:${activeWidgetId}`\"\n\t\t\t\t\trefreshChannel=\"cn:widget:refresh\"\n\t\t\t\t\ttestidBase=\"cn-tabs-widget\">\n\t\t\t\t\t<!-- The open panel's own items, below the built-in trio.\n\t\t\t\t\t A panel draws no header, so these would otherwise have\n\t\t\t\t\t nowhere to go: the data widget's Metadata and full edit\n\t\t\t\t\t dialog vanished outright when the panel stopped drawing\n\t\t\t\t\t a card. They are rendered here rather than rebuilt here\n\t\t\t\t\t because `run` closes over the widget that published it,\n\t\t\t\t\t which is what keeps the dialog's per-widget config and\n\t\t\t\t\t the widget's own save path. See utils/panelActions.js.\n\n\t\t\t\t\t Only the ACTIVE panel's items. `lazy` keeps a visited\n\t\t\t\t\t tab mounted, so several panels publish at once and an\n\t\t\t\t\t unfiltered list would offer actions for a sheet nobody\n\t\t\t\t\t is looking at. -->\n\t\t\t\t\t<template v-if=\"activePanelActions.length\" #action-items>\n\t\t\t\t\t\t<NcActionButton\n\t\t\t\t\t\t\tv-for=\"action in activePanelActions\"\n\t\t\t\t\t\t\t:key=\"action.key\"\n\t\t\t\t\t\t\t:closeAfterClick=\"true\"\n\t\t\t\t\t\t\t@click=\"action.run()\">\n\t\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t\t<CnIcon :name=\"action.icon\" :size=\"20\" />\n\t\t\t\t\t\t\t</template>\n\t\t\t\t\t\t\t{{ action.label }}\n\t\t\t\t\t\t</NcActionButton>\n\t\t\t\t\t</template>\n\t\t\t\t</CnActionsMenu>\n\t\t\t</template>\n\n\t\t\t<CnTab\n\t\t\t\tv-for=\"(entry, index) in resolvedTabs\"\n\t\t\t\t:key=\"entry.key\"\n\t\t\t\t:active=\"index === activeIndex\"\n\t\t\t\t:title=\"entry.label\"\n\t\t\t\t:count=\"entry.count\"\n\t\t\t\t:overflow=\"entry.overflow\"\n\t\t\t\tlazy\n\t\t\t\t@click=\"activeIndex = index\">\n\t\t\t\t<template #title>\n\t\t\t\t\t<span class=\"cn-tabs-widget__title\">\n\t\t\t\t\t\t<CnIcon\n\t\t\t\t\t\t\tv-if=\"entry.icon\"\n\t\t\t\t\t\t\t:name=\"entry.icon\"\n\t\t\t\t\t\t\t:size=\"18\"\n\t\t\t\t\t\t\tclass=\"cn-tabs-widget__title-icon\" />\n\t\t\t\t\t\t{{ entry.label }}\n\t\t\t\t\t</span>\n\t\t\t\t</template>\n\n\t\t\t\t<CnDetailWidgetHost\n\t\t\t\t\tv-if=\"entry.widget\"\n\t\t\t\t\t:widget=\"entry.widget\"\n\t\t\t\t\tchrome=\"bare\"\n\t\t\t\t\t:objectId=\"objectId\"\n\t\t\t\t\t:object=\"objectData\"\n\t\t\t\t\t:objectType=\"objectType\"\n\t\t\t\t\t:schemaObject=\"schemaObject\"\n\t\t\t\t\t:register=\"register\"\n\t\t\t\t\t:schema=\"schema\"\n\t\t\t\t\t:store=\"store\"\n\t\t\t\t\t:surface=\"surface\"\n\t\t\t\t\t:integrationContext=\"integrationContext\"\n\t\t\t\t\t:cnRegistry=\"cnRegistry\"\n\t\t\t\t\t@geoSaved=\"onGeoSaved\"\n\t\t\t\t\t@openIntegration=\"onOpenIntegration\"\n\t\t\t\t\t@selectObject=\"onSelectObject\" />\n\t\t\t\t<NcEmptyContent v-else :name=\"missingLabel(entry)\" />\n\t\t\t</CnTab>\n\t\t</CnTabs>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcActionButton, NcEmptyContent } from '@nextcloud/vue'\nimport CnDetailWidgetHost from '../CnDetailWidgetHost/CnDetailWidgetHost.vue'\nimport CnIcon from '../CnIcon/CnIcon.vue'\nimport CnTab from '../CnTabs/CnTab.vue'\nimport CnTabs from '../CnTabs/CnTabs.vue'\nimport { resolveTabCount } from '../../utils/detailActionModel.js'\nimport { PANEL_ACTION_SINK } from '../../utils/panelActions.js'\nimport { widgetTitleOf } from '../../utils/widgetDispatch.js'\nimport { CnActionsMenu } from '../CnActionsMenu/index.js'\n\n/**\n * CnTabsWidget — a widget that holds other widgets, one per tab.\n *\n * A detail page that carries notes, files, related records, sub-records, mail\n * and appointments renders six cards, each with its own header and its own\n * Actions menu, stacked down the page. All six say roughly the same thing about\n * one record, and only one of them is being read at a time. This widget puts\n * them behind a tab strip instead.\n *\n * ## What the tabs take over\n *\n * The strip owns the title and the card. A child renders through\n * `CnDetailWidgetHost` with `chrome=\"bare\"`, so it draws no header and no\n * border of its own, and its content fills the panel exactly as it would have\n * filled its card. The tab label is what the widget's card header used to say.\n *\n * The Actions menu moves OUT of the panels and into the bar, beside the strip\n * rather than inside it, and rebinds to whichever child is showing. So there is\n * one menu, and it always acts on what you are looking at.\n *\n * ## Configuring it\n *\n * `content.tabs[]` names the children and their labels, so a deployment can\n * relabel or reorder tabs, or drop one, without touching code:\n *\n * ```js\n * content: {\n * tabs: [\n * { widgetId: 'case-notes', label: 'Notes', icon: 'NoteTextOutline' },\n * { widgetId: 'case-files', label: 'Files and attachments' },\n * ],\n * }\n * ```\n *\n * `label` and `icon` are optional and fall back to the child widget's own\n * title and icon, so the common case is a list of `widgetId`s.\n *\n * ## Panels are lazy, and stay mounted\n *\n * Each panel is a `CnTab` with `lazy`, so a child only mounts when its tab is\n * first opened. Six eager panels that each fetch on `mounted()` would fire six\n * requests on page load to answer five questions nobody asked. Once opened, a\n * panel stays mounted, so switching back never refetches.\n *\n * ## A named tab whose widget is missing\n *\n * The tab still renders, and its panel says which widget id did not resolve.\n * Dropping the tab would be worse: `content.tabs[]` is hand-authored config,\n * and a typo that silently removes a tab is a typo nobody finds.\n */\nexport default {\n\tname: 'CnTabsWidget',\n\n\tcomponents: {\n\t\tCnActionsMenu,\n\t\tCnDetailWidgetHost,\n\t\tCnIcon,\n\t\tCnTab,\n\t\tCnTabs,\n\t\tNcActionButton,\n\t\tNcEmptyContent,\n\t},\n\n\t/**\n\t * Offer the panels a way to put their own menu items in the strip's menu.\n\t *\n\t * Re-provided one level down by `CnDetailWidgetHost`, which knows the id to\n\t * key by; see utils/panelActions.js for why the items travel up instead of\n\t * being rebuilt here.\n\t *\n\t * @return {object} The provided sink.\n\t */\n\tprovide() {\n\t\treturn {\n\t\t\t[PANEL_ACTION_SINK]: {\n\t\t\t\t/**\n\t\t\t\t * Publish a panel's items, under one SOURCE.\n\t\t\t\t *\n\t\t\t\t * A panel has two possible publishers: the host, for an action\n\t\t\t\t * the host itself would have drawn (the catalog Add), and the\n\t\t\t\t * widget inside it, for its own menu items. Keyed by source so\n\t\t\t\t * the two coexist; a single slot per widget meant whichever\n\t\t\t\t * published last silently replaced the other.\n\t\t\t\t *\n\t\t\t\t * Replaces the map rather than mutating it so the computed that\n\t\t\t\t * reads it re-evaluates on any change.\n\t\t\t\t *\n\t\t\t\t * @param {string} id The publishing widget's id.\n\t\t\t\t * @param {object[]} items Its PanelAction descriptors.\n\t\t\t\t * @param {string} [source] Who is publishing: `host` or `widget`.\n\t\t\t\t * @return {void}\n\t\t\t\t */\n\t\t\t\tset: (id, items, source = 'widget') => {\n\t\t\t\t\t// `activePanelActions` spreads what is stored, so a\n\t\t\t\t\t// non-array becomes one menu item per character.\n\t\t\t\t\tif (!id || !Array.isArray(items)) {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tconst forId = { ...(this.panelActionsByWidget[id] || {}), [source]: items }\n\t\t\t\t\tthis.panelActionsByWidget = { ...this.panelActionsByWidget, [id]: forId }\n\t\t\t\t},\n\n\t\t\t\t/**\n\t\t\t\t * Withdraw one source's items, on unmount or when that\n\t\t\t\t * publisher's own menu comes back. The other source's items\n\t\t\t\t * stay.\n\t\t\t\t *\n\t\t\t\t * @param {string} id The publishing widget's id.\n\t\t\t\t * @param {string} [source] Who is withdrawing.\n\t\t\t\t * @return {void}\n\t\t\t\t */\n\t\t\t\tclear: (id, source = 'widget') => {\n\t\t\t\t\tconst forId = this.panelActionsByWidget[id]\n\t\t\t\t\tif (!forId || !(source in forId)) {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tconst { [source]: _removed, ...keptSources } = forId\n\t\t\t\t\tif (Object.keys(keptSources).length) {\n\t\t\t\t\t\tthis.panelActionsByWidget = { ...this.panelActionsByWidget, [id]: keptSources }\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tconst { [id]: _gone, ...rest } = this.panelActionsByWidget\n\t\t\t\t\tthis.panelActionsByWidget = rest\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget's config: `{ tabs, ariaLabel }`.\n\t\t *\n\t\t * `tabs[]` entries are `{ widgetId, label?, icon?, count?, countField?, overflow? }`.\n\t\t * `label` and `icon` fall back to the referenced widget's own title and\n\t\t * icon. `count` is a number shown after the label; `countField` reads\n\t\t * it off the record instead (a list counts its items). `overflow: true`\n\t\t * lists the tab under \"More\".\n\t\t *\n\t\t * `maxVisibleTabs` caps the strip: later tabs go under \"More\".\n\t\t * `hideEmpty: true` moves a tab whose count is 0 there too.\n\t\t * `moreLabel` names that menu. `variant: \"segmented\"` draws the strip\n\t\t * as a pill switch (CnTabs' segmented variant); `line` is the default.\n\t\t *\n\t\t * @type {{ tabs?: Array<{widgetId: string, label?: string, icon?: string, count?: number, countField?: string, overflow?: boolean}>, ariaLabel?: string, maxVisibleTabs?: number, hideEmpty?: boolean, moreLabel?: string, variant?: ('line'|'segmented') }}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/**\n\t\t * Every widget definition available on the surface, for `content.tabs[]`\n\t\t * to reference by id.\n\t\t *\n\t\t * The surface passes its whole list rather than the resolved children\n\t\t * because a tab may name a widget that does not exist, and this component\n\t\t * has to be able to say so.\n\t\t *\n\t\t * @type {object[]}\n\t\t */\n\t\tavailableWidgets: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** The bound record's id. */\n\t\tobjectId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The loaded record, or null while it is still being fetched. */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The resolved object-type slug. */\n\t\tobjectType: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The resolved JSON Schema object, needed by a `data` child. */\n\t\tschemaObject: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** OpenRegister register slug of the surface. */\n\t\tregister: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** OpenRegister schema slug of the surface. */\n\t\tschema: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The effective object store. */\n\t\tstore: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Rendering surface forwarded to integration children (AD-19). */\n\t\tsurface: {\n\t\t\ttype: String,\n\t\t\tdefault: 'detail-page',\n\t\t},\n\n\t\t/** Object context forwarded to integration children. */\n\t\tintegrationContext: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The consumer's component registry, for custom child widget types. */\n\t\tcnRegistry: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** Show the Refresh entry in the hoisted Actions menu. */\n\t\tshowRefresh: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Show the Request-a-feature entry in the hoisted Actions menu. */\n\t\tshowRequestFeature: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Show the Documentation entry in the hoisted Actions menu. */\n\t\tshowDocumentation: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Documentation URL for the hoisted Actions menu. */\n\t\tdocumentationUrl: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\t},\n\n\temits: ['geo-saved', 'open-integration', 'select-object'],\n\n\tdata() {\n\t\treturn {\n\t\t\tactiveIndex: 0,\n\t\t\t// Items published by the panels, keyed by widget id. Keyed rather\n\t\t\t// than a flat list because `lazy` keeps a visited tab mounted, so\n\t\t\t// more than one panel publishes at a time.\n\t\t\tpanelActionsByWidget: {},\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The open panel's own menu items, or an empty list.\n\t\t *\n\t\t * @return {object[]} PanelAction descriptors for the active tab.\n\t\t */\n\t\tactivePanelActions() {\n\t\t\tconst forId = this.panelActionsByWidget[this.activeWidgetId]\n\t\t\tif (!forId) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\t// Host first, then the widget's own: the catalog Add is about the\n\t\t\t// panel as a whole, the widget's items about what is in it.\n\t\t\treturn [...(forId.host || []), ...(forId.widget || [])]\n\t\t},\n\n\t\t/**\n\t\t * The configured tabs, each paired with the widget definition it names.\n\t\t *\n\t\t * A tab goes under the strip's \"More\" menu when it says `overflow`, when\n\t\t * `content.hideEmpty` is set and its count is 0, or when\n\t\t * `content.maxVisibleTabs` places are already taken. `count` is a\n\t\t * literal, or read off the record through `countField`.\n\t\t *\n\t\t * @return {object[]} `{ key, widgetId, label, icon, widget, count, overflow }` per tab.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-tab-counts-and-overflow\n\t\t */\n\t\tresolvedTabs() {\n\t\t\tconst tabs = Array.isArray(this.content?.tabs) ? this.content.tabs : []\n\t\t\tconst max = Number(this.content?.maxVisibleTabs)\n\t\t\tconst hideEmpty = this.content?.hideEmpty === true\n\t\t\t// How many tabs have taken a place in the strip so far. A tab that\n\t\t\t// is already going under \"More\" does not use one up.\n\t\t\tlet placed = 0\n\t\t\treturn tabs.map((tab, index) => {\n\t\t\t\tconst widgetId = typeof tab === 'string' ? tab : tab?.widgetId\n\t\t\t\tconst widget = this.availableWidgets.find((w) => w && w.id === widgetId) || null\n\t\t\t\tconst count = resolveTabCount(typeof tab === 'object' ? tab : null, this.objectData)\n\t\t\t\tlet overflow = (tab && tab.overflow === true) || (hideEmpty && count === 0)\n\t\t\t\tif (!overflow) {\n\t\t\t\t\tif (Number.isFinite(max) && max > 0 && placed >= max) {\n\t\t\t\t\t\toverflow = true\n\t\t\t\t\t} else {\n\t\t\t\t\t\tplaced += 1\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn {\n\t\t\t\t\tkey: `${widgetId || 'tab'}-${index}`,\n\t\t\t\t\twidgetId,\n\t\t\t\t\tlabel: (tab && tab.label) || widgetTitleOf(widget) || widgetId || '',\n\t\t\t\t\ticon: (tab && tab.icon) || widget?.icon || '',\n\t\t\t\t\twidget,\n\t\t\t\t\tcount,\n\t\t\t\t\toverflow,\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Name of the menu that holds the tabs that are not in the strip.\n\t\t *\n\t\t * @return {string|undefined} The configured label, or undefined for the default.\n\t\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-tab-counts-and-overflow\n\t\t */\n\t\tmoreLabel() {\n\t\t\tconst label = this.content?.moreLabel\n\t\t\treturn typeof label === 'string' && label !== '' ? label : undefined\n\t\t},\n\n\t\t/**\n\t\t * The tab currently showing.\n\t\t *\n\t\t * @return {object|null} The resolved tab, or null when there are none.\n\t\t */\n\t\tactiveTab() {\n\t\t\treturn this.resolvedTabs[this.activeIndex] || null\n\t\t},\n\n\t\t/**\n\t\t * The active child's id, so the hoisted Refresh reaches THAT child's\n\t\t * fetch over `cn:widget:refresh` rather than a sibling's.\n\t\t *\n\t\t * @return {string} The active widget id.\n\t\t */\n\t\tactiveWidgetId() {\n\t\t\treturn this.activeTab?.widgetId || ''\n\t\t},\n\n\t\t/**\n\t\t * The active child's label, so the Actions menu names what it acts on.\n\t\t *\n\t\t * @return {string} The active tab's label.\n\t\t */\n\t\tactiveTitle() {\n\t\t\treturn this.activeTab?.label || ''\n\t\t},\n\n\t\t/**\n\t\t * The strip's look: `segmented` when the content asks for it, else\n\t\t * CnTabs' default `line`. An unknown value falls back to `line`.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-tabs-widget-renders-a-segmented-strip\n\t\t * @return {('line'|'segmented')} The CnTabs variant.\n\t\t */\n\t\tstripVariant() {\n\t\t\treturn this.content?.variant === 'segmented' ? 'segmented' : 'line'\n\t\t},\n\n\t\t/**\n\t\t * Accessible name for the tab strip.\n\t\t *\n\t\t * @return {string} The aria-label.\n\t\t */\n\t\tstripLabel() {\n\t\t\treturn this.content?.ariaLabel || t('nextcloud-vue', 'Details')\n\t\t},\n\t},\n\n\t/**\n\t * Open on the first tab that is in the strip. Tab 0 is the default, and\n\t * when tab 0 sits under \"More\" the strip would open on a tab the author\n\t * chose to tuck away.\n\t *\n\t * @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-tab-counts-and-overflow\n\t */\n\tcreated() {\n\t\tconst first = this.resolvedTabs.findIndex((tab) => !tab.overflow)\n\t\tif (first > 0) {\n\t\t\tthis.activeIndex = first\n\t\t}\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Re-emit a geo child's save so the surface can reload the record.\n\t\t *\n\t\t * @param {object} geo The saved geometry.\n\t\t * @return {void}\n\t\t */\n\t\tonGeoSaved(geo) {\n\t\t\t/**\n\t\t\t * @event geo-saved Re-emitted from a geo child that saved a geometry.\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('geo-saved', geo)\n\t\t},\n\n\t\t/**\n\t\t * Re-emit a related child's request to open an integration.\n\t\t *\n\t\t * @param {string} integrationId The integration to open.\n\t\t * @return {void}\n\t\t */\n\t\tonOpenIntegration(integrationId) {\n\t\t\t/**\n\t\t\t * @event open-integration Re-emitted from a related child asking to open an integration.\n\t\t\t * @type {string}\n\t\t\t */\n\t\t\tthis.$emit('open-integration', integrationId)\n\t\t},\n\n\t\t/**\n\t\t * Re-emit a related child's object click; the page opens the object.\n\t\t *\n\t\t * @param {object} raw The clicked object.\n\t\t * @return {void}\n\t\t */\n\t\tonSelectObject(raw) {\n\t\t\t/**\n\t\t\t * @event select-object Re-emitted from a related child whose object row was clicked. Payload is the raw object.\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('select-object', raw)\n\t\t},\n\n\t\t/**\n\t\t * Follow the strip's own selection, so keyboard navigation moves the\n\t\t * hoisted Actions menu too.\n\t\t *\n\t\t * @param {number} index The newly selected tab index.\n\t\t * @return {void}\n\t\t */\n\t\tonTabChange(index) {\n\t\t\tif (typeof index === 'number' && index >= 0) {\n\t\t\t\tthis.activeIndex = index\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Empty-state text for a tab whose widget id resolves to nothing.\n\t\t *\n\t\t * @param {object} entry The resolved tab.\n\t\t * @return {string} The message.\n\t\t */\n\t\tmissingLabel(entry) {\n\t\t\treturn t('nextcloud-vue', 'No widget found for \"{id}\"', { id: entry.widgetId || '' })\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n/* The panel is the card; the strip is not inside it. There is no title bar\n above the tabs either: the open tab names the panel, so a title row would\n say the same thing twice and cost a row of height on a card that is mostly\n content.\n\n The card chrome used to wrap the strip as well, which drew a border and a\n pair of rounded corners ABOVE the tabs and boxed them in. Folder tabs are\n drawn as the edge of the sheet they open, so a second edge around them\n reads as a header the widget does not have. Border, radius and background\n now live on the panel below, and the strip sits bare on the page. */\n/* The class is doubled on purpose, and the four properties below are RESET\n rather than simply omitted.\n\n Nextcloud serves every enabled app's assets on every page, each app bundles\n this library's compiled CSS, and the Vue scope id is derived from the file\n PATH, so `data-v-1dbd6122` is byte-identical across library versions. An app\n still on an older release therefore ships a rule with exactly this selector\n and the OLD declarations, and it lands on the pages of an app already on the\n new one. Measured on a dossiq case page: three stale copies of the pre-fix\n rule, one from hermiq's `companion.css` (its companion bundle loads on every\n page by design) and two inline from other apps' bundles. Same specificity,\n so source order decided, and the card border came back around the strip.\n\n Dropping a declaration cannot beat a rule that sets it, so `border` and the\n rest are explicitly zeroed. Doubling the class takes this selector to\n (0,3,0) against the stale (0,2,0), which wins on specificity rather than on\n `!important` or on load order this library does not control. The same\n technique, for the same reason, is used on `.cn-tabs__nav .cn-tabs__nav-item`\n in CnTabs to beat Nextcloud's own button margin. */\n.cn-tabs-widget.cn-tabs-widget {\n\tbackground-color: transparent;\n\tborder: 0;\n\tborder-radius: 0;\n\tdisplay: flex;\n\tflex-direction: column;\n\theight: 100%;\n\tmin-height: 0;\n\toverflow: visible;\n\tpadding: 0;\n}\n\n/* No inset, so the first tab starts at the panel's own left edge. The 8px\n here was there to clear the card's rounded top corner; that corner is gone\n from the strip now, and the inset it existed for left the first tab floating\n 8px inside the sheet it opens.\n\n The bar's own bottom rule is switched off and redrawn as a pseudo-element\n that stops one corner radius short of the right edge. CnTabs draws its rule\n across the bar's whole width, which is the sheet's top edge everywhere\n except at the far right, where the panel's top-right corner curves away\n below it: the rule ran straight on to the widget's edge and read as a\n border sticking out past a rounded corner (Ruben, 2026-09-13). Ending it\n where the curve begins makes the rule and the curve one continuous edge. */\n.cn-tabs-widget__tabs :deep(.cn-tabs__bar) {\n\tborder-bottom: none;\n\tpadding: 0;\n\tposition: relative;\n}\n\n.cn-tabs-widget__tabs :deep(.cn-tabs__bar::after) {\n\tborder-bottom: 1px solid var(--color-border);\n\tcontent: '';\n\tinset-block-end: 0;\n\tinset-inline: 0 var(--border-radius-large);\n\tpointer-events: none;\n\tposition: absolute;\n}\n\n/* The open tab erases the slice of the rule directly above the panel with its\n own background-coloured bottom edge, which only works if it paints ABOVE the\n pseudo-element: a positioned pseudo-element otherwise paints over every\n in-flow sibling, rule included. */\n.cn-tabs-widget__tabs :deep(.cn-tabs__nav-item--active) {\n\tposition: relative;\n\tz-index: 1;\n}\n\n.cn-tabs-widget__tabs {\n\tdisplay: flex;\n\tflex-direction: column;\n\t/* Grow to the widget's height, so the panel fills the grid cell the\n\t layout gave it. Without this the strip plus the open panel took only\n\t the height of the panel's content, and a cell of nine rows ended in\n\t 130px of nothing under a sheet that stopped short of its own card. */\n\tflex: 1 1 auto;\n\tmin-height: 0;\n}\n\n/* The panel area is the scroll region, so the strip stays put while a long\n child scrolls under it.\n\n Padded on all four sides. The top was once zeroed to protect the join with\n the open tab, but padding sits inside the painted box — only a margin would\n have opened that gap, and a zero top put every panel's content against the\n strip. */\n.cn-tabs-widget__tabs :deep(.cn-tabs__content) {\n\tbackground-color: var(--color-main-background);\n\t/* Three sides only: the bar's rule (redrawn above, stopping at the corner)\n\t is this sheet's top edge, and the open tab erases the slice of it\n\t directly above the panel so the two read as one surface. A border-top\n\t here would put a second line under that tab which the tab cannot paint\n\t over. */\n\tborder: 1px solid var(--color-border);\n\tborder-top: none;\n\t/* Three rounded corners. The top-left stays square because the first tab\n\t is drawn joined to the panel there, and a curve under it would curl the\n\t sheet away from that tab. The top-right has no tab above it: the strip\n\t ends where the last tab ends and the Actions menu floats free, so a\n\t square corner there read as a sheet cut off, not as a tab's edge. */\n\tborder-radius: 0 var(--border-radius-large) var(--border-radius-large) var(--border-radius-large);\n\tflex: 1 1 auto;\n\tmin-height: 0;\n\toverflow: auto;\n\tpadding: 12px;\n}\n\n.cn-tabs-widget__tabs :deep(.cn-tab) {\n\theight: 100%;\n}\n\n.cn-tabs-widget__title {\n\talign-items: center;\n\tdisplay: inline-flex;\n\tgap: 6px;\n}\n\n.cn-tabs-widget__title-icon {\n\tflex: 0 0 auto;\n}\n</style>\n"],"names":["_createVNode","_createElementBlock","_Fragment","_renderList","_createBlock","_openBlock","_createElementVNode","_toDisplayString"],"mappings":";;AAWM,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,gBAAgB,GAAA;AA2DjB,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,uBAAuB,GAAA;;;;;;;;;;;;IA1DrCA;AACC,MAAA,YAAU,EAAE,QAAU,CAAA,UAAA;eACb,EAAE,QAAQ,CAAA,SAAA;aACZ,EAAE,QAAY,CAAA,YAAA;AACtB,MAAA,KAAK,EAAC,sBAAY;AACjB,MAAA,sBAAa,EAAA,QAAA,CAAA,WAAA;;AAIH,MAAA,SAAO,gBAoCF;mBAAA,CAAA,wBAAA,EAAA;AAlCb,UAAA,WAAW,EAAE,MAAK,CAAA,WAAA;AAClB,UAAA,kBAAkB,EAAE,MAAe,CAAA,kBAAA;AACnC,UAAA,iBAAiB,EAAE,MAAa,CAAA,iBAAA;AAChC,UAAA,gBAAgB,EAAE,MAAQ,CAAA,gBAAA;AAC1B,UAAA,QAAQ,EAAE;AACN,UAAA,KAAA,EAAG,QAAW,CAAA,WAAA;AACd,UAAA,OAAG,YAAY,QAAc,CAAA,cAAA,CAAA,CAAA;wBACpB,EAAC,mBAAkB;AAC5B,UAAA,UAAK,EAAC,gBAAgB;;AAcX,UAAA,CAAA,QAAA,CAAA,kBAAO,CAAA,MAAA;;sBAAA,cAAA;4BAEpB,MAAmC;oCADpCC,kBASC,CAAAC,QAAA,EAAA,IAAA,EAAAC,UAAA,CARgB,QAAkB,CAAA,kBAAA,EAAA,CAA5B,MAAM,KAAA;yCADbC,WASC,CAAA,yBAAA,EAAA;sBAPA,GAAG,EAAE,MAAI,CAAA,GAAA;AACT,sBAAA,eAAe,EAAC,IAAA;sBAChB,OAAK,EAAA,MAAA,KAAE,MAAM,CAAC,GAAA,EAAA,CAAA;;AACP,sBAAA,IAAA,UACP,MAAW;wBAAXJ,WAAW,CAAA,iBAAA,EAAA;0BAAF,IAAE,EAAA,MAAA,CAAA,IAAA;AAAA,0BAAA,IAAA,EAAA,EAAA;;;uCACN;wCAAA;;;;;;;;;;;uBAQT,MAAyB;AADzB,SAAAK,SAAA,CAAA,IAAA,CAAA,EAAAJ,kBAAA,SACyB,EAAA,IAAA,EAAAE,UAAA,CAAA,QAAA,CAAA,YAAA,EAAjB,CAAA,KAAK,EAAE,KAAK,KAAA;+BADpBC;YAEC,GAAE,EAAA,KAAA,CAAA,GAAA;YACF,MAAM,EAAE,KAAK,KAAM,KAAQ,CAAA,WAAA;YAC3B,KAAK,EAAE,KAAK,CAAC,KAAE;YACf,KAAI,EAAG,KAAK,CAAC,KAAK;YACjB,QAAM,EAAA,KAAA,CAAA,QAAA;AACL,YAAA,IAAC,EAAD,EAAC;YACD,OAAG,EAAA,MAAA,KAAE,KAAW,CAAA,WAAA,GAAC,KAAA,CAAA;;AACT,YAAA,KAAC,UACT,MAOK;AAPL,cAAAE,kBAAA,CAOK,QAPL,UAOK,EAAA;AALD,gBAAA,CAAA,KAAA,CAAA,IAAA;kCADLF,WAIgB,CAAA,iBAAA,EAAA;;sBAFd,IAAI,EAAE,KAAA,CAAA,IAAA;;AAEP,sBAAA,KAAK,EAAC,4BAAS;;;gCAAA,GAChB,GAAAG,eAAA,CAAG,KAAK,CAAC,KAAK,CAAA,EAAA,CAAA,YAAA;;;6BAIf;AACM,cAAA,CAAA,KAAK,CAAC,MAAM;gCADlBH;;oBAEC,MAAM,EAAE,KAAK,CAAC,MAAM;AACrB,oBAAA,MAAM,EAAC,MAAM;AACZ,oBAAA,QAAQ,EAAE,MAAI,CAAA,QAAA;AACd,oBAAA,MAAM,EAAE,MAAI,CAAA,UAAA;AACZ,oBAAA,UAAU,EAAE,MAAI,CAAA,UAAA;AAChB,oBAAA,YAAY,EAAE,MAAY,CAAA,YAAA;AAC1B,oBAAA,QAAQ,EAAE,MAAQ,CAAA,QAAA;AAClB,oBAAA,MAAM,EAAE,MAAM,CAAA,MAAA;AACd,oBAAA,KAAK,EAAE,MAAK,CAAA,KAAA;AACZ,oBAAA,OAAO,EAAE,MAAO,CAAA,OAAA;AAChB,oBAAA,kBAAkB,EAAG,MAAiB,CAAA,kBAAA;AACtC,oBAAA,UAAI,EAAA,MAAA,CAAA,UAAA;AACJ,oBAAA,UAAI,EAAA,QAAA,CAAA,UAAA;AACJ,oBAAA,iBAAA,EAAA,QAAA,CAAA,iBAAA;;;;;;;;;;;;;;;;;;;"}
|
|
@@ -94,7 +94,7 @@ var script = {
|
|
|
94
94
|
},
|
|
95
95
|
},
|
|
96
96
|
|
|
97
|
-
emits: ['layout-change'],
|
|
97
|
+
emits: ['layout-change', 'select-object'],
|
|
98
98
|
|
|
99
99
|
data() {
|
|
100
100
|
return {
|
|
@@ -304,6 +304,20 @@ var script = {
|
|
|
304
304
|
},
|
|
305
305
|
|
|
306
306
|
methods: {
|
|
307
|
+
/**
|
|
308
|
+
* Pass a related widget's object click up; the page opens the object.
|
|
309
|
+
*
|
|
310
|
+
* @param {object} raw The clicked object.
|
|
311
|
+
* @return {void}
|
|
312
|
+
*/
|
|
313
|
+
onSelectObject(raw) {
|
|
314
|
+
/**
|
|
315
|
+
* @event select-object An object row in a related widget of this grid was clicked. Payload is the raw object.
|
|
316
|
+
* @type {object}
|
|
317
|
+
*/
|
|
318
|
+
this.$emit('select-object', raw);
|
|
319
|
+
},
|
|
320
|
+
|
|
307
321
|
// Expose the shared grid helpers to the template.
|
|
308
322
|
cnGridCellStyle,
|
|
309
323
|
hasGridRow,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnWidgetGrid.vue2.js","sources":["../../../../src/components/CnWidgetGrid/CnWidgetGrid.vue"],"sourcesContent":["<!--\n CnWidgetGrid — per-slot CSS grid renderer for v2 manifest pages.\n\n Receives a flat array of widget entries for a single slot and renders\n them in a CSS grid. The number of columns is determined by `slotName`\n per the ADR-036 Decision 2 convention:\n\n body, footer, header-actions, modal, tab:*, section:* → 12 columns\n sidebar → 1 column\n\n Widget key resolution: built-in registry first, then cnRegistry inject.\n Unknown widget keys are skipped with a console.warn.\n gridWidth values exceeding gridColumns are clamped with a console.warn.\n\n Spec: REQ-MVR-004 (manifest-v2-renderer) — per-slot grid renderer\n\n @prop {Array} widgets Array of widget entry objects from the v2 manifest page.\n @prop {string} slotName The slot key (e.g. \"body\", \"sidebar\", \"tab:general\").\n @prop {object} registry The consumer registry from CnAppRoot (passed as prop\n for standalone use; normally injected via cnRegistry).\n-->\n<template>\n\t<!-- Editable body slot: a GridStack drag/resize grid (ADR-041 edit mode). -->\n\t<div\n\t\tv-if=\"editableBody\"\n\t\tref=\"gridContainer\"\n\t\tclass=\"cn-widget-grid cn-widget-grid--editing\"\n\t\t:data-slot=\"slotName\">\n\t\t<div class=\"grid-stack\">\n\t\t\t<div\n\t\t\t\tv-for=\"(widget, index) in resolvedWidgets\"\n\t\t\t\t:key=\"gsId(widget, index)\"\n\t\t\t\tclass=\"grid-stack-item\"\n\t\t\t\t:gs-id=\"gsId(widget, index)\"\n\t\t\t\t:gs-x=\"widget.gridX\"\n\t\t\t\t:gs-y=\"widget.gridY\"\n\t\t\t\t:gs-w=\"widget.gridWidth\"\n\t\t\t\t:gs-h=\"widget.gridHeight\">\n\t\t\t\t<div class=\"grid-stack-item-content\">\n\t\t\t\t\t<component\n\t\t\t\t\t\t:is=\"widget.component\"\n\t\t\t\t\t\tv-bind=\"widget.props\" />\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n\t<!-- Default read-only CSS grid. -->\n\t<div\n\t\tv-else\n\t\tclass=\"cn-widget-grid cn-grid\"\n\t\t:class=\"{ 'cn-grid--responsive': isResponsive }\"\n\t\t:style=\"gridVars\"\n\t\t:data-slot=\"slotName\">\n\t\t<template v-for=\"(widget, index) in resolvedWidgets\" :key=\"`${widget.widgetKey}-${index}`\">\n\t\t\t<div\n\t\t\t\tclass=\"cn-grid__item\"\n\t\t\t\t:class=\"{ 'cn-grid__item--row': hasGridRow(widget) }\"\n\t\t\t\t:style=\"cnGridCellStyle(widget, gridColumns)\">\n\t\t\t\t<component\n\t\t\t\t\t:is=\"widget.component\"\n\t\t\t\t\tv-bind=\"widget.props\" />\n\t\t\t</div>\n\t\t</template>\n\t</div>\n</template>\n\n<script>\nimport CnUnknownWidget from './CnUnknownWidget.vue'\nimport { cnGridCellStyle, hasGridRow } from '../../utils/grid.js'\nimport { initGridStack, readGridGeometry } from '../../utils/gridStack.js'\nimport { resolveSlotColumns } from '../../utils/resolveSlotColumns.js'\nimport { canonicalWidgetType } from '../../utils/widgetTypeAliases.js'\nimport { BUILT_IN_WIDGETS } from './builtInWidgets.js'\nimport { dashboardWidgetRegistry, getWidgetTypeEntry } from './dashboardWidgetRegistry.js'\n\n// Per-slot column counts and slot→columns resolution now live in\n// src/utils/resolveSlotColumns.js (shared with validateManifest so the grid\n// bound the validator enforces matches what this component renders).\n\nexport default {\n\tname: 'CnWidgetGrid',\n\n\tinject: {\n\t\tcnRegistry: { default: () => ({}) },\n\t\t/**\n\t\t * Per-page slot→columns override map, published by `CnPageRenderer`\n\t\t * from `page.config.slotColumns`. Merged UNDER the `columns` prop in\n\t\t * column resolution. `null` when the page declares no override.\n\t\t */\n\t\tcnSlotColumns: { default: null },\n\t\t/**\n\t\t * Object context for a `type:\"detail\"` page, published by\n\t\t * `CnPageRenderer` (reactive holder `{ value: { objectData, schema,\n\t\t * objectType, objectId, register, store } | null }`). Merged UNDER\n\t\t * each widget's own `props` so detail widgets (`data`, `metadata`,\n\t\t * `file-manager`, …) receive the loaded object without the manifest\n\t\t * having to author per-widget props. `null` outside a detail page.\n\t\t */\n\t\tcnDetailObjectContext: { default: null },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Widget entries for this slot from the v2 manifest page.\n\t\t * Each entry: `{ widgetKey, slot, gridX, gridY, gridWidth, gridHeight, props? }`.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\twidgets: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * The slot name this grid is rendering for (e.g. \"body\", \"sidebar\", \"tab:general\").\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tslotName: {\n\t\t\ttype: String,\n\t\t\trequired: true,\n\t\t},\n\n\t\t/**\n\t\t * Consumer registry override. When provided, overrides the injected\n\t\t * cnRegistry. Used for standalone testing.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tregistry: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Explicit column-count override for this slot. When `null` (default),\n\t\t * columns resolve from the injected per-page `slotColumns` map and then\n\t\t * the library default for the slot — preserving the fixed ADR-036\n\t\t * Decision 2 behaviour (body=12, sidebar=1). Set a number to force a\n\t\t * specific column count.\n\t\t *\n\t\t * @type {number|null}\n\t\t */\n\t\tcolumns: {\n\t\t\ttype: Number,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * When `true`, the `body` slot renders a GridStack drag/resize grid\n\t\t * (ADR-041 in-app edit mode) instead of the read-only CSS grid; geometry\n\t\t * changes are written back to the widget entries and emitted via\n\t\t * `@layout-change`. Other slots are unaffected. Default `false` keeps the\n\t\t * existing read-only rendering for every consumer.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\teditable: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\t},\n\n\temits: ['layout-change'],\n\n\tdata() {\n\t\treturn {\n\t\t\tgrid: null,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** Whether this grid should render the editable GridStack body. */\n\t\teditableBody() {\n\t\t\treturn this.editable && this.slotName === 'body'\n\t\t},\n\n\t\tgridColumns() {\n\t\t\t// Three-layer resolution: `columns` prop → injected per-page\n\t\t\t// slotColumns → the library default for the slot (getGridColumns\n\t\t\t// is preserved as the default tier inside resolveSlotColumns).\n\t\t\treturn resolveSlotColumns(this.slotName, this.cnSlotColumns, this.columns)\n\t\t},\n\n\t\teffectiveRegistry() {\n\t\t\treturn this.registry ?? this.cnRegistry ?? {}\n\t\t},\n\n\t\t/**\n\t\t * Resolved detail-page object context (or `{}`). Read from the\n\t\t * reactive holder published by `CnPageRenderer`; the `.value`\n\t\t * indirection lets the grid re-render once the async object load\n\t\t * resolves. Only the well-known object keys are forwarded to\n\t\t * widgets — see `resolvedWidgets`.\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\tdetailContextProps() {\n\t\t\tconst ctx = this.cnDetailObjectContext?.value\n\t\t\tif (!ctx || typeof ctx !== 'object') {\n\t\t\t\treturn {}\n\t\t\t}\n\t\t\tconst out = {}\n\t\t\tfor (const key of ['objectData', 'schema', 'objectType', 'objectId', 'register', 'store']) {\n\t\t\t\tif (ctx[key] !== undefined) {\n\t\t\t\t\tout[key] = ctx[key]\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/**\n\t\t * CSS custom properties driving the shared `.cn-grid` engine — the\n\t\t * desktop column count for this slot. Responsive collapse (12 → 6 →\n\t\t * 1) lives in `grid.css`.\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\tgridVars() {\n\t\t\treturn { '--cn-grid-cols': this.gridColumns }\n\t\t},\n\n\t\t/**\n\t\t * Whether this slot's grid collapses responsively. Multi-column\n\t\t * slots (body/footer/tab/section) do; the single-column sidebar\n\t\t * slot stays put.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tisResponsive() {\n\t\t\treturn this.gridColumns > 1\n\t\t},\n\n\t\tresolvedWidgets() {\n\t\t\tconst columns = this.gridColumns\n\t\t\tconst result = []\n\n\t\t\tfor (const widget of this.widgets) {\n\t\t\t\tconst key = widget.widgetKey\n\n\t\t\t\t// Resolve component: consumer registry first (overrides built-ins),\n\t\t\t\t// then fall back to built-in registry.\n\t\t\t\t// Per spec REQ-MVR-005: \"Custom widget overrides built-in\"\n\t\t\t\tlet component = null\n\n\t\t\t\tif (this.effectiveRegistry[key]) {\n\t\t\t\t\tconst entry = this.effectiveRegistry[key]\n\t\t\t\t\tcomponent = entry.component ?? entry\n\t\t\t\t}\n\n\t\t\t\t// `table` and `object-table` (and `map` / `map-viewer`) are the\n\t\t\t\t// same component registered under two names in two registries.\n\t\t\t\t// Canonicalising here means either spelling resolves, whichever\n\t\t\t\t// registry happens to hold it — see utils/widgetTypeAliases.js.\n\t\t\t\tconst canonical = canonicalWidgetType(key)\n\n\t\t\t\tif (!component) {\n\t\t\t\t\tcomponent = BUILT_IN_WIDGETS[key] ?? BUILT_IN_WIDGETS[canonical] ?? null\n\t\t\t\t}\n\n\t\t\t\tif (!component) {\n\t\t\t\t\t// Dashboard widget catalog (cn-widget-library): the 21 migrated\n\t\t\t\t\t// widgets self-register here. Resolved after cnRegistry + built-ins\n\t\t\t\t\t// so a consumer override still wins.\n\t\t\t\t\tconst entry = getWidgetTypeEntry(key) || getWidgetTypeEntry(canonical)\n\t\t\t\t\tcomponent = (entry && entry.renderer) ?? null\n\t\t\t\t}\n\n\t\t\t\tif (!component) {\n\t\t\t\t\t// An EMPTY catalog is a different failure from an unknown key, and\n\t\t\t\t\t// the fix is different, so say which one this is. It means nothing\n\t\t\t\t\t// ever imported the module that populates the catalog — most often\n\t\t\t\t\t// because a consumer deep-imported a widget's own module\n\t\t\t\t\t// (`.../components/CnMapWidget/index.js`) expecting it to register\n\t\t\t\t\t// its type. Those modules export the component only; the type\n\t\t\t\t\t// registrations all live in `CnWidgetGrid/registerDashboardWidgets.js`,\n\t\t\t\t\t// which the package root and the `components` barrel both import.\n\t\t\t\t\t// Before this was consolidated, two of them self-registered as well,\n\t\t\t\t\t// so a deep import happened to work by accident.\n\t\t\t\t\tconst catalogEmpty = Object.keys(dashboardWidgetRegistry).length === 0\n\t\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\t\tconsole.warn(catalogEmpty\n\t\t\t\t\t\t? `[CnWidgetGrid] The dashboard widget catalog is EMPTY, so widgetKey \"${key}\" `\n\t\t\t\t\t\t+ `in slot \"${this.slotName}\" cannot resolve — this is not a bad key. `\n\t\t\t\t\t\t+ 'Import the package root (`@conduction/nextcloud-vue`) or, if you '\n\t\t\t\t\t\t+ 'cherry-pick modules, `components/CnWidgetGrid/registerDashboardWidgets.js`. '\n\t\t\t\t\t\t+ 'Importing a widget\\'s own module gives you the component but registers no type.'\n\t\t\t\t\t\t: `[CnWidgetGrid] Unknown widgetKey \"${key}\" in slot \"${this.slotName}\". `\n\t\t\t\t\t\t\t+ 'Register it in the built-in registry or pass it via the CnAppRoot registry prop.')\n\t\t\t\t\t// Render a visible, designed placeholder instead of silently\n\t\t\t\t\t// skipping — a page whose widgets ALL fail to resolve must not\n\t\t\t\t\t// leave a blank pane (2026-07-06 audit: petstore dashboard).\n\t\t\t\t\tresult.push({\n\t\t\t\t\t\tid: widget.id ?? `unknown-${key}-${result.length}`,\n\t\t\t\t\t\twidgetKey: key,\n\t\t\t\t\t\tcomponent: CnUnknownWidget,\n\t\t\t\t\t\tprops: { widgetKey: key },\n\t\t\t\t\t\tgridX: widget.gridX,\n\t\t\t\t\t\tgridY: widget.gridY,\n\t\t\t\t\t\tgridWidth: typeof widget.gridWidth === 'number' ? widget.gridWidth : columns,\n\t\t\t\t\t\tgridHeight: widget.gridHeight,\n\t\t\t\t\t})\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\t// Clamp gridWidth\n\t\t\t\tlet gridWidth = typeof widget.gridWidth === 'number' ? widget.gridWidth : 1\n\t\t\t\tif (gridWidth > columns) {\n\t\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\t\tconsole.warn(`[CnWidgetGrid] Widget \"${key}\" in slot \"${this.slotName}\" has gridWidth ${gridWidth} `\n\t\t\t\t\t\t+ `which exceeds the slot's gridColumns (${columns}). Clamping to ${columns}.`)\n\t\t\t\t\tgridWidth = columns\n\t\t\t\t}\n\n\t\t\t\t// Forward the widget's top-level `dataSource` (v2 unified\n\t\t\t\t// widget shape — sibling of `props`/`slot`/`gridX`) as a\n\t\t\t\t// `dataSource` prop so data-bound widgets (CnStatsBlockWidget,\n\t\t\t\t// CnChartWidget, …) receive it without the manifest having to\n\t\t\t\t// nest it inside `props`. A `props.dataSource` (the app-side\n\t\t\t\t// workaround) still wins on collision, because per-widget\n\t\t\t\t// `props` are spread LAST.\n\t\t\t\tconst dataSourceProp = (widget.dataSource !== undefined && widget.dataSource !== null)\n\t\t\t\t\t? { dataSource: widget.dataSource }\n\t\t\t\t\t: {}\n\n\t\t\t\tresult.push({\n\t\t\t\t\tid: widget.id,\n\t\t\t\t\twidgetKey: key,\n\t\t\t\t\tcomponent,\n\t\t\t\t\t// Detail-page object context first, then the top-level\n\t\t\t\t\t// `dataSource` (v2 unified widget shape), then the\n\t\t\t\t\t// entry-level `title` / `documentationUrl` so the widget\n\t\t\t\t\t// chrome (CnWidgetWrapper) can render them, then the\n\t\t\t\t\t// manifest's per-widget `props` last so explicit props win.\n\t\t\t\t\tprops: {\n\t\t\t\t\t\t...this.detailContextProps,\n\t\t\t\t\t\t...dataSourceProp,\n\t\t\t\t\t\t...(widget.title ? { title: widget.title } : {}),\n\t\t\t\t\t\t...(widget.documentationUrl ? { documentationUrl: widget.documentationUrl } : {}),\n\t\t\t\t\t\t...(widget.props ?? {}),\n\t\t\t\t\t},\n\t\t\t\t\tgridX: typeof widget.gridX === 'number' ? widget.gridX : 0,\n\t\t\t\t\tgridY: typeof widget.gridY === 'number' ? widget.gridY : 0,\n\t\t\t\t\tgridWidth,\n\t\t\t\t\tgridHeight: typeof widget.gridHeight === 'number' ? widget.gridHeight : 1,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\treturn result\n\t\t},\n\t},\n\n\twatch: {\n\t\teditableBody(active) {\n\t\t\tif (active) {\n\t\t\t\tthis.$nextTick(() => this.initGrid())\n\t\t\t} else {\n\t\t\t\tthis.destroyGrid()\n\t\t\t}\n\t\t},\n\t},\n\n\tmounted() {\n\t\tif (this.editableBody) {\n\t\t\tthis.initGrid()\n\t\t}\n\t},\n\n\tbeforeUnmount() {\n\t\tthis.destroyGrid()\n\t},\n\n\tmethods: {\n\t\t// Expose the shared grid helpers to the template.\n\t\tcnGridCellStyle,\n\t\thasGridRow,\n\t\t/**\n\t\t * Stable GridStack id for a widget entry (its id, else its index).\n\t\t *\n\t\t * @param {object} widget The resolved widget entry.\n\t\t * @param {number} index The entry's position in the slot.\n\t\t */\n\t\tgsId(widget, index) {\n\t\t\treturn widget.id !== null && widget.id !== undefined ? widget.id : `idx-${index}`\n\t\t},\n\n\t\t/** Initialise the GridStack engine on the editable body container. */\n\t\tinitGrid() {\n\t\t\tconst container = this.$refs.gridContainer\n\t\t\tif (!container) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst el = container.querySelector('.grid-stack')\n\t\t\tif (!el) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.grid = initGridStack(el, { columns: this.gridColumns, editable: true })\n\t\t\tthis.grid.on('change', (_event, items) => this.handleGridChange(items))\n\t\t},\n\n\t\t/** Tear down the GridStack engine. */\n\t\tdestroyGrid() {\n\t\t\tif (this.grid) {\n\t\t\t\tthis.grid.destroy(false)\n\t\t\t\tthis.grid = null\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Write GridStack geometry back into the widget entries (matched by id,\n\t\t * index fallback), clamped within the resolved column bound, and emit the\n\t\t * updated list. Mutating the entries keeps the working manifest in sync.\n\t\t *\n\t\t * @param {Array} items The changed items from GridStack.\n\t\t */\n\t\thandleGridChange(items) {\n\t\t\tconst geom = readGridGeometry(items)\n\t\t\tconst cols = this.gridColumns\n\t\t\tthis.resolvedWidgets.forEach((resolved, index) => {\n\t\t\t\tconst key = String(this.gsId(resolved, index))\n\t\t\t\tconst g = geom.get(key)\n\t\t\t\tconst target = this.widgets[index]\n\t\t\t\tif (!g || !target) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst width = Math.min(g.gridWidth, cols)\n\t\t\t\tconst x = Math.min(g.gridX, Math.max(0, cols - width))\n\t\t\t\ttarget.gridX = x\n\t\t\t\ttarget.gridY = g.gridY\n\t\t\t\ttarget.gridWidth = width\n\t\t\t\ttarget.gridHeight = g.gridHeight\n\t\t\t})\n\t\t\t/**\n\t\t\t * @event layout-change Emitted after a drag/resize with the updated widget entries.\n\t\t\t * @type {Array}\n\t\t\t */\n\t\t\tthis.$emit('layout-change', this.widgets)\n\t\t},\n\t},\n}\n</script>\n\n<style>\n.cn-widget-grid {\n\twidth: 100%;\n}\n\n.cn-widget-grid--editing {\n\tmin-height: 120px;\n}\n\n.cn-widget-grid--editing .grid-stack {\n\tbackground: transparent;\n}\n\n.cn-widget-grid--editing .grid-stack-item-content {\n\toverflow: hidden;\n\tbackground: var(--color-main-background);\n}\n\n.cn-widget-grid--editing .grid-stack-placeholder > .placeholder-content {\n\tbackground: var(--color-primary-element-light);\n\tborder: 2px dashed var(--color-primary-element);\n\tborder-radius: var(--border-radius-large);\n}\n</style>\n"],"names":["CnUnknownWidget"],"mappings":";;;;;;;;AA2EA;AACA;AACA;;AAEA,aAAe;AACd,CAAA,IAAI,EAAE,cAAc;;AAEpB,CAAA,MAAM,EAAE;EACP,UAAU,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,CAAA,EAAG;AACnC;;;;;EAKA,aAAa,EAAE,EAAE,OAAO,EAAE,IAAG,EAAG;AAChC;;;;;;;;AAQA,EAAA,qBAAqB,EAAE,EAAE,OAAO,EAAE,MAAM;EACxC;;AAED,CAAA,KAAK,EAAE;AACN;;AAEC;AACA;AACA;;AAED,EAAA,OAAO,EAAE;GACR,IAAI,EAAE,KAAK;AACX,GAAA,OAAO,EAAE,MAAM,EAAE;GACjB;;AAED;AACC;AACA;;;AAGD,EAAA,QAAQ,EAAE;GACT,IAAI,EAAE,MAAM;GACZ,QAAQ,EAAE,IAAI;GACd;;AAED;;AAEC;AACA;AACA;;AAED,EAAA,QAAQ,EAAE;GACT,IAAI,EAAE,MAAM;GACZ,OAAO,EAAE,IAAI;GACb;;AAED;;;;;AAKC;AACA;AACA;;AAED,EAAA,OAAO,EAAE;GACR,IAAI,EAAE,MAAM;GACZ,OAAO,EAAE,IAAI;GACb;;AAED;;;;AAIC;AACA;AACA;AACA;;AAED,EAAA,QAAQ,EAAE;GACT,IAAI,EAAE,OAAO;GACb,OAAO,EAAE,KAAK;GACd;EACD;;CAED,KAAK,EAAE,CAAC,eAAe,CAAC;;AAExB,CAAA,IAAI,GAAG;AACN,EAAA,OAAO;AACN,GAAA,IAAI,EAAE,IAAI;AACX,GAAA;EACA;;AAED,CAAA,QAAQ,EAAE;;AAET,EAAA,YAAY,GAAG;AACd,GAAA,OAAO,IAAI,CAAC,YAAY,IAAI,CAAC,QAAO,KAAM,MAAK;GAC/C;;EAED,WAAW,GAAG;;;;AAIb,GAAA,OAAO,kBAAkB,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,OAAO,CAAA;GACzE;;AAED,EAAA,iBAAiB,GAAG;AACnB,GAAA,OAAO,IAAI,CAAC,QAAO,IAAK,IAAI,CAAC,UAAW,IAAG,EAAC;GAC5C;;AAED;;;;;;AAMC;AACA;;AAED,EAAA,kBAAkB,GAAG;AACpB,GAAA,MAAM,GAAI,GAAE,IAAI,CAAC,qBAAqB,EAAE,MAAI;AAC5C,GAAA,IAAI,CAAC,GAAE,IAAK,OAAO,GAAI,KAAI,QAAQ,EAAE;AACpC,IAAA,OAAO,EAAC;AACT,IAAA;GACA,MAAM,GAAE,GAAI,GAAC;AACb,GAAA,KAAK,MAAM,OAAO,CAAC,YAAY,EAAE,QAAQ,EAAE,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE;IAC1F,IAAI,GAAG,CAAC,GAAG,CAAE,KAAI,SAAS,EAAE;AAC3B,KAAA,GAAG,CAAC,GAAG,CAAA,GAAI,GAAG,CAAC,GAAG,EAAA;AACnB,KAAA;AACD,IAAA;AACA,GAAA,OAAO,GAAE;GACT;;AAED;;;AAGC;AACA;AACA;;EAED,QAAQ,GAAG;AACV,GAAA,OAAO,EAAE,gBAAgB,EAAE,IAAI,CAAC,WAAY,EAAA;GAC5C;;AAED;;;;AAIC;AACA;;AAED,EAAA,YAAY,GAAG;GACd,OAAO,IAAI,CAAC,WAAU,GAAI,CAAA;GAC1B;;AAED,EAAA,eAAe,GAAG;GACjB,MAAM,OAAM,GAAI,IAAI,CAAC,YAAU;GAC/B,MAAM,MAAO,GAAE,GAAC;;AAEhB,GAAA,KAAK,MAAM,MAAK,IAAK,IAAI,CAAC,OAAO,EAAE;IAClC,MAAM,GAAE,GAAI,MAAM,CAAC,UAAQ;;;AAG3B;;AAEA,IAAA,IAAI,SAAQ,GAAI,KAAG;;AAEnB,IAAA,IAAI,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,EAAE;AAChC,KAAA,MAAM,KAAI,GAAI,IAAI,CAAC,iBAAiB,CAAC,GAAG,EAAA;AACxC,KAAA,SAAQ,GAAI,KAAK,CAAC,SAAQ,IAAK,MAAI;AACpC,KAAA;;;;;;AAMA,IAAA,MAAM,SAAQ,GAAI,mBAAmB,CAAC,GAAG,EAAA;;AAEzC,IAAA,IAAI,CAAC,SAAS,EAAE;AACf,KAAA,SAAQ,GAAI,gBAAgB,CAAC,GAAG,CAAE,IAAG,gBAAgB,CAAC,SAAS,CAAE,IAAG,KAAG;AACxE,KAAA;;AAEA,IAAA,IAAI,CAAC,SAAS,EAAE;;;AAGf;AACA,KAAA,MAAM,KAAM,GAAE,kBAAkB,CAAC,GAAG,CAAA,IAAK,kBAAkB,CAAC,SAAS,EAAA;AACrE,KAAA,SAAQ,GAAI,CAAC,KAAI,IAAK,KAAK,CAAC,QAAQ,KAAK,KAAG;AAC7C,KAAA;;AAEA,IAAA,IAAI,CAAC,SAAS,EAAE;;;;;;;AAOf;;;AAGA;AACA,KAAA,MAAM,YAAW,GAAI,MAAM,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAC,MAAO,KAAI,EAAA;AACrE;KACA,OAAO,CAAC,IAAI,CAAC,YAAW;AACvB,QAAE,CAAC,oEAAoE,EAAE,GAAG,CAAC,EAAE,CAAA;QAC7E,CAAC,SAAS,EAAE,IAAI,CAAC,QAAQ,CAAC,0CAA0C,CAAA;QACpE,mEAAkE;AACpE,QAAE,8EAA6E;AAC/E,QAAE,iFAAgF;AAClF,QAAE,CAAC,kCAAkC,EAAE,GAAG,CAAC,WAAW,EAAE,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAA;AACxE,SAAE,kFAAkF,EAAA;;;;KAItF,MAAM,CAAC,IAAI,CAAC;MACX,EAAE,EAAE,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;MAClD,SAAS,EAAE,GAAG;MACd,SAAS,EAAEA,UAAe;AAC1B,MAAA,KAAK,EAAE,EAAE,SAAS,EAAE,KAAK;AACzB,MAAA,KAAK,EAAE,MAAM,CAAC,KAAK;AACnB,MAAA,KAAK,EAAE,MAAM,CAAC,KAAK;AACnB,MAAA,SAAS,EAAE,OAAO,MAAM,CAAC,cAAc,QAAS,GAAE,MAAM,CAAC,SAAQ,GAAI,OAAO;MAC5E,UAAU,EAAE,MAAM,CAAC,UAAU;MAC7B,EAAA;AACD,KAAA,QAAO;AACR,KAAA;;AAEA;AACA,IAAA,IAAI,SAAU,GAAE,OAAO,MAAM,CAAC,SAAU,KAAI,QAAO,GAAI,MAAM,CAAC,YAAY,EAAA;AAC1E,IAAA,IAAI,SAAU,GAAE,OAAO,EAAE;AACxB;AACA,KAAA,OAAO,CAAC,IAAI,CAAC,CAAC,uBAAuB,EAAE,GAAG,CAAC,WAAW,EAAE,IAAI,CAAC,QAAQ,CAAC,gBAAgB,EAAE,SAAS,CAAE,CAAA,CAAA;AAClG,QAAE,CAAC,sCAAsC,EAAE,OAAO,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC,CAAC,EAAA;AAC/E,KAAA,SAAU,GAAE,QAAM;AACnB,KAAA;;;;;;;;;AASA,IAAA,MAAM,cAAe,GAAE,CAAC,MAAM,CAAC,UAAS,KAAM,aAAa,MAAM,CAAC,UAAS,KAAM,IAAI;AACpF,OAAE,EAAE,UAAU,EAAE,MAAM,CAAC,UAAW,EAAA;AAClC,OAAE,GAAC;;IAEJ,MAAM,CAAC,IAAI,CAAC;KACX,EAAE,EAAE,MAAM,CAAC,EAAE;KACb,SAAS,EAAE,GAAG;AACd,KAAA,SAAS;;AAET;;AAEA;;AAEA,KAAA,KAAK,EAAE;MACN,GAAG,IAAI,CAAC,kBAAkB;AAC1B,MAAA,GAAG,cAAc;AACjB,MAAA,IAAI,MAAM,CAAC,KAAM,GAAE,EAAE,KAAK,EAAE,MAAM,CAAC,KAAI,KAAM,EAAE,CAAC;AAChD,MAAA,IAAI,MAAM,CAAC,gBAAe,GAAI,EAAE,gBAAgB,EAAE,MAAM,CAAC,gBAAiB,EAAA,GAAI,EAAE,CAAC;AACjF,MAAA,IAAI,MAAM,CAAC,KAAI,IAAK,EAAE,CAAC;MACvB;KACD,KAAK,EAAE,OAAO,MAAM,CAAC,KAAM,KAAI,QAAS,GAAE,MAAM,CAAC,KAAM,GAAE,CAAC;KAC1D,KAAK,EAAE,OAAO,MAAM,CAAC,KAAM,KAAI,QAAS,GAAE,MAAM,CAAC,KAAM,GAAE,CAAC;AAC1D,KAAA,SAAS;AACT,KAAA,UAAU,EAAE,OAAO,MAAM,CAAC,UAAS,KAAM,QAAS,GAAE,MAAM,CAAC,UAAS,GAAI,CAAC;KACzE,EAAA;AACF,IAAA;;GAEA,OAAO,MAAK;GACZ;EACD;;AAED,CAAA,KAAK,EAAE;AACN,EAAA,YAAY,CAAC,MAAM,EAAE;GACpB,IAAI,MAAM,EAAE;AACX,IAAA,IAAI,CAAC,SAAS,CAAC,MAAM,IAAI,CAAC,QAAQ,EAAE,EAAA;IACnC,MAAK;AACN,IAAA,IAAI,CAAC,WAAW,GAAC;AAClB,IAAA;GACA;EACD;;AAED,CAAA,OAAO,GAAG;AACT,EAAA,IAAI,IAAI,CAAC,YAAY,EAAE;AACtB,GAAA,IAAI,CAAC,QAAQ,GAAC;AACf,GAAA;EACA;;AAED,CAAA,aAAa,GAAG;AACf,EAAA,IAAI,CAAC,WAAW,GAAC;EACjB;;AAED,CAAA,OAAO,EAAE;AACR;AACA,EAAA,eAAe;EACf,UAAU;AACV;;AAEC;AACA;;;AAGD,EAAA,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE;AACnB,GAAA,OAAO,MAAM,CAAC,EAAG,KAAI,IAAG,IAAK,MAAM,CAAC,EAAG,KAAI,SAAQ,GAAI,MAAM,CAAC,EAAG,GAAE,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;GAChF;;;EAGD,QAAQ,GAAG;AACV,GAAA,MAAM,SAAQ,GAAI,IAAI,CAAC,KAAK,CAAC,cAAY;AACzC,GAAA,IAAI,CAAC,SAAS,EAAE;IACf,MAAK;AACN,IAAA;AACA,GAAA,MAAM,EAAC,GAAI,SAAS,CAAC,aAAa,CAAC,aAAa,EAAA;AAChD,GAAA,IAAI,CAAC,EAAE,EAAE;IACR,MAAK;AACN,IAAA;AACA,GAAA,IAAI,CAAC,IAAG,GAAI,aAAa,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,CAAC,WAAW,EAAE,QAAQ,EAAE,MAAM,EAAA;AAC3E,GAAA,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,KAAK,KAAK,IAAI,CAAC,gBAAgB,CAAC,KAAK,CAAC,EAAA;GACtE;;AAED;EACA,WAAW,GAAG;GACb,IAAI,IAAI,CAAC,IAAI,EAAE;IACd,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAA;IACvB,IAAI,CAAC,IAAK,GAAE,KAAG;AAChB,IAAA;GACA;;AAED;;;;AAIC;;;EAGD,gBAAgB,CAAC,KAAK,EAAE;AACvB,GAAA,MAAM,IAAG,GAAI,gBAAgB,CAAC,KAAK,EAAA;GACnC,MAAM,IAAK,GAAE,IAAI,CAAC,YAAU;GAC5B,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,KAAK,KAAK;AACjD,IAAA,MAAM,GAAI,GAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,KAAK,CAAC,EAAA;AAC7C,IAAA,MAAM,CAAA,GAAI,IAAI,CAAC,GAAG,CAAC,GAAG,EAAA;AACtB,IAAA,MAAM,MAAK,GAAI,IAAI,CAAC,OAAO,CAAC,KAAK,EAAA;AACjC,IAAA,IAAI,CAAC,CAAA,IAAK,CAAC,MAAM,EAAE;KAClB,MAAK;AACN,KAAA;AACA,IAAA,MAAM,KAAI,GAAI,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,EAAE,IAAI,EAAA;IACxC,MAAM,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,KAAK,CAAC,EAAA;IACrD,MAAM,CAAC,KAAI,GAAI,EAAA;AACf,IAAA,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAI;AACrB,IAAA,MAAM,CAAC,SAAQ,GAAI,MAAI;IACvB,MAAM,CAAC,UAAW,GAAE,CAAC,CAAC,WAAS;IAC/B,EAAA;AACD;AACC;;;AAGD,GAAA,IAAI,CAAC,KAAK,CAAC,eAAe,EAAE,IAAI,CAAC,OAAO,EAAA;GACxC;EACD;AACF;;;;"}
|
|
1
|
+
{"version":3,"file":"CnWidgetGrid.vue2.js","sources":["../../../../src/components/CnWidgetGrid/CnWidgetGrid.vue"],"sourcesContent":["<!--\n CnWidgetGrid — per-slot CSS grid renderer for v2 manifest pages.\n\n Receives a flat array of widget entries for a single slot and renders\n them in a CSS grid. The number of columns is determined by `slotName`\n per the ADR-036 Decision 2 convention:\n\n body, footer, header-actions, modal, tab:*, section:* → 12 columns\n sidebar → 1 column\n\n Widget key resolution: built-in registry first, then cnRegistry inject.\n Unknown widget keys are skipped with a console.warn.\n gridWidth values exceeding gridColumns are clamped with a console.warn.\n\n Spec: REQ-MVR-004 (manifest-v2-renderer) — per-slot grid renderer\n\n @prop {Array} widgets Array of widget entry objects from the v2 manifest page.\n @prop {string} slotName The slot key (e.g. \"body\", \"sidebar\", \"tab:general\").\n @prop {object} registry The consumer registry from CnAppRoot (passed as prop\n for standalone use; normally injected via cnRegistry).\n-->\n<template>\n\t<!-- Editable body slot: a GridStack drag/resize grid (ADR-041 edit mode). -->\n\t<div\n\t\tv-if=\"editableBody\"\n\t\tref=\"gridContainer\"\n\t\tclass=\"cn-widget-grid cn-widget-grid--editing\"\n\t\t:data-slot=\"slotName\">\n\t\t<div class=\"grid-stack\">\n\t\t\t<div\n\t\t\t\tv-for=\"(widget, index) in resolvedWidgets\"\n\t\t\t\t:key=\"gsId(widget, index)\"\n\t\t\t\tclass=\"grid-stack-item\"\n\t\t\t\t:gs-id=\"gsId(widget, index)\"\n\t\t\t\t:gs-x=\"widget.gridX\"\n\t\t\t\t:gs-y=\"widget.gridY\"\n\t\t\t\t:gs-w=\"widget.gridWidth\"\n\t\t\t\t:gs-h=\"widget.gridHeight\">\n\t\t\t\t<div class=\"grid-stack-item-content\">\n\t\t\t\t\t<component\n\t\t\t\t\t\t:is=\"widget.component\"\n\t\t\t\t\t\tv-bind=\"widget.props\"\n\t\t\t\t\t\t@selectObject=\"onSelectObject\" />\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n\t<!-- Default read-only CSS grid. -->\n\t<div\n\t\tv-else\n\t\tclass=\"cn-widget-grid cn-grid\"\n\t\t:class=\"{ 'cn-grid--responsive': isResponsive }\"\n\t\t:style=\"gridVars\"\n\t\t:data-slot=\"slotName\">\n\t\t<template v-for=\"(widget, index) in resolvedWidgets\" :key=\"`${widget.widgetKey}-${index}`\">\n\t\t\t<div\n\t\t\t\tclass=\"cn-grid__item\"\n\t\t\t\t:class=\"{ 'cn-grid__item--row': hasGridRow(widget) }\"\n\t\t\t\t:style=\"cnGridCellStyle(widget, gridColumns)\">\n\t\t\t\t<component\n\t\t\t\t\t:is=\"widget.component\"\n\t\t\t\t\tv-bind=\"widget.props\"\n\t\t\t\t\t@selectObject=\"onSelectObject\" />\n\t\t\t</div>\n\t\t</template>\n\t</div>\n</template>\n\n<script>\nimport CnUnknownWidget from './CnUnknownWidget.vue'\nimport { cnGridCellStyle, hasGridRow } from '../../utils/grid.js'\nimport { initGridStack, readGridGeometry } from '../../utils/gridStack.js'\nimport { resolveSlotColumns } from '../../utils/resolveSlotColumns.js'\nimport { canonicalWidgetType } from '../../utils/widgetTypeAliases.js'\nimport { BUILT_IN_WIDGETS } from './builtInWidgets.js'\nimport { dashboardWidgetRegistry, getWidgetTypeEntry } from './dashboardWidgetRegistry.js'\n\n// Per-slot column counts and slot→columns resolution now live in\n// src/utils/resolveSlotColumns.js (shared with validateManifest so the grid\n// bound the validator enforces matches what this component renders).\n\nexport default {\n\tname: 'CnWidgetGrid',\n\n\tinject: {\n\t\tcnRegistry: { default: () => ({}) },\n\t\t/**\n\t\t * Per-page slot→columns override map, published by `CnPageRenderer`\n\t\t * from `page.config.slotColumns`. Merged UNDER the `columns` prop in\n\t\t * column resolution. `null` when the page declares no override.\n\t\t */\n\t\tcnSlotColumns: { default: null },\n\t\t/**\n\t\t * Object context for a `type:\"detail\"` page, published by\n\t\t * `CnPageRenderer` (reactive holder `{ value: { objectData, schema,\n\t\t * objectType, objectId, register, store } | null }`). Merged UNDER\n\t\t * each widget's own `props` so detail widgets (`data`, `metadata`,\n\t\t * `file-manager`, …) receive the loaded object without the manifest\n\t\t * having to author per-widget props. `null` outside a detail page.\n\t\t */\n\t\tcnDetailObjectContext: { default: null },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Widget entries for this slot from the v2 manifest page.\n\t\t * Each entry: `{ widgetKey, slot, gridX, gridY, gridWidth, gridHeight, props? }`.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\twidgets: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * The slot name this grid is rendering for (e.g. \"body\", \"sidebar\", \"tab:general\").\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tslotName: {\n\t\t\ttype: String,\n\t\t\trequired: true,\n\t\t},\n\n\t\t/**\n\t\t * Consumer registry override. When provided, overrides the injected\n\t\t * cnRegistry. Used for standalone testing.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tregistry: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Explicit column-count override for this slot. When `null` (default),\n\t\t * columns resolve from the injected per-page `slotColumns` map and then\n\t\t * the library default for the slot — preserving the fixed ADR-036\n\t\t * Decision 2 behaviour (body=12, sidebar=1). Set a number to force a\n\t\t * specific column count.\n\t\t *\n\t\t * @type {number|null}\n\t\t */\n\t\tcolumns: {\n\t\t\ttype: Number,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * When `true`, the `body` slot renders a GridStack drag/resize grid\n\t\t * (ADR-041 in-app edit mode) instead of the read-only CSS grid; geometry\n\t\t * changes are written back to the widget entries and emitted via\n\t\t * `@layout-change`. Other slots are unaffected. Default `false` keeps the\n\t\t * existing read-only rendering for every consumer.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\teditable: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\t},\n\n\temits: ['layout-change', 'select-object'],\n\n\tdata() {\n\t\treturn {\n\t\t\tgrid: null,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** Whether this grid should render the editable GridStack body. */\n\t\teditableBody() {\n\t\t\treturn this.editable && this.slotName === 'body'\n\t\t},\n\n\t\tgridColumns() {\n\t\t\t// Three-layer resolution: `columns` prop → injected per-page\n\t\t\t// slotColumns → the library default for the slot (getGridColumns\n\t\t\t// is preserved as the default tier inside resolveSlotColumns).\n\t\t\treturn resolveSlotColumns(this.slotName, this.cnSlotColumns, this.columns)\n\t\t},\n\n\t\teffectiveRegistry() {\n\t\t\treturn this.registry ?? this.cnRegistry ?? {}\n\t\t},\n\n\t\t/**\n\t\t * Resolved detail-page object context (or `{}`). Read from the\n\t\t * reactive holder published by `CnPageRenderer`; the `.value`\n\t\t * indirection lets the grid re-render once the async object load\n\t\t * resolves. Only the well-known object keys are forwarded to\n\t\t * widgets — see `resolvedWidgets`.\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\tdetailContextProps() {\n\t\t\tconst ctx = this.cnDetailObjectContext?.value\n\t\t\tif (!ctx || typeof ctx !== 'object') {\n\t\t\t\treturn {}\n\t\t\t}\n\t\t\tconst out = {}\n\t\t\tfor (const key of ['objectData', 'schema', 'objectType', 'objectId', 'register', 'store']) {\n\t\t\t\tif (ctx[key] !== undefined) {\n\t\t\t\t\tout[key] = ctx[key]\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/**\n\t\t * CSS custom properties driving the shared `.cn-grid` engine — the\n\t\t * desktop column count for this slot. Responsive collapse (12 → 6 →\n\t\t * 1) lives in `grid.css`.\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\tgridVars() {\n\t\t\treturn { '--cn-grid-cols': this.gridColumns }\n\t\t},\n\n\t\t/**\n\t\t * Whether this slot's grid collapses responsively. Multi-column\n\t\t * slots (body/footer/tab/section) do; the single-column sidebar\n\t\t * slot stays put.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tisResponsive() {\n\t\t\treturn this.gridColumns > 1\n\t\t},\n\n\t\tresolvedWidgets() {\n\t\t\tconst columns = this.gridColumns\n\t\t\tconst result = []\n\n\t\t\tfor (const widget of this.widgets) {\n\t\t\t\tconst key = widget.widgetKey\n\n\t\t\t\t// Resolve component: consumer registry first (overrides built-ins),\n\t\t\t\t// then fall back to built-in registry.\n\t\t\t\t// Per spec REQ-MVR-005: \"Custom widget overrides built-in\"\n\t\t\t\tlet component = null\n\n\t\t\t\tif (this.effectiveRegistry[key]) {\n\t\t\t\t\tconst entry = this.effectiveRegistry[key]\n\t\t\t\t\tcomponent = entry.component ?? entry\n\t\t\t\t}\n\n\t\t\t\t// `table` and `object-table` (and `map` / `map-viewer`) are the\n\t\t\t\t// same component registered under two names in two registries.\n\t\t\t\t// Canonicalising here means either spelling resolves, whichever\n\t\t\t\t// registry happens to hold it — see utils/widgetTypeAliases.js.\n\t\t\t\tconst canonical = canonicalWidgetType(key)\n\n\t\t\t\tif (!component) {\n\t\t\t\t\tcomponent = BUILT_IN_WIDGETS[key] ?? BUILT_IN_WIDGETS[canonical] ?? null\n\t\t\t\t}\n\n\t\t\t\tif (!component) {\n\t\t\t\t\t// Dashboard widget catalog (cn-widget-library): the 21 migrated\n\t\t\t\t\t// widgets self-register here. Resolved after cnRegistry + built-ins\n\t\t\t\t\t// so a consumer override still wins.\n\t\t\t\t\tconst entry = getWidgetTypeEntry(key) || getWidgetTypeEntry(canonical)\n\t\t\t\t\tcomponent = (entry && entry.renderer) ?? null\n\t\t\t\t}\n\n\t\t\t\tif (!component) {\n\t\t\t\t\t// An EMPTY catalog is a different failure from an unknown key, and\n\t\t\t\t\t// the fix is different, so say which one this is. It means nothing\n\t\t\t\t\t// ever imported the module that populates the catalog — most often\n\t\t\t\t\t// because a consumer deep-imported a widget's own module\n\t\t\t\t\t// (`.../components/CnMapWidget/index.js`) expecting it to register\n\t\t\t\t\t// its type. Those modules export the component only; the type\n\t\t\t\t\t// registrations all live in `CnWidgetGrid/registerDashboardWidgets.js`,\n\t\t\t\t\t// which the package root and the `components` barrel both import.\n\t\t\t\t\t// Before this was consolidated, two of them self-registered as well,\n\t\t\t\t\t// so a deep import happened to work by accident.\n\t\t\t\t\tconst catalogEmpty = Object.keys(dashboardWidgetRegistry).length === 0\n\t\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\t\tconsole.warn(catalogEmpty\n\t\t\t\t\t\t? `[CnWidgetGrid] The dashboard widget catalog is EMPTY, so widgetKey \"${key}\" `\n\t\t\t\t\t\t+ `in slot \"${this.slotName}\" cannot resolve — this is not a bad key. `\n\t\t\t\t\t\t+ 'Import the package root (`@conduction/nextcloud-vue`) or, if you '\n\t\t\t\t\t\t+ 'cherry-pick modules, `components/CnWidgetGrid/registerDashboardWidgets.js`. '\n\t\t\t\t\t\t+ 'Importing a widget\\'s own module gives you the component but registers no type.'\n\t\t\t\t\t\t: `[CnWidgetGrid] Unknown widgetKey \"${key}\" in slot \"${this.slotName}\". `\n\t\t\t\t\t\t\t+ 'Register it in the built-in registry or pass it via the CnAppRoot registry prop.')\n\t\t\t\t\t// Render a visible, designed placeholder instead of silently\n\t\t\t\t\t// skipping — a page whose widgets ALL fail to resolve must not\n\t\t\t\t\t// leave a blank pane (2026-07-06 audit: petstore dashboard).\n\t\t\t\t\tresult.push({\n\t\t\t\t\t\tid: widget.id ?? `unknown-${key}-${result.length}`,\n\t\t\t\t\t\twidgetKey: key,\n\t\t\t\t\t\tcomponent: CnUnknownWidget,\n\t\t\t\t\t\tprops: { widgetKey: key },\n\t\t\t\t\t\tgridX: widget.gridX,\n\t\t\t\t\t\tgridY: widget.gridY,\n\t\t\t\t\t\tgridWidth: typeof widget.gridWidth === 'number' ? widget.gridWidth : columns,\n\t\t\t\t\t\tgridHeight: widget.gridHeight,\n\t\t\t\t\t})\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\t// Clamp gridWidth\n\t\t\t\tlet gridWidth = typeof widget.gridWidth === 'number' ? widget.gridWidth : 1\n\t\t\t\tif (gridWidth > columns) {\n\t\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\t\tconsole.warn(`[CnWidgetGrid] Widget \"${key}\" in slot \"${this.slotName}\" has gridWidth ${gridWidth} `\n\t\t\t\t\t\t+ `which exceeds the slot's gridColumns (${columns}). Clamping to ${columns}.`)\n\t\t\t\t\tgridWidth = columns\n\t\t\t\t}\n\n\t\t\t\t// Forward the widget's top-level `dataSource` (v2 unified\n\t\t\t\t// widget shape — sibling of `props`/`slot`/`gridX`) as a\n\t\t\t\t// `dataSource` prop so data-bound widgets (CnStatsBlockWidget,\n\t\t\t\t// CnChartWidget, …) receive it without the manifest having to\n\t\t\t\t// nest it inside `props`. A `props.dataSource` (the app-side\n\t\t\t\t// workaround) still wins on collision, because per-widget\n\t\t\t\t// `props` are spread LAST.\n\t\t\t\tconst dataSourceProp = (widget.dataSource !== undefined && widget.dataSource !== null)\n\t\t\t\t\t? { dataSource: widget.dataSource }\n\t\t\t\t\t: {}\n\n\t\t\t\tresult.push({\n\t\t\t\t\tid: widget.id,\n\t\t\t\t\twidgetKey: key,\n\t\t\t\t\tcomponent,\n\t\t\t\t\t// Detail-page object context first, then the top-level\n\t\t\t\t\t// `dataSource` (v2 unified widget shape), then the\n\t\t\t\t\t// entry-level `title` / `documentationUrl` so the widget\n\t\t\t\t\t// chrome (CnWidgetWrapper) can render them, then the\n\t\t\t\t\t// manifest's per-widget `props` last so explicit props win.\n\t\t\t\t\tprops: {\n\t\t\t\t\t\t...this.detailContextProps,\n\t\t\t\t\t\t...dataSourceProp,\n\t\t\t\t\t\t...(widget.title ? { title: widget.title } : {}),\n\t\t\t\t\t\t...(widget.documentationUrl ? { documentationUrl: widget.documentationUrl } : {}),\n\t\t\t\t\t\t...(widget.props ?? {}),\n\t\t\t\t\t},\n\t\t\t\t\tgridX: typeof widget.gridX === 'number' ? widget.gridX : 0,\n\t\t\t\t\tgridY: typeof widget.gridY === 'number' ? widget.gridY : 0,\n\t\t\t\t\tgridWidth,\n\t\t\t\t\tgridHeight: typeof widget.gridHeight === 'number' ? widget.gridHeight : 1,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\treturn result\n\t\t},\n\t},\n\n\twatch: {\n\t\teditableBody(active) {\n\t\t\tif (active) {\n\t\t\t\tthis.$nextTick(() => this.initGrid())\n\t\t\t} else {\n\t\t\t\tthis.destroyGrid()\n\t\t\t}\n\t\t},\n\t},\n\n\tmounted() {\n\t\tif (this.editableBody) {\n\t\t\tthis.initGrid()\n\t\t}\n\t},\n\n\tbeforeUnmount() {\n\t\tthis.destroyGrid()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Pass a related widget's object click up; the page opens the object.\n\t\t *\n\t\t * @param {object} raw The clicked object.\n\t\t * @return {void}\n\t\t */\n\t\tonSelectObject(raw) {\n\t\t\t/**\n\t\t\t * @event select-object An object row in a related widget of this grid was clicked. Payload is the raw object.\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('select-object', raw)\n\t\t},\n\n\t\t// Expose the shared grid helpers to the template.\n\t\tcnGridCellStyle,\n\t\thasGridRow,\n\t\t/**\n\t\t * Stable GridStack id for a widget entry (its id, else its index).\n\t\t *\n\t\t * @param {object} widget The resolved widget entry.\n\t\t * @param {number} index The entry's position in the slot.\n\t\t */\n\t\tgsId(widget, index) {\n\t\t\treturn widget.id !== null && widget.id !== undefined ? widget.id : `idx-${index}`\n\t\t},\n\n\t\t/** Initialise the GridStack engine on the editable body container. */\n\t\tinitGrid() {\n\t\t\tconst container = this.$refs.gridContainer\n\t\t\tif (!container) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst el = container.querySelector('.grid-stack')\n\t\t\tif (!el) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.grid = initGridStack(el, { columns: this.gridColumns, editable: true })\n\t\t\tthis.grid.on('change', (_event, items) => this.handleGridChange(items))\n\t\t},\n\n\t\t/** Tear down the GridStack engine. */\n\t\tdestroyGrid() {\n\t\t\tif (this.grid) {\n\t\t\t\tthis.grid.destroy(false)\n\t\t\t\tthis.grid = null\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Write GridStack geometry back into the widget entries (matched by id,\n\t\t * index fallback), clamped within the resolved column bound, and emit the\n\t\t * updated list. Mutating the entries keeps the working manifest in sync.\n\t\t *\n\t\t * @param {Array} items The changed items from GridStack.\n\t\t */\n\t\thandleGridChange(items) {\n\t\t\tconst geom = readGridGeometry(items)\n\t\t\tconst cols = this.gridColumns\n\t\t\tthis.resolvedWidgets.forEach((resolved, index) => {\n\t\t\t\tconst key = String(this.gsId(resolved, index))\n\t\t\t\tconst g = geom.get(key)\n\t\t\t\tconst target = this.widgets[index]\n\t\t\t\tif (!g || !target) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst width = Math.min(g.gridWidth, cols)\n\t\t\t\tconst x = Math.min(g.gridX, Math.max(0, cols - width))\n\t\t\t\ttarget.gridX = x\n\t\t\t\ttarget.gridY = g.gridY\n\t\t\t\ttarget.gridWidth = width\n\t\t\t\ttarget.gridHeight = g.gridHeight\n\t\t\t})\n\t\t\t/**\n\t\t\t * @event layout-change Emitted after a drag/resize with the updated widget entries.\n\t\t\t * @type {Array}\n\t\t\t */\n\t\t\tthis.$emit('layout-change', this.widgets)\n\t\t},\n\t},\n}\n</script>\n\n<style>\n.cn-widget-grid {\n\twidth: 100%;\n}\n\n.cn-widget-grid--editing {\n\tmin-height: 120px;\n}\n\n.cn-widget-grid--editing .grid-stack {\n\tbackground: transparent;\n}\n\n.cn-widget-grid--editing .grid-stack-item-content {\n\toverflow: hidden;\n\tbackground: var(--color-main-background);\n}\n\n.cn-widget-grid--editing .grid-stack-placeholder > .placeholder-content {\n\tbackground: var(--color-primary-element-light);\n\tborder: 2px dashed var(--color-primary-element);\n\tborder-radius: var(--border-radius-large);\n}\n</style>\n"],"names":["CnUnknownWidget"],"mappings":"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"}
|
|
@@ -26,7 +26,7 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
|
|
|
26
26
|
"gs-h": widget.gridHeight
|
|
27
27
|
}, [
|
|
28
28
|
createElementVNode("div", _hoisted_4, [
|
|
29
|
-
(openBlock(), createBlock(resolveDynamicComponent(widget.component), mergeProps({ ref_for: true }, widget.props), null, 16 /* FULL_PROPS
|
|
29
|
+
(openBlock(), createBlock(resolveDynamicComponent(widget.component), mergeProps({ ref_for: true }, widget.props, { onSelectObject: $options.onSelectObject }), null, 16 /* FULL_PROPS */, ["onSelectObject"]))
|
|
30
30
|
])
|
|
31
31
|
], 8 /* PROPS */, _hoisted_3))
|
|
32
32
|
}), 128 /* KEYED_FRAGMENT */))
|
|
@@ -44,7 +44,7 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
|
|
|
44
44
|
class: normalizeClass(["cn-grid__item", { 'cn-grid__item--row': $options.hasGridRow(widget) }]),
|
|
45
45
|
style: normalizeStyle($options.cnGridCellStyle(widget, $options.gridColumns))
|
|
46
46
|
}, [
|
|
47
|
-
(openBlock(), createBlock(resolveDynamicComponent(widget.component), mergeProps({ ref_for: true }, widget.props), null, 16 /* FULL_PROPS
|
|
47
|
+
(openBlock(), createBlock(resolveDynamicComponent(widget.component), mergeProps({ ref_for: true }, widget.props, { onSelectObject: $options.onSelectObject }), null, 16 /* FULL_PROPS */, ["onSelectObject"]))
|
|
48
48
|
], 6 /* CLASS, STYLE */))
|
|
49
49
|
}), 128 /* KEYED_FRAGMENT */))
|
|
50
50
|
], 14 /* CLASS, STYLE, PROPS */, _hoisted_5))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnWidgetGrid.vue3.js","sources":["../../../../src/components/CnWidgetGrid/CnWidgetGrid.vue?vue&type=template&id=c117f722&lang.js"],"sourcesContent":["<!--\n CnWidgetGrid — per-slot CSS grid renderer for v2 manifest pages.\n\n Receives a flat array of widget entries for a single slot and renders\n them in a CSS grid. The number of columns is determined by `slotName`\n per the ADR-036 Decision 2 convention:\n\n body, footer, header-actions, modal, tab:*, section:* → 12 columns\n sidebar → 1 column\n\n Widget key resolution: built-in registry first, then cnRegistry inject.\n Unknown widget keys are skipped with a console.warn.\n gridWidth values exceeding gridColumns are clamped with a console.warn.\n\n Spec: REQ-MVR-004 (manifest-v2-renderer) — per-slot grid renderer\n\n @prop {Array} widgets Array of widget entry objects from the v2 manifest page.\n @prop {string} slotName The slot key (e.g. \"body\", \"sidebar\", \"tab:general\").\n @prop {object} registry The consumer registry from CnAppRoot (passed as prop\n for standalone use; normally injected via cnRegistry).\n-->\n<template>\n\t<!-- Editable body slot: a GridStack drag/resize grid (ADR-041 edit mode). -->\n\t<div\n\t\tv-if=\"editableBody\"\n\t\tref=\"gridContainer\"\n\t\tclass=\"cn-widget-grid cn-widget-grid--editing\"\n\t\t:data-slot=\"slotName\">\n\t\t<div class=\"grid-stack\">\n\t\t\t<div\n\t\t\t\tv-for=\"(widget, index) in resolvedWidgets\"\n\t\t\t\t:key=\"gsId(widget, index)\"\n\t\t\t\tclass=\"grid-stack-item\"\n\t\t\t\t:gs-id=\"gsId(widget, index)\"\n\t\t\t\t:gs-x=\"widget.gridX\"\n\t\t\t\t:gs-y=\"widget.gridY\"\n\t\t\t\t:gs-w=\"widget.gridWidth\"\n\t\t\t\t:gs-h=\"widget.gridHeight\">\n\t\t\t\t<div class=\"grid-stack-item-content\">\n\t\t\t\t\t<component\n\t\t\t\t\t\t:is=\"widget.component\"\n\t\t\t\t\t\tv-bind=\"widget.props\" />\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n\t<!-- Default read-only CSS grid. -->\n\t<div\n\t\tv-else\n\t\tclass=\"cn-widget-grid cn-grid\"\n\t\t:class=\"{ 'cn-grid--responsive': isResponsive }\"\n\t\t:style=\"gridVars\"\n\t\t:data-slot=\"slotName\">\n\t\t<template v-for=\"(widget, index) in resolvedWidgets\" :key=\"`${widget.widgetKey}-${index}`\">\n\t\t\t<div\n\t\t\t\tclass=\"cn-grid__item\"\n\t\t\t\t:class=\"{ 'cn-grid__item--row': hasGridRow(widget) }\"\n\t\t\t\t:style=\"cnGridCellStyle(widget, gridColumns)\">\n\t\t\t\t<component\n\t\t\t\t\t:is=\"widget.component\"\n\t\t\t\t\tv-bind=\"widget.props\" />\n\t\t\t</div>\n\t\t</template>\n\t</div>\n</template>\n\n<script>\nimport CnUnknownWidget from './CnUnknownWidget.vue'\nimport { cnGridCellStyle, hasGridRow } from '../../utils/grid.js'\nimport { initGridStack, readGridGeometry } from '../../utils/gridStack.js'\nimport { resolveSlotColumns } from '../../utils/resolveSlotColumns.js'\nimport { canonicalWidgetType } from '../../utils/widgetTypeAliases.js'\nimport { BUILT_IN_WIDGETS } from './builtInWidgets.js'\nimport { dashboardWidgetRegistry, getWidgetTypeEntry } from './dashboardWidgetRegistry.js'\n\n// Per-slot column counts and slot→columns resolution now live in\n// src/utils/resolveSlotColumns.js (shared with validateManifest so the grid\n// bound the validator enforces matches what this component renders).\n\nexport default {\n\tname: 'CnWidgetGrid',\n\n\tinject: {\n\t\tcnRegistry: { default: () => ({}) },\n\t\t/**\n\t\t * Per-page slot→columns override map, published by `CnPageRenderer`\n\t\t * from `page.config.slotColumns`. Merged UNDER the `columns` prop in\n\t\t * column resolution. `null` when the page declares no override.\n\t\t */\n\t\tcnSlotColumns: { default: null },\n\t\t/**\n\t\t * Object context for a `type:\"detail\"` page, published by\n\t\t * `CnPageRenderer` (reactive holder `{ value: { objectData, schema,\n\t\t * objectType, objectId, register, store } | null }`). Merged UNDER\n\t\t * each widget's own `props` so detail widgets (`data`, `metadata`,\n\t\t * `file-manager`, …) receive the loaded object without the manifest\n\t\t * having to author per-widget props. `null` outside a detail page.\n\t\t */\n\t\tcnDetailObjectContext: { default: null },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Widget entries for this slot from the v2 manifest page.\n\t\t * Each entry: `{ widgetKey, slot, gridX, gridY, gridWidth, gridHeight, props? }`.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\twidgets: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * The slot name this grid is rendering for (e.g. \"body\", \"sidebar\", \"tab:general\").\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tslotName: {\n\t\t\ttype: String,\n\t\t\trequired: true,\n\t\t},\n\n\t\t/**\n\t\t * Consumer registry override. When provided, overrides the injected\n\t\t * cnRegistry. Used for standalone testing.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tregistry: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Explicit column-count override for this slot. When `null` (default),\n\t\t * columns resolve from the injected per-page `slotColumns` map and then\n\t\t * the library default for the slot — preserving the fixed ADR-036\n\t\t * Decision 2 behaviour (body=12, sidebar=1). Set a number to force a\n\t\t * specific column count.\n\t\t *\n\t\t * @type {number|null}\n\t\t */\n\t\tcolumns: {\n\t\t\ttype: Number,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * When `true`, the `body` slot renders a GridStack drag/resize grid\n\t\t * (ADR-041 in-app edit mode) instead of the read-only CSS grid; geometry\n\t\t * changes are written back to the widget entries and emitted via\n\t\t * `@layout-change`. Other slots are unaffected. Default `false` keeps the\n\t\t * existing read-only rendering for every consumer.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\teditable: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\t},\n\n\temits: ['layout-change'],\n\n\tdata() {\n\t\treturn {\n\t\t\tgrid: null,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** Whether this grid should render the editable GridStack body. */\n\t\teditableBody() {\n\t\t\treturn this.editable && this.slotName === 'body'\n\t\t},\n\n\t\tgridColumns() {\n\t\t\t// Three-layer resolution: `columns` prop → injected per-page\n\t\t\t// slotColumns → the library default for the slot (getGridColumns\n\t\t\t// is preserved as the default tier inside resolveSlotColumns).\n\t\t\treturn resolveSlotColumns(this.slotName, this.cnSlotColumns, this.columns)\n\t\t},\n\n\t\teffectiveRegistry() {\n\t\t\treturn this.registry ?? this.cnRegistry ?? {}\n\t\t},\n\n\t\t/**\n\t\t * Resolved detail-page object context (or `{}`). Read from the\n\t\t * reactive holder published by `CnPageRenderer`; the `.value`\n\t\t * indirection lets the grid re-render once the async object load\n\t\t * resolves. Only the well-known object keys are forwarded to\n\t\t * widgets — see `resolvedWidgets`.\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\tdetailContextProps() {\n\t\t\tconst ctx = this.cnDetailObjectContext?.value\n\t\t\tif (!ctx || typeof ctx !== 'object') {\n\t\t\t\treturn {}\n\t\t\t}\n\t\t\tconst out = {}\n\t\t\tfor (const key of ['objectData', 'schema', 'objectType', 'objectId', 'register', 'store']) {\n\t\t\t\tif (ctx[key] !== undefined) {\n\t\t\t\t\tout[key] = ctx[key]\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/**\n\t\t * CSS custom properties driving the shared `.cn-grid` engine — the\n\t\t * desktop column count for this slot. Responsive collapse (12 → 6 →\n\t\t * 1) lives in `grid.css`.\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\tgridVars() {\n\t\t\treturn { '--cn-grid-cols': this.gridColumns }\n\t\t},\n\n\t\t/**\n\t\t * Whether this slot's grid collapses responsively. Multi-column\n\t\t * slots (body/footer/tab/section) do; the single-column sidebar\n\t\t * slot stays put.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tisResponsive() {\n\t\t\treturn this.gridColumns > 1\n\t\t},\n\n\t\tresolvedWidgets() {\n\t\t\tconst columns = this.gridColumns\n\t\t\tconst result = []\n\n\t\t\tfor (const widget of this.widgets) {\n\t\t\t\tconst key = widget.widgetKey\n\n\t\t\t\t// Resolve component: consumer registry first (overrides built-ins),\n\t\t\t\t// then fall back to built-in registry.\n\t\t\t\t// Per spec REQ-MVR-005: \"Custom widget overrides built-in\"\n\t\t\t\tlet component = null\n\n\t\t\t\tif (this.effectiveRegistry[key]) {\n\t\t\t\t\tconst entry = this.effectiveRegistry[key]\n\t\t\t\t\tcomponent = entry.component ?? entry\n\t\t\t\t}\n\n\t\t\t\t// `table` and `object-table` (and `map` / `map-viewer`) are the\n\t\t\t\t// same component registered under two names in two registries.\n\t\t\t\t// Canonicalising here means either spelling resolves, whichever\n\t\t\t\t// registry happens to hold it — see utils/widgetTypeAliases.js.\n\t\t\t\tconst canonical = canonicalWidgetType(key)\n\n\t\t\t\tif (!component) {\n\t\t\t\t\tcomponent = BUILT_IN_WIDGETS[key] ?? BUILT_IN_WIDGETS[canonical] ?? null\n\t\t\t\t}\n\n\t\t\t\tif (!component) {\n\t\t\t\t\t// Dashboard widget catalog (cn-widget-library): the 21 migrated\n\t\t\t\t\t// widgets self-register here. Resolved after cnRegistry + built-ins\n\t\t\t\t\t// so a consumer override still wins.\n\t\t\t\t\tconst entry = getWidgetTypeEntry(key) || getWidgetTypeEntry(canonical)\n\t\t\t\t\tcomponent = (entry && entry.renderer) ?? null\n\t\t\t\t}\n\n\t\t\t\tif (!component) {\n\t\t\t\t\t// An EMPTY catalog is a different failure from an unknown key, and\n\t\t\t\t\t// the fix is different, so say which one this is. It means nothing\n\t\t\t\t\t// ever imported the module that populates the catalog — most often\n\t\t\t\t\t// because a consumer deep-imported a widget's own module\n\t\t\t\t\t// (`.../components/CnMapWidget/index.js`) expecting it to register\n\t\t\t\t\t// its type. Those modules export the component only; the type\n\t\t\t\t\t// registrations all live in `CnWidgetGrid/registerDashboardWidgets.js`,\n\t\t\t\t\t// which the package root and the `components` barrel both import.\n\t\t\t\t\t// Before this was consolidated, two of them self-registered as well,\n\t\t\t\t\t// so a deep import happened to work by accident.\n\t\t\t\t\tconst catalogEmpty = Object.keys(dashboardWidgetRegistry).length === 0\n\t\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\t\tconsole.warn(catalogEmpty\n\t\t\t\t\t\t? `[CnWidgetGrid] The dashboard widget catalog is EMPTY, so widgetKey \"${key}\" `\n\t\t\t\t\t\t+ `in slot \"${this.slotName}\" cannot resolve — this is not a bad key. `\n\t\t\t\t\t\t+ 'Import the package root (`@conduction/nextcloud-vue`) or, if you '\n\t\t\t\t\t\t+ 'cherry-pick modules, `components/CnWidgetGrid/registerDashboardWidgets.js`. '\n\t\t\t\t\t\t+ 'Importing a widget\\'s own module gives you the component but registers no type.'\n\t\t\t\t\t\t: `[CnWidgetGrid] Unknown widgetKey \"${key}\" in slot \"${this.slotName}\". `\n\t\t\t\t\t\t\t+ 'Register it in the built-in registry or pass it via the CnAppRoot registry prop.')\n\t\t\t\t\t// Render a visible, designed placeholder instead of silently\n\t\t\t\t\t// skipping — a page whose widgets ALL fail to resolve must not\n\t\t\t\t\t// leave a blank pane (2026-07-06 audit: petstore dashboard).\n\t\t\t\t\tresult.push({\n\t\t\t\t\t\tid: widget.id ?? `unknown-${key}-${result.length}`,\n\t\t\t\t\t\twidgetKey: key,\n\t\t\t\t\t\tcomponent: CnUnknownWidget,\n\t\t\t\t\t\tprops: { widgetKey: key },\n\t\t\t\t\t\tgridX: widget.gridX,\n\t\t\t\t\t\tgridY: widget.gridY,\n\t\t\t\t\t\tgridWidth: typeof widget.gridWidth === 'number' ? widget.gridWidth : columns,\n\t\t\t\t\t\tgridHeight: widget.gridHeight,\n\t\t\t\t\t})\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\t// Clamp gridWidth\n\t\t\t\tlet gridWidth = typeof widget.gridWidth === 'number' ? widget.gridWidth : 1\n\t\t\t\tif (gridWidth > columns) {\n\t\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\t\tconsole.warn(`[CnWidgetGrid] Widget \"${key}\" in slot \"${this.slotName}\" has gridWidth ${gridWidth} `\n\t\t\t\t\t\t+ `which exceeds the slot's gridColumns (${columns}). Clamping to ${columns}.`)\n\t\t\t\t\tgridWidth = columns\n\t\t\t\t}\n\n\t\t\t\t// Forward the widget's top-level `dataSource` (v2 unified\n\t\t\t\t// widget shape — sibling of `props`/`slot`/`gridX`) as a\n\t\t\t\t// `dataSource` prop so data-bound widgets (CnStatsBlockWidget,\n\t\t\t\t// CnChartWidget, …) receive it without the manifest having to\n\t\t\t\t// nest it inside `props`. A `props.dataSource` (the app-side\n\t\t\t\t// workaround) still wins on collision, because per-widget\n\t\t\t\t// `props` are spread LAST.\n\t\t\t\tconst dataSourceProp = (widget.dataSource !== undefined && widget.dataSource !== null)\n\t\t\t\t\t? { dataSource: widget.dataSource }\n\t\t\t\t\t: {}\n\n\t\t\t\tresult.push({\n\t\t\t\t\tid: widget.id,\n\t\t\t\t\twidgetKey: key,\n\t\t\t\t\tcomponent,\n\t\t\t\t\t// Detail-page object context first, then the top-level\n\t\t\t\t\t// `dataSource` (v2 unified widget shape), then the\n\t\t\t\t\t// entry-level `title` / `documentationUrl` so the widget\n\t\t\t\t\t// chrome (CnWidgetWrapper) can render them, then the\n\t\t\t\t\t// manifest's per-widget `props` last so explicit props win.\n\t\t\t\t\tprops: {\n\t\t\t\t\t\t...this.detailContextProps,\n\t\t\t\t\t\t...dataSourceProp,\n\t\t\t\t\t\t...(widget.title ? { title: widget.title } : {}),\n\t\t\t\t\t\t...(widget.documentationUrl ? { documentationUrl: widget.documentationUrl } : {}),\n\t\t\t\t\t\t...(widget.props ?? {}),\n\t\t\t\t\t},\n\t\t\t\t\tgridX: typeof widget.gridX === 'number' ? widget.gridX : 0,\n\t\t\t\t\tgridY: typeof widget.gridY === 'number' ? widget.gridY : 0,\n\t\t\t\t\tgridWidth,\n\t\t\t\t\tgridHeight: typeof widget.gridHeight === 'number' ? widget.gridHeight : 1,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\treturn result\n\t\t},\n\t},\n\n\twatch: {\n\t\teditableBody(active) {\n\t\t\tif (active) {\n\t\t\t\tthis.$nextTick(() => this.initGrid())\n\t\t\t} else {\n\t\t\t\tthis.destroyGrid()\n\t\t\t}\n\t\t},\n\t},\n\n\tmounted() {\n\t\tif (this.editableBody) {\n\t\t\tthis.initGrid()\n\t\t}\n\t},\n\n\tbeforeUnmount() {\n\t\tthis.destroyGrid()\n\t},\n\n\tmethods: {\n\t\t// Expose the shared grid helpers to the template.\n\t\tcnGridCellStyle,\n\t\thasGridRow,\n\t\t/**\n\t\t * Stable GridStack id for a widget entry (its id, else its index).\n\t\t *\n\t\t * @param {object} widget The resolved widget entry.\n\t\t * @param {number} index The entry's position in the slot.\n\t\t */\n\t\tgsId(widget, index) {\n\t\t\treturn widget.id !== null && widget.id !== undefined ? widget.id : `idx-${index}`\n\t\t},\n\n\t\t/** Initialise the GridStack engine on the editable body container. */\n\t\tinitGrid() {\n\t\t\tconst container = this.$refs.gridContainer\n\t\t\tif (!container) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst el = container.querySelector('.grid-stack')\n\t\t\tif (!el) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.grid = initGridStack(el, { columns: this.gridColumns, editable: true })\n\t\t\tthis.grid.on('change', (_event, items) => this.handleGridChange(items))\n\t\t},\n\n\t\t/** Tear down the GridStack engine. */\n\t\tdestroyGrid() {\n\t\t\tif (this.grid) {\n\t\t\t\tthis.grid.destroy(false)\n\t\t\t\tthis.grid = null\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Write GridStack geometry back into the widget entries (matched by id,\n\t\t * index fallback), clamped within the resolved column bound, and emit the\n\t\t * updated list. Mutating the entries keeps the working manifest in sync.\n\t\t *\n\t\t * @param {Array} items The changed items from GridStack.\n\t\t */\n\t\thandleGridChange(items) {\n\t\t\tconst geom = readGridGeometry(items)\n\t\t\tconst cols = this.gridColumns\n\t\t\tthis.resolvedWidgets.forEach((resolved, index) => {\n\t\t\t\tconst key = String(this.gsId(resolved, index))\n\t\t\t\tconst g = geom.get(key)\n\t\t\t\tconst target = this.widgets[index]\n\t\t\t\tif (!g || !target) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst width = Math.min(g.gridWidth, cols)\n\t\t\t\tconst x = Math.min(g.gridX, Math.max(0, cols - width))\n\t\t\t\ttarget.gridX = x\n\t\t\t\ttarget.gridY = g.gridY\n\t\t\t\ttarget.gridWidth = width\n\t\t\t\ttarget.gridHeight = g.gridHeight\n\t\t\t})\n\t\t\t/**\n\t\t\t * @event layout-change Emitted after a drag/resize with the updated widget entries.\n\t\t\t * @type {Array}\n\t\t\t */\n\t\t\tthis.$emit('layout-change', this.widgets)\n\t\t},\n\t},\n}\n</script>\n\n<style>\n.cn-widget-grid {\n\twidth: 100%;\n}\n\n.cn-widget-grid--editing {\n\tmin-height: 120px;\n}\n\n.cn-widget-grid--editing .grid-stack {\n\tbackground: transparent;\n}\n\n.cn-widget-grid--editing .grid-stack-item-content {\n\toverflow: hidden;\n\tbackground: var(--color-main-background);\n}\n\n.cn-widget-grid--editing .grid-stack-placeholder > .placeholder-content {\n\tbackground: var(--color-primary-element-light);\n\tborder: 2px dashed var(--color-primary-element);\n\tborder-radius: var(--border-radius-large);\n}\n</style>\n"],"names":["_createElementVNode","_openBlock","_createElementBlock","_renderList","_createBlock","_resolveDynamicComponent","_mergeProps"],"mappings":";;;AAiDO,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,YAAY,GAAA;;AAUhB,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,yBAAW,GAAA;;;;UAdpB,QAAA,CAAA,YAAA;;;AACJ,QAAA,GAAI,EAAA,eAAe;AACnB,QAAA,KAAE,EAAA,wCAAA;AACD,QAAA,WAAI,EAAA,MAAA,CAAA,QAAA;;AACL,QAAAA,kBAAA,QAAA;AACC,WAAAC,SAAA,CAAA,IAAA,CAAA,EAAAC,kBAAA,SACe,EAAA,IAAA,EAAAC,UAAA,CAAA,QAAA,CAAA,eAAA,EAAN,CAAA,MAAM,EAAA,KAAA,KAAA;iCADfD;AAEE,cAAA,GAAG,EAAE,QAAA,CAAA,IAAI,CAAC,MAAM,EAAE,KAAA,CAAA;AACnB,cAAA,KAAK,EAAC,iBAAkB;AACvB,cAAA,OAAC,EAAA,QAAA,CAAA,IAAA,CAAA,MAAA,EAAA,KAAA,CAAA;cACD,MAAI,EAAE,MAAM,CAAC,KAAK;cAClB,MAAI,EAAE,MAAM,CAAC,KAAK;cAClB,MAAI,EAAE,MAAM,CAAC,SAAS;cACtB,MAAI,EAAE,MAAE,CAAA,UAAA;;AACR,cAAAF,kBAAA,CAIC,OAJD,UAIC,EAAA;8BAHDI,WAEO,CAAAC,uBAAA,CADJ,MAAA,CAAA,SAAA,GADHC,UAEO,CAAA,EAAA,OAAA,EAAA,IAAA,EAAA,EAAA,MAAA,CAAA,KAAA,CAAA,EAAA,IAAA,EAAA,EAAA,kBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
|
1
|
+
{"version":3,"file":"CnWidgetGrid.vue3.js","sources":["../../../../src/components/CnWidgetGrid/CnWidgetGrid.vue?vue&type=template&id=c117f722&lang.js"],"sourcesContent":["<!--\n CnWidgetGrid — per-slot CSS grid renderer for v2 manifest pages.\n\n Receives a flat array of widget entries for a single slot and renders\n them in a CSS grid. The number of columns is determined by `slotName`\n per the ADR-036 Decision 2 convention:\n\n body, footer, header-actions, modal, tab:*, section:* → 12 columns\n sidebar → 1 column\n\n Widget key resolution: built-in registry first, then cnRegistry inject.\n Unknown widget keys are skipped with a console.warn.\n gridWidth values exceeding gridColumns are clamped with a console.warn.\n\n Spec: REQ-MVR-004 (manifest-v2-renderer) — per-slot grid renderer\n\n @prop {Array} widgets Array of widget entry objects from the v2 manifest page.\n @prop {string} slotName The slot key (e.g. \"body\", \"sidebar\", \"tab:general\").\n @prop {object} registry The consumer registry from CnAppRoot (passed as prop\n for standalone use; normally injected via cnRegistry).\n-->\n<template>\n\t<!-- Editable body slot: a GridStack drag/resize grid (ADR-041 edit mode). -->\n\t<div\n\t\tv-if=\"editableBody\"\n\t\tref=\"gridContainer\"\n\t\tclass=\"cn-widget-grid cn-widget-grid--editing\"\n\t\t:data-slot=\"slotName\">\n\t\t<div class=\"grid-stack\">\n\t\t\t<div\n\t\t\t\tv-for=\"(widget, index) in resolvedWidgets\"\n\t\t\t\t:key=\"gsId(widget, index)\"\n\t\t\t\tclass=\"grid-stack-item\"\n\t\t\t\t:gs-id=\"gsId(widget, index)\"\n\t\t\t\t:gs-x=\"widget.gridX\"\n\t\t\t\t:gs-y=\"widget.gridY\"\n\t\t\t\t:gs-w=\"widget.gridWidth\"\n\t\t\t\t:gs-h=\"widget.gridHeight\">\n\t\t\t\t<div class=\"grid-stack-item-content\">\n\t\t\t\t\t<component\n\t\t\t\t\t\t:is=\"widget.component\"\n\t\t\t\t\t\tv-bind=\"widget.props\"\n\t\t\t\t\t\t@selectObject=\"onSelectObject\" />\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n\t<!-- Default read-only CSS grid. -->\n\t<div\n\t\tv-else\n\t\tclass=\"cn-widget-grid cn-grid\"\n\t\t:class=\"{ 'cn-grid--responsive': isResponsive }\"\n\t\t:style=\"gridVars\"\n\t\t:data-slot=\"slotName\">\n\t\t<template v-for=\"(widget, index) in resolvedWidgets\" :key=\"`${widget.widgetKey}-${index}`\">\n\t\t\t<div\n\t\t\t\tclass=\"cn-grid__item\"\n\t\t\t\t:class=\"{ 'cn-grid__item--row': hasGridRow(widget) }\"\n\t\t\t\t:style=\"cnGridCellStyle(widget, gridColumns)\">\n\t\t\t\t<component\n\t\t\t\t\t:is=\"widget.component\"\n\t\t\t\t\tv-bind=\"widget.props\"\n\t\t\t\t\t@selectObject=\"onSelectObject\" />\n\t\t\t</div>\n\t\t</template>\n\t</div>\n</template>\n\n<script>\nimport CnUnknownWidget from './CnUnknownWidget.vue'\nimport { cnGridCellStyle, hasGridRow } from '../../utils/grid.js'\nimport { initGridStack, readGridGeometry } from '../../utils/gridStack.js'\nimport { resolveSlotColumns } from '../../utils/resolveSlotColumns.js'\nimport { canonicalWidgetType } from '../../utils/widgetTypeAliases.js'\nimport { BUILT_IN_WIDGETS } from './builtInWidgets.js'\nimport { dashboardWidgetRegistry, getWidgetTypeEntry } from './dashboardWidgetRegistry.js'\n\n// Per-slot column counts and slot→columns resolution now live in\n// src/utils/resolveSlotColumns.js (shared with validateManifest so the grid\n// bound the validator enforces matches what this component renders).\n\nexport default {\n\tname: 'CnWidgetGrid',\n\n\tinject: {\n\t\tcnRegistry: { default: () => ({}) },\n\t\t/**\n\t\t * Per-page slot→columns override map, published by `CnPageRenderer`\n\t\t * from `page.config.slotColumns`. Merged UNDER the `columns` prop in\n\t\t * column resolution. `null` when the page declares no override.\n\t\t */\n\t\tcnSlotColumns: { default: null },\n\t\t/**\n\t\t * Object context for a `type:\"detail\"` page, published by\n\t\t * `CnPageRenderer` (reactive holder `{ value: { objectData, schema,\n\t\t * objectType, objectId, register, store } | null }`). Merged UNDER\n\t\t * each widget's own `props` so detail widgets (`data`, `metadata`,\n\t\t * `file-manager`, …) receive the loaded object without the manifest\n\t\t * having to author per-widget props. `null` outside a detail page.\n\t\t */\n\t\tcnDetailObjectContext: { default: null },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Widget entries for this slot from the v2 manifest page.\n\t\t * Each entry: `{ widgetKey, slot, gridX, gridY, gridWidth, gridHeight, props? }`.\n\t\t *\n\t\t * @type {Array<object>}\n\t\t */\n\t\twidgets: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * The slot name this grid is rendering for (e.g. \"body\", \"sidebar\", \"tab:general\").\n\t\t *\n\t\t * @type {string}\n\t\t */\n\t\tslotName: {\n\t\t\ttype: String,\n\t\t\trequired: true,\n\t\t},\n\n\t\t/**\n\t\t * Consumer registry override. When provided, overrides the injected\n\t\t * cnRegistry. Used for standalone testing.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tregistry: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Explicit column-count override for this slot. When `null` (default),\n\t\t * columns resolve from the injected per-page `slotColumns` map and then\n\t\t * the library default for the slot — preserving the fixed ADR-036\n\t\t * Decision 2 behaviour (body=12, sidebar=1). Set a number to force a\n\t\t * specific column count.\n\t\t *\n\t\t * @type {number|null}\n\t\t */\n\t\tcolumns: {\n\t\t\ttype: Number,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * When `true`, the `body` slot renders a GridStack drag/resize grid\n\t\t * (ADR-041 in-app edit mode) instead of the read-only CSS grid; geometry\n\t\t * changes are written back to the widget entries and emitted via\n\t\t * `@layout-change`. Other slots are unaffected. Default `false` keeps the\n\t\t * existing read-only rendering for every consumer.\n\t\t *\n\t\t * @type {boolean}\n\t\t */\n\t\teditable: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\t},\n\n\temits: ['layout-change', 'select-object'],\n\n\tdata() {\n\t\treturn {\n\t\t\tgrid: null,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** Whether this grid should render the editable GridStack body. */\n\t\teditableBody() {\n\t\t\treturn this.editable && this.slotName === 'body'\n\t\t},\n\n\t\tgridColumns() {\n\t\t\t// Three-layer resolution: `columns` prop → injected per-page\n\t\t\t// slotColumns → the library default for the slot (getGridColumns\n\t\t\t// is preserved as the default tier inside resolveSlotColumns).\n\t\t\treturn resolveSlotColumns(this.slotName, this.cnSlotColumns, this.columns)\n\t\t},\n\n\t\teffectiveRegistry() {\n\t\t\treturn this.registry ?? this.cnRegistry ?? {}\n\t\t},\n\n\t\t/**\n\t\t * Resolved detail-page object context (or `{}`). Read from the\n\t\t * reactive holder published by `CnPageRenderer`; the `.value`\n\t\t * indirection lets the grid re-render once the async object load\n\t\t * resolves. Only the well-known object keys are forwarded to\n\t\t * widgets — see `resolvedWidgets`.\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\tdetailContextProps() {\n\t\t\tconst ctx = this.cnDetailObjectContext?.value\n\t\t\tif (!ctx || typeof ctx !== 'object') {\n\t\t\t\treturn {}\n\t\t\t}\n\t\t\tconst out = {}\n\t\t\tfor (const key of ['objectData', 'schema', 'objectType', 'objectId', 'register', 'store']) {\n\t\t\t\tif (ctx[key] !== undefined) {\n\t\t\t\t\tout[key] = ctx[key]\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn out\n\t\t},\n\n\t\t/**\n\t\t * CSS custom properties driving the shared `.cn-grid` engine — the\n\t\t * desktop column count for this slot. Responsive collapse (12 → 6 →\n\t\t * 1) lives in `grid.css`.\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\tgridVars() {\n\t\t\treturn { '--cn-grid-cols': this.gridColumns }\n\t\t},\n\n\t\t/**\n\t\t * Whether this slot's grid collapses responsively. Multi-column\n\t\t * slots (body/footer/tab/section) do; the single-column sidebar\n\t\t * slot stays put.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tisResponsive() {\n\t\t\treturn this.gridColumns > 1\n\t\t},\n\n\t\tresolvedWidgets() {\n\t\t\tconst columns = this.gridColumns\n\t\t\tconst result = []\n\n\t\t\tfor (const widget of this.widgets) {\n\t\t\t\tconst key = widget.widgetKey\n\n\t\t\t\t// Resolve component: consumer registry first (overrides built-ins),\n\t\t\t\t// then fall back to built-in registry.\n\t\t\t\t// Per spec REQ-MVR-005: \"Custom widget overrides built-in\"\n\t\t\t\tlet component = null\n\n\t\t\t\tif (this.effectiveRegistry[key]) {\n\t\t\t\t\tconst entry = this.effectiveRegistry[key]\n\t\t\t\t\tcomponent = entry.component ?? entry\n\t\t\t\t}\n\n\t\t\t\t// `table` and `object-table` (and `map` / `map-viewer`) are the\n\t\t\t\t// same component registered under two names in two registries.\n\t\t\t\t// Canonicalising here means either spelling resolves, whichever\n\t\t\t\t// registry happens to hold it — see utils/widgetTypeAliases.js.\n\t\t\t\tconst canonical = canonicalWidgetType(key)\n\n\t\t\t\tif (!component) {\n\t\t\t\t\tcomponent = BUILT_IN_WIDGETS[key] ?? BUILT_IN_WIDGETS[canonical] ?? null\n\t\t\t\t}\n\n\t\t\t\tif (!component) {\n\t\t\t\t\t// Dashboard widget catalog (cn-widget-library): the 21 migrated\n\t\t\t\t\t// widgets self-register here. Resolved after cnRegistry + built-ins\n\t\t\t\t\t// so a consumer override still wins.\n\t\t\t\t\tconst entry = getWidgetTypeEntry(key) || getWidgetTypeEntry(canonical)\n\t\t\t\t\tcomponent = (entry && entry.renderer) ?? null\n\t\t\t\t}\n\n\t\t\t\tif (!component) {\n\t\t\t\t\t// An EMPTY catalog is a different failure from an unknown key, and\n\t\t\t\t\t// the fix is different, so say which one this is. It means nothing\n\t\t\t\t\t// ever imported the module that populates the catalog — most often\n\t\t\t\t\t// because a consumer deep-imported a widget's own module\n\t\t\t\t\t// (`.../components/CnMapWidget/index.js`) expecting it to register\n\t\t\t\t\t// its type. Those modules export the component only; the type\n\t\t\t\t\t// registrations all live in `CnWidgetGrid/registerDashboardWidgets.js`,\n\t\t\t\t\t// which the package root and the `components` barrel both import.\n\t\t\t\t\t// Before this was consolidated, two of them self-registered as well,\n\t\t\t\t\t// so a deep import happened to work by accident.\n\t\t\t\t\tconst catalogEmpty = Object.keys(dashboardWidgetRegistry).length === 0\n\t\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\t\tconsole.warn(catalogEmpty\n\t\t\t\t\t\t? `[CnWidgetGrid] The dashboard widget catalog is EMPTY, so widgetKey \"${key}\" `\n\t\t\t\t\t\t+ `in slot \"${this.slotName}\" cannot resolve — this is not a bad key. `\n\t\t\t\t\t\t+ 'Import the package root (`@conduction/nextcloud-vue`) or, if you '\n\t\t\t\t\t\t+ 'cherry-pick modules, `components/CnWidgetGrid/registerDashboardWidgets.js`. '\n\t\t\t\t\t\t+ 'Importing a widget\\'s own module gives you the component but registers no type.'\n\t\t\t\t\t\t: `[CnWidgetGrid] Unknown widgetKey \"${key}\" in slot \"${this.slotName}\". `\n\t\t\t\t\t\t\t+ 'Register it in the built-in registry or pass it via the CnAppRoot registry prop.')\n\t\t\t\t\t// Render a visible, designed placeholder instead of silently\n\t\t\t\t\t// skipping — a page whose widgets ALL fail to resolve must not\n\t\t\t\t\t// leave a blank pane (2026-07-06 audit: petstore dashboard).\n\t\t\t\t\tresult.push({\n\t\t\t\t\t\tid: widget.id ?? `unknown-${key}-${result.length}`,\n\t\t\t\t\t\twidgetKey: key,\n\t\t\t\t\t\tcomponent: CnUnknownWidget,\n\t\t\t\t\t\tprops: { widgetKey: key },\n\t\t\t\t\t\tgridX: widget.gridX,\n\t\t\t\t\t\tgridY: widget.gridY,\n\t\t\t\t\t\tgridWidth: typeof widget.gridWidth === 'number' ? widget.gridWidth : columns,\n\t\t\t\t\t\tgridHeight: widget.gridHeight,\n\t\t\t\t\t})\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\t// Clamp gridWidth\n\t\t\t\tlet gridWidth = typeof widget.gridWidth === 'number' ? widget.gridWidth : 1\n\t\t\t\tif (gridWidth > columns) {\n\t\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\t\tconsole.warn(`[CnWidgetGrid] Widget \"${key}\" in slot \"${this.slotName}\" has gridWidth ${gridWidth} `\n\t\t\t\t\t\t+ `which exceeds the slot's gridColumns (${columns}). Clamping to ${columns}.`)\n\t\t\t\t\tgridWidth = columns\n\t\t\t\t}\n\n\t\t\t\t// Forward the widget's top-level `dataSource` (v2 unified\n\t\t\t\t// widget shape — sibling of `props`/`slot`/`gridX`) as a\n\t\t\t\t// `dataSource` prop so data-bound widgets (CnStatsBlockWidget,\n\t\t\t\t// CnChartWidget, …) receive it without the manifest having to\n\t\t\t\t// nest it inside `props`. A `props.dataSource` (the app-side\n\t\t\t\t// workaround) still wins on collision, because per-widget\n\t\t\t\t// `props` are spread LAST.\n\t\t\t\tconst dataSourceProp = (widget.dataSource !== undefined && widget.dataSource !== null)\n\t\t\t\t\t? { dataSource: widget.dataSource }\n\t\t\t\t\t: {}\n\n\t\t\t\tresult.push({\n\t\t\t\t\tid: widget.id,\n\t\t\t\t\twidgetKey: key,\n\t\t\t\t\tcomponent,\n\t\t\t\t\t// Detail-page object context first, then the top-level\n\t\t\t\t\t// `dataSource` (v2 unified widget shape), then the\n\t\t\t\t\t// entry-level `title` / `documentationUrl` so the widget\n\t\t\t\t\t// chrome (CnWidgetWrapper) can render them, then the\n\t\t\t\t\t// manifest's per-widget `props` last so explicit props win.\n\t\t\t\t\tprops: {\n\t\t\t\t\t\t...this.detailContextProps,\n\t\t\t\t\t\t...dataSourceProp,\n\t\t\t\t\t\t...(widget.title ? { title: widget.title } : {}),\n\t\t\t\t\t\t...(widget.documentationUrl ? { documentationUrl: widget.documentationUrl } : {}),\n\t\t\t\t\t\t...(widget.props ?? {}),\n\t\t\t\t\t},\n\t\t\t\t\tgridX: typeof widget.gridX === 'number' ? widget.gridX : 0,\n\t\t\t\t\tgridY: typeof widget.gridY === 'number' ? widget.gridY : 0,\n\t\t\t\t\tgridWidth,\n\t\t\t\t\tgridHeight: typeof widget.gridHeight === 'number' ? widget.gridHeight : 1,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\treturn result\n\t\t},\n\t},\n\n\twatch: {\n\t\teditableBody(active) {\n\t\t\tif (active) {\n\t\t\t\tthis.$nextTick(() => this.initGrid())\n\t\t\t} else {\n\t\t\t\tthis.destroyGrid()\n\t\t\t}\n\t\t},\n\t},\n\n\tmounted() {\n\t\tif (this.editableBody) {\n\t\t\tthis.initGrid()\n\t\t}\n\t},\n\n\tbeforeUnmount() {\n\t\tthis.destroyGrid()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Pass a related widget's object click up; the page opens the object.\n\t\t *\n\t\t * @param {object} raw The clicked object.\n\t\t * @return {void}\n\t\t */\n\t\tonSelectObject(raw) {\n\t\t\t/**\n\t\t\t * @event select-object An object row in a related widget of this grid was clicked. Payload is the raw object.\n\t\t\t * @type {object}\n\t\t\t */\n\t\t\tthis.$emit('select-object', raw)\n\t\t},\n\n\t\t// Expose the shared grid helpers to the template.\n\t\tcnGridCellStyle,\n\t\thasGridRow,\n\t\t/**\n\t\t * Stable GridStack id for a widget entry (its id, else its index).\n\t\t *\n\t\t * @param {object} widget The resolved widget entry.\n\t\t * @param {number} index The entry's position in the slot.\n\t\t */\n\t\tgsId(widget, index) {\n\t\t\treturn widget.id !== null && widget.id !== undefined ? widget.id : `idx-${index}`\n\t\t},\n\n\t\t/** Initialise the GridStack engine on the editable body container. */\n\t\tinitGrid() {\n\t\t\tconst container = this.$refs.gridContainer\n\t\t\tif (!container) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst el = container.querySelector('.grid-stack')\n\t\t\tif (!el) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.grid = initGridStack(el, { columns: this.gridColumns, editable: true })\n\t\t\tthis.grid.on('change', (_event, items) => this.handleGridChange(items))\n\t\t},\n\n\t\t/** Tear down the GridStack engine. */\n\t\tdestroyGrid() {\n\t\t\tif (this.grid) {\n\t\t\t\tthis.grid.destroy(false)\n\t\t\t\tthis.grid = null\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Write GridStack geometry back into the widget entries (matched by id,\n\t\t * index fallback), clamped within the resolved column bound, and emit the\n\t\t * updated list. Mutating the entries keeps the working manifest in sync.\n\t\t *\n\t\t * @param {Array} items The changed items from GridStack.\n\t\t */\n\t\thandleGridChange(items) {\n\t\t\tconst geom = readGridGeometry(items)\n\t\t\tconst cols = this.gridColumns\n\t\t\tthis.resolvedWidgets.forEach((resolved, index) => {\n\t\t\t\tconst key = String(this.gsId(resolved, index))\n\t\t\t\tconst g = geom.get(key)\n\t\t\t\tconst target = this.widgets[index]\n\t\t\t\tif (!g || !target) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst width = Math.min(g.gridWidth, cols)\n\t\t\t\tconst x = Math.min(g.gridX, Math.max(0, cols - width))\n\t\t\t\ttarget.gridX = x\n\t\t\t\ttarget.gridY = g.gridY\n\t\t\t\ttarget.gridWidth = width\n\t\t\t\ttarget.gridHeight = g.gridHeight\n\t\t\t})\n\t\t\t/**\n\t\t\t * @event layout-change Emitted after a drag/resize with the updated widget entries.\n\t\t\t * @type {Array}\n\t\t\t */\n\t\t\tthis.$emit('layout-change', this.widgets)\n\t\t},\n\t},\n}\n</script>\n\n<style>\n.cn-widget-grid {\n\twidth: 100%;\n}\n\n.cn-widget-grid--editing {\n\tmin-height: 120px;\n}\n\n.cn-widget-grid--editing .grid-stack {\n\tbackground: transparent;\n}\n\n.cn-widget-grid--editing .grid-stack-item-content {\n\toverflow: hidden;\n\tbackground: var(--color-main-background);\n}\n\n.cn-widget-grid--editing .grid-stack-placeholder > .placeholder-content {\n\tbackground: var(--color-primary-element-light);\n\tborder: 2px dashed var(--color-primary-element);\n\tborder-radius: var(--border-radius-large);\n}\n</style>\n"],"names":["_createElementVNode","_openBlock","_createElementBlock","_Fragment","_renderList","_createBlock","_resolveDynamicComponent","_mergeProps"],"mappings":";;;AAiDO,MAAA,UAAA,GAAA,EAAA,KAAA,EAAA,YAAA,GAAA;;AAUE,MAAA,UAAA,GAAA,EAAA,KAAI,EAAA,yBAAA,GAAA;;;;UAdN,QAAA,CAAA,YAAA;;;AACL,QAAA,GAAG,EAAC,eAAA;AACJ,QAAA,KAAK,EAAC,wCAA4B;AACjC,QAAA,WAAC,EAAA,MAAA,CAAA,QAAA;;AACF,QAAAA,kBAAA,QAAA;AACC,WAAAC,SAAA,CAAA,IAAA,CAAA,EAAAC,kBAAA,CAeGC,QAdwB,EAAA,IAAA,EAAAC,UAAA,CAAA,QAAA,CAAA,eAAe,EAAjC,CAAA,MAAM,EAAE,KAAK,KAAA;iCADtBF,kBAeG,CAAA,KAAA,EAAA;AAbD,cAAA,GAAG,EAAE,QAAA,CAAA,IAAI,CAAC,MAAG,EAAA,KAAA,CAAA;AACd,cAAA,KAAK,EAAC,iBAAa;AAClB,cAAA,OAAK,EAAE,QAAA,CAAA,IAAI,CAAC,MAAM,EAAE,KAAK,CAAA;cACzB,MAAC,EAAA,MAAA,CAAA,KAAA;cACD,MAAI,EAAE,MAAM,CAAC,KAAK;cAClB,MAAI,EAAE,MAAM,CAAC,SAAS;cACtB,MAAI,EAAE,MAAM,CAAC,UAAU;;AACxB,cAAAF,kBAAA,CAKM,OALN,UAKM,EAAA;AAJL,iBAAAC,SAAA,EAAA,EAAAI,WAAA,CAGGC,uBAFG,CAAA,MAAM,CAAC,SAAO,CADpB,EAAAC,UAAA,CAGG,EADM,OAAA,EAAA,IAAA,EAAA,EAAA,MAAM,CAAC,KAAK,EACnB,EAAA,cAAC,EAAA,QAAA,CAAA,cAAA,EAAA,CAAA,EAAA,IAAA,EAAA,EAAA,mBAAA,CAAA,gBAAA,CAAA,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
package/dist/esm/index.js
CHANGED
|
@@ -21,6 +21,7 @@ export { SAFE_MARKDOWN_DOMPURIFY_CONFIG } from './utils/safeMarkdownDompurifyCon
|
|
|
21
21
|
export { FOLDER_COLORS, FOLDER_ICONS, folderColorTint, resolveFolderColor, resolveFolderIcon, searchFolderIcons } from './utils/folderCustomization.js';
|
|
22
22
|
export { currentTheme, useCurrentTheme } from './composables/useCurrentTheme.js';
|
|
23
23
|
export { ROADMAP_LABEL_BLOCKLIST } from './utils/roadmapLabelBlocklist.js';
|
|
24
|
+
export { humaniseRuleKey, notificationRuleLabel } from './utils/notificationRuleLabel.js';
|
|
24
25
|
export { BSN_ERROR_CHECKSUM, BSN_ERROR_LENGTH, maskBsn, validateBsn } from './utils/validators/bsn.js';
|
|
25
26
|
export { registerTranslations } from './l10n/index.js';
|
|
26
27
|
export { CnHttpError, cnFetch, cnFetchJson } from './utils/cnFetch.js';
|