@conduction/nextcloud-vue 2.84.0 → 2.85.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue.js +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js +131 -3
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +54 -18
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue.js +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +232 -4
- package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue3.js +95 -51
- package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue.js +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js +114 -0
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js +2 -2
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +45 -0
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +22 -12
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +23 -4
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +3 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +18 -5
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +548 -77
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +191 -82
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/useSelfFetchList.js +11 -2
- package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue.js +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +95 -0
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js +24 -2
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +70 -2
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue.js +1 -1
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js +67 -5
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js.map +1 -1
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js +181 -124
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js.map +1 -1
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue.js +1 -1
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js +5 -1
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js.map +1 -1
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue3.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue.js +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +134 -10
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +6 -2
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +6 -3
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue.js +1 -1
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js +140 -7
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue.js +1 -1
- package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue2.js +37 -9
- package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue2.js.map +1 -1
- package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js +6 -5
- package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js.map +1 -1
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue.js +1 -1
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js +2 -1
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue2.js +53 -1
- package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue3.js +3 -2
- package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue.js +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +135 -0
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue.js +1 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js +19 -0
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js +37 -19
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js.map +1 -1
- package/dist/esm/composables/useListView.js +21 -0
- package/dist/esm/composables/useListView.js.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +23 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +41 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/nextcloud-vue.css +742 -397
- package/dist/esm/store/useObjectStore.js +16 -2
- package/dist/esm/store/useObjectStore.js.map +1 -1
- package/dist/esm/utils/boardColumns.js +8 -1
- package/dist/esm/utils/boardColumns.js.map +1 -1
- package/dist/esm/utils/boardReferenceColumns.js +278 -0
- package/dist/esm/utils/boardReferenceColumns.js.map +1 -0
- package/dist/esm/utils/buildManifest.js +4 -1
- package/dist/esm/utils/buildManifest.js.map +1 -1
- package/dist/esm/utils/gridStack.js +40 -1
- package/dist/esm/utils/gridStack.js.map +1 -1
- package/dist/esm/utils/humaniseKey.js +41 -0
- package/dist/esm/utils/humaniseKey.js.map +1 -0
- package/dist/esm/utils/notificationRuleLabel.js +6 -23
- package/dist/esm/utils/notificationRuleLabel.js.map +1 -1
- package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/esm/utils/widgetDispatch.js +8 -2
- package/dist/esm/utils/widgetDispatch.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +15075 -12590
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +742 -397
- package/l10n/en.json +23 -1
- package/l10n/nl.json +41 -1
- package/package.json +1 -1
- package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +160 -8
- package/src/components/CnBoardView/CnBoardView.vue +262 -6
- package/src/components/CnDashboardGrid/CnDashboardGrid.vue +116 -2
- package/src/components/CnDashboardPage/CnDashboardPage.vue +57 -2
- package/src/components/CnDetailPage/CnDetailPage.md +1 -1
- package/src/components/CnDetailPage/CnDetailPage.vue +23 -4
- package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +3 -1
- package/src/components/CnFactsWidget/CnFactsWidget.vue +18 -5
- package/src/components/CnIndexPage/CnIndexPage.vue +586 -25
- package/src/components/CnIndexPage/useSelfFetchList.js +10 -1
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +123 -3
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +70 -2
- package/src/components/CnRegisterMapping/CnRegisterMapping.md +5 -0
- package/src/components/CnRegisterMapping/CnRegisterMapping.vue +231 -96
- package/src/components/CnSettingsPage/CnSettingsPage.vue +5 -1
- package/src/components/CnStatWidget/CnStatWidget.vue +136 -11
- package/src/components/CnTabsWidget/CnTabsWidget.vue +6 -3
- package/src/components/CnToggleTableWidget/CnToggleTableWidget.vue +140 -7
- package/src/components/CnVersionInfoCard/CnVersionInfoCard.md +2 -1
- package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +42 -13
- package/src/components/CnWeekStripWidget/CnWeekStripWidget.vue +2 -1
- package/src/components/CnWidgetCardGrid/CnWidgetCardGrid.vue +55 -2
- package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +135 -0
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.md +10 -0
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +44 -1
- package/src/composables/useListView.js +21 -0
- package/src/css/board.css +65 -4
- package/src/css/dialog.css +4 -2
- package/src/css/index-page.css +49 -0
- package/src/css/kpi-card.css +13 -0
- package/src/css/look-board.css +96 -7
- package/src/index.js +2 -0
- package/src/schemas/app-manifest-v2.schema.json +115 -5
- package/src/store/useObjectStore.js +16 -2
- package/src/utils/boardColumns.js +8 -1
- package/src/utils/boardReferenceColumns.js +274 -0
- package/src/utils/buildManifest.js +4 -1
- package/src/utils/gridStack.js +39 -0
- package/src/utils/humaniseKey.js +38 -0
- package/src/utils/notificationRuleLabel.js +5 -23
- package/src/utils/validateManifestV2.compiled.js +3 -3
- package/src/utils/widgetDispatch.js +8 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnDashboardGrid.vue2.js","sources":["../../../../src/components/CnDashboardGrid/CnDashboardGrid.vue"],"sourcesContent":["<!--\n CnDashboardGrid — GridStack-powered drag-and-drop dashboard grid.\n\n Renders widgets in a configurable grid layout. Supports drag, resize,\n and dynamic item addition/removal. Emits layout changes for persistence.\n\n Accessibility: every grid item is an ARIA `group` with an accessible name\n and — in edit mode — a tab stop, so the pointer-only GridStack drag/resize\n gestures have a full keyboard equivalent (WCAG 2.1 SC 2.1.1). Keyboard\n moves are routed through `GridStack.update()`, i.e. the exact same engine\n call drag/resize ends in, so collision handling, persistence and the\n `layout-change` emission are byte-identical between the two input modes.\n-->\n<template>\n\t<div ref=\"gridContainer\" class=\"cn-dashboard-grid\">\n\t\t<!--\n\t\t\tKeyboard instructions, referenced by every grid item through\n\t\t\t`aria-describedby`. Rendered once (not per item) so a screen\n\t\t\treader user hears the key map on the first item they land on\n\t\t\twithout it being duplicated N times in the accessibility tree.\n\t\t-->\n\t\t<p\n\t\t\tv-if=\"keyboardActive\"\n\t\t\t:id=\"keyboardHelpDomId\"\n\t\t\tclass=\"cn-dashboard-grid__sr-only\">\n\t\t\t{{ keyboardHelpText }}\n\t\t</p>\n\n\t\t<div class=\"grid-stack\">\n\t\t\t<div\n\t\t\t\tv-for=\"(item, index) in layout\"\n\t\t\t\t:key=\"resolveItemKey(item)\"\n\t\t\t\tclass=\"grid-stack-item\"\n\t\t\t\t:class=\"{ 'cn-dashboard-grid__item--fit': sizeToContentAttr(item) !== undefined }\"\n\t\t\t\trole=\"group\"\n\t\t\t\t:aria-label=\"resolveItemLabel(item, index)\"\n\t\t\t\t:aria-describedby=\"itemDescribedBy\"\n\t\t\t\t:tabindex=\"itemTabindex\"\n\t\t\t\t:gs-id=\"item.id\"\n\t\t\t\t:gs-x=\"item.gridX\"\n\t\t\t\t:gs-y=\"scaledRow(item.gridY)\"\n\t\t\t\t:gs-w=\"item.gridWidth\"\n\t\t\t\t:gs-h=\"scaledRow(item.gridHeight)\"\n\t\t\t\t:gs-min-w=\"minWidth\"\n\t\t\t\t:gs-min-h=\"minRows(item)\"\n\t\t\t\t:gs-size-to-content=\"sizeToContentAttr(item)\"\n\t\t\t\t@keydown=\"onItemKeydown($event, item)\">\n\t\t\t\t<div class=\"grid-stack-item-content\">\n\t\t\t\t\t<slot name=\"widget\" :item=\"item\">\n\t\t\t\t\t\t<!-- Default: render nothing; CnDashboardPage provides content -->\n\t\t\t\t\t</slot>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!--\n\t\t\tPolite live region for keyboard move/resize feedback. A sighted\n\t\t\tuser sees the widget slide; a screen-reader user needs the new\n\t\t\tcoordinates spoken, otherwise the arrow keys are silent.\n\t\t-->\n\t\t<p\n\t\t\tclass=\"cn-dashboard-grid__sr-only\"\n\t\t\trole=\"status\"\n\t\t\taria-live=\"polite\"\n\t\t\taria-atomic=\"true\">\n\t\t\t{{ announcement }}\n\t\t</p>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { GridStack } from 'gridstack'\n// `gridstack` is a peerDependency (see package.json), not bundled — this\n// component's JS and the consumer's GridStack stylesheet MUST come from the\n// exact same installed copy, because GridStack's JS writes layout state\n// (e.g. the `--gs-column-width` CSS custom property) that only its OWN\n// matching CSS reads. Baking a copy of `gridstack/dist/gridstack.min.css`\n// into this library's build (as a previous version did) freezes it at\n// nc-vue's build-time version, which can silently drift from whatever the\n// consumer's peer-installed copy actually is — the exact mismatch that once\n// made every grid item render at 0px width. So nc-vue ships NO GridStack\n// CSS of its own: the consumer MUST install `gridstack` themselves and\n// import `gridstack/dist/gridstack.min.css` (or their preferred GridStack\n// stylesheet) from that same copy.\n\n/**\n * Monotonic counter backing the per-instance DOM ids (the shared\n * `aria-describedby` help text). Module-level so two grids on one page\n * never collide — the same pattern CnCommandPalette uses.\n */\nlet domIdCounter = 0\n\n/**\n * Pixel offset applied to the grid item's top-left corner when synthesising\n * the `contextmenu` coordinates for a keyboard activation. Nudging the anchor\n * inside the item keeps consumer menus (which position at `clientX/clientY`)\n * visually attached to the widget instead of straddling its border.\n */\nconst MENU_ANCHOR_OFFSET = 8\n\n/**\n * How many GridStack rows a layout row is drawn as on a grid with\n * size-to-content cells (`wantsRowSplit`): 80px rows become 10px rows.\n */\nconst ROW_SPLIT = 8\n\n/**\n * Whether a layout holds a size-to-content cell (`sizeToContent` true or a\n * positive row maximum).\n *\n * @param {Array<object>} layout The layout items.\n * @return {boolean} True when one does.\n */\nfunction hasFitCell(layout) {\n\treturn Array.isArray(layout) && layout.some((item) => item\n\t\t&& (item.sizeToContent === true || (Number.isFinite(item.sizeToContent) && item.sizeToContent > 0)))\n}\n\n/**\n * CnDashboardGrid — Low-level grid layout engine powered by GridStack.\n *\n * Manages the drag-and-drop grid, syncs positions, and emits layout\n * changes. Does NOT handle widget rendering — that's done by the parent\n * via the `#widget` scoped slot.\n *\n * ```vue\n * <CnDashboardGrid\n * :layout=\"placements\"\n * :editable=\"isEditing\"\n * @layout-change=\"onLayoutChange\">\n * <template #widget=\"{ item }\">\n * <MyWidget :config=\"item\" />\n * </template>\n * </CnDashboardGrid>\n * ```\n *\n * ### Keyboard operation\n *\n * With `keyboardRepositioning` on (the default) and `editable` true, each\n * grid item is a tab stop. With an item focused:\n *\n * | Key | Action |\n * | --- | ------ |\n * | `ArrowLeft` / `ArrowRight` | move one column left / right |\n * | `ArrowUp` / `ArrowDown` | move one row up / down |\n * | `Shift` + `ArrowLeft` / `ArrowRight` | shrink / grow width by one column |\n * | `Shift` + `ArrowUp` / `ArrowDown` | shrink / grow height by one row |\n * | `Home` / `End` | jump to the first / last column of the current row |\n * | `Enter` / `Space` | activate the item — see `item-activate` |\n *\n * Keys are only honoured when the grid item itself holds focus, so\n * interactive content rendered inside the `#widget` slot keeps its own\n * key handling untouched.\n */\nexport default {\n\tname: 'CnDashboardGrid',\n\n\tprops: {\n\t\t/** Array of layout items: { id, gridX, gridY, gridWidth, gridHeight, ...extra } */\n\t\tlayout: {\n\t\t\ttype: Array,\n\t\t\trequired: true,\n\t\t},\n\n\t\t/** Whether drag and resize are enabled */\n\t\teditable: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Number of grid columns */\n\t\tcolumns: {\n\t\t\ttype: Number,\n\t\t\tdefault: 12,\n\t\t},\n\n\t\t/** Cell height in pixels */\n\t\tcellHeight: {\n\t\t\ttype: Number,\n\t\t\tdefault: 80,\n\t\t},\n\n\t\t/** Grid margin in pixels */\n\t\tmargin: {\n\t\t\ttype: Number,\n\t\t\tdefault: 12,\n\t\t},\n\n\t\t/** Minimum widget width in grid units */\n\t\tminWidth: {\n\t\t\ttype: Number,\n\t\t\tdefault: 2,\n\t\t},\n\n\t\t/** Minimum widget height in grid units */\n\t\tminHeight: {\n\t\t\ttype: Number,\n\t\t\tdefault: 2,\n\t\t},\n\n\t\t/**\n\t\t * GridStack v12 responsive `columnOpts` bag (breakpoints + reflow\n\t\t * layout). When set, the grid reflows column count across screen sizes.\n\t\t * Build it with `getDashboardColumnOpts()`. Default `null` = fixed\n\t\t * `columns`, no responsive reflow (backwards-compatible).\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tcolumnOpts: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * When set, `cellHeight` is mirrored into this CSS custom property on\n\t\t * the document root at init (e.g. `'--app-cell-height'`), so app CSS can\n\t\t * align to the grid geometry. Default `null` = no CSS var written.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tcellHeightCssVar: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Optional `(item) => string|number` to derive each item's render key.\n\t\t * Use it to force a re-render when an item changes in a way its `id`\n\t\t * doesn't capture (e.g. style edits — return `${item.id}:${item.updatedAt}`).\n\t\t * Default `null` = key on `item.id`.\n\t\t *\n\t\t * @type {((item: object) => (string|number))|null}\n\t\t */\n\t\titemKey: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Whether grid items are keyboard-operable: focusable in edit mode and\n\t\t * repositionable/resizable with the arrow keys (WCAG 2.1 SC 2.1.1 —\n\t\t * the keyboard equivalent of the pointer-only GridStack drag). Turning\n\t\t * it off leaves the items non-focusable and drag-only. Default `true`.\n\t\t */\n\t\tkeyboardRepositioning: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Optional `(item, index) => string` returning the accessible name for\n\t\t * a grid item. Use it when the layout items don't carry a human-readable\n\t\t * field (the built-in fallback walks `title` → `name` → `label` →\n\t\t * `widgetTitle` → `widgetId`, then a positional \"Widget N\"). The returned\n\t\t * string is used verbatim — no coordinates are appended.\n\t\t *\n\t\t * @type {((item: object, index: number) => string)|null}\n\t\t */\n\t\titemLabel: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Whether a keyboard activation (`Enter` / `Space` on a focused grid\n\t\t * item) also dispatches a bubbling `contextmenu` event from inside the\n\t\t * item, so consumers that open a per-widget menu on right-click get the\n\t\t * keyboard path for free — mirroring what browsers do for the `Menu`\n\t\t * key. Set `false` to rely purely on the `item-activate` event.\n\t\t * Default `true`.\n\t\t */\n\t\tactivateOpensContextMenu: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * GridStack's `float`. True (the default) keeps every item on the row\n\t\t * it was placed on, with empty rows above it if need be. False packs\n\t\t * items upward, so a size-to-content cell that shrinks, or a widget\n\t\t * that is not shown, closes its gap instead of leaving a band.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-dashboard-closes-the-gap-a-shrinking-or-hidden-widget-leaves\n\t\t */\n\t\tfloat: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\t},\n\n\temits: [\n\t\t/**\n\t\t * Fired whenever the grid geometry changed — by drag, by resize, or by\n\t\t * a keyboard nudge. Payload is the full `layout` array with the moved\n\t\t * items' `gridX`/`gridY`/`gridWidth`/`gridHeight` updated.\n\t\t */\n\t\t'layout-change',\n\t\t/**\n\t\t * Fired when the user activates a focused grid item with `Enter` or\n\t\t * `Space`. Payload `{ item, element, clientX, clientY }` — the layout\n\t\t * item, its DOM element, and viewport coordinates anchored to the\n\t\t * item's top-left corner so a consumer menu can position itself the\n\t\t * same way it does for a right-click.\n\t\t */\n\t\t'item-activate',\n\t],\n\n\tdata() {\n\t\treturn {\n\t\t\tgrid: null,\n\t\t\tcontentObserver: null,\n\t\t\t/** True while the grid moves an item to where the layout put it. */\n\t\t\tapplyingLayout: false,\n\t\t\tdomId: `cn-dashboard-grid-${++domIdCounter}`,\n\t\t\t/** Text currently held in the polite live region. */\n\t\t\tannouncement: '',\n\t\t\t/**\n\t\t\t * Whether a `columnOpts` breakpoint is currently in force. Mirrored\n\t\t\t * from the engine by `syncInteractivity` because `isReflowed()` reads\n\t\t\t * `grid.getColumn()` imperatively, and a computed cannot re-evaluate\n\t\t\t * on that.\n\t\t\t */\n\t\t\treflowed: false,\n\t\t\t/**\n\t\t\t * How many GridStack rows one layout row is drawn as: 1, or\n\t\t\t * ROW_SPLIT while `wantsRowSplit` holds. Set before the first render,\n\t\t\t * since GridStack reads the items' places from their attributes,\n\t\t\t * and again when the grid is rebuilt.\n\t\t\t */\n\t\t\trowSplit: (!this.editable && !this.float && hasFitCell(this.layout)) ? ROW_SPLIT : 1,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Whether the grid draws each layout row as ROW_SPLIT finer rows. A\n\t\t * size-to-content cell can only be a whole number of rows tall, so on\n\t\t * 80px rows it kept up to a row of empty space under its content and\n\t\t * a packing grid (`float: false`) could not close it (pipelinq and\n\t\t * dossiq Mijn werk, decidiq Mijn acties). Split into 10px rows the\n\t\t * cell ends within 10px of its content and the widgets below move up.\n\t\t *\n\t\t * Only in view mode, only on a packing grid with a size-to-content\n\t\t * cell: every other grid is built exactly as before. Item positions\n\t\t * and sizes scale with the rows, so every widget keeps its pixel\n\t\t * place and size until a cell shrinks; the layout itself, what edit\n\t\t * mode shows and what `layout-change` reports, stays in layout rows.\n\t\t *\n\t\t * @spec openspec/changes/dashboard-fit-cells-close-up/specs/dashboard-grid/spec.md#requirement-a-size-to-content-cell-ends-at-its-content\n\t\t * @return {boolean} True to split the rows.\n\t\t */\n\t\twantsRowSplit() {\n\t\t\treturn !this.editable && !this.float && hasFitCell(this.layout)\n\t\t},\n\n\t\t/**\n\t\t * @return {boolean} Whether the keyboard lane is live — items are\n\t\t * focusable and the arrow keys reposition. Requires the opt-in prop,\n\t\t * edit mode (a read-only dashboard has nothing to move, so adding N tab\n\t\t * stops there would be pure noise), and a grid that is not reflowed,\n\t\t * which is where a move cannot be kept and so must not be offered.\n\t\t */\n\t\tkeyboardActive() {\n\t\t\treturn this.keyboardRepositioning && this.editable && !this.reflowed\n\t\t},\n\n\t\t/**\n\t\t * @return {number|null} `0` when grid items should be tab stops,\n\t\t * otherwise `null` so the attribute is omitted entirely.\n\t\t */\n\t\titemTabindex() {\n\t\t\treturn this.keyboardActive ? 0 : null\n\t\t},\n\n\t\t/**\n\t\t * @return {string} DOM id of the shared keyboard-instructions element.\n\t\t */\n\t\tkeyboardHelpDomId() {\n\t\t\treturn `${this.domId}-keyboard-help`\n\t\t},\n\n\t\t/**\n\t\t * @return {string|null} `aria-describedby` target for each grid item —\n\t\t * the instructions element while the keyboard lane is live, `null`\n\t\t * otherwise (a dangling id reference is an axe violation).\n\t\t */\n\t\titemDescribedBy() {\n\t\t\treturn this.keyboardActive ? this.keyboardHelpDomId : null\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The spoken key map for the keyboard lane.\n\t\t */\n\t\tkeyboardHelpText() {\n\t\t\treturn t(\n\t\t\t\t'nextcloud-vue',\n\t\t\t\t'Use the arrow keys to move this widget one cell. Hold Shift and use the arrow keys to resize it. Press Home or End to jump to the first or last column. Press Enter to open the widget menu.',\n\t\t\t)\n\t\t},\n\n\t\t/**\n\t\t * @return {number} The column count currently in force. With responsive\n\t\t * `columnOpts` GridStack reflows to fewer columns on small screens, so\n\t\t * clamping must read the live value rather than the `columns` prop.\n\t\t */\n\t\teffectiveColumns() {\n\t\t\tif (this.grid && typeof this.grid.getColumn === 'function') {\n\t\t\t\tconst live = this.grid.getColumn()\n\t\t\t\tif (Number.isFinite(live) && live > 0) {\n\t\t\t\t\treturn live\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn this.columns\n\t\t},\n\t},\n\n\twatch: {\n\t\teditable() {\n\t\t\tthis.syncInteractivity()\n\t\t},\n\n\t\twantsRowSplit(wants) {\n\t\t\tif (this.grid && (wants ? ROW_SPLIT : 1) !== this.rowSplit) {\n\t\t\t\tthis.rebuildGrid()\n\t\t\t}\n\t\t},\n\n\t\tlayout: {\n\t\t\tdeep: true,\n\t\t\thandler(newLayout) {\n\t\t\t\tif (this.grid) {\n\t\t\t\t\tthis.syncGridItems(newLayout)\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.initGrid()\n\t\tthis.$nextTick(() => this.observeContentSizes())\n\t},\n\n\tbeforeUnmount() {\n\t\tthis.disconnectContentObserver()\n\t\tif (this.grid) {\n\t\t\tthis.grid.destroy(false)\n\t\t}\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Derive the v-for render key for a layout item, honouring the optional\n\t\t * `itemKey` prop and falling back to `item.id`.\n\t\t *\n\t\t * @param {object} item the layout item.\n\t\t * @return {string|number} the render key.\n\t\t */\n\t\tresolveItemKey(item) {\n\t\t\treturn this.itemKey ? this.itemKey(item) : item.id\n\t\t},\n\n\t\t/**\n\t\t * The `gs-size-to-content` attribute for a layout item, or `undefined` to\n\t\t * leave the item on its authored `gridHeight`.\n\t\t *\n\t\t * A widget that decides AT RUNTIME it has nothing to show — a banner whose\n\t\t * condition is false, a list that came back empty — still costs its whole\n\t\t * authored row, because the row is reserved from the layout before the\n\t\t * component renders and the engine positions items absolutely, so no CSS\n\t\t * can reclaim it. `sizeToContent` hands the row's height to the content, so\n\t\t * an empty widget collapses and the grid closes up behind it.\n\t\t *\n\t\t * Opt-in per item: it makes `gridHeight` advisory for that widget, which is\n\t\t * wrong for anything with a deliberate size (a map, a chart) and right for\n\t\t * anything whose height is whatever it turned out to have.\n\t\t *\n\t\t * @param {object} item the layout item.\n\t\t * @return {string|undefined} `'true'`, a row maximum as a string, or undefined.\n\t\t */\n\t\tsizeToContentAttr(item) {\n\t\t\tconst v = item && item.sizeToContent\n\t\t\tif (v === true) {\n\t\t\t\treturn 'true'\n\t\t\t}\n\t\t\t// A number is GridStack's \"size to content, but never taller than N rows\" (a maximum).\n\t\t\tif (Number.isFinite(v) && v > 0) {\n\t\t\t\treturn String(v * this.rowSplit)\n\t\t\t}\n\t\t\treturn undefined\n\t\t},\n\n\t\t/**\n\t\t * A layout row coordinate or height in GridStack rows (`rowSplit`).\n\t\t *\n\t\t * @spec openspec/changes/dashboard-fit-cells-close-up/specs/dashboard-grid/spec.md#requirement-a-size-to-content-cell-ends-at-its-content\n\t\t * @param {number|undefined} value The value in layout rows.\n\t\t * @return {number|undefined} The value in grid rows, or undefined when none is set.\n\t\t */\n\t\tscaledRow(value) {\n\t\t\treturn Number.isFinite(value) ? value * this.rowSplit : undefined\n\t\t},\n\n\t\t/**\n\t\t * The smallest height an item may take, in grid rows: `minHeight`\n\t\t * layout rows, except for a size-to-content cell on split rows, which\n\t\t * may shrink to its content down to one grid row. The minimum is for\n\t\t * resizing by hand, and that only happens on whole rows in edit mode.\n\t\t *\n\t\t * @spec openspec/changes/dashboard-fit-cells-close-up/specs/dashboard-grid/spec.md#requirement-a-size-to-content-cell-ends-at-its-content\n\t\t * @param {object} item The layout item.\n\t\t * @return {number} The minimum height in grid rows.\n\t\t */\n\t\tminRows(item) {\n\t\t\tif (this.rowSplit !== 1 && this.sizeToContentAttr(item) !== undefined) {\n\t\t\t\treturn 1\n\t\t\t}\n\t\t\treturn this.minHeight * this.rowSplit\n\t\t},\n\n\t\t/**\n\t\t * Build the grid again with the row split `wantsRowSplit` asks for, as\n\t\t * edit mode turns on or off: the items go back to their layout places in\n\t\t * the new rows, and the size-to-content cells are measured again.\n\t\t *\n\t\t * @spec openspec/changes/dashboard-fit-cells-close-up/specs/dashboard-grid/spec.md#requirement-a-size-to-content-cell-ends-at-its-content\n\t\t * @return {void}\n\t\t */\n\t\trebuildGrid() {\n\t\t\tthis.disconnectContentObserver()\n\t\t\tthis.grid.destroy(false)\n\t\t\tthis.grid = null\n\t\t\tthis.rowSplit = this.wantsRowSplit ? ROW_SPLIT : 1\n\t\t\tthis.$nextTick(() => {\n\t\t\t\tif (!this.$refs.gridContainer) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\t// GridStack wrote its own places onto the items; put the layout's back.\n\t\t\t\tfor (const item of this.layout) {\n\t\t\t\t\tconst el = this.$refs.gridContainer.querySelector(`[gs-id=\"${item.id}\"]`)\n\t\t\t\t\tif (!el) {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tfor (const [name, value] of [['gs-y', this.scaledRow(item.gridY)], ['gs-h', this.scaledRow(item.gridHeight)], ['gs-min-h', this.minRows(item)], ['gs-size-to-content', this.sizeToContentAttr(item)]]) {\n\t\t\t\t\t\tif (value === undefined) {\n\t\t\t\t\t\t\tel.removeAttribute(name)\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tel.setAttribute(name, String(value))\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tthis.initGrid()\n\t\t\t\tthis.observeContentSizes()\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Build the accessible name for a grid item. A custom `itemLabel` wins\n\t\t * outright; otherwise the first human-readable field on the item is used\n\t\t * and, while the keyboard lane is live, the current grid coordinates are\n\t\t * appended — without them a screen-reader user tabbing through an edit-mode\n\t\t * dashboard has no way to tell where anything sits.\n\t\t *\n\t\t * @param {object} item the layout item.\n\t\t * @param {number} index the item's index in `layout`, used for the\n\t\t * last-resort positional name.\n\t\t * @return {string} the accessible name.\n\t\t */\n\t\tresolveItemLabel(item, index) {\n\t\t\tif (this.itemLabel) {\n\t\t\t\treturn String(this.itemLabel(item, index) ?? '')\n\t\t\t}\n\n\t\t\tconst base = item.title\n\t\t\t\t|| item.name\n\t\t\t\t|| item.label\n\t\t\t\t|| item.widgetTitle\n\t\t\t\t|| item.widgetId\n\t\t\t\t|| t('nextcloud-vue', 'Widget {number}', { number: index + 1 })\n\n\t\t\tif (!this.keyboardActive) {\n\t\t\t\treturn String(base)\n\t\t\t}\n\n\t\t\t// Report the rectangle exactly as the reactive `layout` prop states\n\t\t\t// it — unclamped, so the spoken coordinates always match the\n\t\t\t// rendered `gs-*` attributes — and read from the prop (not the\n\t\t\t// GridStack node) so the name re-renders when a move round-trips\n\t\t\t// back through `layout-change`; engine-node mutations are invisible\n\t\t\t// to Vue's reactivity.\n\t\t\tconst rect = {\n\t\t\t\tx: Number.isFinite(item.gridX) ? item.gridX : 0,\n\t\t\t\ty: Number.isFinite(item.gridY) ? item.gridY : 0,\n\t\t\t\tw: Number.isFinite(item.gridWidth) ? item.gridWidth : this.minWidth,\n\t\t\t\th: Number.isFinite(item.gridHeight) ? item.gridHeight : this.minHeight,\n\t\t\t}\n\t\t\treturn t(\n\t\t\t\t'nextcloud-vue',\n\t\t\t\t'{label}, column {column} of {columns}, row {row}, {width} columns wide, {height} rows tall',\n\t\t\t\t{\n\t\t\t\t\tlabel: String(base),\n\t\t\t\t\tcolumn: rect.x + 1,\n\t\t\t\t\tcolumns: this.effectiveColumns,\n\t\t\t\t\trow: rect.y + 1,\n\t\t\t\t\twidth: rect.w,\n\t\t\t\t\theight: rect.h,\n\t\t\t\t},\n\t\t\t)\n\t\t},\n\n\t\tinitGrid() {\n\t\t\tif (this.cellHeightCssVar && typeof document !== 'undefined' && document.documentElement) {\n\t\t\t\tdocument.documentElement.style.setProperty(this.cellHeightCssVar, `${this.cellHeight}px`)\n\t\t\t}\n\t\t\tconst el = this.$refs.gridContainer.querySelector('.grid-stack')\n\t\t\tthis.grid = GridStack.init({\n\t\t\t\tcolumn: this.columns,\n\t\t\t\tcellHeight: this.cellHeight / this.rowSplit,\n\t\t\t\tmargin: this.margin,\n\t\t\t\tfloat: this.float,\n\t\t\t\tanimate: true,\n\t\t\t\tdisableDrag: !this.editable,\n\t\t\t\tdisableResize: !this.editable,\n\t\t\t\tremovable: false,\n\t\t\t\t...(this.columnOpts ? { columnOpts: this.columnOpts } : {}),\n\t\t\t}, el)\n\n\t\t\tthis.grid.on('change', (_event, items) => {\n\t\t\t\tthis.handleGridChange(items)\n\t\t\t})\n\n\t\t\t// A grid that mounts already narrow is reflowed before anyone touches\n\t\t\t// it, so the handles have to be judged here and not only on a later\n\t\t\t// change.\n\t\t\tthis.syncInteractivity()\n\t\t},\n\n\t\t/**\n\t\t * Keep both editing lanes in step with whether an edit could actually be\n\t\t * kept: the pointer's handles, and — through `reflowed` feeding\n\t\t * `keyboardActive` — the tab stops, the help text and the arrow keys.\n\t\t *\n\t\t * A reflowed grid drops every `change` it receives, for the reason\n\t\t * `handleGridChange` gives. Leaving either lane live there offers an edit\n\t\t * that silently snaps back, which is the same class of surprise as\n\t\t * persisting the reflow — pointed the other way, and harder to explain\n\t\t * because the author did everything right. It is worse on the keyboard,\n\t\t * where the move is also announced as done. The affordance has to say what\n\t\t * the grid will do at this width, not what edit mode does at the authored\n\t\t * one.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tsyncInteractivity() {\n\t\t\t// Ahead of the engine guard: the keyboard lane reads this whether or\n\t\t\t// not the engine can be enabled.\n\t\t\tthis.reflowed = this.isReflowed()\n\t\t\tif (!this.grid || typeof this.grid.enable !== 'function') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.editable && !this.reflowed) {\n\t\t\t\tthis.grid.enable()\n\t\t\t} else {\n\t\t\t\tthis.grid.disable()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Whether the grid is currently rescaled to a column count other than the\n\t\t * authored one, i.e. a `columnOpts` breakpoint is in force.\n\t\t *\n\t\t * The authored count is `columnOpts.columnMax` when there is one, not the\n\t\t * `columns` prop: GridStack widens to `columnMax` above the top breakpoint,\n\t\t * so a consumer whose two disagree is at its widest, not reflowed.\n\t\t *\n\t\t * @return {boolean} True while a responsive reflow is active.\n\t\t */\n\t\tisReflowed() {\n\t\t\tif (!this.grid || typeof this.grid.getColumn !== 'function') {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tconst authored = Number.isFinite(this.columnOpts?.columnMax) ? this.columnOpts.columnMax : this.columns\n\t\t\tconst live = this.grid.getColumn()\n\t\t\treturn Number.isFinite(live) && live > 0 && live !== authored\n\t\t},\n\n\t\thandleGridChange(items) {\n\t\t\t// The rescale that crossed a breakpoint arrives here as a `change`, so\n\t\t\t// this is also where the handles are re-judged in both directions:\n\t\t\t// switched off as the grid narrows, and back on as it widens.\n\t\t\tthis.syncInteractivity()\n\n\t\t\tif (this.applyingLayout) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// GridStack fires `change` for its own responsive rescale too, and at a\n\t\t\t// 12 → 1 breakpoint every item reads `gridX: 0, gridWidth: 1`. Emitting\n\t\t\t// that has the host persist the reflow AS the authored layout, which\n\t\t\t// widening back does not undo. A move made while reflowed is the same\n\t\t\t// rescaled geometry, so it is dropped for the same reason.\n\t\t\tif (this.isReflowed()) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// A grid on split rows is a view-mode grid: what changes in it is\n\t\t\t// packing and size-to-content measuring, never a user's\n\t\t\t// arrangement, and it is not reported as one.\n\t\t\tif (this.rowSplit !== 1) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!items || items.length === 0) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst updated = this.layout.map((item) => {\n\t\t\t\tconst gridItem = items.find((gi) => String(gi.id) === String(item.id))\n\t\t\t\tif (gridItem) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\t...item,\n\t\t\t\t\t\tgridX: gridItem.x,\n\t\t\t\t\t\tgridY: gridItem.y,\n\t\t\t\t\t\tgridWidth: gridItem.w,\n\t\t\t\t\t\tgridHeight: gridItem.h,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn item\n\t\t\t})\n\n\t\t\tthis.$emit('layout-change', updated)\n\t\t},\n\n\t\t/**\n\t\t * Keydown handler bound to every grid item.\n\t\t *\n\t\t * Only reacts when the grid item element itself is the event target:\n\t\t * a button, input or menu rendered inside the `#widget` slot must keep\n\t\t * its own arrow/Enter semantics, and a grid that swallowed them would\n\t\t * be a worse a11y regression than the one this handler fixes.\n\t\t *\n\t\t * @param {KeyboardEvent} event the keydown event.\n\t\t * @param {object} item the layout item the key was pressed on.\n\t\t * @return {void}\n\t\t */\n\t\tonItemKeydown(event, item) {\n\t\t\tif (!this.keyboardRepositioning) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (event.target !== event.currentTarget) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (event.altKey || event.ctrlKey || event.metaKey) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif (event.key === 'Enter' || event.key === ' ' || event.key === 'Spacebar') {\n\t\t\t\tevent.preventDefault()\n\t\t\t\tthis.activateItem(item, event.currentTarget)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Repositioning only, and only where it can be kept — an item that\n\t\t\t// still holds focus from before a reflow keeps its Enter, but its\n\t\t\t// arrow keys stop moving something that would snap back.\n\t\t\tif (!this.keyboardActive) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst handled = this.applyKey(event.key, event.shiftKey, item)\n\t\t\tif (handled) {\n\t\t\t\t// Arrows would otherwise scroll the dashboard out from under\n\t\t\t\t// the widget the user is moving.\n\t\t\t\tevent.preventDefault()\n\t\t\t\tevent.stopPropagation()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Translate one repositioning key into a geometry change.\n\t\t *\n\t\t * @param {string} key the `KeyboardEvent.key` value.\n\t\t * @param {boolean} shift whether Shift was held (resize instead of move).\n\t\t * @param {object} item the layout item being operated on.\n\t\t * @return {boolean} `true` when the key belongs to the grid and the\n\t\t * default action must be suppressed.\n\t\t */\n\t\tapplyKey(key, shift, item) {\n\t\t\tconst rect = this.currentRect(item)\n\n\t\t\tswitch (key) {\n\t\t\t\tcase 'ArrowLeft':\n\t\t\t\t\treturn shift\n\t\t\t\t\t\t? this.applyGeometry(item, { w: rect.w - 1 }, 'resize')\n\t\t\t\t\t\t: this.applyGeometry(item, { x: rect.x - 1 }, 'move')\n\t\t\t\tcase 'ArrowRight':\n\t\t\t\t\treturn shift\n\t\t\t\t\t\t? this.applyGeometry(item, { w: rect.w + 1 }, 'resize')\n\t\t\t\t\t\t: this.applyGeometry(item, { x: rect.x + 1 }, 'move')\n\t\t\t\tcase 'ArrowUp':\n\t\t\t\t\treturn shift\n\t\t\t\t\t\t? this.applyGeometry(item, { h: rect.h - 1 }, 'resize')\n\t\t\t\t\t\t: this.applyGeometry(item, { y: rect.y - 1 }, 'move')\n\t\t\t\tcase 'ArrowDown':\n\t\t\t\t\treturn shift\n\t\t\t\t\t\t? this.applyGeometry(item, { h: rect.h + 1 }, 'resize')\n\t\t\t\t\t\t: this.applyGeometry(item, { y: rect.y + 1 }, 'move')\n\t\t\t\tcase 'Home':\n\t\t\t\t\treturn this.applyGeometry(item, { x: 0 }, 'move')\n\t\t\t\tcase 'End':\n\t\t\t\t\treturn this.applyGeometry(item, { x: this.effectiveColumns - rect.w }, 'move')\n\t\t\t\tdefault:\n\t\t\t\t\treturn false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Read an item's live rectangle. The GridStack node is preferred over\n\t\t * the `layout` prop because a drag that has not yet round-tripped\n\t\t * through the consumer's persistence leaves the prop momentarily stale,\n\t\t * and a keyboard nudge must continue from where the widget actually is.\n\t\t *\n\t\t * @param {object} item the layout item.\n\t\t * @return {{x: number, y: number, w: number, h: number}} the rectangle\n\t\t * in grid units.\n\t\t */\n\t\tcurrentRect(item) {\n\t\t\tconst node = this.gridNode(item.id)\n\t\t\tif (node && Number.isFinite(node.x) && Number.isFinite(node.y)) {\n\t\t\t\treturn {\n\t\t\t\t\tx: node.x,\n\t\t\t\t\ty: node.y,\n\t\t\t\t\tw: Number.isFinite(node.w) ? node.w : this.minWidth,\n\t\t\t\t\th: Number.isFinite(node.h) ? node.h : this.minHeight,\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tx: Number.isFinite(item.gridX) ? item.gridX : 0,\n\t\t\t\ty: Number.isFinite(item.gridY) ? item.gridY : 0,\n\t\t\t\tw: Number.isFinite(item.gridWidth) ? item.gridWidth : this.minWidth,\n\t\t\t\th: Number.isFinite(item.gridHeight) ? item.gridHeight : this.minHeight,\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Clamp a candidate rectangle to the grid's bounds and the configured\n\t\t * minimum widget size, so a held-down arrow key parks the widget at the\n\t\t * edge instead of pushing it into negative coordinates.\n\t\t *\n\t\t * @param {{x: number, y: number, w: number, h: number}} rect the candidate.\n\t\t * @return {{x: number, y: number, w: number, h: number}} the clamped rectangle.\n\t\t */\n\t\tclampRect(rect) {\n\t\t\tconst cols = this.effectiveColumns\n\t\t\tconst w = Math.min(Math.max(rect.w, this.minWidth), cols)\n\t\t\tconst h = Math.max(rect.h, this.minHeight)\n\t\t\treturn {\n\t\t\t\tx: Math.min(Math.max(rect.x, 0), Math.max(cols - w, 0)),\n\t\t\t\ty: Math.max(rect.y, 0),\n\t\t\t\tw,\n\t\t\t\th,\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Apply a partial geometry change to an item.\n\t\t *\n\t\t * The change is pushed through `GridStack.update()` — the very call the\n\t\t * drag/resize handlers end in — so collision resolution, the `change`\n\t\t * event and therefore the `layout-change` emission and the consumer's\n\t\t * persistence are identical for keyboard and pointer. There is\n\t\t * deliberately no second, direct-to-`layout` update path.\n\t\t *\n\t\t * @param {object} item the layout item to move or resize.\n\t\t * @param {{x?: number, y?: number, w?: number, h?: number}} patch the\n\t\t * requested change, merged over the item's current rectangle.\n\t\t * @param {'move'|'resize'} kind which announcement to speak.\n\t\t * @return {boolean} `true` when the key was consumed — including when\n\t\t * the widget was already at the edge, because the arrow still belongs\n\t\t * to the grid and must not scroll the page.\n\t\t */\n\t\tapplyGeometry(item, patch, kind) {\n\t\t\tconst el = this.itemElement(item.id)\n\t\t\tif (!el || !this.grid || typeof this.grid.update !== 'function') {\n\t\t\t\treturn false\n\t\t\t}\n\n\t\t\tconst current = this.currentRect(item)\n\t\t\tconst next = this.clampRect({ ...current, ...patch })\n\n\t\t\tif (next.x === current.x && next.y === current.y\n\t\t\t\t&& next.w === current.w && next.h === current.h) {\n\t\t\t\tthis.announce(t('nextcloud-vue', '{label} cannot move further in that direction', {\n\t\t\t\t\tlabel: this.announcementLabel(item),\n\t\t\t\t}))\n\t\t\t\treturn true\n\t\t\t}\n\n\t\t\tthis.grid.update(el, next)\n\n\t\t\tthis.announce(kind === 'resize'\n\t\t\t\t? t('nextcloud-vue', '{label} resized to {width} columns by {height} rows', {\n\t\t\t\t\t\tlabel: this.announcementLabel(item),\n\t\t\t\t\t\twidth: next.w,\n\t\t\t\t\t\theight: next.h,\n\t\t\t\t\t})\n\t\t\t\t: t('nextcloud-vue', '{label} moved to column {column}, row {row}', {\n\t\t\t\t\t\tlabel: this.announcementLabel(item),\n\t\t\t\t\t\tcolumn: next.x + 1,\n\t\t\t\t\t\trow: next.y + 1,\n\t\t\t\t\t}))\n\n\t\t\treturn true\n\t\t},\n\n\t\t/**\n\t\t * The short, coordinate-free name used inside live-region sentences —\n\t\t * the full `aria-label` already carries the coordinates and repeating\n\t\t * them would make every announcement unlistenable.\n\t\t *\n\t\t * @param {object} item the layout item.\n\t\t * @return {string} the short name.\n\t\t */\n\t\tannouncementLabel(item) {\n\t\t\treturn String(item.title\n\t\t\t\t|| item.name\n\t\t\t\t|| item.label\n\t\t\t\t|| item.widgetTitle\n\t\t\t\t|| item.widgetId\n\t\t\t\t|| t('nextcloud-vue', 'Widget'))\n\t\t},\n\n\t\t/**\n\t\t * Push text into the polite live region. The region is cleared first so\n\t\t * two identical consecutive announcements (e.g. two failed nudges at the\n\t\t * grid edge) are both spoken — assistive tech ignores a re-render that\n\t\t * leaves the text unchanged.\n\t\t *\n\t\t * @param {string} text the message to announce.\n\t\t * @return {void}\n\t\t */\n\t\tannounce(text) {\n\t\t\tthis.announcement = ''\n\t\t\tthis.$nextTick(() => {\n\t\t\t\tthis.announcement = text\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Handle `Enter` / `Space` on a focused grid item: tell the consumer via\n\t\t * `item-activate`, and — unless opted out — synthesise the `contextmenu`\n\t\t * event a right-click would have produced, anchored to the item, so\n\t\t * pointer-only widget menus become keyboard-reachable without the\n\t\t * consumer wiring anything new. The synthetic event is dispatched from\n\t\t * inside the item's content so it bubbles through the slot's own root\n\t\t * element (where such handlers live) and on up.\n\t\t *\n\t\t * @param {object} item the layout item being activated.\n\t\t * @param {HTMLElement} el the grid item element.\n\t\t * @return {void}\n\t\t */\n\t\tactivateItem(item, el) {\n\t\t\tconst rect = typeof el.getBoundingClientRect === 'function'\n\t\t\t\t? el.getBoundingClientRect()\n\t\t\t\t: { left: 0, top: 0 }\n\t\t\tconst clientX = Math.round((rect.left || 0) + MENU_ANCHOR_OFFSET)\n\t\t\tconst clientY = Math.round((rect.top || 0) + MENU_ANCHOR_OFFSET)\n\n\t\t\tthis.$emit('item-activate', { item, element: el, clientX, clientY })\n\n\t\t\tif (this.activateOpensContextMenu) {\n\t\t\t\tthis.dispatchContextMenu(el, clientX, clientY)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Dispatch a bubbling, cancelable `contextmenu` MouseEvent from inside a\n\t\t * grid item.\n\t\t *\n\t\t * @param {HTMLElement} el the grid item element.\n\t\t * @param {number} clientX viewport x for the menu anchor.\n\t\t * @param {number} clientY viewport y for the menu anchor.\n\t\t * @return {void}\n\t\t */\n\t\tdispatchContextMenu(el, clientX, clientY) {\n\t\t\tif (typeof MouseEvent !== 'function') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst content = el.querySelector('.grid-stack-item-content')\n\t\t\tconst target = (content && content.firstElementChild) || content || el\n\t\t\ttarget.dispatchEvent(new MouseEvent('contextmenu', {\n\t\t\t\tbubbles: true,\n\t\t\t\tcancelable: true,\n\t\t\t\tclientX,\n\t\t\t\tclientY,\n\t\t\t\tbutton: 2,\n\t\t\t}))\n\t\t},\n\n\t\t/**\n\t\t * Look up a grid item's DOM element by its layout id.\n\t\t *\n\t\t * @param {string|number} id the layout item id.\n\t\t * @return {HTMLElement|null} the element, or `null` when not rendered.\n\t\t */\n\t\titemElement(id) {\n\t\t\tconst container = this.$refs.gridContainer\n\t\t\tif (!container) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn container.querySelector(`[gs-id=\"${id}\"]`)\n\t\t},\n\n\t\t/**\n\t\t * Look up the GridStack engine node backing a layout item.\n\t\t *\n\t\t * @param {string|number} id the layout item id.\n\t\t * @return {object|null} the node, or `null` when GridStack isn't tracking it.\n\t\t */\n\t\t/**\n\t\t * Re-measure a `sizeToContent` item when its content changes height.\n\t\t * GridStack watches only the grid element, so a widget that loaded its data\n\t\t * in `mounted()` keeps the height its empty state had.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tobserveContentSizes() {\n\t\t\tthis.disconnectContentObserver()\n\t\t\tif (typeof ResizeObserver === 'undefined' || !this.$refs.gridContainer) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tthis.contentObserver = new ResizeObserver((entries) => {\n\t\t\t\tconst cells = new Set()\n\t\t\t\tfor (const entry of entries) {\n\t\t\t\t\tconst cell = entry.target.closest('.grid-stack-item')\n\t\t\t\t\tif (cell) {\n\t\t\t\t\t\tcells.add(cell)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (cells.size === 0) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\t// A frame later, not inline: `resizeToContent` resizes the cell the\n\t\t\t\t// observed child sits in, and a child that stretches to its parent\n\t\t\t\t// grows with it. Resizing from inside the callback makes that the\n\t\t\t\t// classic ResizeObserver loop (\"undelivered notifications\"); deferred,\n\t\t\t\t// it is an ordinary second pass that settles.\n\t\t\t\trequestAnimationFrame(() => {\n\t\t\t\t\tif (!this.contentObserver || !this.grid || typeof this.grid.resizeToContent !== 'function') {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tfor (const cell of cells) {\n\t\t\t\t\t\tthis.grid.resizeToContent(cell)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t})\n\n\t\t\tconst cells = this.$refs.gridContainer.querySelectorAll('.grid-stack-item[gs-size-to-content]')\n\t\t\tfor (const cell of cells) {\n\t\t\t\tconst measured = cell.querySelector('.grid-stack-item-content')?.firstElementChild\n\t\t\t\tif (measured) {\n\t\t\t\t\tthis.contentObserver.observe(measured)\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Drop the content observer, on unmount or before rebuilding it.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tdisconnectContentObserver() {\n\t\t\tif (this.contentObserver) {\n\t\t\t\tthis.contentObserver.disconnect()\n\t\t\t\tthis.contentObserver = null\n\t\t\t}\n\t\t},\n\n\t\tgridNode(id) {\n\t\t\tconst nodes = this.grid && this.grid.engine && this.grid.engine.nodes\n\t\t\tif (!Array.isArray(nodes)) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn nodes.find((n) => String(n.id) === String(id)) || null\n\t\t},\n\n\t\tsyncGridItems(newLayout) {\n\t\t\t// Add new items, and re-adopt items whose backing DOM element was\n\t\t\t// replaced. When a consumer's `itemKey` changes for an unchanged\n\t\t\t// `id` (e.g. a style edit — the documented use of `itemKey`), Vue\n\t\t\t// swaps the element. GridStack keeps tracking the old, now-detached\n\t\t\t// element, leaving the new one unmanaged (no positioning/sizing) —\n\t\t\t// so re-register it against the new element.\n\t\t\tfor (const item of newLayout) {\n\t\t\t\tthis.$nextTick(() => {\n\t\t\t\t\tconst el = this.$refs.gridContainer.querySelector(`[gs-id=\"${item.id}\"]`)\n\t\t\t\t\tif (!el) {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tconst node = this.grid.engine.nodes.find((n) => String(n.id) === String(item.id))\n\t\t\t\t\tif (!node) {\n\t\t\t\t\t\tthis.grid.makeWidget(el)\n\t\t\t\t\t} else if (node.el === el) {\n\t\t\t\t\t\t// The layout MOVED an item the grid already tracks: the\n\t\t\t\t\t\t// page re-compacts its display layout when a widget's\n\t\t\t\t\t\t// condition hides it, and the item below was given a new\n\t\t\t\t\t\t// gridY that never reached GridStack, so a band stayed\n\t\t\t\t\t\t// where the hidden widget had been. A drag reports the\n\t\t\t\t\t\t// grid's own position back, which matches and does\n\t\t\t\t\t\t// nothing here.\n\t\t\t\t\t\tconst x = item.gridX ?? node.x\n\t\t\t\t\t\tconst y = Number.isFinite(item.gridY) ? item.gridY * this.rowSplit : node.y\n\t\t\t\t\t\tif (x !== node.x || y !== node.y) {\n\t\t\t\t\t\t\t// The layout said where it goes; GridStack's `change`\n\t\t\t\t\t\t\t// for it is not a user's arrangement and is not\n\t\t\t\t\t\t\t// reported back (it would write the display-only\n\t\t\t\t\t\t\t// compaction into the authored layout).\n\t\t\t\t\t\t\tthis.applyingLayout = true\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tthis.grid.update(el, { x, y })\n\t\t\t\t\t\t\t} finally {\n\t\t\t\t\t\t\t\tthis.applyingLayout = false\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Drop the stale node without touching its detached DOM,\n\t\t\t\t\t\t// then adopt the new element so GridStack sizes/places it.\n\t\t\t\t\t\tthis.grid.removeWidget(node.el, false, false)\n\t\t\t\t\t\tthis.grid.makeWidget(el)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\n\t\t\t// Remove items no longer in layout\n\t\t\tconst ids = newLayout.map((i) => String(i.id))\n\t\t\tconst toRemove = this.grid.engine.nodes.filter((n) => !ids.includes(String(n.id)))\n\t\t\tfor (const node of toRemove) {\n\t\t\t\tconst el = this.$refs.gridContainer.querySelector(`[gs-id=\"${node.id}\"]`)\n\t\t\t\tif (el) {\n\t\t\t\t\tthis.grid.removeWidget(el, false)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Elements were added or replaced, so the observer's are stale.\n\t\t\tthis.$nextTick(() => this.observeContentSizes())\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-dashboard-grid {\n\twidth: 100%;\n\tmin-height: 200px;\n}\n\n.grid-stack {\n\tbackground: transparent;\n}\n\n/* Screen-reader-only text: the keyboard key map and the live region. Kept in\n the layout (not `display: none`) so assistive tech still reads it. */\n.cn-dashboard-grid__sr-only {\n\tposition: absolute;\n\twidth: 1px;\n\theight: 1px;\n\tpadding: 0;\n\tmargin: -1px;\n\toverflow: hidden;\n\tclip-path: inset(50%);\n\twhite-space: nowrap;\n\tborder: 0;\n}\n\n/* Grid items become tab stops in edit mode — give them a visible focus ring\n that satisfies WCAG 2.1 SC 2.4.7 without relying on the widget's own\n styling. `outline-offset` keeps the ring outside the rounded card. */\n.grid-stack-item:focus-visible {\n\toutline: 2px solid var(--color-primary-element, #0082c9);\n\toutline-offset: 2px;\n\tborder-radius: var(--border-radius-container-large, 16px);\n}\n\n/* A size-to-content cell (`sizeToContent`) is measured by GridStack from the\n content's first child, and a widget wrapper there filled the cell\n (`height: 100%`), so the measure returned the cell's own height and the\n cell never shrank: \"Waiting for me\" stayed a tall card around one row. In\n such a cell the child takes its own height. A cell without the key keeps\n the filling child. */\n.cn-dashboard-grid__item--fit > :deep(.grid-stack-item-content > *) {\n\theight: auto;\n}\n\n:deep(.grid-stack-item-content) {\n\tbackground: var(--color-main-background);\n\tborder: none;\n\tbox-shadow: none;\n\toverflow: hidden;\n}\n\n:deep(.grid-stack-item-content:has(.cn-widget-wrapper--borderless)) {\n\tbackground: transparent;\n}\n\n:deep(.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":["t"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CnDashboardGrid.vue2.js","sources":["../../../../src/components/CnDashboardGrid/CnDashboardGrid.vue"],"sourcesContent":["<!--\n CnDashboardGrid — GridStack-powered drag-and-drop dashboard grid.\n\n Renders widgets in a configurable grid layout. Supports drag, resize,\n and dynamic item addition/removal. Emits layout changes for persistence.\n\n Accessibility: every grid item is an ARIA `group` with an accessible name\n and — in edit mode — a tab stop, so the pointer-only GridStack drag/resize\n gestures have a full keyboard equivalent (WCAG 2.1 SC 2.1.1). Keyboard\n moves are routed through `GridStack.update()`, i.e. the exact same engine\n call drag/resize ends in, so collision handling, persistence and the\n `layout-change` emission are byte-identical between the two input modes.\n-->\n<template>\n\t<div ref=\"gridContainer\" class=\"cn-dashboard-grid\">\n\t\t<!--\n\t\t\tKeyboard instructions, referenced by every grid item through\n\t\t\t`aria-describedby`. Rendered once (not per item) so a screen\n\t\t\treader user hears the key map on the first item they land on\n\t\t\twithout it being duplicated N times in the accessibility tree.\n\t\t-->\n\t\t<p\n\t\t\tv-if=\"keyboardActive\"\n\t\t\t:id=\"keyboardHelpDomId\"\n\t\t\tclass=\"cn-dashboard-grid__sr-only\">\n\t\t\t{{ keyboardHelpText }}\n\t\t</p>\n\n\t\t<div class=\"grid-stack\">\n\t\t\t<div\n\t\t\t\tv-for=\"item in itemsInRowOrder\"\n\t\t\t\t:key=\"resolveItemKey(item)\"\n\t\t\t\tclass=\"grid-stack-item\"\n\t\t\t\t:class=\"{ 'cn-dashboard-grid__item--fit': sizeToContentAttr(item) !== undefined }\"\n\t\t\t\trole=\"group\"\n\t\t\t\t:aria-label=\"resolveItemLabel(item, layout.indexOf(item))\"\n\t\t\t\t:aria-describedby=\"itemDescribedBy\"\n\t\t\t\t:tabindex=\"itemTabindex\"\n\t\t\t\t:gs-id=\"item.id\"\n\t\t\t\t:gs-x=\"item.gridX\"\n\t\t\t\t:gs-y=\"scaledRow(item.gridY)\"\n\t\t\t\t:gs-w=\"item.gridWidth\"\n\t\t\t\t:gs-h=\"scaledRow(item.gridHeight)\"\n\t\t\t\t:gs-min-w=\"minWidth\"\n\t\t\t\t:gs-min-h=\"minRows(item)\"\n\t\t\t\t:gs-size-to-content=\"sizeToContentAttr(item)\"\n\t\t\t\t@keydown=\"onItemKeydown($event, item)\">\n\t\t\t\t<div class=\"grid-stack-item-content\">\n\t\t\t\t\t<slot name=\"widget\" :item=\"item\">\n\t\t\t\t\t\t<!-- Default: render nothing; CnDashboardPage provides content -->\n\t\t\t\t\t</slot>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!--\n\t\t\tPolite live region for keyboard move/resize feedback. A sighted\n\t\t\tuser sees the widget slide; a screen-reader user needs the new\n\t\t\tcoordinates spoken, otherwise the arrow keys are silent.\n\t\t-->\n\t\t<p\n\t\t\tclass=\"cn-dashboard-grid__sr-only\"\n\t\t\trole=\"status\"\n\t\t\taria-live=\"polite\"\n\t\t\taria-atomic=\"true\">\n\t\t\t{{ announcement }}\n\t\t</p>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { GridStack } from 'gridstack'\nimport { planGrowth } from '../../utils/gridStack.js'\n// `gridstack` is a peerDependency (see package.json), not bundled — this\n// component's JS and the consumer's GridStack stylesheet MUST come from the\n// exact same installed copy, because GridStack's JS writes layout state\n// (e.g. the `--gs-column-width` CSS custom property) that only its OWN\n// matching CSS reads. Baking a copy of `gridstack/dist/gridstack.min.css`\n// into this library's build (as a previous version did) freezes it at\n// nc-vue's build-time version, which can silently drift from whatever the\n// consumer's peer-installed copy actually is — the exact mismatch that once\n// made every grid item render at 0px width. So nc-vue ships NO GridStack\n// CSS of its own: the consumer MUST install `gridstack` themselves and\n// import `gridstack/dist/gridstack.min.css` (or their preferred GridStack\n// stylesheet) from that same copy.\n\n/**\n * Monotonic counter backing the per-instance DOM ids (the shared\n * `aria-describedby` help text). Module-level so two grids on one page\n * never collide — the same pattern CnCommandPalette uses.\n */\nlet domIdCounter = 0\n\n/**\n * Pixel offset applied to the grid item's top-left corner when synthesising\n * the `contextmenu` coordinates for a keyboard activation. Nudging the anchor\n * inside the item keeps consumer menus (which position at `clientX/clientY`)\n * visually attached to the widget instead of straddling its border.\n */\nconst MENU_ANCHOR_OFFSET = 8\n\n/**\n * How many GridStack rows a layout row is drawn as on a grid with\n * size-to-content cells (`wantsRowSplit`): 80px rows become 10px rows.\n */\nconst ROW_SPLIT = 8\n\n/**\n * Whether a layout holds a size-to-content cell (`sizeToContent` true or a\n * positive row maximum).\n *\n * @param {Array<object>} layout The layout items.\n * @return {boolean} True when one does.\n */\nfunction hasFitCell(layout) {\n\treturn Array.isArray(layout) && layout.some((item) => item\n\t\t&& (item.sizeToContent === true || (Number.isFinite(item.sizeToContent) && item.sizeToContent > 0)))\n}\n\n/**\n * CnDashboardGrid — Low-level grid layout engine powered by GridStack.\n *\n * Manages the drag-and-drop grid, syncs positions, and emits layout\n * changes. Does NOT handle widget rendering — that's done by the parent\n * via the `#widget` scoped slot.\n *\n * ```vue\n * <CnDashboardGrid\n * :layout=\"placements\"\n * :editable=\"isEditing\"\n * @layout-change=\"onLayoutChange\">\n * <template #widget=\"{ item }\">\n * <MyWidget :config=\"item\" />\n * </template>\n * </CnDashboardGrid>\n * ```\n *\n * ### Keyboard operation\n *\n * With `keyboardRepositioning` on (the default) and `editable` true, each\n * grid item is a tab stop. With an item focused:\n *\n * | Key | Action |\n * | --- | ------ |\n * | `ArrowLeft` / `ArrowRight` | move one column left / right |\n * | `ArrowUp` / `ArrowDown` | move one row up / down |\n * | `Shift` + `ArrowLeft` / `ArrowRight` | shrink / grow width by one column |\n * | `Shift` + `ArrowUp` / `ArrowDown` | shrink / grow height by one row |\n * | `Home` / `End` | jump to the first / last column of the current row |\n * | `Enter` / `Space` | activate the item — see `item-activate` |\n *\n * Keys are only honoured when the grid item itself holds focus, so\n * interactive content rendered inside the `#widget` slot keeps its own\n * key handling untouched.\n */\nexport default {\n\tname: 'CnDashboardGrid',\n\n\tprops: {\n\t\t/** Array of layout items: { id, gridX, gridY, gridWidth, gridHeight, ...extra } */\n\t\tlayout: {\n\t\t\ttype: Array,\n\t\t\trequired: true,\n\t\t},\n\n\t\t/** Whether drag and resize are enabled */\n\t\teditable: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Number of grid columns */\n\t\tcolumns: {\n\t\t\ttype: Number,\n\t\t\tdefault: 12,\n\t\t},\n\n\t\t/** Cell height in pixels */\n\t\tcellHeight: {\n\t\t\ttype: Number,\n\t\t\tdefault: 80,\n\t\t},\n\n\t\t/** Grid margin in pixels */\n\t\tmargin: {\n\t\t\ttype: Number,\n\t\t\tdefault: 12,\n\t\t},\n\n\t\t/** Minimum widget width in grid units */\n\t\tminWidth: {\n\t\t\ttype: Number,\n\t\t\tdefault: 2,\n\t\t},\n\n\t\t/** Minimum widget height in grid units */\n\t\tminHeight: {\n\t\t\ttype: Number,\n\t\t\tdefault: 2,\n\t\t},\n\n\t\t/**\n\t\t * GridStack v12 responsive `columnOpts` bag (breakpoints + reflow\n\t\t * layout). When set, the grid reflows column count across screen sizes.\n\t\t * Build it with `getDashboardColumnOpts()`. Default `null` = fixed\n\t\t * `columns`, no responsive reflow (backwards-compatible).\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tcolumnOpts: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * When set, `cellHeight` is mirrored into this CSS custom property on\n\t\t * the document root at init (e.g. `'--app-cell-height'`), so app CSS can\n\t\t * align to the grid geometry. Default `null` = no CSS var written.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tcellHeightCssVar: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Optional `(item) => string|number` to derive each item's render key.\n\t\t * Use it to force a re-render when an item changes in a way its `id`\n\t\t * doesn't capture (e.g. style edits — return `${item.id}:${item.updatedAt}`).\n\t\t * Default `null` = key on `item.id`.\n\t\t *\n\t\t * @type {((item: object) => (string|number))|null}\n\t\t */\n\t\titemKey: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Whether grid items are keyboard-operable: focusable in edit mode and\n\t\t * repositionable/resizable with the arrow keys (WCAG 2.1 SC 2.1.1 —\n\t\t * the keyboard equivalent of the pointer-only GridStack drag). Turning\n\t\t * it off leaves the items non-focusable and drag-only. Default `true`.\n\t\t */\n\t\tkeyboardRepositioning: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Optional `(item, index) => string` returning the accessible name for\n\t\t * a grid item. Use it when the layout items don't carry a human-readable\n\t\t * field (the built-in fallback walks `title` → `name` → `label` →\n\t\t * `widgetTitle` → `widgetId`, then a positional \"Widget N\"). The returned\n\t\t * string is used verbatim — no coordinates are appended.\n\t\t *\n\t\t * @type {((item: object, index: number) => string)|null}\n\t\t */\n\t\titemLabel: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Whether a keyboard activation (`Enter` / `Space` on a focused grid\n\t\t * item) also dispatches a bubbling `contextmenu` event from inside the\n\t\t * item, so consumers that open a per-widget menu on right-click get the\n\t\t * keyboard path for free — mirroring what browsers do for the `Menu`\n\t\t * key. Set `false` to rely purely on the `item-activate` event.\n\t\t * Default `true`.\n\t\t */\n\t\tactivateOpensContextMenu: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * GridStack's `float`. True (the default) keeps every item on the row\n\t\t * it was placed on, with empty rows above it if need be. False packs\n\t\t * items upward, so a size-to-content cell that shrinks, or a widget\n\t\t * that is not shown, closes its gap instead of leaving a band.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-dashboard-closes-the-gap-a-shrinking-or-hidden-widget-leaves\n\t\t */\n\t\tfloat: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\t},\n\n\temits: [\n\t\t/**\n\t\t * Fired whenever the grid geometry changed — by drag, by resize, or by\n\t\t * a keyboard nudge. Payload is the full `layout` array with the moved\n\t\t * items' `gridX`/`gridY`/`gridWidth`/`gridHeight` updated.\n\t\t */\n\t\t'layout-change',\n\t\t/**\n\t\t * Fired when the user activates a focused grid item with `Enter` or\n\t\t * `Space`. Payload `{ item, element, clientX, clientY }` — the layout\n\t\t * item, its DOM element, and viewport coordinates anchored to the\n\t\t * item's top-left corner so a consumer menu can position itself the\n\t\t * same way it does for a right-click.\n\t\t */\n\t\t'item-activate',\n\t],\n\n\tdata() {\n\t\treturn {\n\t\t\tgrid: null,\n\t\t\tcontentObserver: null,\n\t\t\t/** True while the grid moves an item to where the layout put it. */\n\t\t\tapplyingLayout: false,\n\t\t\tdomId: `cn-dashboard-grid-${++domIdCounter}`,\n\t\t\t/** Text currently held in the polite live region. */\n\t\t\tannouncement: '',\n\t\t\t/**\n\t\t\t * Whether a `columnOpts` breakpoint is currently in force. Mirrored\n\t\t\t * from the engine by `syncInteractivity` because `isReflowed()` reads\n\t\t\t * `grid.getColumn()` imperatively, and a computed cannot re-evaluate\n\t\t\t * on that.\n\t\t\t */\n\t\t\treflowed: false,\n\t\t\t/**\n\t\t\t * How many GridStack rows one layout row is drawn as: 1, or\n\t\t\t * ROW_SPLIT while `wantsRowSplit` holds. Set before the first render,\n\t\t\t * since GridStack reads the items' places from their attributes,\n\t\t\t * and again when the grid is rebuilt.\n\t\t\t */\n\t\t\trowSplit: (!this.editable && !this.float && hasFitCell(this.layout)) ? ROW_SPLIT : 1,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The layout's items top to bottom, then left to right: the order the\n\t\t * items are in the page, so GridStack places them in that order.\n\t\t *\n\t\t * GridStack sizes a size-to-content cell while it adds the items, one\n\t\t * at a time, and an item it adds onto a cell that has already grown\n\t\t * moves itself below whatever it then collides with. In the layout's\n\t\t * own order a card declared after the lists (pipelinq's full-width\n\t\t * \"client in focus\" card) was added last, so when the banner above\n\t\t * it grew it skipped past the stat tiles AND the lists and landed at\n\t\t * the bottom. Added top to bottom, every card lands under the one\n\t\t * above it. It is also the order a screen reader and the Tab key\n\t\t * follow. An item without a row or column sorts after the items\n\t\t * that have one, in the layout's order.\n\t\t *\n\t\t * @spec openspec/changes/dashboard-growth-keeps-order/specs/dashboard-grid/spec.md#requirement-a-growing-cell-keeps-the-order-of-the-cards-under-it\n\t\t * @return {Array<object>} The items in row order.\n\t\t */\n\t\titemsInRowOrder() {\n\t\t\tconst items = Array.isArray(this.layout) ? [...this.layout] : []\n\t\t\tconst at = (value) => (Number.isFinite(value) ? value : Number.MAX_SAFE_INTEGER)\n\t\t\treturn items.sort((a, b) => (at(a.gridY) - at(b.gridY)) || (at(a.gridX) - at(b.gridX)))\n\t\t},\n\n\t\t/**\n\t\t * Whether the grid draws each layout row as ROW_SPLIT finer rows. A\n\t\t * size-to-content cell can only be a whole number of rows tall, so on\n\t\t * 80px rows it kept up to a row of empty space under its content and\n\t\t * a packing grid (`float: false`) could not close it (pipelinq and\n\t\t * dossiq Mijn werk, decidiq Mijn acties). Split into 10px rows the\n\t\t * cell ends within 10px of its content and the widgets below move up.\n\t\t *\n\t\t * Only in view mode, only on a packing grid with a size-to-content\n\t\t * cell: every other grid is built exactly as before. Item positions\n\t\t * and sizes scale with the rows, so every widget keeps its pixel\n\t\t * place and size until a cell shrinks; the layout itself, what edit\n\t\t * mode shows and what `layout-change` reports, stays in layout rows.\n\t\t *\n\t\t * @spec openspec/changes/dashboard-fit-cells-close-up/specs/dashboard-grid/spec.md#requirement-a-size-to-content-cell-ends-at-its-content\n\t\t * @return {boolean} True to split the rows.\n\t\t */\n\t\twantsRowSplit() {\n\t\t\treturn !this.editable && !this.float && hasFitCell(this.layout)\n\t\t},\n\n\t\t/**\n\t\t * @return {boolean} Whether the keyboard lane is live — items are\n\t\t * focusable and the arrow keys reposition. Requires the opt-in prop,\n\t\t * edit mode (a read-only dashboard has nothing to move, so adding N tab\n\t\t * stops there would be pure noise), and a grid that is not reflowed,\n\t\t * which is where a move cannot be kept and so must not be offered.\n\t\t */\n\t\tkeyboardActive() {\n\t\t\treturn this.keyboardRepositioning && this.editable && !this.reflowed\n\t\t},\n\n\t\t/**\n\t\t * @return {number|null} `0` when grid items should be tab stops,\n\t\t * otherwise `null` so the attribute is omitted entirely.\n\t\t */\n\t\titemTabindex() {\n\t\t\treturn this.keyboardActive ? 0 : null\n\t\t},\n\n\t\t/**\n\t\t * @return {string} DOM id of the shared keyboard-instructions element.\n\t\t */\n\t\tkeyboardHelpDomId() {\n\t\t\treturn `${this.domId}-keyboard-help`\n\t\t},\n\n\t\t/**\n\t\t * @return {string|null} `aria-describedby` target for each grid item —\n\t\t * the instructions element while the keyboard lane is live, `null`\n\t\t * otherwise (a dangling id reference is an axe violation).\n\t\t */\n\t\titemDescribedBy() {\n\t\t\treturn this.keyboardActive ? this.keyboardHelpDomId : null\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The spoken key map for the keyboard lane.\n\t\t */\n\t\tkeyboardHelpText() {\n\t\t\treturn t(\n\t\t\t\t'nextcloud-vue',\n\t\t\t\t'Use the arrow keys to move this widget one cell. Hold Shift and use the arrow keys to resize it. Press Home or End to jump to the first or last column. Press Enter to open the widget menu.',\n\t\t\t)\n\t\t},\n\n\t\t/**\n\t\t * @return {number} The column count currently in force. With responsive\n\t\t * `columnOpts` GridStack reflows to fewer columns on small screens, so\n\t\t * clamping must read the live value rather than the `columns` prop.\n\t\t */\n\t\teffectiveColumns() {\n\t\t\tif (this.grid && typeof this.grid.getColumn === 'function') {\n\t\t\t\tconst live = this.grid.getColumn()\n\t\t\t\tif (Number.isFinite(live) && live > 0) {\n\t\t\t\t\treturn live\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn this.columns\n\t\t},\n\t},\n\n\twatch: {\n\t\teditable() {\n\t\t\tthis.syncInteractivity()\n\t\t},\n\n\t\twantsRowSplit(wants) {\n\t\t\tif (this.grid && (wants ? ROW_SPLIT : 1) !== this.rowSplit) {\n\t\t\t\tthis.rebuildGrid()\n\t\t\t}\n\t\t},\n\n\t\tlayout: {\n\t\t\tdeep: true,\n\t\t\thandler(newLayout) {\n\t\t\t\tif (this.grid) {\n\t\t\t\t\tthis.syncGridItems(newLayout)\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.initGrid()\n\t\tthis.$nextTick(() => this.observeContentSizes())\n\t},\n\n\tbeforeUnmount() {\n\t\tthis.disconnectContentObserver()\n\t\tif (this.grid) {\n\t\t\tthis.grid.destroy(false)\n\t\t}\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Derive the v-for render key for a layout item, honouring the optional\n\t\t * `itemKey` prop and falling back to `item.id`.\n\t\t *\n\t\t * @param {object} item the layout item.\n\t\t * @return {string|number} the render key.\n\t\t */\n\t\tresolveItemKey(item) {\n\t\t\treturn this.itemKey ? this.itemKey(item) : item.id\n\t\t},\n\n\t\t/**\n\t\t * The `gs-size-to-content` attribute for a layout item, or `undefined` to\n\t\t * leave the item on its authored `gridHeight`.\n\t\t *\n\t\t * A widget that decides AT RUNTIME it has nothing to show — a banner whose\n\t\t * condition is false, a list that came back empty — still costs its whole\n\t\t * authored row, because the row is reserved from the layout before the\n\t\t * component renders and the engine positions items absolutely, so no CSS\n\t\t * can reclaim it. `sizeToContent` hands the row's height to the content, so\n\t\t * an empty widget collapses and the grid closes up behind it.\n\t\t *\n\t\t * Opt-in per item: it makes `gridHeight` advisory for that widget, which is\n\t\t * wrong for anything with a deliberate size (a map, a chart) and right for\n\t\t * anything whose height is whatever it turned out to have.\n\t\t *\n\t\t * @param {object} item the layout item.\n\t\t * @return {string|undefined} `'true'`, a row maximum as a string, or undefined.\n\t\t */\n\t\tsizeToContentAttr(item) {\n\t\t\tconst v = item && item.sizeToContent\n\t\t\tif (v === true) {\n\t\t\t\treturn 'true'\n\t\t\t}\n\t\t\t// A number is GridStack's \"size to content, but never taller than N rows\" (a maximum).\n\t\t\tif (Number.isFinite(v) && v > 0) {\n\t\t\t\treturn String(v * this.rowSplit)\n\t\t\t}\n\t\t\treturn undefined\n\t\t},\n\n\t\t/**\n\t\t * A layout row coordinate or height in GridStack rows (`rowSplit`).\n\t\t *\n\t\t * @spec openspec/changes/dashboard-fit-cells-close-up/specs/dashboard-grid/spec.md#requirement-a-size-to-content-cell-ends-at-its-content\n\t\t * @param {number|undefined} value The value in layout rows.\n\t\t * @return {number|undefined} The value in grid rows, or undefined when none is set.\n\t\t */\n\t\tscaledRow(value) {\n\t\t\treturn Number.isFinite(value) ? value * this.rowSplit : undefined\n\t\t},\n\n\t\t/**\n\t\t * The smallest height an item may take, in grid rows: `minHeight`\n\t\t * layout rows, except for a size-to-content cell on split rows, which\n\t\t * may shrink to its content down to one grid row. The minimum is for\n\t\t * resizing by hand, and that only happens on whole rows in edit mode.\n\t\t *\n\t\t * @spec openspec/changes/dashboard-fit-cells-close-up/specs/dashboard-grid/spec.md#requirement-a-size-to-content-cell-ends-at-its-content\n\t\t * @param {object} item The layout item.\n\t\t * @return {number} The minimum height in grid rows.\n\t\t */\n\t\tminRows(item) {\n\t\t\tif (this.rowSplit !== 1 && this.sizeToContentAttr(item) !== undefined) {\n\t\t\t\treturn 1\n\t\t\t}\n\t\t\treturn this.minHeight * this.rowSplit\n\t\t},\n\n\t\t/**\n\t\t * Build the grid again with the row split `wantsRowSplit` asks for, as\n\t\t * edit mode turns on or off: the items go back to their layout places in\n\t\t * the new rows, and the size-to-content cells are measured again.\n\t\t *\n\t\t * @spec openspec/changes/dashboard-fit-cells-close-up/specs/dashboard-grid/spec.md#requirement-a-size-to-content-cell-ends-at-its-content\n\t\t * @return {void}\n\t\t */\n\t\trebuildGrid() {\n\t\t\tthis.disconnectContentObserver()\n\t\t\tthis.grid.destroy(false)\n\t\t\tthis.grid = null\n\t\t\tthis.rowSplit = this.wantsRowSplit ? ROW_SPLIT : 1\n\t\t\tthis.$nextTick(() => {\n\t\t\t\tif (!this.$refs.gridContainer) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\t// GridStack wrote its own places onto the items; put the layout's back.\n\t\t\t\tfor (const item of this.layout) {\n\t\t\t\t\tconst el = this.$refs.gridContainer.querySelector(`[gs-id=\"${item.id}\"]`)\n\t\t\t\t\tif (!el) {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tfor (const [name, value] of [['gs-y', this.scaledRow(item.gridY)], ['gs-h', this.scaledRow(item.gridHeight)], ['gs-min-h', this.minRows(item)], ['gs-size-to-content', this.sizeToContentAttr(item)]]) {\n\t\t\t\t\t\tif (value === undefined) {\n\t\t\t\t\t\t\tel.removeAttribute(name)\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tel.setAttribute(name, String(value))\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tthis.initGrid()\n\t\t\t\tthis.observeContentSizes()\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Build the accessible name for a grid item. A custom `itemLabel` wins\n\t\t * outright; otherwise the first human-readable field on the item is used\n\t\t * and, while the keyboard lane is live, the current grid coordinates are\n\t\t * appended — without them a screen-reader user tabbing through an edit-mode\n\t\t * dashboard has no way to tell where anything sits.\n\t\t *\n\t\t * @param {object} item the layout item.\n\t\t * @param {number} index the item's index in `layout`, used for the\n\t\t * last-resort positional name.\n\t\t * @return {string} the accessible name.\n\t\t */\n\t\tresolveItemLabel(item, index) {\n\t\t\tif (this.itemLabel) {\n\t\t\t\treturn String(this.itemLabel(item, index) ?? '')\n\t\t\t}\n\n\t\t\tconst base = item.title\n\t\t\t\t|| item.name\n\t\t\t\t|| item.label\n\t\t\t\t|| item.widgetTitle\n\t\t\t\t|| item.widgetId\n\t\t\t\t|| t('nextcloud-vue', 'Widget {number}', { number: index + 1 })\n\n\t\t\tif (!this.keyboardActive) {\n\t\t\t\treturn String(base)\n\t\t\t}\n\n\t\t\t// Report the rectangle exactly as the reactive `layout` prop states\n\t\t\t// it — unclamped, so the spoken coordinates always match the\n\t\t\t// rendered `gs-*` attributes — and read from the prop (not the\n\t\t\t// GridStack node) so the name re-renders when a move round-trips\n\t\t\t// back through `layout-change`; engine-node mutations are invisible\n\t\t\t// to Vue's reactivity.\n\t\t\tconst rect = {\n\t\t\t\tx: Number.isFinite(item.gridX) ? item.gridX : 0,\n\t\t\t\ty: Number.isFinite(item.gridY) ? item.gridY : 0,\n\t\t\t\tw: Number.isFinite(item.gridWidth) ? item.gridWidth : this.minWidth,\n\t\t\t\th: Number.isFinite(item.gridHeight) ? item.gridHeight : this.minHeight,\n\t\t\t}\n\t\t\treturn t(\n\t\t\t\t'nextcloud-vue',\n\t\t\t\t'{label}, column {column} of {columns}, row {row}, {width} columns wide, {height} rows tall',\n\t\t\t\t{\n\t\t\t\t\tlabel: String(base),\n\t\t\t\t\tcolumn: rect.x + 1,\n\t\t\t\t\tcolumns: this.effectiveColumns,\n\t\t\t\t\trow: rect.y + 1,\n\t\t\t\t\twidth: rect.w,\n\t\t\t\t\theight: rect.h,\n\t\t\t\t},\n\t\t\t)\n\t\t},\n\n\t\t/**\n\t\t * Build the GridStack grid on the rendered items, install the growth\n\t\t * wrapper (`keepOrderOnGrowth`) and listen for its changes.\n\t\t *\n\t\t * @spec openspec/changes/dashboard-growth-keeps-order/specs/dashboard-grid/spec.md#requirement-a-growing-cell-keeps-the-order-of-the-cards-under-it\n\t\t * @return {void}\n\t\t */\n\t\tinitGrid() {\n\t\t\tif (this.cellHeightCssVar && typeof document !== 'undefined' && document.documentElement) {\n\t\t\t\tdocument.documentElement.style.setProperty(this.cellHeightCssVar, `${this.cellHeight}px`)\n\t\t\t}\n\t\t\tconst el = this.$refs.gridContainer.querySelector('.grid-stack')\n\t\t\tthis.grid = GridStack.init({\n\t\t\t\tcolumn: this.columns,\n\t\t\t\tcellHeight: this.cellHeight / this.rowSplit,\n\t\t\t\tmargin: this.margin,\n\t\t\t\tfloat: this.float,\n\t\t\t\tanimate: true,\n\t\t\t\tdisableDrag: !this.editable,\n\t\t\t\tdisableResize: !this.editable,\n\t\t\t\tremovable: false,\n\t\t\t\t...(this.columnOpts ? { columnOpts: this.columnOpts } : {}),\n\t\t\t}, el)\n\t\t\tthis.keepOrderOnGrowth()\n\n\t\t\tthis.grid.on('change', (_event, items) => {\n\t\t\t\tthis.handleGridChange(items)\n\t\t\t})\n\n\t\t\t// A grid that mounts already narrow is reflowed before anyone touches\n\t\t\t// it, so the handles have to be judged here and not only on a later\n\t\t\t// change.\n\t\t\tthis.syncInteractivity()\n\t\t},\n\n\t\t/**\n\t\t * Let a size-to-content cell that grows push the cards under it down in\n\t\t * their order. GridStack resolves that collision itself, but not in\n\t\t * order: when pipelinq's banner grew from two rows to three, the\n\t\t * full-width card under the stat tiles landed below the lists, also\n\t\t * with `float: false`. Every grow after the grid is built, whether\n\t\t * GridStack measures it on resize or this grid's content observer asks\n\t\t * for it, now first moves the cards the growth reaches down by the\n\t\t * growth (`planGrowth`), bottom-most first, and only then grows the\n\t\t * cell, so GridStack has no collision left to resolve. The wrapper is\n\t\t * installed after `GridStack.init` has already measured the cells on\n\t\t * load; the order on load comes from adding the items in row order\n\t\t * (`itemsInRowOrder`). A cell that shrinks is left to GridStack as\n\t\t * before.\n\t\t *\n\t\t * @spec openspec/changes/dashboard-growth-keeps-order/specs/dashboard-grid/spec.md#requirement-a-growing-cell-keeps-the-order-of-the-cards-under-it\n\t\t * @return {void}\n\t\t */\n\t\tkeepOrderOnGrowth() {\n\t\t\tconst grid = this.grid\n\t\t\tif (!grid || typeof grid.resizeToContent !== 'function' || typeof grid.update !== 'function') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst resize = grid.resizeToContent.bind(grid)\n\t\t\tgrid.resizeToContent = (el) => {\n\t\t\t\tconst node = el && el.gridstackNode\n\t\t\t\tconst nodes = grid.engine && Array.isArray(grid.engine.nodes) ? grid.engine.nodes : null\n\t\t\t\tconst rows = node && nodes ? this.contentRows(el, node) : null\n\t\t\t\tconst plan = rows ? planGrowth(nodes, node, rows) : []\n\t\t\t\tif (plan.length === 0) {\n\t\t\t\t\tresize(el)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\t// Inside a batch the moves are not packed until the cell has grown.\n\t\t\t\tconst batching = Boolean(grid.engine.batchMode)\n\t\t\t\tif (!batching) {\n\t\t\t\t\tgrid.batchUpdate()\n\t\t\t\t}\n\t\t\t\tfor (const step of plan) {\n\t\t\t\t\tgrid.update(step.node.el, { y: step.y })\n\t\t\t\t}\n\t\t\t\tresize(el)\n\t\t\t\tif (!batching) {\n\t\t\t\t\tgrid.batchUpdate(false)\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The rows a size-to-content cell needs for its content, measured the\n\t\t * way GridStack's `resizeToContent` measures it (the content's first\n\t\t * child, plus the cell's own padding), within its size-to-content row\n\t\t * maximum and its minimum and maximum height.\n\t\t *\n\t\t * @spec openspec/changes/dashboard-growth-keeps-order/specs/dashboard-grid/spec.md#requirement-a-growing-cell-keeps-the-order-of-the-cards-under-it\n\t\t * @param {HTMLElement} el The cell.\n\t\t * @param {object} node Its GridStack node.\n\t\t * @return {number|null} The rows, or null when it cannot be measured.\n\t\t */\n\t\tcontentRows(el, node) {\n\t\t\tconst cell = this.grid.getCellHeight(true)\n\t\t\tconst item = el.querySelector('.grid-stack-item-content')\n\t\t\tconst child = item && item.firstElementChild\n\t\t\tif (!el.clientHeight || !cell || !child || !Number.isFinite(node.h)) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst padding = el.clientHeight - item.clientHeight\n\t\t\tconst itemHeight = node.h * cell - padding\n\t\t\tconst wanted = child.getBoundingClientRect().height || itemHeight\n\t\t\tlet rows = Math.ceil((node.h * cell + wanted - itemHeight) / cell)\n\t\t\tif (Number.isInteger(node.sizeToContent) && rows > node.sizeToContent) {\n\t\t\t\trows = node.sizeToContent\n\t\t\t}\n\t\t\tif (node.minH && rows < node.minH) {\n\t\t\t\trows = node.minH\n\t\t\t} else if (node.maxH && rows > node.maxH) {\n\t\t\t\trows = node.maxH\n\t\t\t}\n\t\t\treturn rows\n\t\t},\n\n\t\t/**\n\t\t * Keep both editing lanes in step with whether an edit could actually be\n\t\t * kept: the pointer's handles, and — through `reflowed` feeding\n\t\t * `keyboardActive` — the tab stops, the help text and the arrow keys.\n\t\t *\n\t\t * A reflowed grid drops every `change` it receives, for the reason\n\t\t * `handleGridChange` gives. Leaving either lane live there offers an edit\n\t\t * that silently snaps back, which is the same class of surprise as\n\t\t * persisting the reflow — pointed the other way, and harder to explain\n\t\t * because the author did everything right. It is worse on the keyboard,\n\t\t * where the move is also announced as done. The affordance has to say what\n\t\t * the grid will do at this width, not what edit mode does at the authored\n\t\t * one.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tsyncInteractivity() {\n\t\t\t// Ahead of the engine guard: the keyboard lane reads this whether or\n\t\t\t// not the engine can be enabled.\n\t\t\tthis.reflowed = this.isReflowed()\n\t\t\tif (!this.grid || typeof this.grid.enable !== 'function') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.editable && !this.reflowed) {\n\t\t\t\tthis.grid.enable()\n\t\t\t} else {\n\t\t\t\tthis.grid.disable()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Whether the grid is currently rescaled to a column count other than the\n\t\t * authored one, i.e. a `columnOpts` breakpoint is in force.\n\t\t *\n\t\t * The authored count is `columnOpts.columnMax` when there is one, not the\n\t\t * `columns` prop: GridStack widens to `columnMax` above the top breakpoint,\n\t\t * so a consumer whose two disagree is at its widest, not reflowed.\n\t\t *\n\t\t * @return {boolean} True while a responsive reflow is active.\n\t\t */\n\t\tisReflowed() {\n\t\t\tif (!this.grid || typeof this.grid.getColumn !== 'function') {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tconst authored = Number.isFinite(this.columnOpts?.columnMax) ? this.columnOpts.columnMax : this.columns\n\t\t\tconst live = this.grid.getColumn()\n\t\t\treturn Number.isFinite(live) && live > 0 && live !== authored\n\t\t},\n\n\t\thandleGridChange(items) {\n\t\t\t// The rescale that crossed a breakpoint arrives here as a `change`, so\n\t\t\t// this is also where the handles are re-judged in both directions:\n\t\t\t// switched off as the grid narrows, and back on as it widens.\n\t\t\tthis.syncInteractivity()\n\n\t\t\tif (this.applyingLayout) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// GridStack fires `change` for its own responsive rescale too, and at a\n\t\t\t// 12 → 1 breakpoint every item reads `gridX: 0, gridWidth: 1`. Emitting\n\t\t\t// that has the host persist the reflow AS the authored layout, which\n\t\t\t// widening back does not undo. A move made while reflowed is the same\n\t\t\t// rescaled geometry, so it is dropped for the same reason.\n\t\t\tif (this.isReflowed()) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// A grid on split rows is a view-mode grid: what changes in it is\n\t\t\t// packing and size-to-content measuring, never a user's\n\t\t\t// arrangement, and it is not reported as one.\n\t\t\tif (this.rowSplit !== 1) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!items || items.length === 0) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst updated = this.layout.map((item) => {\n\t\t\t\tconst gridItem = items.find((gi) => String(gi.id) === String(item.id))\n\t\t\t\tif (gridItem) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\t...item,\n\t\t\t\t\t\tgridX: gridItem.x,\n\t\t\t\t\t\tgridY: gridItem.y,\n\t\t\t\t\t\tgridWidth: gridItem.w,\n\t\t\t\t\t\tgridHeight: gridItem.h,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn item\n\t\t\t})\n\n\t\t\tthis.$emit('layout-change', updated)\n\t\t},\n\n\t\t/**\n\t\t * Keydown handler bound to every grid item.\n\t\t *\n\t\t * Only reacts when the grid item element itself is the event target:\n\t\t * a button, input or menu rendered inside the `#widget` slot must keep\n\t\t * its own arrow/Enter semantics, and a grid that swallowed them would\n\t\t * be a worse a11y regression than the one this handler fixes.\n\t\t *\n\t\t * @param {KeyboardEvent} event the keydown event.\n\t\t * @param {object} item the layout item the key was pressed on.\n\t\t * @return {void}\n\t\t */\n\t\tonItemKeydown(event, item) {\n\t\t\tif (!this.keyboardRepositioning) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (event.target !== event.currentTarget) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (event.altKey || event.ctrlKey || event.metaKey) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif (event.key === 'Enter' || event.key === ' ' || event.key === 'Spacebar') {\n\t\t\t\tevent.preventDefault()\n\t\t\t\tthis.activateItem(item, event.currentTarget)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Repositioning only, and only where it can be kept — an item that\n\t\t\t// still holds focus from before a reflow keeps its Enter, but its\n\t\t\t// arrow keys stop moving something that would snap back.\n\t\t\tif (!this.keyboardActive) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst handled = this.applyKey(event.key, event.shiftKey, item)\n\t\t\tif (handled) {\n\t\t\t\t// Arrows would otherwise scroll the dashboard out from under\n\t\t\t\t// the widget the user is moving.\n\t\t\t\tevent.preventDefault()\n\t\t\t\tevent.stopPropagation()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Translate one repositioning key into a geometry change.\n\t\t *\n\t\t * @param {string} key the `KeyboardEvent.key` value.\n\t\t * @param {boolean} shift whether Shift was held (resize instead of move).\n\t\t * @param {object} item the layout item being operated on.\n\t\t * @return {boolean} `true` when the key belongs to the grid and the\n\t\t * default action must be suppressed.\n\t\t */\n\t\tapplyKey(key, shift, item) {\n\t\t\tconst rect = this.currentRect(item)\n\n\t\t\tswitch (key) {\n\t\t\t\tcase 'ArrowLeft':\n\t\t\t\t\treturn shift\n\t\t\t\t\t\t? this.applyGeometry(item, { w: rect.w - 1 }, 'resize')\n\t\t\t\t\t\t: this.applyGeometry(item, { x: rect.x - 1 }, 'move')\n\t\t\t\tcase 'ArrowRight':\n\t\t\t\t\treturn shift\n\t\t\t\t\t\t? this.applyGeometry(item, { w: rect.w + 1 }, 'resize')\n\t\t\t\t\t\t: this.applyGeometry(item, { x: rect.x + 1 }, 'move')\n\t\t\t\tcase 'ArrowUp':\n\t\t\t\t\treturn shift\n\t\t\t\t\t\t? this.applyGeometry(item, { h: rect.h - 1 }, 'resize')\n\t\t\t\t\t\t: this.applyGeometry(item, { y: rect.y - 1 }, 'move')\n\t\t\t\tcase 'ArrowDown':\n\t\t\t\t\treturn shift\n\t\t\t\t\t\t? this.applyGeometry(item, { h: rect.h + 1 }, 'resize')\n\t\t\t\t\t\t: this.applyGeometry(item, { y: rect.y + 1 }, 'move')\n\t\t\t\tcase 'Home':\n\t\t\t\t\treturn this.applyGeometry(item, { x: 0 }, 'move')\n\t\t\t\tcase 'End':\n\t\t\t\t\treturn this.applyGeometry(item, { x: this.effectiveColumns - rect.w }, 'move')\n\t\t\t\tdefault:\n\t\t\t\t\treturn false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Read an item's live rectangle. The GridStack node is preferred over\n\t\t * the `layout` prop because a drag that has not yet round-tripped\n\t\t * through the consumer's persistence leaves the prop momentarily stale,\n\t\t * and a keyboard nudge must continue from where the widget actually is.\n\t\t *\n\t\t * @param {object} item the layout item.\n\t\t * @return {{x: number, y: number, w: number, h: number}} the rectangle\n\t\t * in grid units.\n\t\t */\n\t\tcurrentRect(item) {\n\t\t\tconst node = this.gridNode(item.id)\n\t\t\tif (node && Number.isFinite(node.x) && Number.isFinite(node.y)) {\n\t\t\t\treturn {\n\t\t\t\t\tx: node.x,\n\t\t\t\t\ty: node.y,\n\t\t\t\t\tw: Number.isFinite(node.w) ? node.w : this.minWidth,\n\t\t\t\t\th: Number.isFinite(node.h) ? node.h : this.minHeight,\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tx: Number.isFinite(item.gridX) ? item.gridX : 0,\n\t\t\t\ty: Number.isFinite(item.gridY) ? item.gridY : 0,\n\t\t\t\tw: Number.isFinite(item.gridWidth) ? item.gridWidth : this.minWidth,\n\t\t\t\th: Number.isFinite(item.gridHeight) ? item.gridHeight : this.minHeight,\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Clamp a candidate rectangle to the grid's bounds and the configured\n\t\t * minimum widget size, so a held-down arrow key parks the widget at the\n\t\t * edge instead of pushing it into negative coordinates.\n\t\t *\n\t\t * @param {{x: number, y: number, w: number, h: number}} rect the candidate.\n\t\t * @return {{x: number, y: number, w: number, h: number}} the clamped rectangle.\n\t\t */\n\t\tclampRect(rect) {\n\t\t\tconst cols = this.effectiveColumns\n\t\t\tconst w = Math.min(Math.max(rect.w, this.minWidth), cols)\n\t\t\tconst h = Math.max(rect.h, this.minHeight)\n\t\t\treturn {\n\t\t\t\tx: Math.min(Math.max(rect.x, 0), Math.max(cols - w, 0)),\n\t\t\t\ty: Math.max(rect.y, 0),\n\t\t\t\tw,\n\t\t\t\th,\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Apply a partial geometry change to an item.\n\t\t *\n\t\t * The change is pushed through `GridStack.update()` — the very call the\n\t\t * drag/resize handlers end in — so collision resolution, the `change`\n\t\t * event and therefore the `layout-change` emission and the consumer's\n\t\t * persistence are identical for keyboard and pointer. There is\n\t\t * deliberately no second, direct-to-`layout` update path.\n\t\t *\n\t\t * @param {object} item the layout item to move or resize.\n\t\t * @param {{x?: number, y?: number, w?: number, h?: number}} patch the\n\t\t * requested change, merged over the item's current rectangle.\n\t\t * @param {'move'|'resize'} kind which announcement to speak.\n\t\t * @return {boolean} `true` when the key was consumed — including when\n\t\t * the widget was already at the edge, because the arrow still belongs\n\t\t * to the grid and must not scroll the page.\n\t\t */\n\t\tapplyGeometry(item, patch, kind) {\n\t\t\tconst el = this.itemElement(item.id)\n\t\t\tif (!el || !this.grid || typeof this.grid.update !== 'function') {\n\t\t\t\treturn false\n\t\t\t}\n\n\t\t\tconst current = this.currentRect(item)\n\t\t\tconst next = this.clampRect({ ...current, ...patch })\n\n\t\t\tif (next.x === current.x && next.y === current.y\n\t\t\t\t&& next.w === current.w && next.h === current.h) {\n\t\t\t\tthis.announce(t('nextcloud-vue', '{label} cannot move further in that direction', {\n\t\t\t\t\tlabel: this.announcementLabel(item),\n\t\t\t\t}))\n\t\t\t\treturn true\n\t\t\t}\n\n\t\t\tthis.grid.update(el, next)\n\n\t\t\tthis.announce(kind === 'resize'\n\t\t\t\t? t('nextcloud-vue', '{label} resized to {width} columns by {height} rows', {\n\t\t\t\t\t\tlabel: this.announcementLabel(item),\n\t\t\t\t\t\twidth: next.w,\n\t\t\t\t\t\theight: next.h,\n\t\t\t\t\t})\n\t\t\t\t: t('nextcloud-vue', '{label} moved to column {column}, row {row}', {\n\t\t\t\t\t\tlabel: this.announcementLabel(item),\n\t\t\t\t\t\tcolumn: next.x + 1,\n\t\t\t\t\t\trow: next.y + 1,\n\t\t\t\t\t}))\n\n\t\t\treturn true\n\t\t},\n\n\t\t/**\n\t\t * The short, coordinate-free name used inside live-region sentences —\n\t\t * the full `aria-label` already carries the coordinates and repeating\n\t\t * them would make every announcement unlistenable.\n\t\t *\n\t\t * @param {object} item the layout item.\n\t\t * @return {string} the short name.\n\t\t */\n\t\tannouncementLabel(item) {\n\t\t\treturn String(item.title\n\t\t\t\t|| item.name\n\t\t\t\t|| item.label\n\t\t\t\t|| item.widgetTitle\n\t\t\t\t|| item.widgetId\n\t\t\t\t|| t('nextcloud-vue', 'Widget'))\n\t\t},\n\n\t\t/**\n\t\t * Push text into the polite live region. The region is cleared first so\n\t\t * two identical consecutive announcements (e.g. two failed nudges at the\n\t\t * grid edge) are both spoken — assistive tech ignores a re-render that\n\t\t * leaves the text unchanged.\n\t\t *\n\t\t * @param {string} text the message to announce.\n\t\t * @return {void}\n\t\t */\n\t\tannounce(text) {\n\t\t\tthis.announcement = ''\n\t\t\tthis.$nextTick(() => {\n\t\t\t\tthis.announcement = text\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Handle `Enter` / `Space` on a focused grid item: tell the consumer via\n\t\t * `item-activate`, and — unless opted out — synthesise the `contextmenu`\n\t\t * event a right-click would have produced, anchored to the item, so\n\t\t * pointer-only widget menus become keyboard-reachable without the\n\t\t * consumer wiring anything new. The synthetic event is dispatched from\n\t\t * inside the item's content so it bubbles through the slot's own root\n\t\t * element (where such handlers live) and on up.\n\t\t *\n\t\t * @param {object} item the layout item being activated.\n\t\t * @param {HTMLElement} el the grid item element.\n\t\t * @return {void}\n\t\t */\n\t\tactivateItem(item, el) {\n\t\t\tconst rect = typeof el.getBoundingClientRect === 'function'\n\t\t\t\t? el.getBoundingClientRect()\n\t\t\t\t: { left: 0, top: 0 }\n\t\t\tconst clientX = Math.round((rect.left || 0) + MENU_ANCHOR_OFFSET)\n\t\t\tconst clientY = Math.round((rect.top || 0) + MENU_ANCHOR_OFFSET)\n\n\t\t\tthis.$emit('item-activate', { item, element: el, clientX, clientY })\n\n\t\t\tif (this.activateOpensContextMenu) {\n\t\t\t\tthis.dispatchContextMenu(el, clientX, clientY)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Dispatch a bubbling, cancelable `contextmenu` MouseEvent from inside a\n\t\t * grid item.\n\t\t *\n\t\t * @param {HTMLElement} el the grid item element.\n\t\t * @param {number} clientX viewport x for the menu anchor.\n\t\t * @param {number} clientY viewport y for the menu anchor.\n\t\t * @return {void}\n\t\t */\n\t\tdispatchContextMenu(el, clientX, clientY) {\n\t\t\tif (typeof MouseEvent !== 'function') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst content = el.querySelector('.grid-stack-item-content')\n\t\t\tconst target = (content && content.firstElementChild) || content || el\n\t\t\ttarget.dispatchEvent(new MouseEvent('contextmenu', {\n\t\t\t\tbubbles: true,\n\t\t\t\tcancelable: true,\n\t\t\t\tclientX,\n\t\t\t\tclientY,\n\t\t\t\tbutton: 2,\n\t\t\t}))\n\t\t},\n\n\t\t/**\n\t\t * Look up a grid item's DOM element by its layout id.\n\t\t *\n\t\t * @param {string|number} id the layout item id.\n\t\t * @return {HTMLElement|null} the element, or `null` when not rendered.\n\t\t */\n\t\titemElement(id) {\n\t\t\tconst container = this.$refs.gridContainer\n\t\t\tif (!container) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn container.querySelector(`[gs-id=\"${id}\"]`)\n\t\t},\n\n\t\t/**\n\t\t * Look up the GridStack engine node backing a layout item.\n\t\t *\n\t\t * @param {string|number} id the layout item id.\n\t\t * @return {object|null} the node, or `null` when GridStack isn't tracking it.\n\t\t */\n\t\t/**\n\t\t * Re-measure a `sizeToContent` item when its content changes height.\n\t\t * GridStack watches only the grid element, so a widget that loaded its data\n\t\t * in `mounted()` keeps the height its empty state had.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tobserveContentSizes() {\n\t\t\tthis.disconnectContentObserver()\n\t\t\tif (typeof ResizeObserver === 'undefined' || !this.$refs.gridContainer) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tthis.contentObserver = new ResizeObserver((entries) => {\n\t\t\t\tconst cells = new Set()\n\t\t\t\tfor (const entry of entries) {\n\t\t\t\t\tconst cell = entry.target.closest('.grid-stack-item')\n\t\t\t\t\tif (cell) {\n\t\t\t\t\t\tcells.add(cell)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (cells.size === 0) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\t// A frame later, not inline: `resizeToContent` resizes the cell the\n\t\t\t\t// observed child sits in, and a child that stretches to its parent\n\t\t\t\t// grows with it. Resizing from inside the callback makes that the\n\t\t\t\t// classic ResizeObserver loop (\"undelivered notifications\"); deferred,\n\t\t\t\t// it is an ordinary second pass that settles.\n\t\t\t\trequestAnimationFrame(() => {\n\t\t\t\t\tif (!this.contentObserver || !this.grid || typeof this.grid.resizeToContent !== 'function') {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tfor (const cell of cells) {\n\t\t\t\t\t\tthis.grid.resizeToContent(cell)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t})\n\n\t\t\tconst cells = this.$refs.gridContainer.querySelectorAll('.grid-stack-item[gs-size-to-content]')\n\t\t\tfor (const cell of cells) {\n\t\t\t\tconst measured = cell.querySelector('.grid-stack-item-content')?.firstElementChild\n\t\t\t\tif (measured) {\n\t\t\t\t\tthis.contentObserver.observe(measured)\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Drop the content observer, on unmount or before rebuilding it.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tdisconnectContentObserver() {\n\t\t\tif (this.contentObserver) {\n\t\t\t\tthis.contentObserver.disconnect()\n\t\t\t\tthis.contentObserver = null\n\t\t\t}\n\t\t},\n\n\t\tgridNode(id) {\n\t\t\tconst nodes = this.grid && this.grid.engine && this.grid.engine.nodes\n\t\t\tif (!Array.isArray(nodes)) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn nodes.find((n) => String(n.id) === String(id)) || null\n\t\t},\n\n\t\tsyncGridItems(newLayout) {\n\t\t\t// Add new items, and re-adopt items whose backing DOM element was\n\t\t\t// replaced. When a consumer's `itemKey` changes for an unchanged\n\t\t\t// `id` (e.g. a style edit — the documented use of `itemKey`), Vue\n\t\t\t// swaps the element. GridStack keeps tracking the old, now-detached\n\t\t\t// element, leaving the new one unmanaged (no positioning/sizing) —\n\t\t\t// so re-register it against the new element.\n\t\t\tfor (const item of newLayout) {\n\t\t\t\tthis.$nextTick(() => {\n\t\t\t\t\tconst el = this.$refs.gridContainer.querySelector(`[gs-id=\"${item.id}\"]`)\n\t\t\t\t\tif (!el) {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tconst node = this.grid.engine.nodes.find((n) => String(n.id) === String(item.id))\n\t\t\t\t\tif (!node) {\n\t\t\t\t\t\tthis.grid.makeWidget(el)\n\t\t\t\t\t} else if (node.el === el) {\n\t\t\t\t\t\t// The layout MOVED an item the grid already tracks: the\n\t\t\t\t\t\t// page re-compacts its display layout when a widget's\n\t\t\t\t\t\t// condition hides it, and the item below was given a new\n\t\t\t\t\t\t// gridY that never reached GridStack, so a band stayed\n\t\t\t\t\t\t// where the hidden widget had been. A drag reports the\n\t\t\t\t\t\t// grid's own position back, which matches and does\n\t\t\t\t\t\t// nothing here.\n\t\t\t\t\t\tconst x = item.gridX ?? node.x\n\t\t\t\t\t\tconst y = Number.isFinite(item.gridY) ? item.gridY * this.rowSplit : node.y\n\t\t\t\t\t\tif (x !== node.x || y !== node.y) {\n\t\t\t\t\t\t\t// The layout said where it goes; GridStack's `change`\n\t\t\t\t\t\t\t// for it is not a user's arrangement and is not\n\t\t\t\t\t\t\t// reported back (it would write the display-only\n\t\t\t\t\t\t\t// compaction into the authored layout).\n\t\t\t\t\t\t\tthis.applyingLayout = true\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tthis.grid.update(el, { x, y })\n\t\t\t\t\t\t\t} finally {\n\t\t\t\t\t\t\t\tthis.applyingLayout = false\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Drop the stale node without touching its detached DOM,\n\t\t\t\t\t\t// then adopt the new element so GridStack sizes/places it.\n\t\t\t\t\t\tthis.grid.removeWidget(node.el, false, false)\n\t\t\t\t\t\tthis.grid.makeWidget(el)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\n\t\t\t// Remove items no longer in layout\n\t\t\tconst ids = newLayout.map((i) => String(i.id))\n\t\t\tconst toRemove = this.grid.engine.nodes.filter((n) => !ids.includes(String(n.id)))\n\t\t\tfor (const node of toRemove) {\n\t\t\t\tconst el = this.$refs.gridContainer.querySelector(`[gs-id=\"${node.id}\"]`)\n\t\t\t\tif (el) {\n\t\t\t\t\tthis.grid.removeWidget(el, false)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Elements were added or replaced, so the observer's are stale.\n\t\t\tthis.$nextTick(() => this.observeContentSizes())\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-dashboard-grid {\n\twidth: 100%;\n\tmin-height: 200px;\n}\n\n.grid-stack {\n\tbackground: transparent;\n}\n\n/* Screen-reader-only text: the keyboard key map and the live region. Kept in\n the layout (not `display: none`) so assistive tech still reads it. */\n.cn-dashboard-grid__sr-only {\n\tposition: absolute;\n\twidth: 1px;\n\theight: 1px;\n\tpadding: 0;\n\tmargin: -1px;\n\toverflow: hidden;\n\tclip-path: inset(50%);\n\twhite-space: nowrap;\n\tborder: 0;\n}\n\n/* Grid items become tab stops in edit mode — give them a visible focus ring\n that satisfies WCAG 2.1 SC 2.4.7 without relying on the widget's own\n styling. `outline-offset` keeps the ring outside the rounded card. */\n.grid-stack-item:focus-visible {\n\toutline: 2px solid var(--color-primary-element, #0082c9);\n\toutline-offset: 2px;\n\tborder-radius: var(--border-radius-container-large, 16px);\n}\n\n/* A size-to-content cell (`sizeToContent`) is measured by GridStack from the\n content's first child, and a widget wrapper there filled the cell\n (`height: 100%`), so the measure returned the cell's own height and the\n cell never shrank: \"Waiting for me\" stayed a tall card around one row. In\n such a cell the child takes its own height. A cell without the key keeps\n the filling child. */\n.cn-dashboard-grid__item--fit > :deep(.grid-stack-item-content > *) {\n\theight: auto;\n}\n\n:deep(.grid-stack-item-content) {\n\tbackground: var(--color-main-background);\n\tborder: none;\n\tbox-shadow: none;\n\toverflow: hidden;\n}\n\n:deep(.grid-stack-item-content:has(.cn-widget-wrapper--borderless)) {\n\tbackground: transparent;\n}\n\n:deep(.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":["t"],"mappings":"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"}
|
|
@@ -25,12 +25,12 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
|
|
|
25
25
|
}, toDisplayString($options.keyboardHelpText), 9 /* TEXT, PROPS */, _hoisted_2))
|
|
26
26
|
: createCommentVNode("v-if", true),
|
|
27
27
|
createElementVNode("div", _hoisted_3, [
|
|
28
|
-
(openBlock(true), createElementBlock(Fragment, null, renderList($
|
|
28
|
+
(openBlock(true), createElementBlock(Fragment, null, renderList($options.itemsInRowOrder, (item) => {
|
|
29
29
|
return (openBlock(), createElementBlock("div", {
|
|
30
30
|
key: $options.resolveItemKey(item),
|
|
31
31
|
class: normalizeClass(["grid-stack-item", { 'cn-dashboard-grid__item--fit': $options.sizeToContentAttr(item) !== undefined }]),
|
|
32
32
|
role: "group",
|
|
33
|
-
"aria-label": $options.resolveItemLabel(item,
|
|
33
|
+
"aria-label": $options.resolveItemLabel(item, $props.layout.indexOf(item)),
|
|
34
34
|
"aria-describedby": $options.itemDescribedBy,
|
|
35
35
|
tabindex: $options.itemTabindex,
|
|
36
36
|
"gs-id": item.id,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CnDashboardGrid.vue3.js","sources":["../../../../src/components/CnDashboardGrid/CnDashboardGrid.vue?vue&type=template&id=ca2ae49c&lang.js"],"sourcesContent":["<!--\n CnDashboardGrid — GridStack-powered drag-and-drop dashboard grid.\n\n Renders widgets in a configurable grid layout. Supports drag, resize,\n and dynamic item addition/removal. Emits layout changes for persistence.\n\n Accessibility: every grid item is an ARIA `group` with an accessible name\n and — in edit mode — a tab stop, so the pointer-only GridStack drag/resize\n gestures have a full keyboard equivalent (WCAG 2.1 SC 2.1.1). Keyboard\n moves are routed through `GridStack.update()`, i.e. the exact same engine\n call drag/resize ends in, so collision handling, persistence and the\n `layout-change` emission are byte-identical between the two input modes.\n-->\n<template>\n\t<div ref=\"gridContainer\" class=\"cn-dashboard-grid\">\n\t\t<!--\n\t\t\tKeyboard instructions, referenced by every grid item through\n\t\t\t`aria-describedby`. Rendered once (not per item) so a screen\n\t\t\treader user hears the key map on the first item they land on\n\t\t\twithout it being duplicated N times in the accessibility tree.\n\t\t-->\n\t\t<p\n\t\t\tv-if=\"keyboardActive\"\n\t\t\t:id=\"keyboardHelpDomId\"\n\t\t\tclass=\"cn-dashboard-grid__sr-only\">\n\t\t\t{{ keyboardHelpText }}\n\t\t</p>\n\n\t\t<div class=\"grid-stack\">\n\t\t\t<div\n\t\t\t\tv-for=\"(item, index) in layout\"\n\t\t\t\t:key=\"resolveItemKey(item)\"\n\t\t\t\tclass=\"grid-stack-item\"\n\t\t\t\t:class=\"{ 'cn-dashboard-grid__item--fit': sizeToContentAttr(item) !== undefined }\"\n\t\t\t\trole=\"group\"\n\t\t\t\t:aria-label=\"resolveItemLabel(item, index)\"\n\t\t\t\t:aria-describedby=\"itemDescribedBy\"\n\t\t\t\t:tabindex=\"itemTabindex\"\n\t\t\t\t:gs-id=\"item.id\"\n\t\t\t\t:gs-x=\"item.gridX\"\n\t\t\t\t:gs-y=\"scaledRow(item.gridY)\"\n\t\t\t\t:gs-w=\"item.gridWidth\"\n\t\t\t\t:gs-h=\"scaledRow(item.gridHeight)\"\n\t\t\t\t:gs-min-w=\"minWidth\"\n\t\t\t\t:gs-min-h=\"minRows(item)\"\n\t\t\t\t:gs-size-to-content=\"sizeToContentAttr(item)\"\n\t\t\t\t@keydown=\"onItemKeydown($event, item)\">\n\t\t\t\t<div class=\"grid-stack-item-content\">\n\t\t\t\t\t<slot name=\"widget\" :item=\"item\">\n\t\t\t\t\t\t<!-- Default: render nothing; CnDashboardPage provides content -->\n\t\t\t\t\t</slot>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!--\n\t\t\tPolite live region for keyboard move/resize feedback. A sighted\n\t\t\tuser sees the widget slide; a screen-reader user needs the new\n\t\t\tcoordinates spoken, otherwise the arrow keys are silent.\n\t\t-->\n\t\t<p\n\t\t\tclass=\"cn-dashboard-grid__sr-only\"\n\t\t\trole=\"status\"\n\t\t\taria-live=\"polite\"\n\t\t\taria-atomic=\"true\">\n\t\t\t{{ announcement }}\n\t\t</p>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { GridStack } from 'gridstack'\n// `gridstack` is a peerDependency (see package.json), not bundled — this\n// component's JS and the consumer's GridStack stylesheet MUST come from the\n// exact same installed copy, because GridStack's JS writes layout state\n// (e.g. the `--gs-column-width` CSS custom property) that only its OWN\n// matching CSS reads. Baking a copy of `gridstack/dist/gridstack.min.css`\n// into this library's build (as a previous version did) freezes it at\n// nc-vue's build-time version, which can silently drift from whatever the\n// consumer's peer-installed copy actually is — the exact mismatch that once\n// made every grid item render at 0px width. So nc-vue ships NO GridStack\n// CSS of its own: the consumer MUST install `gridstack` themselves and\n// import `gridstack/dist/gridstack.min.css` (or their preferred GridStack\n// stylesheet) from that same copy.\n\n/**\n * Monotonic counter backing the per-instance DOM ids (the shared\n * `aria-describedby` help text). Module-level so two grids on one page\n * never collide — the same pattern CnCommandPalette uses.\n */\nlet domIdCounter = 0\n\n/**\n * Pixel offset applied to the grid item's top-left corner when synthesising\n * the `contextmenu` coordinates for a keyboard activation. Nudging the anchor\n * inside the item keeps consumer menus (which position at `clientX/clientY`)\n * visually attached to the widget instead of straddling its border.\n */\nconst MENU_ANCHOR_OFFSET = 8\n\n/**\n * How many GridStack rows a layout row is drawn as on a grid with\n * size-to-content cells (`wantsRowSplit`): 80px rows become 10px rows.\n */\nconst ROW_SPLIT = 8\n\n/**\n * Whether a layout holds a size-to-content cell (`sizeToContent` true or a\n * positive row maximum).\n *\n * @param {Array<object>} layout The layout items.\n * @return {boolean} True when one does.\n */\nfunction hasFitCell(layout) {\n\treturn Array.isArray(layout) && layout.some((item) => item\n\t\t&& (item.sizeToContent === true || (Number.isFinite(item.sizeToContent) && item.sizeToContent > 0)))\n}\n\n/**\n * CnDashboardGrid — Low-level grid layout engine powered by GridStack.\n *\n * Manages the drag-and-drop grid, syncs positions, and emits layout\n * changes. Does NOT handle widget rendering — that's done by the parent\n * via the `#widget` scoped slot.\n *\n * ```vue\n * <CnDashboardGrid\n * :layout=\"placements\"\n * :editable=\"isEditing\"\n * @layout-change=\"onLayoutChange\">\n * <template #widget=\"{ item }\">\n * <MyWidget :config=\"item\" />\n * </template>\n * </CnDashboardGrid>\n * ```\n *\n * ### Keyboard operation\n *\n * With `keyboardRepositioning` on (the default) and `editable` true, each\n * grid item is a tab stop. With an item focused:\n *\n * | Key | Action |\n * | --- | ------ |\n * | `ArrowLeft` / `ArrowRight` | move one column left / right |\n * | `ArrowUp` / `ArrowDown` | move one row up / down |\n * | `Shift` + `ArrowLeft` / `ArrowRight` | shrink / grow width by one column |\n * | `Shift` + `ArrowUp` / `ArrowDown` | shrink / grow height by one row |\n * | `Home` / `End` | jump to the first / last column of the current row |\n * | `Enter` / `Space` | activate the item — see `item-activate` |\n *\n * Keys are only honoured when the grid item itself holds focus, so\n * interactive content rendered inside the `#widget` slot keeps its own\n * key handling untouched.\n */\nexport default {\n\tname: 'CnDashboardGrid',\n\n\tprops: {\n\t\t/** Array of layout items: { id, gridX, gridY, gridWidth, gridHeight, ...extra } */\n\t\tlayout: {\n\t\t\ttype: Array,\n\t\t\trequired: true,\n\t\t},\n\n\t\t/** Whether drag and resize are enabled */\n\t\teditable: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Number of grid columns */\n\t\tcolumns: {\n\t\t\ttype: Number,\n\t\t\tdefault: 12,\n\t\t},\n\n\t\t/** Cell height in pixels */\n\t\tcellHeight: {\n\t\t\ttype: Number,\n\t\t\tdefault: 80,\n\t\t},\n\n\t\t/** Grid margin in pixels */\n\t\tmargin: {\n\t\t\ttype: Number,\n\t\t\tdefault: 12,\n\t\t},\n\n\t\t/** Minimum widget width in grid units */\n\t\tminWidth: {\n\t\t\ttype: Number,\n\t\t\tdefault: 2,\n\t\t},\n\n\t\t/** Minimum widget height in grid units */\n\t\tminHeight: {\n\t\t\ttype: Number,\n\t\t\tdefault: 2,\n\t\t},\n\n\t\t/**\n\t\t * GridStack v12 responsive `columnOpts` bag (breakpoints + reflow\n\t\t * layout). When set, the grid reflows column count across screen sizes.\n\t\t * Build it with `getDashboardColumnOpts()`. Default `null` = fixed\n\t\t * `columns`, no responsive reflow (backwards-compatible).\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tcolumnOpts: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * When set, `cellHeight` is mirrored into this CSS custom property on\n\t\t * the document root at init (e.g. `'--app-cell-height'`), so app CSS can\n\t\t * align to the grid geometry. Default `null` = no CSS var written.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tcellHeightCssVar: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Optional `(item) => string|number` to derive each item's render key.\n\t\t * Use it to force a re-render when an item changes in a way its `id`\n\t\t * doesn't capture (e.g. style edits — return `${item.id}:${item.updatedAt}`).\n\t\t * Default `null` = key on `item.id`.\n\t\t *\n\t\t * @type {((item: object) => (string|number))|null}\n\t\t */\n\t\titemKey: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Whether grid items are keyboard-operable: focusable in edit mode and\n\t\t * repositionable/resizable with the arrow keys (WCAG 2.1 SC 2.1.1 —\n\t\t * the keyboard equivalent of the pointer-only GridStack drag). Turning\n\t\t * it off leaves the items non-focusable and drag-only. Default `true`.\n\t\t */\n\t\tkeyboardRepositioning: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Optional `(item, index) => string` returning the accessible name for\n\t\t * a grid item. Use it when the layout items don't carry a human-readable\n\t\t * field (the built-in fallback walks `title` → `name` → `label` →\n\t\t * `widgetTitle` → `widgetId`, then a positional \"Widget N\"). The returned\n\t\t * string is used verbatim — no coordinates are appended.\n\t\t *\n\t\t * @type {((item: object, index: number) => string)|null}\n\t\t */\n\t\titemLabel: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Whether a keyboard activation (`Enter` / `Space` on a focused grid\n\t\t * item) also dispatches a bubbling `contextmenu` event from inside the\n\t\t * item, so consumers that open a per-widget menu on right-click get the\n\t\t * keyboard path for free — mirroring what browsers do for the `Menu`\n\t\t * key. Set `false` to rely purely on the `item-activate` event.\n\t\t * Default `true`.\n\t\t */\n\t\tactivateOpensContextMenu: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * GridStack's `float`. True (the default) keeps every item on the row\n\t\t * it was placed on, with empty rows above it if need be. False packs\n\t\t * items upward, so a size-to-content cell that shrinks, or a widget\n\t\t * that is not shown, closes its gap instead of leaving a band.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-dashboard-closes-the-gap-a-shrinking-or-hidden-widget-leaves\n\t\t */\n\t\tfloat: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\t},\n\n\temits: [\n\t\t/**\n\t\t * Fired whenever the grid geometry changed — by drag, by resize, or by\n\t\t * a keyboard nudge. Payload is the full `layout` array with the moved\n\t\t * items' `gridX`/`gridY`/`gridWidth`/`gridHeight` updated.\n\t\t */\n\t\t'layout-change',\n\t\t/**\n\t\t * Fired when the user activates a focused grid item with `Enter` or\n\t\t * `Space`. Payload `{ item, element, clientX, clientY }` — the layout\n\t\t * item, its DOM element, and viewport coordinates anchored to the\n\t\t * item's top-left corner so a consumer menu can position itself the\n\t\t * same way it does for a right-click.\n\t\t */\n\t\t'item-activate',\n\t],\n\n\tdata() {\n\t\treturn {\n\t\t\tgrid: null,\n\t\t\tcontentObserver: null,\n\t\t\t/** True while the grid moves an item to where the layout put it. */\n\t\t\tapplyingLayout: false,\n\t\t\tdomId: `cn-dashboard-grid-${++domIdCounter}`,\n\t\t\t/** Text currently held in the polite live region. */\n\t\t\tannouncement: '',\n\t\t\t/**\n\t\t\t * Whether a `columnOpts` breakpoint is currently in force. Mirrored\n\t\t\t * from the engine by `syncInteractivity` because `isReflowed()` reads\n\t\t\t * `grid.getColumn()` imperatively, and a computed cannot re-evaluate\n\t\t\t * on that.\n\t\t\t */\n\t\t\treflowed: false,\n\t\t\t/**\n\t\t\t * How many GridStack rows one layout row is drawn as: 1, or\n\t\t\t * ROW_SPLIT while `wantsRowSplit` holds. Set before the first render,\n\t\t\t * since GridStack reads the items' places from their attributes,\n\t\t\t * and again when the grid is rebuilt.\n\t\t\t */\n\t\t\trowSplit: (!this.editable && !this.float && hasFitCell(this.layout)) ? ROW_SPLIT : 1,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Whether the grid draws each layout row as ROW_SPLIT finer rows. A\n\t\t * size-to-content cell can only be a whole number of rows tall, so on\n\t\t * 80px rows it kept up to a row of empty space under its content and\n\t\t * a packing grid (`float: false`) could not close it (pipelinq and\n\t\t * dossiq Mijn werk, decidiq Mijn acties). Split into 10px rows the\n\t\t * cell ends within 10px of its content and the widgets below move up.\n\t\t *\n\t\t * Only in view mode, only on a packing grid with a size-to-content\n\t\t * cell: every other grid is built exactly as before. Item positions\n\t\t * and sizes scale with the rows, so every widget keeps its pixel\n\t\t * place and size until a cell shrinks; the layout itself, what edit\n\t\t * mode shows and what `layout-change` reports, stays in layout rows.\n\t\t *\n\t\t * @spec openspec/changes/dashboard-fit-cells-close-up/specs/dashboard-grid/spec.md#requirement-a-size-to-content-cell-ends-at-its-content\n\t\t * @return {boolean} True to split the rows.\n\t\t */\n\t\twantsRowSplit() {\n\t\t\treturn !this.editable && !this.float && hasFitCell(this.layout)\n\t\t},\n\n\t\t/**\n\t\t * @return {boolean} Whether the keyboard lane is live — items are\n\t\t * focusable and the arrow keys reposition. Requires the opt-in prop,\n\t\t * edit mode (a read-only dashboard has nothing to move, so adding N tab\n\t\t * stops there would be pure noise), and a grid that is not reflowed,\n\t\t * which is where a move cannot be kept and so must not be offered.\n\t\t */\n\t\tkeyboardActive() {\n\t\t\treturn this.keyboardRepositioning && this.editable && !this.reflowed\n\t\t},\n\n\t\t/**\n\t\t * @return {number|null} `0` when grid items should be tab stops,\n\t\t * otherwise `null` so the attribute is omitted entirely.\n\t\t */\n\t\titemTabindex() {\n\t\t\treturn this.keyboardActive ? 0 : null\n\t\t},\n\n\t\t/**\n\t\t * @return {string} DOM id of the shared keyboard-instructions element.\n\t\t */\n\t\tkeyboardHelpDomId() {\n\t\t\treturn `${this.domId}-keyboard-help`\n\t\t},\n\n\t\t/**\n\t\t * @return {string|null} `aria-describedby` target for each grid item —\n\t\t * the instructions element while the keyboard lane is live, `null`\n\t\t * otherwise (a dangling id reference is an axe violation).\n\t\t */\n\t\titemDescribedBy() {\n\t\t\treturn this.keyboardActive ? this.keyboardHelpDomId : null\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The spoken key map for the keyboard lane.\n\t\t */\n\t\tkeyboardHelpText() {\n\t\t\treturn t(\n\t\t\t\t'nextcloud-vue',\n\t\t\t\t'Use the arrow keys to move this widget one cell. Hold Shift and use the arrow keys to resize it. Press Home or End to jump to the first or last column. Press Enter to open the widget menu.',\n\t\t\t)\n\t\t},\n\n\t\t/**\n\t\t * @return {number} The column count currently in force. With responsive\n\t\t * `columnOpts` GridStack reflows to fewer columns on small screens, so\n\t\t * clamping must read the live value rather than the `columns` prop.\n\t\t */\n\t\teffectiveColumns() {\n\t\t\tif (this.grid && typeof this.grid.getColumn === 'function') {\n\t\t\t\tconst live = this.grid.getColumn()\n\t\t\t\tif (Number.isFinite(live) && live > 0) {\n\t\t\t\t\treturn live\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn this.columns\n\t\t},\n\t},\n\n\twatch: {\n\t\teditable() {\n\t\t\tthis.syncInteractivity()\n\t\t},\n\n\t\twantsRowSplit(wants) {\n\t\t\tif (this.grid && (wants ? ROW_SPLIT : 1) !== this.rowSplit) {\n\t\t\t\tthis.rebuildGrid()\n\t\t\t}\n\t\t},\n\n\t\tlayout: {\n\t\t\tdeep: true,\n\t\t\thandler(newLayout) {\n\t\t\t\tif (this.grid) {\n\t\t\t\t\tthis.syncGridItems(newLayout)\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.initGrid()\n\t\tthis.$nextTick(() => this.observeContentSizes())\n\t},\n\n\tbeforeUnmount() {\n\t\tthis.disconnectContentObserver()\n\t\tif (this.grid) {\n\t\t\tthis.grid.destroy(false)\n\t\t}\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Derive the v-for render key for a layout item, honouring the optional\n\t\t * `itemKey` prop and falling back to `item.id`.\n\t\t *\n\t\t * @param {object} item the layout item.\n\t\t * @return {string|number} the render key.\n\t\t */\n\t\tresolveItemKey(item) {\n\t\t\treturn this.itemKey ? this.itemKey(item) : item.id\n\t\t},\n\n\t\t/**\n\t\t * The `gs-size-to-content` attribute for a layout item, or `undefined` to\n\t\t * leave the item on its authored `gridHeight`.\n\t\t *\n\t\t * A widget that decides AT RUNTIME it has nothing to show — a banner whose\n\t\t * condition is false, a list that came back empty — still costs its whole\n\t\t * authored row, because the row is reserved from the layout before the\n\t\t * component renders and the engine positions items absolutely, so no CSS\n\t\t * can reclaim it. `sizeToContent` hands the row's height to the content, so\n\t\t * an empty widget collapses and the grid closes up behind it.\n\t\t *\n\t\t * Opt-in per item: it makes `gridHeight` advisory for that widget, which is\n\t\t * wrong for anything with a deliberate size (a map, a chart) and right for\n\t\t * anything whose height is whatever it turned out to have.\n\t\t *\n\t\t * @param {object} item the layout item.\n\t\t * @return {string|undefined} `'true'`, a row maximum as a string, or undefined.\n\t\t */\n\t\tsizeToContentAttr(item) {\n\t\t\tconst v = item && item.sizeToContent\n\t\t\tif (v === true) {\n\t\t\t\treturn 'true'\n\t\t\t}\n\t\t\t// A number is GridStack's \"size to content, but never taller than N rows\" (a maximum).\n\t\t\tif (Number.isFinite(v) && v > 0) {\n\t\t\t\treturn String(v * this.rowSplit)\n\t\t\t}\n\t\t\treturn undefined\n\t\t},\n\n\t\t/**\n\t\t * A layout row coordinate or height in GridStack rows (`rowSplit`).\n\t\t *\n\t\t * @spec openspec/changes/dashboard-fit-cells-close-up/specs/dashboard-grid/spec.md#requirement-a-size-to-content-cell-ends-at-its-content\n\t\t * @param {number|undefined} value The value in layout rows.\n\t\t * @return {number|undefined} The value in grid rows, or undefined when none is set.\n\t\t */\n\t\tscaledRow(value) {\n\t\t\treturn Number.isFinite(value) ? value * this.rowSplit : undefined\n\t\t},\n\n\t\t/**\n\t\t * The smallest height an item may take, in grid rows: `minHeight`\n\t\t * layout rows, except for a size-to-content cell on split rows, which\n\t\t * may shrink to its content down to one grid row. The minimum is for\n\t\t * resizing by hand, and that only happens on whole rows in edit mode.\n\t\t *\n\t\t * @spec openspec/changes/dashboard-fit-cells-close-up/specs/dashboard-grid/spec.md#requirement-a-size-to-content-cell-ends-at-its-content\n\t\t * @param {object} item The layout item.\n\t\t * @return {number} The minimum height in grid rows.\n\t\t */\n\t\tminRows(item) {\n\t\t\tif (this.rowSplit !== 1 && this.sizeToContentAttr(item) !== undefined) {\n\t\t\t\treturn 1\n\t\t\t}\n\t\t\treturn this.minHeight * this.rowSplit\n\t\t},\n\n\t\t/**\n\t\t * Build the grid again with the row split `wantsRowSplit` asks for, as\n\t\t * edit mode turns on or off: the items go back to their layout places in\n\t\t * the new rows, and the size-to-content cells are measured again.\n\t\t *\n\t\t * @spec openspec/changes/dashboard-fit-cells-close-up/specs/dashboard-grid/spec.md#requirement-a-size-to-content-cell-ends-at-its-content\n\t\t * @return {void}\n\t\t */\n\t\trebuildGrid() {\n\t\t\tthis.disconnectContentObserver()\n\t\t\tthis.grid.destroy(false)\n\t\t\tthis.grid = null\n\t\t\tthis.rowSplit = this.wantsRowSplit ? ROW_SPLIT : 1\n\t\t\tthis.$nextTick(() => {\n\t\t\t\tif (!this.$refs.gridContainer) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\t// GridStack wrote its own places onto the items; put the layout's back.\n\t\t\t\tfor (const item of this.layout) {\n\t\t\t\t\tconst el = this.$refs.gridContainer.querySelector(`[gs-id=\"${item.id}\"]`)\n\t\t\t\t\tif (!el) {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tfor (const [name, value] of [['gs-y', this.scaledRow(item.gridY)], ['gs-h', this.scaledRow(item.gridHeight)], ['gs-min-h', this.minRows(item)], ['gs-size-to-content', this.sizeToContentAttr(item)]]) {\n\t\t\t\t\t\tif (value === undefined) {\n\t\t\t\t\t\t\tel.removeAttribute(name)\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tel.setAttribute(name, String(value))\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tthis.initGrid()\n\t\t\t\tthis.observeContentSizes()\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Build the accessible name for a grid item. A custom `itemLabel` wins\n\t\t * outright; otherwise the first human-readable field on the item is used\n\t\t * and, while the keyboard lane is live, the current grid coordinates are\n\t\t * appended — without them a screen-reader user tabbing through an edit-mode\n\t\t * dashboard has no way to tell where anything sits.\n\t\t *\n\t\t * @param {object} item the layout item.\n\t\t * @param {number} index the item's index in `layout`, used for the\n\t\t * last-resort positional name.\n\t\t * @return {string} the accessible name.\n\t\t */\n\t\tresolveItemLabel(item, index) {\n\t\t\tif (this.itemLabel) {\n\t\t\t\treturn String(this.itemLabel(item, index) ?? '')\n\t\t\t}\n\n\t\t\tconst base = item.title\n\t\t\t\t|| item.name\n\t\t\t\t|| item.label\n\t\t\t\t|| item.widgetTitle\n\t\t\t\t|| item.widgetId\n\t\t\t\t|| t('nextcloud-vue', 'Widget {number}', { number: index + 1 })\n\n\t\t\tif (!this.keyboardActive) {\n\t\t\t\treturn String(base)\n\t\t\t}\n\n\t\t\t// Report the rectangle exactly as the reactive `layout` prop states\n\t\t\t// it — unclamped, so the spoken coordinates always match the\n\t\t\t// rendered `gs-*` attributes — and read from the prop (not the\n\t\t\t// GridStack node) so the name re-renders when a move round-trips\n\t\t\t// back through `layout-change`; engine-node mutations are invisible\n\t\t\t// to Vue's reactivity.\n\t\t\tconst rect = {\n\t\t\t\tx: Number.isFinite(item.gridX) ? item.gridX : 0,\n\t\t\t\ty: Number.isFinite(item.gridY) ? item.gridY : 0,\n\t\t\t\tw: Number.isFinite(item.gridWidth) ? item.gridWidth : this.minWidth,\n\t\t\t\th: Number.isFinite(item.gridHeight) ? item.gridHeight : this.minHeight,\n\t\t\t}\n\t\t\treturn t(\n\t\t\t\t'nextcloud-vue',\n\t\t\t\t'{label}, column {column} of {columns}, row {row}, {width} columns wide, {height} rows tall',\n\t\t\t\t{\n\t\t\t\t\tlabel: String(base),\n\t\t\t\t\tcolumn: rect.x + 1,\n\t\t\t\t\tcolumns: this.effectiveColumns,\n\t\t\t\t\trow: rect.y + 1,\n\t\t\t\t\twidth: rect.w,\n\t\t\t\t\theight: rect.h,\n\t\t\t\t},\n\t\t\t)\n\t\t},\n\n\t\tinitGrid() {\n\t\t\tif (this.cellHeightCssVar && typeof document !== 'undefined' && document.documentElement) {\n\t\t\t\tdocument.documentElement.style.setProperty(this.cellHeightCssVar, `${this.cellHeight}px`)\n\t\t\t}\n\t\t\tconst el = this.$refs.gridContainer.querySelector('.grid-stack')\n\t\t\tthis.grid = GridStack.init({\n\t\t\t\tcolumn: this.columns,\n\t\t\t\tcellHeight: this.cellHeight / this.rowSplit,\n\t\t\t\tmargin: this.margin,\n\t\t\t\tfloat: this.float,\n\t\t\t\tanimate: true,\n\t\t\t\tdisableDrag: !this.editable,\n\t\t\t\tdisableResize: !this.editable,\n\t\t\t\tremovable: false,\n\t\t\t\t...(this.columnOpts ? { columnOpts: this.columnOpts } : {}),\n\t\t\t}, el)\n\n\t\t\tthis.grid.on('change', (_event, items) => {\n\t\t\t\tthis.handleGridChange(items)\n\t\t\t})\n\n\t\t\t// A grid that mounts already narrow is reflowed before anyone touches\n\t\t\t// it, so the handles have to be judged here and not only on a later\n\t\t\t// change.\n\t\t\tthis.syncInteractivity()\n\t\t},\n\n\t\t/**\n\t\t * Keep both editing lanes in step with whether an edit could actually be\n\t\t * kept: the pointer's handles, and — through `reflowed` feeding\n\t\t * `keyboardActive` — the tab stops, the help text and the arrow keys.\n\t\t *\n\t\t * A reflowed grid drops every `change` it receives, for the reason\n\t\t * `handleGridChange` gives. Leaving either lane live there offers an edit\n\t\t * that silently snaps back, which is the same class of surprise as\n\t\t * persisting the reflow — pointed the other way, and harder to explain\n\t\t * because the author did everything right. It is worse on the keyboard,\n\t\t * where the move is also announced as done. The affordance has to say what\n\t\t * the grid will do at this width, not what edit mode does at the authored\n\t\t * one.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tsyncInteractivity() {\n\t\t\t// Ahead of the engine guard: the keyboard lane reads this whether or\n\t\t\t// not the engine can be enabled.\n\t\t\tthis.reflowed = this.isReflowed()\n\t\t\tif (!this.grid || typeof this.grid.enable !== 'function') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.editable && !this.reflowed) {\n\t\t\t\tthis.grid.enable()\n\t\t\t} else {\n\t\t\t\tthis.grid.disable()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Whether the grid is currently rescaled to a column count other than the\n\t\t * authored one, i.e. a `columnOpts` breakpoint is in force.\n\t\t *\n\t\t * The authored count is `columnOpts.columnMax` when there is one, not the\n\t\t * `columns` prop: GridStack widens to `columnMax` above the top breakpoint,\n\t\t * so a consumer whose two disagree is at its widest, not reflowed.\n\t\t *\n\t\t * @return {boolean} True while a responsive reflow is active.\n\t\t */\n\t\tisReflowed() {\n\t\t\tif (!this.grid || typeof this.grid.getColumn !== 'function') {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tconst authored = Number.isFinite(this.columnOpts?.columnMax) ? this.columnOpts.columnMax : this.columns\n\t\t\tconst live = this.grid.getColumn()\n\t\t\treturn Number.isFinite(live) && live > 0 && live !== authored\n\t\t},\n\n\t\thandleGridChange(items) {\n\t\t\t// The rescale that crossed a breakpoint arrives here as a `change`, so\n\t\t\t// this is also where the handles are re-judged in both directions:\n\t\t\t// switched off as the grid narrows, and back on as it widens.\n\t\t\tthis.syncInteractivity()\n\n\t\t\tif (this.applyingLayout) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// GridStack fires `change` for its own responsive rescale too, and at a\n\t\t\t// 12 → 1 breakpoint every item reads `gridX: 0, gridWidth: 1`. Emitting\n\t\t\t// that has the host persist the reflow AS the authored layout, which\n\t\t\t// widening back does not undo. A move made while reflowed is the same\n\t\t\t// rescaled geometry, so it is dropped for the same reason.\n\t\t\tif (this.isReflowed()) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// A grid on split rows is a view-mode grid: what changes in it is\n\t\t\t// packing and size-to-content measuring, never a user's\n\t\t\t// arrangement, and it is not reported as one.\n\t\t\tif (this.rowSplit !== 1) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!items || items.length === 0) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst updated = this.layout.map((item) => {\n\t\t\t\tconst gridItem = items.find((gi) => String(gi.id) === String(item.id))\n\t\t\t\tif (gridItem) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\t...item,\n\t\t\t\t\t\tgridX: gridItem.x,\n\t\t\t\t\t\tgridY: gridItem.y,\n\t\t\t\t\t\tgridWidth: gridItem.w,\n\t\t\t\t\t\tgridHeight: gridItem.h,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn item\n\t\t\t})\n\n\t\t\tthis.$emit('layout-change', updated)\n\t\t},\n\n\t\t/**\n\t\t * Keydown handler bound to every grid item.\n\t\t *\n\t\t * Only reacts when the grid item element itself is the event target:\n\t\t * a button, input or menu rendered inside the `#widget` slot must keep\n\t\t * its own arrow/Enter semantics, and a grid that swallowed them would\n\t\t * be a worse a11y regression than the one this handler fixes.\n\t\t *\n\t\t * @param {KeyboardEvent} event the keydown event.\n\t\t * @param {object} item the layout item the key was pressed on.\n\t\t * @return {void}\n\t\t */\n\t\tonItemKeydown(event, item) {\n\t\t\tif (!this.keyboardRepositioning) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (event.target !== event.currentTarget) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (event.altKey || event.ctrlKey || event.metaKey) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif (event.key === 'Enter' || event.key === ' ' || event.key === 'Spacebar') {\n\t\t\t\tevent.preventDefault()\n\t\t\t\tthis.activateItem(item, event.currentTarget)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Repositioning only, and only where it can be kept — an item that\n\t\t\t// still holds focus from before a reflow keeps its Enter, but its\n\t\t\t// arrow keys stop moving something that would snap back.\n\t\t\tif (!this.keyboardActive) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst handled = this.applyKey(event.key, event.shiftKey, item)\n\t\t\tif (handled) {\n\t\t\t\t// Arrows would otherwise scroll the dashboard out from under\n\t\t\t\t// the widget the user is moving.\n\t\t\t\tevent.preventDefault()\n\t\t\t\tevent.stopPropagation()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Translate one repositioning key into a geometry change.\n\t\t *\n\t\t * @param {string} key the `KeyboardEvent.key` value.\n\t\t * @param {boolean} shift whether Shift was held (resize instead of move).\n\t\t * @param {object} item the layout item being operated on.\n\t\t * @return {boolean} `true` when the key belongs to the grid and the\n\t\t * default action must be suppressed.\n\t\t */\n\t\tapplyKey(key, shift, item) {\n\t\t\tconst rect = this.currentRect(item)\n\n\t\t\tswitch (key) {\n\t\t\t\tcase 'ArrowLeft':\n\t\t\t\t\treturn shift\n\t\t\t\t\t\t? this.applyGeometry(item, { w: rect.w - 1 }, 'resize')\n\t\t\t\t\t\t: this.applyGeometry(item, { x: rect.x - 1 }, 'move')\n\t\t\t\tcase 'ArrowRight':\n\t\t\t\t\treturn shift\n\t\t\t\t\t\t? this.applyGeometry(item, { w: rect.w + 1 }, 'resize')\n\t\t\t\t\t\t: this.applyGeometry(item, { x: rect.x + 1 }, 'move')\n\t\t\t\tcase 'ArrowUp':\n\t\t\t\t\treturn shift\n\t\t\t\t\t\t? this.applyGeometry(item, { h: rect.h - 1 }, 'resize')\n\t\t\t\t\t\t: this.applyGeometry(item, { y: rect.y - 1 }, 'move')\n\t\t\t\tcase 'ArrowDown':\n\t\t\t\t\treturn shift\n\t\t\t\t\t\t? this.applyGeometry(item, { h: rect.h + 1 }, 'resize')\n\t\t\t\t\t\t: this.applyGeometry(item, { y: rect.y + 1 }, 'move')\n\t\t\t\tcase 'Home':\n\t\t\t\t\treturn this.applyGeometry(item, { x: 0 }, 'move')\n\t\t\t\tcase 'End':\n\t\t\t\t\treturn this.applyGeometry(item, { x: this.effectiveColumns - rect.w }, 'move')\n\t\t\t\tdefault:\n\t\t\t\t\treturn false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Read an item's live rectangle. The GridStack node is preferred over\n\t\t * the `layout` prop because a drag that has not yet round-tripped\n\t\t * through the consumer's persistence leaves the prop momentarily stale,\n\t\t * and a keyboard nudge must continue from where the widget actually is.\n\t\t *\n\t\t * @param {object} item the layout item.\n\t\t * @return {{x: number, y: number, w: number, h: number}} the rectangle\n\t\t * in grid units.\n\t\t */\n\t\tcurrentRect(item) {\n\t\t\tconst node = this.gridNode(item.id)\n\t\t\tif (node && Number.isFinite(node.x) && Number.isFinite(node.y)) {\n\t\t\t\treturn {\n\t\t\t\t\tx: node.x,\n\t\t\t\t\ty: node.y,\n\t\t\t\t\tw: Number.isFinite(node.w) ? node.w : this.minWidth,\n\t\t\t\t\th: Number.isFinite(node.h) ? node.h : this.minHeight,\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tx: Number.isFinite(item.gridX) ? item.gridX : 0,\n\t\t\t\ty: Number.isFinite(item.gridY) ? item.gridY : 0,\n\t\t\t\tw: Number.isFinite(item.gridWidth) ? item.gridWidth : this.minWidth,\n\t\t\t\th: Number.isFinite(item.gridHeight) ? item.gridHeight : this.minHeight,\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Clamp a candidate rectangle to the grid's bounds and the configured\n\t\t * minimum widget size, so a held-down arrow key parks the widget at the\n\t\t * edge instead of pushing it into negative coordinates.\n\t\t *\n\t\t * @param {{x: number, y: number, w: number, h: number}} rect the candidate.\n\t\t * @return {{x: number, y: number, w: number, h: number}} the clamped rectangle.\n\t\t */\n\t\tclampRect(rect) {\n\t\t\tconst cols = this.effectiveColumns\n\t\t\tconst w = Math.min(Math.max(rect.w, this.minWidth), cols)\n\t\t\tconst h = Math.max(rect.h, this.minHeight)\n\t\t\treturn {\n\t\t\t\tx: Math.min(Math.max(rect.x, 0), Math.max(cols - w, 0)),\n\t\t\t\ty: Math.max(rect.y, 0),\n\t\t\t\tw,\n\t\t\t\th,\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Apply a partial geometry change to an item.\n\t\t *\n\t\t * The change is pushed through `GridStack.update()` — the very call the\n\t\t * drag/resize handlers end in — so collision resolution, the `change`\n\t\t * event and therefore the `layout-change` emission and the consumer's\n\t\t * persistence are identical for keyboard and pointer. There is\n\t\t * deliberately no second, direct-to-`layout` update path.\n\t\t *\n\t\t * @param {object} item the layout item to move or resize.\n\t\t * @param {{x?: number, y?: number, w?: number, h?: number}} patch the\n\t\t * requested change, merged over the item's current rectangle.\n\t\t * @param {'move'|'resize'} kind which announcement to speak.\n\t\t * @return {boolean} `true` when the key was consumed — including when\n\t\t * the widget was already at the edge, because the arrow still belongs\n\t\t * to the grid and must not scroll the page.\n\t\t */\n\t\tapplyGeometry(item, patch, kind) {\n\t\t\tconst el = this.itemElement(item.id)\n\t\t\tif (!el || !this.grid || typeof this.grid.update !== 'function') {\n\t\t\t\treturn false\n\t\t\t}\n\n\t\t\tconst current = this.currentRect(item)\n\t\t\tconst next = this.clampRect({ ...current, ...patch })\n\n\t\t\tif (next.x === current.x && next.y === current.y\n\t\t\t\t&& next.w === current.w && next.h === current.h) {\n\t\t\t\tthis.announce(t('nextcloud-vue', '{label} cannot move further in that direction', {\n\t\t\t\t\tlabel: this.announcementLabel(item),\n\t\t\t\t}))\n\t\t\t\treturn true\n\t\t\t}\n\n\t\t\tthis.grid.update(el, next)\n\n\t\t\tthis.announce(kind === 'resize'\n\t\t\t\t? t('nextcloud-vue', '{label} resized to {width} columns by {height} rows', {\n\t\t\t\t\t\tlabel: this.announcementLabel(item),\n\t\t\t\t\t\twidth: next.w,\n\t\t\t\t\t\theight: next.h,\n\t\t\t\t\t})\n\t\t\t\t: t('nextcloud-vue', '{label} moved to column {column}, row {row}', {\n\t\t\t\t\t\tlabel: this.announcementLabel(item),\n\t\t\t\t\t\tcolumn: next.x + 1,\n\t\t\t\t\t\trow: next.y + 1,\n\t\t\t\t\t}))\n\n\t\t\treturn true\n\t\t},\n\n\t\t/**\n\t\t * The short, coordinate-free name used inside live-region sentences —\n\t\t * the full `aria-label` already carries the coordinates and repeating\n\t\t * them would make every announcement unlistenable.\n\t\t *\n\t\t * @param {object} item the layout item.\n\t\t * @return {string} the short name.\n\t\t */\n\t\tannouncementLabel(item) {\n\t\t\treturn String(item.title\n\t\t\t\t|| item.name\n\t\t\t\t|| item.label\n\t\t\t\t|| item.widgetTitle\n\t\t\t\t|| item.widgetId\n\t\t\t\t|| t('nextcloud-vue', 'Widget'))\n\t\t},\n\n\t\t/**\n\t\t * Push text into the polite live region. The region is cleared first so\n\t\t * two identical consecutive announcements (e.g. two failed nudges at the\n\t\t * grid edge) are both spoken — assistive tech ignores a re-render that\n\t\t * leaves the text unchanged.\n\t\t *\n\t\t * @param {string} text the message to announce.\n\t\t * @return {void}\n\t\t */\n\t\tannounce(text) {\n\t\t\tthis.announcement = ''\n\t\t\tthis.$nextTick(() => {\n\t\t\t\tthis.announcement = text\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Handle `Enter` / `Space` on a focused grid item: tell the consumer via\n\t\t * `item-activate`, and — unless opted out — synthesise the `contextmenu`\n\t\t * event a right-click would have produced, anchored to the item, so\n\t\t * pointer-only widget menus become keyboard-reachable without the\n\t\t * consumer wiring anything new. The synthetic event is dispatched from\n\t\t * inside the item's content so it bubbles through the slot's own root\n\t\t * element (where such handlers live) and on up.\n\t\t *\n\t\t * @param {object} item the layout item being activated.\n\t\t * @param {HTMLElement} el the grid item element.\n\t\t * @return {void}\n\t\t */\n\t\tactivateItem(item, el) {\n\t\t\tconst rect = typeof el.getBoundingClientRect === 'function'\n\t\t\t\t? el.getBoundingClientRect()\n\t\t\t\t: { left: 0, top: 0 }\n\t\t\tconst clientX = Math.round((rect.left || 0) + MENU_ANCHOR_OFFSET)\n\t\t\tconst clientY = Math.round((rect.top || 0) + MENU_ANCHOR_OFFSET)\n\n\t\t\tthis.$emit('item-activate', { item, element: el, clientX, clientY })\n\n\t\t\tif (this.activateOpensContextMenu) {\n\t\t\t\tthis.dispatchContextMenu(el, clientX, clientY)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Dispatch a bubbling, cancelable `contextmenu` MouseEvent from inside a\n\t\t * grid item.\n\t\t *\n\t\t * @param {HTMLElement} el the grid item element.\n\t\t * @param {number} clientX viewport x for the menu anchor.\n\t\t * @param {number} clientY viewport y for the menu anchor.\n\t\t * @return {void}\n\t\t */\n\t\tdispatchContextMenu(el, clientX, clientY) {\n\t\t\tif (typeof MouseEvent !== 'function') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst content = el.querySelector('.grid-stack-item-content')\n\t\t\tconst target = (content && content.firstElementChild) || content || el\n\t\t\ttarget.dispatchEvent(new MouseEvent('contextmenu', {\n\t\t\t\tbubbles: true,\n\t\t\t\tcancelable: true,\n\t\t\t\tclientX,\n\t\t\t\tclientY,\n\t\t\t\tbutton: 2,\n\t\t\t}))\n\t\t},\n\n\t\t/**\n\t\t * Look up a grid item's DOM element by its layout id.\n\t\t *\n\t\t * @param {string|number} id the layout item id.\n\t\t * @return {HTMLElement|null} the element, or `null` when not rendered.\n\t\t */\n\t\titemElement(id) {\n\t\t\tconst container = this.$refs.gridContainer\n\t\t\tif (!container) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn container.querySelector(`[gs-id=\"${id}\"]`)\n\t\t},\n\n\t\t/**\n\t\t * Look up the GridStack engine node backing a layout item.\n\t\t *\n\t\t * @param {string|number} id the layout item id.\n\t\t * @return {object|null} the node, or `null` when GridStack isn't tracking it.\n\t\t */\n\t\t/**\n\t\t * Re-measure a `sizeToContent` item when its content changes height.\n\t\t * GridStack watches only the grid element, so a widget that loaded its data\n\t\t * in `mounted()` keeps the height its empty state had.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tobserveContentSizes() {\n\t\t\tthis.disconnectContentObserver()\n\t\t\tif (typeof ResizeObserver === 'undefined' || !this.$refs.gridContainer) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tthis.contentObserver = new ResizeObserver((entries) => {\n\t\t\t\tconst cells = new Set()\n\t\t\t\tfor (const entry of entries) {\n\t\t\t\t\tconst cell = entry.target.closest('.grid-stack-item')\n\t\t\t\t\tif (cell) {\n\t\t\t\t\t\tcells.add(cell)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (cells.size === 0) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\t// A frame later, not inline: `resizeToContent` resizes the cell the\n\t\t\t\t// observed child sits in, and a child that stretches to its parent\n\t\t\t\t// grows with it. Resizing from inside the callback makes that the\n\t\t\t\t// classic ResizeObserver loop (\"undelivered notifications\"); deferred,\n\t\t\t\t// it is an ordinary second pass that settles.\n\t\t\t\trequestAnimationFrame(() => {\n\t\t\t\t\tif (!this.contentObserver || !this.grid || typeof this.grid.resizeToContent !== 'function') {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tfor (const cell of cells) {\n\t\t\t\t\t\tthis.grid.resizeToContent(cell)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t})\n\n\t\t\tconst cells = this.$refs.gridContainer.querySelectorAll('.grid-stack-item[gs-size-to-content]')\n\t\t\tfor (const cell of cells) {\n\t\t\t\tconst measured = cell.querySelector('.grid-stack-item-content')?.firstElementChild\n\t\t\t\tif (measured) {\n\t\t\t\t\tthis.contentObserver.observe(measured)\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Drop the content observer, on unmount or before rebuilding it.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tdisconnectContentObserver() {\n\t\t\tif (this.contentObserver) {\n\t\t\t\tthis.contentObserver.disconnect()\n\t\t\t\tthis.contentObserver = null\n\t\t\t}\n\t\t},\n\n\t\tgridNode(id) {\n\t\t\tconst nodes = this.grid && this.grid.engine && this.grid.engine.nodes\n\t\t\tif (!Array.isArray(nodes)) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn nodes.find((n) => String(n.id) === String(id)) || null\n\t\t},\n\n\t\tsyncGridItems(newLayout) {\n\t\t\t// Add new items, and re-adopt items whose backing DOM element was\n\t\t\t// replaced. When a consumer's `itemKey` changes for an unchanged\n\t\t\t// `id` (e.g. a style edit — the documented use of `itemKey`), Vue\n\t\t\t// swaps the element. GridStack keeps tracking the old, now-detached\n\t\t\t// element, leaving the new one unmanaged (no positioning/sizing) —\n\t\t\t// so re-register it against the new element.\n\t\t\tfor (const item of newLayout) {\n\t\t\t\tthis.$nextTick(() => {\n\t\t\t\t\tconst el = this.$refs.gridContainer.querySelector(`[gs-id=\"${item.id}\"]`)\n\t\t\t\t\tif (!el) {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tconst node = this.grid.engine.nodes.find((n) => String(n.id) === String(item.id))\n\t\t\t\t\tif (!node) {\n\t\t\t\t\t\tthis.grid.makeWidget(el)\n\t\t\t\t\t} else if (node.el === el) {\n\t\t\t\t\t\t// The layout MOVED an item the grid already tracks: the\n\t\t\t\t\t\t// page re-compacts its display layout when a widget's\n\t\t\t\t\t\t// condition hides it, and the item below was given a new\n\t\t\t\t\t\t// gridY that never reached GridStack, so a band stayed\n\t\t\t\t\t\t// where the hidden widget had been. A drag reports the\n\t\t\t\t\t\t// grid's own position back, which matches and does\n\t\t\t\t\t\t// nothing here.\n\t\t\t\t\t\tconst x = item.gridX ?? node.x\n\t\t\t\t\t\tconst y = Number.isFinite(item.gridY) ? item.gridY * this.rowSplit : node.y\n\t\t\t\t\t\tif (x !== node.x || y !== node.y) {\n\t\t\t\t\t\t\t// The layout said where it goes; GridStack's `change`\n\t\t\t\t\t\t\t// for it is not a user's arrangement and is not\n\t\t\t\t\t\t\t// reported back (it would write the display-only\n\t\t\t\t\t\t\t// compaction into the authored layout).\n\t\t\t\t\t\t\tthis.applyingLayout = true\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tthis.grid.update(el, { x, y })\n\t\t\t\t\t\t\t} finally {\n\t\t\t\t\t\t\t\tthis.applyingLayout = false\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Drop the stale node without touching its detached DOM,\n\t\t\t\t\t\t// then adopt the new element so GridStack sizes/places it.\n\t\t\t\t\t\tthis.grid.removeWidget(node.el, false, false)\n\t\t\t\t\t\tthis.grid.makeWidget(el)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\n\t\t\t// Remove items no longer in layout\n\t\t\tconst ids = newLayout.map((i) => String(i.id))\n\t\t\tconst toRemove = this.grid.engine.nodes.filter((n) => !ids.includes(String(n.id)))\n\t\t\tfor (const node of toRemove) {\n\t\t\t\tconst el = this.$refs.gridContainer.querySelector(`[gs-id=\"${node.id}\"]`)\n\t\t\t\tif (el) {\n\t\t\t\t\tthis.grid.removeWidget(el, false)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Elements were added or replaced, so the observer's are stale.\n\t\t\tthis.$nextTick(() => this.observeContentSizes())\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-dashboard-grid {\n\twidth: 100%;\n\tmin-height: 200px;\n}\n\n.grid-stack {\n\tbackground: transparent;\n}\n\n/* Screen-reader-only text: the keyboard key map and the live region. Kept in\n the layout (not `display: none`) so assistive tech still reads it. */\n.cn-dashboard-grid__sr-only {\n\tposition: absolute;\n\twidth: 1px;\n\theight: 1px;\n\tpadding: 0;\n\tmargin: -1px;\n\toverflow: hidden;\n\tclip-path: inset(50%);\n\twhite-space: nowrap;\n\tborder: 0;\n}\n\n/* Grid items become tab stops in edit mode — give them a visible focus ring\n that satisfies WCAG 2.1 SC 2.4.7 without relying on the widget's own\n styling. `outline-offset` keeps the ring outside the rounded card. */\n.grid-stack-item:focus-visible {\n\toutline: 2px solid var(--color-primary-element, #0082c9);\n\toutline-offset: 2px;\n\tborder-radius: var(--border-radius-container-large, 16px);\n}\n\n/* A size-to-content cell (`sizeToContent`) is measured by GridStack from the\n content's first child, and a widget wrapper there filled the cell\n (`height: 100%`), so the measure returned the cell's own height and the\n cell never shrank: \"Waiting for me\" stayed a tall card around one row. In\n such a cell the child takes its own height. A cell without the key keeps\n the filling child. */\n.cn-dashboard-grid__item--fit > :deep(.grid-stack-item-content > *) {\n\theight: auto;\n}\n\n:deep(.grid-stack-item-content) {\n\tbackground: var(--color-main-background);\n\tborder: none;\n\tbox-shadow: none;\n\toverflow: hidden;\n}\n\n:deep(.grid-stack-item-content:has(.cn-widget-wrapper--borderless)) {\n\tbackground: transparent;\n}\n\n:deep(.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","_renderSlot"],"mappings":";;;;;;;AAyCO,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,YAAY,GAAA;;AAmBtB,MAAA,UAAA,GAAA,EAAA,KAAA,EAAA,yBAAA,GAAA;;;;;;;;;;KAzBM,QAAc,CAAA,cAAA;wCAIjB,CAAA,GAAA,EAAA;;AAHF,UAAA,EAAE,EAAE,QAAiB,CAAA,iBAAA;eACjB,EAAC,4BAAkB;2BACrB,QAAa,CAAA,gBAAA,CAAA,EAAA,CAAA,oBAAA,UAAA,CAAA;;AAGf,IAAAA,kBAAA,CAyBC,OAzBD,UAyBC,EAAA;AAxBD,OAAAC,SAAA,CAAA,IAAA,CAAA,EAAAC,kBAAA,CAuBKC,QAtBc,EAAA,IAAA,EAAAC,UAAA,CAAA,MAAA,CAAA,MAAA,EAAX,CAAA,IAAI,EAAE,KAAK,KAAA;6BADnBF,kBAuBK,CAAA,KAAA,EAAA;UArBJ,GAAG,EAAE,QAAc,CAAA,cAAA,CAAC,IAAG,CAAA;AACxB,UAAA,KAAK,kBAAC,iBAAiB,EAAA,EAAA,8BAAA,EACe,0BAAA,CAAA,IAAA,MAAA,SAAA,EAAA,CAAA,CAAA;AACtC,UAAA,IAAK,EAAA,OAAO;AACX,UAAA,YAAU,EAAE,QAAA,CAAA,gBAAgB,CAAC,IAAG,EAAA,KAAA,CAAA;4BAChB,EAAE,QAAe,CAAA,eAAA;AACjC,UAAA,QAAM,EAAA,QAAA,CAAA,YAAA;UACN,OAAI,EAAA,IAAA,CAAA,EAAA;UACJ,MAAG,EAAA,IAAA,CAAA,KAAA;AACH,UAAA,MAAE,EAAA,QAAA,CAAA,SAAA,CAAA,IAAA,CAAA,KAAA,CAAA;;AAEF,UAAA,MAAA,EAAA,QAAA,CAAA,SAAA,CAAA,IAAA,CAAA,UAAA,CAAA;AACA,UAAA,UAAQ,EAAE,MAAQ,CAAA,QAAA;UAClB,UAAQ,EAAE,QAAO,CAAA,OAAA,CAAE,IAAG,CAAA;UACtB,oBAAkB,EAAE,QAAkB,CAAA,iBAAA,CAAA,IAAG,CAAA;AAC1C,UAAA,SAAA,EAAA,MAAA,KAAA,QAAA,CAAA,aAAA,CAAA,MAAA,EAAA,IAAA,CAAA,CAAA;;AACD,UAAAF,kBAAA,CAIO,OAJP,UAIO,EAAA;YAHLK,UAEO,CAAA,IAAA,CAAA,MAAA,EAAA,QAAA,EAAA,EAFc,IAAI,EAAE,IAAI,EAAA,CAAA;;;;;;;;;;;"}
|
|
1
|
+
{"version":3,"file":"CnDashboardGrid.vue3.js","sources":["../../../../src/components/CnDashboardGrid/CnDashboardGrid.vue?vue&type=template&id=a91998fc&lang.js"],"sourcesContent":["<!--\n CnDashboardGrid — GridStack-powered drag-and-drop dashboard grid.\n\n Renders widgets in a configurable grid layout. Supports drag, resize,\n and dynamic item addition/removal. Emits layout changes for persistence.\n\n Accessibility: every grid item is an ARIA `group` with an accessible name\n and — in edit mode — a tab stop, so the pointer-only GridStack drag/resize\n gestures have a full keyboard equivalent (WCAG 2.1 SC 2.1.1). Keyboard\n moves are routed through `GridStack.update()`, i.e. the exact same engine\n call drag/resize ends in, so collision handling, persistence and the\n `layout-change` emission are byte-identical between the two input modes.\n-->\n<template>\n\t<div ref=\"gridContainer\" class=\"cn-dashboard-grid\">\n\t\t<!--\n\t\t\tKeyboard instructions, referenced by every grid item through\n\t\t\t`aria-describedby`. Rendered once (not per item) so a screen\n\t\t\treader user hears the key map on the first item they land on\n\t\t\twithout it being duplicated N times in the accessibility tree.\n\t\t-->\n\t\t<p\n\t\t\tv-if=\"keyboardActive\"\n\t\t\t:id=\"keyboardHelpDomId\"\n\t\t\tclass=\"cn-dashboard-grid__sr-only\">\n\t\t\t{{ keyboardHelpText }}\n\t\t</p>\n\n\t\t<div class=\"grid-stack\">\n\t\t\t<div\n\t\t\t\tv-for=\"item in itemsInRowOrder\"\n\t\t\t\t:key=\"resolveItemKey(item)\"\n\t\t\t\tclass=\"grid-stack-item\"\n\t\t\t\t:class=\"{ 'cn-dashboard-grid__item--fit': sizeToContentAttr(item) !== undefined }\"\n\t\t\t\trole=\"group\"\n\t\t\t\t:aria-label=\"resolveItemLabel(item, layout.indexOf(item))\"\n\t\t\t\t:aria-describedby=\"itemDescribedBy\"\n\t\t\t\t:tabindex=\"itemTabindex\"\n\t\t\t\t:gs-id=\"item.id\"\n\t\t\t\t:gs-x=\"item.gridX\"\n\t\t\t\t:gs-y=\"scaledRow(item.gridY)\"\n\t\t\t\t:gs-w=\"item.gridWidth\"\n\t\t\t\t:gs-h=\"scaledRow(item.gridHeight)\"\n\t\t\t\t:gs-min-w=\"minWidth\"\n\t\t\t\t:gs-min-h=\"minRows(item)\"\n\t\t\t\t:gs-size-to-content=\"sizeToContentAttr(item)\"\n\t\t\t\t@keydown=\"onItemKeydown($event, item)\">\n\t\t\t\t<div class=\"grid-stack-item-content\">\n\t\t\t\t\t<slot name=\"widget\" :item=\"item\">\n\t\t\t\t\t\t<!-- Default: render nothing; CnDashboardPage provides content -->\n\t\t\t\t\t</slot>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!--\n\t\t\tPolite live region for keyboard move/resize feedback. A sighted\n\t\t\tuser sees the widget slide; a screen-reader user needs the new\n\t\t\tcoordinates spoken, otherwise the arrow keys are silent.\n\t\t-->\n\t\t<p\n\t\t\tclass=\"cn-dashboard-grid__sr-only\"\n\t\t\trole=\"status\"\n\t\t\taria-live=\"polite\"\n\t\t\taria-atomic=\"true\">\n\t\t\t{{ announcement }}\n\t\t</p>\n\t</div>\n</template>\n\n<script>\nimport { translate as t } from '@nextcloud/l10n'\nimport { GridStack } from 'gridstack'\nimport { planGrowth } from '../../utils/gridStack.js'\n// `gridstack` is a peerDependency (see package.json), not bundled — this\n// component's JS and the consumer's GridStack stylesheet MUST come from the\n// exact same installed copy, because GridStack's JS writes layout state\n// (e.g. the `--gs-column-width` CSS custom property) that only its OWN\n// matching CSS reads. Baking a copy of `gridstack/dist/gridstack.min.css`\n// into this library's build (as a previous version did) freezes it at\n// nc-vue's build-time version, which can silently drift from whatever the\n// consumer's peer-installed copy actually is — the exact mismatch that once\n// made every grid item render at 0px width. So nc-vue ships NO GridStack\n// CSS of its own: the consumer MUST install `gridstack` themselves and\n// import `gridstack/dist/gridstack.min.css` (or their preferred GridStack\n// stylesheet) from that same copy.\n\n/**\n * Monotonic counter backing the per-instance DOM ids (the shared\n * `aria-describedby` help text). Module-level so two grids on one page\n * never collide — the same pattern CnCommandPalette uses.\n */\nlet domIdCounter = 0\n\n/**\n * Pixel offset applied to the grid item's top-left corner when synthesising\n * the `contextmenu` coordinates for a keyboard activation. Nudging the anchor\n * inside the item keeps consumer menus (which position at `clientX/clientY`)\n * visually attached to the widget instead of straddling its border.\n */\nconst MENU_ANCHOR_OFFSET = 8\n\n/**\n * How many GridStack rows a layout row is drawn as on a grid with\n * size-to-content cells (`wantsRowSplit`): 80px rows become 10px rows.\n */\nconst ROW_SPLIT = 8\n\n/**\n * Whether a layout holds a size-to-content cell (`sizeToContent` true or a\n * positive row maximum).\n *\n * @param {Array<object>} layout The layout items.\n * @return {boolean} True when one does.\n */\nfunction hasFitCell(layout) {\n\treturn Array.isArray(layout) && layout.some((item) => item\n\t\t&& (item.sizeToContent === true || (Number.isFinite(item.sizeToContent) && item.sizeToContent > 0)))\n}\n\n/**\n * CnDashboardGrid — Low-level grid layout engine powered by GridStack.\n *\n * Manages the drag-and-drop grid, syncs positions, and emits layout\n * changes. Does NOT handle widget rendering — that's done by the parent\n * via the `#widget` scoped slot.\n *\n * ```vue\n * <CnDashboardGrid\n * :layout=\"placements\"\n * :editable=\"isEditing\"\n * @layout-change=\"onLayoutChange\">\n * <template #widget=\"{ item }\">\n * <MyWidget :config=\"item\" />\n * </template>\n * </CnDashboardGrid>\n * ```\n *\n * ### Keyboard operation\n *\n * With `keyboardRepositioning` on (the default) and `editable` true, each\n * grid item is a tab stop. With an item focused:\n *\n * | Key | Action |\n * | --- | ------ |\n * | `ArrowLeft` / `ArrowRight` | move one column left / right |\n * | `ArrowUp` / `ArrowDown` | move one row up / down |\n * | `Shift` + `ArrowLeft` / `ArrowRight` | shrink / grow width by one column |\n * | `Shift` + `ArrowUp` / `ArrowDown` | shrink / grow height by one row |\n * | `Home` / `End` | jump to the first / last column of the current row |\n * | `Enter` / `Space` | activate the item — see `item-activate` |\n *\n * Keys are only honoured when the grid item itself holds focus, so\n * interactive content rendered inside the `#widget` slot keeps its own\n * key handling untouched.\n */\nexport default {\n\tname: 'CnDashboardGrid',\n\n\tprops: {\n\t\t/** Array of layout items: { id, gridX, gridY, gridWidth, gridHeight, ...extra } */\n\t\tlayout: {\n\t\t\ttype: Array,\n\t\t\trequired: true,\n\t\t},\n\n\t\t/** Whether drag and resize are enabled */\n\t\teditable: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Number of grid columns */\n\t\tcolumns: {\n\t\t\ttype: Number,\n\t\t\tdefault: 12,\n\t\t},\n\n\t\t/** Cell height in pixels */\n\t\tcellHeight: {\n\t\t\ttype: Number,\n\t\t\tdefault: 80,\n\t\t},\n\n\t\t/** Grid margin in pixels */\n\t\tmargin: {\n\t\t\ttype: Number,\n\t\t\tdefault: 12,\n\t\t},\n\n\t\t/** Minimum widget width in grid units */\n\t\tminWidth: {\n\t\t\ttype: Number,\n\t\t\tdefault: 2,\n\t\t},\n\n\t\t/** Minimum widget height in grid units */\n\t\tminHeight: {\n\t\t\ttype: Number,\n\t\t\tdefault: 2,\n\t\t},\n\n\t\t/**\n\t\t * GridStack v12 responsive `columnOpts` bag (breakpoints + reflow\n\t\t * layout). When set, the grid reflows column count across screen sizes.\n\t\t * Build it with `getDashboardColumnOpts()`. Default `null` = fixed\n\t\t * `columns`, no responsive reflow (backwards-compatible).\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tcolumnOpts: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * When set, `cellHeight` is mirrored into this CSS custom property on\n\t\t * the document root at init (e.g. `'--app-cell-height'`), so app CSS can\n\t\t * align to the grid geometry. Default `null` = no CSS var written.\n\t\t *\n\t\t * @type {string|null}\n\t\t */\n\t\tcellHeightCssVar: {\n\t\t\ttype: String,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Optional `(item) => string|number` to derive each item's render key.\n\t\t * Use it to force a re-render when an item changes in a way its `id`\n\t\t * doesn't capture (e.g. style edits — return `${item.id}:${item.updatedAt}`).\n\t\t * Default `null` = key on `item.id`.\n\t\t *\n\t\t * @type {((item: object) => (string|number))|null}\n\t\t */\n\t\titemKey: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Whether grid items are keyboard-operable: focusable in edit mode and\n\t\t * repositionable/resizable with the arrow keys (WCAG 2.1 SC 2.1.1 —\n\t\t * the keyboard equivalent of the pointer-only GridStack drag). Turning\n\t\t * it off leaves the items non-focusable and drag-only. Default `true`.\n\t\t */\n\t\tkeyboardRepositioning: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Optional `(item, index) => string` returning the accessible name for\n\t\t * a grid item. Use it when the layout items don't carry a human-readable\n\t\t * field (the built-in fallback walks `title` → `name` → `label` →\n\t\t * `widgetTitle` → `widgetId`, then a positional \"Widget N\"). The returned\n\t\t * string is used verbatim — no coordinates are appended.\n\t\t *\n\t\t * @type {((item: object, index: number) => string)|null}\n\t\t */\n\t\titemLabel: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Whether a keyboard activation (`Enter` / `Space` on a focused grid\n\t\t * item) also dispatches a bubbling `contextmenu` event from inside the\n\t\t * item, so consumers that open a per-widget menu on right-click get the\n\t\t * keyboard path for free — mirroring what browsers do for the `Menu`\n\t\t * key. Set `false` to rely purely on the `item-activate` event.\n\t\t * Default `true`.\n\t\t */\n\t\tactivateOpensContextMenu: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * GridStack's `float`. True (the default) keeps every item on the row\n\t\t * it was placed on, with empty rows above it if need be. False packs\n\t\t * items upward, so a size-to-content cell that shrinks, or a widget\n\t\t * that is not shown, closes its gap instead of leaving a band.\n\t\t *\n\t\t * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-dashboard-closes-the-gap-a-shrinking-or-hidden-widget-leaves\n\t\t */\n\t\tfloat: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\t},\n\n\temits: [\n\t\t/**\n\t\t * Fired whenever the grid geometry changed — by drag, by resize, or by\n\t\t * a keyboard nudge. Payload is the full `layout` array with the moved\n\t\t * items' `gridX`/`gridY`/`gridWidth`/`gridHeight` updated.\n\t\t */\n\t\t'layout-change',\n\t\t/**\n\t\t * Fired when the user activates a focused grid item with `Enter` or\n\t\t * `Space`. Payload `{ item, element, clientX, clientY }` — the layout\n\t\t * item, its DOM element, and viewport coordinates anchored to the\n\t\t * item's top-left corner so a consumer menu can position itself the\n\t\t * same way it does for a right-click.\n\t\t */\n\t\t'item-activate',\n\t],\n\n\tdata() {\n\t\treturn {\n\t\t\tgrid: null,\n\t\t\tcontentObserver: null,\n\t\t\t/** True while the grid moves an item to where the layout put it. */\n\t\t\tapplyingLayout: false,\n\t\t\tdomId: `cn-dashboard-grid-${++domIdCounter}`,\n\t\t\t/** Text currently held in the polite live region. */\n\t\t\tannouncement: '',\n\t\t\t/**\n\t\t\t * Whether a `columnOpts` breakpoint is currently in force. Mirrored\n\t\t\t * from the engine by `syncInteractivity` because `isReflowed()` reads\n\t\t\t * `grid.getColumn()` imperatively, and a computed cannot re-evaluate\n\t\t\t * on that.\n\t\t\t */\n\t\t\treflowed: false,\n\t\t\t/**\n\t\t\t * How many GridStack rows one layout row is drawn as: 1, or\n\t\t\t * ROW_SPLIT while `wantsRowSplit` holds. Set before the first render,\n\t\t\t * since GridStack reads the items' places from their attributes,\n\t\t\t * and again when the grid is rebuilt.\n\t\t\t */\n\t\t\trowSplit: (!this.editable && !this.float && hasFitCell(this.layout)) ? ROW_SPLIT : 1,\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The layout's items top to bottom, then left to right: the order the\n\t\t * items are in the page, so GridStack places them in that order.\n\t\t *\n\t\t * GridStack sizes a size-to-content cell while it adds the items, one\n\t\t * at a time, and an item it adds onto a cell that has already grown\n\t\t * moves itself below whatever it then collides with. In the layout's\n\t\t * own order a card declared after the lists (pipelinq's full-width\n\t\t * \"client in focus\" card) was added last, so when the banner above\n\t\t * it grew it skipped past the stat tiles AND the lists and landed at\n\t\t * the bottom. Added top to bottom, every card lands under the one\n\t\t * above it. It is also the order a screen reader and the Tab key\n\t\t * follow. An item without a row or column sorts after the items\n\t\t * that have one, in the layout's order.\n\t\t *\n\t\t * @spec openspec/changes/dashboard-growth-keeps-order/specs/dashboard-grid/spec.md#requirement-a-growing-cell-keeps-the-order-of-the-cards-under-it\n\t\t * @return {Array<object>} The items in row order.\n\t\t */\n\t\titemsInRowOrder() {\n\t\t\tconst items = Array.isArray(this.layout) ? [...this.layout] : []\n\t\t\tconst at = (value) => (Number.isFinite(value) ? value : Number.MAX_SAFE_INTEGER)\n\t\t\treturn items.sort((a, b) => (at(a.gridY) - at(b.gridY)) || (at(a.gridX) - at(b.gridX)))\n\t\t},\n\n\t\t/**\n\t\t * Whether the grid draws each layout row as ROW_SPLIT finer rows. A\n\t\t * size-to-content cell can only be a whole number of rows tall, so on\n\t\t * 80px rows it kept up to a row of empty space under its content and\n\t\t * a packing grid (`float: false`) could not close it (pipelinq and\n\t\t * dossiq Mijn werk, decidiq Mijn acties). Split into 10px rows the\n\t\t * cell ends within 10px of its content and the widgets below move up.\n\t\t *\n\t\t * Only in view mode, only on a packing grid with a size-to-content\n\t\t * cell: every other grid is built exactly as before. Item positions\n\t\t * and sizes scale with the rows, so every widget keeps its pixel\n\t\t * place and size until a cell shrinks; the layout itself, what edit\n\t\t * mode shows and what `layout-change` reports, stays in layout rows.\n\t\t *\n\t\t * @spec openspec/changes/dashboard-fit-cells-close-up/specs/dashboard-grid/spec.md#requirement-a-size-to-content-cell-ends-at-its-content\n\t\t * @return {boolean} True to split the rows.\n\t\t */\n\t\twantsRowSplit() {\n\t\t\treturn !this.editable && !this.float && hasFitCell(this.layout)\n\t\t},\n\n\t\t/**\n\t\t * @return {boolean} Whether the keyboard lane is live — items are\n\t\t * focusable and the arrow keys reposition. Requires the opt-in prop,\n\t\t * edit mode (a read-only dashboard has nothing to move, so adding N tab\n\t\t * stops there would be pure noise), and a grid that is not reflowed,\n\t\t * which is where a move cannot be kept and so must not be offered.\n\t\t */\n\t\tkeyboardActive() {\n\t\t\treturn this.keyboardRepositioning && this.editable && !this.reflowed\n\t\t},\n\n\t\t/**\n\t\t * @return {number|null} `0` when grid items should be tab stops,\n\t\t * otherwise `null` so the attribute is omitted entirely.\n\t\t */\n\t\titemTabindex() {\n\t\t\treturn this.keyboardActive ? 0 : null\n\t\t},\n\n\t\t/**\n\t\t * @return {string} DOM id of the shared keyboard-instructions element.\n\t\t */\n\t\tkeyboardHelpDomId() {\n\t\t\treturn `${this.domId}-keyboard-help`\n\t\t},\n\n\t\t/**\n\t\t * @return {string|null} `aria-describedby` target for each grid item —\n\t\t * the instructions element while the keyboard lane is live, `null`\n\t\t * otherwise (a dangling id reference is an axe violation).\n\t\t */\n\t\titemDescribedBy() {\n\t\t\treturn this.keyboardActive ? this.keyboardHelpDomId : null\n\t\t},\n\n\t\t/**\n\t\t * @return {string} The spoken key map for the keyboard lane.\n\t\t */\n\t\tkeyboardHelpText() {\n\t\t\treturn t(\n\t\t\t\t'nextcloud-vue',\n\t\t\t\t'Use the arrow keys to move this widget one cell. Hold Shift and use the arrow keys to resize it. Press Home or End to jump to the first or last column. Press Enter to open the widget menu.',\n\t\t\t)\n\t\t},\n\n\t\t/**\n\t\t * @return {number} The column count currently in force. With responsive\n\t\t * `columnOpts` GridStack reflows to fewer columns on small screens, so\n\t\t * clamping must read the live value rather than the `columns` prop.\n\t\t */\n\t\teffectiveColumns() {\n\t\t\tif (this.grid && typeof this.grid.getColumn === 'function') {\n\t\t\t\tconst live = this.grid.getColumn()\n\t\t\t\tif (Number.isFinite(live) && live > 0) {\n\t\t\t\t\treturn live\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn this.columns\n\t\t},\n\t},\n\n\twatch: {\n\t\teditable() {\n\t\t\tthis.syncInteractivity()\n\t\t},\n\n\t\twantsRowSplit(wants) {\n\t\t\tif (this.grid && (wants ? ROW_SPLIT : 1) !== this.rowSplit) {\n\t\t\t\tthis.rebuildGrid()\n\t\t\t}\n\t\t},\n\n\t\tlayout: {\n\t\t\tdeep: true,\n\t\t\thandler(newLayout) {\n\t\t\t\tif (this.grid) {\n\t\t\t\t\tthis.syncGridItems(newLayout)\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t},\n\n\tmounted() {\n\t\tthis.initGrid()\n\t\tthis.$nextTick(() => this.observeContentSizes())\n\t},\n\n\tbeforeUnmount() {\n\t\tthis.disconnectContentObserver()\n\t\tif (this.grid) {\n\t\t\tthis.grid.destroy(false)\n\t\t}\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Derive the v-for render key for a layout item, honouring the optional\n\t\t * `itemKey` prop and falling back to `item.id`.\n\t\t *\n\t\t * @param {object} item the layout item.\n\t\t * @return {string|number} the render key.\n\t\t */\n\t\tresolveItemKey(item) {\n\t\t\treturn this.itemKey ? this.itemKey(item) : item.id\n\t\t},\n\n\t\t/**\n\t\t * The `gs-size-to-content` attribute for a layout item, or `undefined` to\n\t\t * leave the item on its authored `gridHeight`.\n\t\t *\n\t\t * A widget that decides AT RUNTIME it has nothing to show — a banner whose\n\t\t * condition is false, a list that came back empty — still costs its whole\n\t\t * authored row, because the row is reserved from the layout before the\n\t\t * component renders and the engine positions items absolutely, so no CSS\n\t\t * can reclaim it. `sizeToContent` hands the row's height to the content, so\n\t\t * an empty widget collapses and the grid closes up behind it.\n\t\t *\n\t\t * Opt-in per item: it makes `gridHeight` advisory for that widget, which is\n\t\t * wrong for anything with a deliberate size (a map, a chart) and right for\n\t\t * anything whose height is whatever it turned out to have.\n\t\t *\n\t\t * @param {object} item the layout item.\n\t\t * @return {string|undefined} `'true'`, a row maximum as a string, or undefined.\n\t\t */\n\t\tsizeToContentAttr(item) {\n\t\t\tconst v = item && item.sizeToContent\n\t\t\tif (v === true) {\n\t\t\t\treturn 'true'\n\t\t\t}\n\t\t\t// A number is GridStack's \"size to content, but never taller than N rows\" (a maximum).\n\t\t\tif (Number.isFinite(v) && v > 0) {\n\t\t\t\treturn String(v * this.rowSplit)\n\t\t\t}\n\t\t\treturn undefined\n\t\t},\n\n\t\t/**\n\t\t * A layout row coordinate or height in GridStack rows (`rowSplit`).\n\t\t *\n\t\t * @spec openspec/changes/dashboard-fit-cells-close-up/specs/dashboard-grid/spec.md#requirement-a-size-to-content-cell-ends-at-its-content\n\t\t * @param {number|undefined} value The value in layout rows.\n\t\t * @return {number|undefined} The value in grid rows, or undefined when none is set.\n\t\t */\n\t\tscaledRow(value) {\n\t\t\treturn Number.isFinite(value) ? value * this.rowSplit : undefined\n\t\t},\n\n\t\t/**\n\t\t * The smallest height an item may take, in grid rows: `minHeight`\n\t\t * layout rows, except for a size-to-content cell on split rows, which\n\t\t * may shrink to its content down to one grid row. The minimum is for\n\t\t * resizing by hand, and that only happens on whole rows in edit mode.\n\t\t *\n\t\t * @spec openspec/changes/dashboard-fit-cells-close-up/specs/dashboard-grid/spec.md#requirement-a-size-to-content-cell-ends-at-its-content\n\t\t * @param {object} item The layout item.\n\t\t * @return {number} The minimum height in grid rows.\n\t\t */\n\t\tminRows(item) {\n\t\t\tif (this.rowSplit !== 1 && this.sizeToContentAttr(item) !== undefined) {\n\t\t\t\treturn 1\n\t\t\t}\n\t\t\treturn this.minHeight * this.rowSplit\n\t\t},\n\n\t\t/**\n\t\t * Build the grid again with the row split `wantsRowSplit` asks for, as\n\t\t * edit mode turns on or off: the items go back to their layout places in\n\t\t * the new rows, and the size-to-content cells are measured again.\n\t\t *\n\t\t * @spec openspec/changes/dashboard-fit-cells-close-up/specs/dashboard-grid/spec.md#requirement-a-size-to-content-cell-ends-at-its-content\n\t\t * @return {void}\n\t\t */\n\t\trebuildGrid() {\n\t\t\tthis.disconnectContentObserver()\n\t\t\tthis.grid.destroy(false)\n\t\t\tthis.grid = null\n\t\t\tthis.rowSplit = this.wantsRowSplit ? ROW_SPLIT : 1\n\t\t\tthis.$nextTick(() => {\n\t\t\t\tif (!this.$refs.gridContainer) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\t// GridStack wrote its own places onto the items; put the layout's back.\n\t\t\t\tfor (const item of this.layout) {\n\t\t\t\t\tconst el = this.$refs.gridContainer.querySelector(`[gs-id=\"${item.id}\"]`)\n\t\t\t\t\tif (!el) {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tfor (const [name, value] of [['gs-y', this.scaledRow(item.gridY)], ['gs-h', this.scaledRow(item.gridHeight)], ['gs-min-h', this.minRows(item)], ['gs-size-to-content', this.sizeToContentAttr(item)]]) {\n\t\t\t\t\t\tif (value === undefined) {\n\t\t\t\t\t\t\tel.removeAttribute(name)\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tel.setAttribute(name, String(value))\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tthis.initGrid()\n\t\t\t\tthis.observeContentSizes()\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Build the accessible name for a grid item. A custom `itemLabel` wins\n\t\t * outright; otherwise the first human-readable field on the item is used\n\t\t * and, while the keyboard lane is live, the current grid coordinates are\n\t\t * appended — without them a screen-reader user tabbing through an edit-mode\n\t\t * dashboard has no way to tell where anything sits.\n\t\t *\n\t\t * @param {object} item the layout item.\n\t\t * @param {number} index the item's index in `layout`, used for the\n\t\t * last-resort positional name.\n\t\t * @return {string} the accessible name.\n\t\t */\n\t\tresolveItemLabel(item, index) {\n\t\t\tif (this.itemLabel) {\n\t\t\t\treturn String(this.itemLabel(item, index) ?? '')\n\t\t\t}\n\n\t\t\tconst base = item.title\n\t\t\t\t|| item.name\n\t\t\t\t|| item.label\n\t\t\t\t|| item.widgetTitle\n\t\t\t\t|| item.widgetId\n\t\t\t\t|| t('nextcloud-vue', 'Widget {number}', { number: index + 1 })\n\n\t\t\tif (!this.keyboardActive) {\n\t\t\t\treturn String(base)\n\t\t\t}\n\n\t\t\t// Report the rectangle exactly as the reactive `layout` prop states\n\t\t\t// it — unclamped, so the spoken coordinates always match the\n\t\t\t// rendered `gs-*` attributes — and read from the prop (not the\n\t\t\t// GridStack node) so the name re-renders when a move round-trips\n\t\t\t// back through `layout-change`; engine-node mutations are invisible\n\t\t\t// to Vue's reactivity.\n\t\t\tconst rect = {\n\t\t\t\tx: Number.isFinite(item.gridX) ? item.gridX : 0,\n\t\t\t\ty: Number.isFinite(item.gridY) ? item.gridY : 0,\n\t\t\t\tw: Number.isFinite(item.gridWidth) ? item.gridWidth : this.minWidth,\n\t\t\t\th: Number.isFinite(item.gridHeight) ? item.gridHeight : this.minHeight,\n\t\t\t}\n\t\t\treturn t(\n\t\t\t\t'nextcloud-vue',\n\t\t\t\t'{label}, column {column} of {columns}, row {row}, {width} columns wide, {height} rows tall',\n\t\t\t\t{\n\t\t\t\t\tlabel: String(base),\n\t\t\t\t\tcolumn: rect.x + 1,\n\t\t\t\t\tcolumns: this.effectiveColumns,\n\t\t\t\t\trow: rect.y + 1,\n\t\t\t\t\twidth: rect.w,\n\t\t\t\t\theight: rect.h,\n\t\t\t\t},\n\t\t\t)\n\t\t},\n\n\t\t/**\n\t\t * Build the GridStack grid on the rendered items, install the growth\n\t\t * wrapper (`keepOrderOnGrowth`) and listen for its changes.\n\t\t *\n\t\t * @spec openspec/changes/dashboard-growth-keeps-order/specs/dashboard-grid/spec.md#requirement-a-growing-cell-keeps-the-order-of-the-cards-under-it\n\t\t * @return {void}\n\t\t */\n\t\tinitGrid() {\n\t\t\tif (this.cellHeightCssVar && typeof document !== 'undefined' && document.documentElement) {\n\t\t\t\tdocument.documentElement.style.setProperty(this.cellHeightCssVar, `${this.cellHeight}px`)\n\t\t\t}\n\t\t\tconst el = this.$refs.gridContainer.querySelector('.grid-stack')\n\t\t\tthis.grid = GridStack.init({\n\t\t\t\tcolumn: this.columns,\n\t\t\t\tcellHeight: this.cellHeight / this.rowSplit,\n\t\t\t\tmargin: this.margin,\n\t\t\t\tfloat: this.float,\n\t\t\t\tanimate: true,\n\t\t\t\tdisableDrag: !this.editable,\n\t\t\t\tdisableResize: !this.editable,\n\t\t\t\tremovable: false,\n\t\t\t\t...(this.columnOpts ? { columnOpts: this.columnOpts } : {}),\n\t\t\t}, el)\n\t\t\tthis.keepOrderOnGrowth()\n\n\t\t\tthis.grid.on('change', (_event, items) => {\n\t\t\t\tthis.handleGridChange(items)\n\t\t\t})\n\n\t\t\t// A grid that mounts already narrow is reflowed before anyone touches\n\t\t\t// it, so the handles have to be judged here and not only on a later\n\t\t\t// change.\n\t\t\tthis.syncInteractivity()\n\t\t},\n\n\t\t/**\n\t\t * Let a size-to-content cell that grows push the cards under it down in\n\t\t * their order. GridStack resolves that collision itself, but not in\n\t\t * order: when pipelinq's banner grew from two rows to three, the\n\t\t * full-width card under the stat tiles landed below the lists, also\n\t\t * with `float: false`. Every grow after the grid is built, whether\n\t\t * GridStack measures it on resize or this grid's content observer asks\n\t\t * for it, now first moves the cards the growth reaches down by the\n\t\t * growth (`planGrowth`), bottom-most first, and only then grows the\n\t\t * cell, so GridStack has no collision left to resolve. The wrapper is\n\t\t * installed after `GridStack.init` has already measured the cells on\n\t\t * load; the order on load comes from adding the items in row order\n\t\t * (`itemsInRowOrder`). A cell that shrinks is left to GridStack as\n\t\t * before.\n\t\t *\n\t\t * @spec openspec/changes/dashboard-growth-keeps-order/specs/dashboard-grid/spec.md#requirement-a-growing-cell-keeps-the-order-of-the-cards-under-it\n\t\t * @return {void}\n\t\t */\n\t\tkeepOrderOnGrowth() {\n\t\t\tconst grid = this.grid\n\t\t\tif (!grid || typeof grid.resizeToContent !== 'function' || typeof grid.update !== 'function') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst resize = grid.resizeToContent.bind(grid)\n\t\t\tgrid.resizeToContent = (el) => {\n\t\t\t\tconst node = el && el.gridstackNode\n\t\t\t\tconst nodes = grid.engine && Array.isArray(grid.engine.nodes) ? grid.engine.nodes : null\n\t\t\t\tconst rows = node && nodes ? this.contentRows(el, node) : null\n\t\t\t\tconst plan = rows ? planGrowth(nodes, node, rows) : []\n\t\t\t\tif (plan.length === 0) {\n\t\t\t\t\tresize(el)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\t// Inside a batch the moves are not packed until the cell has grown.\n\t\t\t\tconst batching = Boolean(grid.engine.batchMode)\n\t\t\t\tif (!batching) {\n\t\t\t\t\tgrid.batchUpdate()\n\t\t\t\t}\n\t\t\t\tfor (const step of plan) {\n\t\t\t\t\tgrid.update(step.node.el, { y: step.y })\n\t\t\t\t}\n\t\t\t\tresize(el)\n\t\t\t\tif (!batching) {\n\t\t\t\t\tgrid.batchUpdate(false)\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The rows a size-to-content cell needs for its content, measured the\n\t\t * way GridStack's `resizeToContent` measures it (the content's first\n\t\t * child, plus the cell's own padding), within its size-to-content row\n\t\t * maximum and its minimum and maximum height.\n\t\t *\n\t\t * @spec openspec/changes/dashboard-growth-keeps-order/specs/dashboard-grid/spec.md#requirement-a-growing-cell-keeps-the-order-of-the-cards-under-it\n\t\t * @param {HTMLElement} el The cell.\n\t\t * @param {object} node Its GridStack node.\n\t\t * @return {number|null} The rows, or null when it cannot be measured.\n\t\t */\n\t\tcontentRows(el, node) {\n\t\t\tconst cell = this.grid.getCellHeight(true)\n\t\t\tconst item = el.querySelector('.grid-stack-item-content')\n\t\t\tconst child = item && item.firstElementChild\n\t\t\tif (!el.clientHeight || !cell || !child || !Number.isFinite(node.h)) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst padding = el.clientHeight - item.clientHeight\n\t\t\tconst itemHeight = node.h * cell - padding\n\t\t\tconst wanted = child.getBoundingClientRect().height || itemHeight\n\t\t\tlet rows = Math.ceil((node.h * cell + wanted - itemHeight) / cell)\n\t\t\tif (Number.isInteger(node.sizeToContent) && rows > node.sizeToContent) {\n\t\t\t\trows = node.sizeToContent\n\t\t\t}\n\t\t\tif (node.minH && rows < node.minH) {\n\t\t\t\trows = node.minH\n\t\t\t} else if (node.maxH && rows > node.maxH) {\n\t\t\t\trows = node.maxH\n\t\t\t}\n\t\t\treturn rows\n\t\t},\n\n\t\t/**\n\t\t * Keep both editing lanes in step with whether an edit could actually be\n\t\t * kept: the pointer's handles, and — through `reflowed` feeding\n\t\t * `keyboardActive` — the tab stops, the help text and the arrow keys.\n\t\t *\n\t\t * A reflowed grid drops every `change` it receives, for the reason\n\t\t * `handleGridChange` gives. Leaving either lane live there offers an edit\n\t\t * that silently snaps back, which is the same class of surprise as\n\t\t * persisting the reflow — pointed the other way, and harder to explain\n\t\t * because the author did everything right. It is worse on the keyboard,\n\t\t * where the move is also announced as done. The affordance has to say what\n\t\t * the grid will do at this width, not what edit mode does at the authored\n\t\t * one.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tsyncInteractivity() {\n\t\t\t// Ahead of the engine guard: the keyboard lane reads this whether or\n\t\t\t// not the engine can be enabled.\n\t\t\tthis.reflowed = this.isReflowed()\n\t\t\tif (!this.grid || typeof this.grid.enable !== 'function') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.editable && !this.reflowed) {\n\t\t\t\tthis.grid.enable()\n\t\t\t} else {\n\t\t\t\tthis.grid.disable()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Whether the grid is currently rescaled to a column count other than the\n\t\t * authored one, i.e. a `columnOpts` breakpoint is in force.\n\t\t *\n\t\t * The authored count is `columnOpts.columnMax` when there is one, not the\n\t\t * `columns` prop: GridStack widens to `columnMax` above the top breakpoint,\n\t\t * so a consumer whose two disagree is at its widest, not reflowed.\n\t\t *\n\t\t * @return {boolean} True while a responsive reflow is active.\n\t\t */\n\t\tisReflowed() {\n\t\t\tif (!this.grid || typeof this.grid.getColumn !== 'function') {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tconst authored = Number.isFinite(this.columnOpts?.columnMax) ? this.columnOpts.columnMax : this.columns\n\t\t\tconst live = this.grid.getColumn()\n\t\t\treturn Number.isFinite(live) && live > 0 && live !== authored\n\t\t},\n\n\t\thandleGridChange(items) {\n\t\t\t// The rescale that crossed a breakpoint arrives here as a `change`, so\n\t\t\t// this is also where the handles are re-judged in both directions:\n\t\t\t// switched off as the grid narrows, and back on as it widens.\n\t\t\tthis.syncInteractivity()\n\n\t\t\tif (this.applyingLayout) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// GridStack fires `change` for its own responsive rescale too, and at a\n\t\t\t// 12 → 1 breakpoint every item reads `gridX: 0, gridWidth: 1`. Emitting\n\t\t\t// that has the host persist the reflow AS the authored layout, which\n\t\t\t// widening back does not undo. A move made while reflowed is the same\n\t\t\t// rescaled geometry, so it is dropped for the same reason.\n\t\t\tif (this.isReflowed()) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// A grid on split rows is a view-mode grid: what changes in it is\n\t\t\t// packing and size-to-content measuring, never a user's\n\t\t\t// arrangement, and it is not reported as one.\n\t\t\tif (this.rowSplit !== 1) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (!items || items.length === 0) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst updated = this.layout.map((item) => {\n\t\t\t\tconst gridItem = items.find((gi) => String(gi.id) === String(item.id))\n\t\t\t\tif (gridItem) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\t...item,\n\t\t\t\t\t\tgridX: gridItem.x,\n\t\t\t\t\t\tgridY: gridItem.y,\n\t\t\t\t\t\tgridWidth: gridItem.w,\n\t\t\t\t\t\tgridHeight: gridItem.h,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn item\n\t\t\t})\n\n\t\t\tthis.$emit('layout-change', updated)\n\t\t},\n\n\t\t/**\n\t\t * Keydown handler bound to every grid item.\n\t\t *\n\t\t * Only reacts when the grid item element itself is the event target:\n\t\t * a button, input or menu rendered inside the `#widget` slot must keep\n\t\t * its own arrow/Enter semantics, and a grid that swallowed them would\n\t\t * be a worse a11y regression than the one this handler fixes.\n\t\t *\n\t\t * @param {KeyboardEvent} event the keydown event.\n\t\t * @param {object} item the layout item the key was pressed on.\n\t\t * @return {void}\n\t\t */\n\t\tonItemKeydown(event, item) {\n\t\t\tif (!this.keyboardRepositioning) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (event.target !== event.currentTarget) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (event.altKey || event.ctrlKey || event.metaKey) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif (event.key === 'Enter' || event.key === ' ' || event.key === 'Spacebar') {\n\t\t\t\tevent.preventDefault()\n\t\t\t\tthis.activateItem(item, event.currentTarget)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Repositioning only, and only where it can be kept — an item that\n\t\t\t// still holds focus from before a reflow keeps its Enter, but its\n\t\t\t// arrow keys stop moving something that would snap back.\n\t\t\tif (!this.keyboardActive) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst handled = this.applyKey(event.key, event.shiftKey, item)\n\t\t\tif (handled) {\n\t\t\t\t// Arrows would otherwise scroll the dashboard out from under\n\t\t\t\t// the widget the user is moving.\n\t\t\t\tevent.preventDefault()\n\t\t\t\tevent.stopPropagation()\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Translate one repositioning key into a geometry change.\n\t\t *\n\t\t * @param {string} key the `KeyboardEvent.key` value.\n\t\t * @param {boolean} shift whether Shift was held (resize instead of move).\n\t\t * @param {object} item the layout item being operated on.\n\t\t * @return {boolean} `true` when the key belongs to the grid and the\n\t\t * default action must be suppressed.\n\t\t */\n\t\tapplyKey(key, shift, item) {\n\t\t\tconst rect = this.currentRect(item)\n\n\t\t\tswitch (key) {\n\t\t\t\tcase 'ArrowLeft':\n\t\t\t\t\treturn shift\n\t\t\t\t\t\t? this.applyGeometry(item, { w: rect.w - 1 }, 'resize')\n\t\t\t\t\t\t: this.applyGeometry(item, { x: rect.x - 1 }, 'move')\n\t\t\t\tcase 'ArrowRight':\n\t\t\t\t\treturn shift\n\t\t\t\t\t\t? this.applyGeometry(item, { w: rect.w + 1 }, 'resize')\n\t\t\t\t\t\t: this.applyGeometry(item, { x: rect.x + 1 }, 'move')\n\t\t\t\tcase 'ArrowUp':\n\t\t\t\t\treturn shift\n\t\t\t\t\t\t? this.applyGeometry(item, { h: rect.h - 1 }, 'resize')\n\t\t\t\t\t\t: this.applyGeometry(item, { y: rect.y - 1 }, 'move')\n\t\t\t\tcase 'ArrowDown':\n\t\t\t\t\treturn shift\n\t\t\t\t\t\t? this.applyGeometry(item, { h: rect.h + 1 }, 'resize')\n\t\t\t\t\t\t: this.applyGeometry(item, { y: rect.y + 1 }, 'move')\n\t\t\t\tcase 'Home':\n\t\t\t\t\treturn this.applyGeometry(item, { x: 0 }, 'move')\n\t\t\t\tcase 'End':\n\t\t\t\t\treturn this.applyGeometry(item, { x: this.effectiveColumns - rect.w }, 'move')\n\t\t\t\tdefault:\n\t\t\t\t\treturn false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Read an item's live rectangle. The GridStack node is preferred over\n\t\t * the `layout` prop because a drag that has not yet round-tripped\n\t\t * through the consumer's persistence leaves the prop momentarily stale,\n\t\t * and a keyboard nudge must continue from where the widget actually is.\n\t\t *\n\t\t * @param {object} item the layout item.\n\t\t * @return {{x: number, y: number, w: number, h: number}} the rectangle\n\t\t * in grid units.\n\t\t */\n\t\tcurrentRect(item) {\n\t\t\tconst node = this.gridNode(item.id)\n\t\t\tif (node && Number.isFinite(node.x) && Number.isFinite(node.y)) {\n\t\t\t\treturn {\n\t\t\t\t\tx: node.x,\n\t\t\t\t\ty: node.y,\n\t\t\t\t\tw: Number.isFinite(node.w) ? node.w : this.minWidth,\n\t\t\t\t\th: Number.isFinite(node.h) ? node.h : this.minHeight,\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tx: Number.isFinite(item.gridX) ? item.gridX : 0,\n\t\t\t\ty: Number.isFinite(item.gridY) ? item.gridY : 0,\n\t\t\t\tw: Number.isFinite(item.gridWidth) ? item.gridWidth : this.minWidth,\n\t\t\t\th: Number.isFinite(item.gridHeight) ? item.gridHeight : this.minHeight,\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Clamp a candidate rectangle to the grid's bounds and the configured\n\t\t * minimum widget size, so a held-down arrow key parks the widget at the\n\t\t * edge instead of pushing it into negative coordinates.\n\t\t *\n\t\t * @param {{x: number, y: number, w: number, h: number}} rect the candidate.\n\t\t * @return {{x: number, y: number, w: number, h: number}} the clamped rectangle.\n\t\t */\n\t\tclampRect(rect) {\n\t\t\tconst cols = this.effectiveColumns\n\t\t\tconst w = Math.min(Math.max(rect.w, this.minWidth), cols)\n\t\t\tconst h = Math.max(rect.h, this.minHeight)\n\t\t\treturn {\n\t\t\t\tx: Math.min(Math.max(rect.x, 0), Math.max(cols - w, 0)),\n\t\t\t\ty: Math.max(rect.y, 0),\n\t\t\t\tw,\n\t\t\t\th,\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Apply a partial geometry change to an item.\n\t\t *\n\t\t * The change is pushed through `GridStack.update()` — the very call the\n\t\t * drag/resize handlers end in — so collision resolution, the `change`\n\t\t * event and therefore the `layout-change` emission and the consumer's\n\t\t * persistence are identical for keyboard and pointer. There is\n\t\t * deliberately no second, direct-to-`layout` update path.\n\t\t *\n\t\t * @param {object} item the layout item to move or resize.\n\t\t * @param {{x?: number, y?: number, w?: number, h?: number}} patch the\n\t\t * requested change, merged over the item's current rectangle.\n\t\t * @param {'move'|'resize'} kind which announcement to speak.\n\t\t * @return {boolean} `true` when the key was consumed — including when\n\t\t * the widget was already at the edge, because the arrow still belongs\n\t\t * to the grid and must not scroll the page.\n\t\t */\n\t\tapplyGeometry(item, patch, kind) {\n\t\t\tconst el = this.itemElement(item.id)\n\t\t\tif (!el || !this.grid || typeof this.grid.update !== 'function') {\n\t\t\t\treturn false\n\t\t\t}\n\n\t\t\tconst current = this.currentRect(item)\n\t\t\tconst next = this.clampRect({ ...current, ...patch })\n\n\t\t\tif (next.x === current.x && next.y === current.y\n\t\t\t\t&& next.w === current.w && next.h === current.h) {\n\t\t\t\tthis.announce(t('nextcloud-vue', '{label} cannot move further in that direction', {\n\t\t\t\t\tlabel: this.announcementLabel(item),\n\t\t\t\t}))\n\t\t\t\treturn true\n\t\t\t}\n\n\t\t\tthis.grid.update(el, next)\n\n\t\t\tthis.announce(kind === 'resize'\n\t\t\t\t? t('nextcloud-vue', '{label} resized to {width} columns by {height} rows', {\n\t\t\t\t\t\tlabel: this.announcementLabel(item),\n\t\t\t\t\t\twidth: next.w,\n\t\t\t\t\t\theight: next.h,\n\t\t\t\t\t})\n\t\t\t\t: t('nextcloud-vue', '{label} moved to column {column}, row {row}', {\n\t\t\t\t\t\tlabel: this.announcementLabel(item),\n\t\t\t\t\t\tcolumn: next.x + 1,\n\t\t\t\t\t\trow: next.y + 1,\n\t\t\t\t\t}))\n\n\t\t\treturn true\n\t\t},\n\n\t\t/**\n\t\t * The short, coordinate-free name used inside live-region sentences —\n\t\t * the full `aria-label` already carries the coordinates and repeating\n\t\t * them would make every announcement unlistenable.\n\t\t *\n\t\t * @param {object} item the layout item.\n\t\t * @return {string} the short name.\n\t\t */\n\t\tannouncementLabel(item) {\n\t\t\treturn String(item.title\n\t\t\t\t|| item.name\n\t\t\t\t|| item.label\n\t\t\t\t|| item.widgetTitle\n\t\t\t\t|| item.widgetId\n\t\t\t\t|| t('nextcloud-vue', 'Widget'))\n\t\t},\n\n\t\t/**\n\t\t * Push text into the polite live region. The region is cleared first so\n\t\t * two identical consecutive announcements (e.g. two failed nudges at the\n\t\t * grid edge) are both spoken — assistive tech ignores a re-render that\n\t\t * leaves the text unchanged.\n\t\t *\n\t\t * @param {string} text the message to announce.\n\t\t * @return {void}\n\t\t */\n\t\tannounce(text) {\n\t\t\tthis.announcement = ''\n\t\t\tthis.$nextTick(() => {\n\t\t\t\tthis.announcement = text\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Handle `Enter` / `Space` on a focused grid item: tell the consumer via\n\t\t * `item-activate`, and — unless opted out — synthesise the `contextmenu`\n\t\t * event a right-click would have produced, anchored to the item, so\n\t\t * pointer-only widget menus become keyboard-reachable without the\n\t\t * consumer wiring anything new. The synthetic event is dispatched from\n\t\t * inside the item's content so it bubbles through the slot's own root\n\t\t * element (where such handlers live) and on up.\n\t\t *\n\t\t * @param {object} item the layout item being activated.\n\t\t * @param {HTMLElement} el the grid item element.\n\t\t * @return {void}\n\t\t */\n\t\tactivateItem(item, el) {\n\t\t\tconst rect = typeof el.getBoundingClientRect === 'function'\n\t\t\t\t? el.getBoundingClientRect()\n\t\t\t\t: { left: 0, top: 0 }\n\t\t\tconst clientX = Math.round((rect.left || 0) + MENU_ANCHOR_OFFSET)\n\t\t\tconst clientY = Math.round((rect.top || 0) + MENU_ANCHOR_OFFSET)\n\n\t\t\tthis.$emit('item-activate', { item, element: el, clientX, clientY })\n\n\t\t\tif (this.activateOpensContextMenu) {\n\t\t\t\tthis.dispatchContextMenu(el, clientX, clientY)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Dispatch a bubbling, cancelable `contextmenu` MouseEvent from inside a\n\t\t * grid item.\n\t\t *\n\t\t * @param {HTMLElement} el the grid item element.\n\t\t * @param {number} clientX viewport x for the menu anchor.\n\t\t * @param {number} clientY viewport y for the menu anchor.\n\t\t * @return {void}\n\t\t */\n\t\tdispatchContextMenu(el, clientX, clientY) {\n\t\t\tif (typeof MouseEvent !== 'function') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst content = el.querySelector('.grid-stack-item-content')\n\t\t\tconst target = (content && content.firstElementChild) || content || el\n\t\t\ttarget.dispatchEvent(new MouseEvent('contextmenu', {\n\t\t\t\tbubbles: true,\n\t\t\t\tcancelable: true,\n\t\t\t\tclientX,\n\t\t\t\tclientY,\n\t\t\t\tbutton: 2,\n\t\t\t}))\n\t\t},\n\n\t\t/**\n\t\t * Look up a grid item's DOM element by its layout id.\n\t\t *\n\t\t * @param {string|number} id the layout item id.\n\t\t * @return {HTMLElement|null} the element, or `null` when not rendered.\n\t\t */\n\t\titemElement(id) {\n\t\t\tconst container = this.$refs.gridContainer\n\t\t\tif (!container) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn container.querySelector(`[gs-id=\"${id}\"]`)\n\t\t},\n\n\t\t/**\n\t\t * Look up the GridStack engine node backing a layout item.\n\t\t *\n\t\t * @param {string|number} id the layout item id.\n\t\t * @return {object|null} the node, or `null` when GridStack isn't tracking it.\n\t\t */\n\t\t/**\n\t\t * Re-measure a `sizeToContent` item when its content changes height.\n\t\t * GridStack watches only the grid element, so a widget that loaded its data\n\t\t * in `mounted()` keeps the height its empty state had.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tobserveContentSizes() {\n\t\t\tthis.disconnectContentObserver()\n\t\t\tif (typeof ResizeObserver === 'undefined' || !this.$refs.gridContainer) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tthis.contentObserver = new ResizeObserver((entries) => {\n\t\t\t\tconst cells = new Set()\n\t\t\t\tfor (const entry of entries) {\n\t\t\t\t\tconst cell = entry.target.closest('.grid-stack-item')\n\t\t\t\t\tif (cell) {\n\t\t\t\t\t\tcells.add(cell)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (cells.size === 0) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\t// A frame later, not inline: `resizeToContent` resizes the cell the\n\t\t\t\t// observed child sits in, and a child that stretches to its parent\n\t\t\t\t// grows with it. Resizing from inside the callback makes that the\n\t\t\t\t// classic ResizeObserver loop (\"undelivered notifications\"); deferred,\n\t\t\t\t// it is an ordinary second pass that settles.\n\t\t\t\trequestAnimationFrame(() => {\n\t\t\t\t\tif (!this.contentObserver || !this.grid || typeof this.grid.resizeToContent !== 'function') {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tfor (const cell of cells) {\n\t\t\t\t\t\tthis.grid.resizeToContent(cell)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t})\n\n\t\t\tconst cells = this.$refs.gridContainer.querySelectorAll('.grid-stack-item[gs-size-to-content]')\n\t\t\tfor (const cell of cells) {\n\t\t\t\tconst measured = cell.querySelector('.grid-stack-item-content')?.firstElementChild\n\t\t\t\tif (measured) {\n\t\t\t\t\tthis.contentObserver.observe(measured)\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Drop the content observer, on unmount or before rebuilding it.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tdisconnectContentObserver() {\n\t\t\tif (this.contentObserver) {\n\t\t\t\tthis.contentObserver.disconnect()\n\t\t\t\tthis.contentObserver = null\n\t\t\t}\n\t\t},\n\n\t\tgridNode(id) {\n\t\t\tconst nodes = this.grid && this.grid.engine && this.grid.engine.nodes\n\t\t\tif (!Array.isArray(nodes)) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn nodes.find((n) => String(n.id) === String(id)) || null\n\t\t},\n\n\t\tsyncGridItems(newLayout) {\n\t\t\t// Add new items, and re-adopt items whose backing DOM element was\n\t\t\t// replaced. When a consumer's `itemKey` changes for an unchanged\n\t\t\t// `id` (e.g. a style edit — the documented use of `itemKey`), Vue\n\t\t\t// swaps the element. GridStack keeps tracking the old, now-detached\n\t\t\t// element, leaving the new one unmanaged (no positioning/sizing) —\n\t\t\t// so re-register it against the new element.\n\t\t\tfor (const item of newLayout) {\n\t\t\t\tthis.$nextTick(() => {\n\t\t\t\t\tconst el = this.$refs.gridContainer.querySelector(`[gs-id=\"${item.id}\"]`)\n\t\t\t\t\tif (!el) {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tconst node = this.grid.engine.nodes.find((n) => String(n.id) === String(item.id))\n\t\t\t\t\tif (!node) {\n\t\t\t\t\t\tthis.grid.makeWidget(el)\n\t\t\t\t\t} else if (node.el === el) {\n\t\t\t\t\t\t// The layout MOVED an item the grid already tracks: the\n\t\t\t\t\t\t// page re-compacts its display layout when a widget's\n\t\t\t\t\t\t// condition hides it, and the item below was given a new\n\t\t\t\t\t\t// gridY that never reached GridStack, so a band stayed\n\t\t\t\t\t\t// where the hidden widget had been. A drag reports the\n\t\t\t\t\t\t// grid's own position back, which matches and does\n\t\t\t\t\t\t// nothing here.\n\t\t\t\t\t\tconst x = item.gridX ?? node.x\n\t\t\t\t\t\tconst y = Number.isFinite(item.gridY) ? item.gridY * this.rowSplit : node.y\n\t\t\t\t\t\tif (x !== node.x || y !== node.y) {\n\t\t\t\t\t\t\t// The layout said where it goes; GridStack's `change`\n\t\t\t\t\t\t\t// for it is not a user's arrangement and is not\n\t\t\t\t\t\t\t// reported back (it would write the display-only\n\t\t\t\t\t\t\t// compaction into the authored layout).\n\t\t\t\t\t\t\tthis.applyingLayout = true\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tthis.grid.update(el, { x, y })\n\t\t\t\t\t\t\t} finally {\n\t\t\t\t\t\t\t\tthis.applyingLayout = false\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Drop the stale node without touching its detached DOM,\n\t\t\t\t\t\t// then adopt the new element so GridStack sizes/places it.\n\t\t\t\t\t\tthis.grid.removeWidget(node.el, false, false)\n\t\t\t\t\t\tthis.grid.makeWidget(el)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\n\t\t\t// Remove items no longer in layout\n\t\t\tconst ids = newLayout.map((i) => String(i.id))\n\t\t\tconst toRemove = this.grid.engine.nodes.filter((n) => !ids.includes(String(n.id)))\n\t\t\tfor (const node of toRemove) {\n\t\t\t\tconst el = this.$refs.gridContainer.querySelector(`[gs-id=\"${node.id}\"]`)\n\t\t\t\tif (el) {\n\t\t\t\t\tthis.grid.removeWidget(el, false)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Elements were added or replaced, so the observer's are stale.\n\t\t\tthis.$nextTick(() => this.observeContentSizes())\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-dashboard-grid {\n\twidth: 100%;\n\tmin-height: 200px;\n}\n\n.grid-stack {\n\tbackground: transparent;\n}\n\n/* Screen-reader-only text: the keyboard key map and the live region. Kept in\n the layout (not `display: none`) so assistive tech still reads it. */\n.cn-dashboard-grid__sr-only {\n\tposition: absolute;\n\twidth: 1px;\n\theight: 1px;\n\tpadding: 0;\n\tmargin: -1px;\n\toverflow: hidden;\n\tclip-path: inset(50%);\n\twhite-space: nowrap;\n\tborder: 0;\n}\n\n/* Grid items become tab stops in edit mode — give them a visible focus ring\n that satisfies WCAG 2.1 SC 2.4.7 without relying on the widget's own\n styling. `outline-offset` keeps the ring outside the rounded card. */\n.grid-stack-item:focus-visible {\n\toutline: 2px solid var(--color-primary-element, #0082c9);\n\toutline-offset: 2px;\n\tborder-radius: var(--border-radius-container-large, 16px);\n}\n\n/* A size-to-content cell (`sizeToContent`) is measured by GridStack from the\n content's first child, and a widget wrapper there filled the cell\n (`height: 100%`), so the measure returned the cell's own height and the\n cell never shrank: \"Waiting for me\" stayed a tall card around one row. In\n such a cell the child takes its own height. A cell without the key keeps\n the filling child. */\n.cn-dashboard-grid__item--fit > :deep(.grid-stack-item-content > *) {\n\theight: auto;\n}\n\n:deep(.grid-stack-item-content) {\n\tbackground: var(--color-main-background);\n\tborder: none;\n\tbox-shadow: none;\n\toverflow: hidden;\n}\n\n:deep(.grid-stack-item-content:has(.cn-widget-wrapper--borderless)) {\n\tbackground: transparent;\n}\n\n:deep(.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","_createElementBlock","_Fragment","_renderList","_renderSlot"],"mappings":";;;;;;;AAyCO,MAAA,UAAA,GAAA,EAAA,KAAK,EAAC,YAAY,GAAA;;AAmBtB,MAAA,UAAA,GAAA,EAAA,KAAA,EAAA,yBAAA,GAAA;;;;;;;;;;KAzBM,QAAc,CAAA,cAAA;wCAIjB,CAAA,GAAA,EAAA;;AAHF,UAAA,EAAE,EAAE,QAAiB,CAAA,iBAAA;eACjB,EAAC,4BAAkB;2BACrB,QAAa,CAAA,gBAAA,CAAA,EAAA,CAAA,oBAAA,UAAA,CAAA;;AAGf,IAAAA,kBAAA,CAyBC,OAzBD,UAyBC,EAAA;wBAxBDC,kBAuBK,CAAAC,QAAA,EAAA,IAAA,EAAAC,UAAA,CAtBU,QAAI,CAAA,eAAA,EAAA,CAAZ,IAAI,KAAA;6BADXF,kBAuBK,CAAA,KAAA,EAAA;UArBJ,GAAG,EAAE,QAAc,CAAA,cAAA,CAAC,IAAG,CAAA;AACxB,UAAA,KAAK,kBAAC,iBAAiB,EAAA,EAAA,8BAAA,EACe,0BAAA,CAAA,IAAA,MAAA,SAAA,EAAA,CAAA,CAAA;AACtC,UAAA,IAAK,EAAA,OAAO;UACX,YAAU,EAAE,yBAAgB,CAAC,IAAG,EAAA,MAAA,CAAA,MAAA,CAAA,OAAA,CAAA,IAAA,CAAA,CAAA;4BAChB,EAAE,QAAe,CAAA,eAAA;AACjC,UAAA,QAAM,EAAA,QAAA,CAAA,YAAA;UACN,OAAI,EAAA,IAAA,CAAA,EAAA;UACJ,MAAG,EAAA,IAAA,CAAA,KAAA;AACH,UAAA,MAAE,EAAA,QAAA,CAAA,SAAA,CAAA,IAAA,CAAA,KAAA,CAAA;;AAEF,UAAA,MAAA,EAAA,QAAA,CAAA,SAAA,CAAA,IAAA,CAAA,UAAA,CAAA;AACA,UAAA,UAAQ,EAAE,MAAQ,CAAA,QAAA;UAClB,UAAQ,EAAE,QAAO,CAAA,OAAA,CAAE,IAAG,CAAA;UACtB,oBAAkB,EAAE,QAAkB,CAAA,iBAAA,CAAA,IAAG,CAAA;AAC1C,UAAA,SAAA,EAAA,MAAA,KAAA,QAAA,CAAA,aAAA,CAAA,MAAA,EAAA,IAAA,CAAA,CAAA;;AACD,UAAAD,kBAAA,CAIO,OAJP,UAIO,EAAA;YAHLI,UAEO,CAAA,IAAA,CAAA,MAAA,EAAA,QAAA,EAAA,EAFc,IAAI,EAAE,IAAI,EAAA,CAAA;;;;;;;;;;;"}
|