@conduction/nextcloud-vue 2.121.0 → 2.123.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +1 -0
  2. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  3. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  4. package/dist/esm/components/CnAttentionFlagWidget/CnAttentionFlagWidget.vue.js +1 -1
  5. package/dist/esm/components/CnAttentionFlagWidget/CnAttentionFlagWidget.vue2.js +18 -4
  6. package/dist/esm/components/CnAttentionFlagWidget/CnAttentionFlagWidget.vue2.js.map +1 -1
  7. package/dist/esm/components/CnAttentionFlagWidget/CnAttentionFlagWidget.vue3.js +63 -32
  8. package/dist/esm/components/CnAttentionFlagWidget/CnAttentionFlagWidget.vue3.js.map +1 -1
  9. package/dist/esm/components/CnChartWidget/CnChartWidget.vue.js +1 -1
  10. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
  11. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
  12. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +35 -0
  13. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  14. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +11 -5
  15. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  16. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
  17. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +13 -0
  18. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  19. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +37 -5
  20. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  21. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
  22. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +40 -0
  23. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  24. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +71 -65
  25. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  26. package/dist/esm/components/CnFlowDetail/CnFlowDetail.vue.js +1 -1
  27. package/dist/esm/components/CnFlowDetail/CnFlowDetail.vue2.js +20 -0
  28. package/dist/esm/components/CnFlowDetail/CnFlowDetail.vue2.js.map +1 -1
  29. package/dist/esm/components/CnFlowDetail/CnFlowDetail.vue3.js +2 -1
  30. package/dist/esm/components/CnFlowDetail/CnFlowDetail.vue3.js.map +1 -1
  31. package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue.js +1 -1
  32. package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue2.js +2 -0
  33. package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue2.js.map +1 -1
  34. package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue3.js +49 -44
  35. package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue3.js.map +1 -1
  36. package/dist/esm/components/CnFlowsPage/CnFlowEditorPage.vue2.js +10 -0
  37. package/dist/esm/components/CnFlowsPage/CnFlowEditorPage.vue2.js.map +1 -1
  38. package/dist/esm/components/CnFlowsPage/CnFlowEditorPage.vue3.js +2 -1
  39. package/dist/esm/components/CnFlowsPage/CnFlowEditorPage.vue3.js.map +1 -1
  40. package/dist/esm/components/CnGraphCanvas/CnGraphCanvas.vue2.js +31 -0
  41. package/dist/esm/components/CnGraphCanvas/CnGraphCanvas.vue2.js.map +1 -1
  42. package/dist/esm/components/CnGraphCanvas/CnGraphCanvas.vue3.js +4 -3
  43. package/dist/esm/components/CnGraphCanvas/CnGraphCanvas.vue3.js.map +1 -1
  44. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +34 -2
  45. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  46. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  47. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js.map +1 -1
  48. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js +1 -0
  49. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js.map +1 -1
  50. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue.js +1 -1
  51. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +2 -0
  52. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  53. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +2 -1
  54. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  55. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue.js +1 -1
  56. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue2.js +31 -1
  57. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue2.js.map +1 -1
  58. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue3.js +14 -4
  59. package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue3.js.map +1 -1
  60. package/dist/esm/composables/flowGraphLayout.js +36 -7
  61. package/dist/esm/composables/flowGraphLayout.js.map +1 -1
  62. package/dist/esm/composables/useFlowStore.js +7 -3
  63. package/dist/esm/composables/useFlowStore.js.map +1 -1
  64. package/dist/esm/l10n/en.json.js +3 -1
  65. package/dist/esm/l10n/en.json.js.map +1 -1
  66. package/dist/esm/l10n/nl.json.js +3 -1
  67. package/dist/esm/l10n/nl.json.js.map +1 -1
  68. package/dist/esm/library-style.js +1 -1
  69. package/dist/esm/nextcloud-vue.css +670 -316
  70. package/dist/esm/utils/manifestTranslate.js +12 -3
  71. package/dist/esm/utils/manifestTranslate.js.map +1 -1
  72. package/dist/esm/utils/routeFilters.js +3 -3
  73. package/dist/esm/utils/routeFilters.js.map +1 -1
  74. package/dist/nextcloud-vue.cjs.js +1218 -847
  75. package/dist/nextcloud-vue.cjs.js.map +2 -2
  76. package/dist/nextcloud-vue.css +670 -316
  77. package/l10n/en.json +3 -1
  78. package/l10n/nl.json +3 -1
  79. package/package.json +1 -1
  80. package/src/components/CnAppRoot/CnAppRoot.vue +1 -0
  81. package/src/components/CnAttentionFlagWidget/CnAttentionFlagWidget.vue +50 -5
  82. package/src/components/CnChartWidget/CnChartWidget.vue +10 -8
  83. package/src/components/CnDetailPage/CnDetailPage.md +10 -0
  84. package/src/components/CnDetailPage/CnDetailPage.vue +42 -1
  85. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +39 -0
  86. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +95 -37
  87. package/src/components/CnFlowDetail/CnFlowDetail.vue +21 -0
  88. package/src/components/CnFlowsPage/CnFlowBoard.vue +35 -18
  89. package/src/components/CnFlowsPage/CnFlowEditorPage.vue +11 -0
  90. package/src/components/CnGraphCanvas/CnGraphCanvas.vue +34 -2
  91. package/src/components/CnIndexPage/CnIndexPage.vue +34 -2
  92. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +1 -0
  93. package/src/components/CnObjectSidebar/CnFilesTab.vue +3 -0
  94. package/src/components/CnSectionsWidget/CnSectionsWidget.vue +53 -2
  95. package/src/composables/flowGraphLayout.js +35 -6
  96. package/src/composables/useFlowStore.js +7 -3
  97. package/src/css/detail-page.css +6 -0
  98. package/src/css/look-board-detail.css +53 -6
  99. package/src/css/look-board-index.css +151 -12
  100. package/src/css/look-board.css +116 -4
  101. package/src/utils/manifestTranslate.js +12 -3
  102. package/src/utils/routeFilters.js +3 -3
@@ -2,7 +2,7 @@ import script from './CnSectionsWidget.vue2.js';
2
2
  import { render } from './CnSectionsWidget.vue3.js';
3
3
 
4
4
  script.render = render;
5
- script.__scopeId = "data-v-1bba4b0c";
5
+ script.__scopeId = "data-v-28baf01e";
6
6
  const __sfc_main = script;
7
7
 
8
8
  export { __sfc_main as default };
@@ -58,7 +58,11 @@ var script = {
58
58
  * - `dividers` — draw a rule between sections (default true). `false` stacks them 8px apart with no rule,
59
59
  * for a row of strips that each say one thing (a record's banners).
60
60
  *
61
- * @type {{sections?: Array<object>, cards?: boolean, hideEmpty?: boolean, dividers?: boolean}}
61
+ * - `columns` — lay the sections out side by side in this many columns (2 to 4), one column on a narrow
62
+ * screen. Ignored when a section asks for the side column.
63
+ * - `footnote` — a line of muted text under the sections.
64
+ *
65
+ * @type {{sections?: Array<object>, cards?: boolean, hideEmpty?: boolean, dividers?: boolean, columns?: number, footnote?: string}}
62
66
  */
63
67
  content: {
64
68
  type: Object,
@@ -220,6 +224,32 @@ var script = {
220
224
  return this.sections.some((section) => section.column === 'side')
221
225
  },
222
226
 
227
+ /**
228
+ * How many columns the sections are laid out in side by side: the
229
+ * configured `columns`, capped at four; 1 (stacked) without it or
230
+ * when a section asks for the side column.
231
+ *
232
+ * @return {number}
233
+ * @spec openspec/changes/sections-card-grid/specs/sections-card-grid/spec.md#requirement-a-sections-widget-can-lay-its-cards-out-in-columns
234
+ */
235
+ gridColumns() {
236
+ const n = Math.floor(Number(this.content?.columns));
237
+ if (!Number.isFinite(n) || n < 2 || this.twoColumn) {
238
+ return 1
239
+ }
240
+ return Math.min(n, 4)
241
+ },
242
+
243
+ /**
244
+ * The translated footnote under the sections, or ''.
245
+ *
246
+ * @return {string}
247
+ * @spec openspec/changes/sections-card-grid/specs/sections-card-grid/spec.md#requirement-a-sections-widget-can-close-with-a-footnote
248
+ */
249
+ footnote() {
250
+ return this.tr(this.content?.footnote || '')
251
+ },
252
+
223
253
  /**
224
254
  * The columns to draw, each with its sections in order.
225
255
  *
@@ -1 +1 @@
1
- {"version":3,"file":"CnSectionsWidget.vue2.js","sources":["../../../../src/components/CnSectionsWidget/CnSectionsWidget.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div\n\t\tclass=\"cn-sections-widget\"\n\t\t:class=\"{\n\t\t\t'cn-sections-widget--cards': cards,\n\t\t\t'cn-sections-widget--two-column': twoColumn,\n\t\t\t'cn-sections-widget--plain': plain,\n\t\t}\"\n\t\tdata-testid=\"cn-sections-widget\">\n\t\t<div\n\t\t\tv-for=\"column in columns\"\n\t\t\t:key=\"column.id\"\n\t\t\tclass=\"cn-sections-widget__column\"\n\t\t\t:class=\"`cn-sections-widget__column--${column.id}`\">\n\t\t\t<section\n\t\t\t\tv-for=\"section in column.sections\"\n\t\t\t\t:key=\"section.key\"\n\t\t\t\tclass=\"cn-sections-widget__section\"\n\t\t\t\t:class=\"{ 'cn-sections-widget__section--empty': !isShown(section.key) }\"\n\t\t\t\t:aria-labelledby=\"section.title && isShown(section.key) ? headingId(section.key) : undefined\"\n\t\t\t\t:data-testid=\"`cn-section-${section.key}`\">\n\t\t\t\t<!-- The heading waits for content: a widget type that does not\n\t\t\t\t resolve renders nothing, and a heading over nothing is a\n\t\t\t\t label on a void. -->\n\t\t\t\t<h3\n\t\t\t\t\tv-if=\"section.title && isShown(section.key)\"\n\t\t\t\t\t:id=\"headingId(section.key)\"\n\t\t\t\t\tclass=\"cn-sections-widget__heading\">\n\t\t\t\t\t{{ section.title }}\n\t\t\t\t</h3>\n\t\t\t\t<p\n\t\t\t\t\tv-if=\"section.description && isShown(section.key)\"\n\t\t\t\t\tclass=\"cn-sections-widget__description\">\n\t\t\t\t\t{{ section.description }}\n\t\t\t\t</p>\n\t\t\t\t<!-- The host always mounts, also while the section reads as\n\t\t\t\t empty: a widget that has not mounted cannot fetch, so it\n\t\t\t\t could never gain the content that would show it. -->\n\t\t\t\t<div :ref=\"(el) => setHostRef(section.key, el)\" class=\"cn-sections-widget__body\">\n\t\t\t\t\t<CnDetailWidgetHost\n\t\t\t\t\t\t:widget=\"section.widget\"\n\t\t\t\t\t\tchrome=\"bare\"\n\t\t\t\t\t\t:objectId=\"objectId\"\n\t\t\t\t\t\t:object=\"objectData\"\n\t\t\t\t\t\t:objectType=\"objectType\"\n\t\t\t\t\t\t:schemaObject=\"schemaObject\"\n\t\t\t\t\t\t:register=\"register\"\n\t\t\t\t\t\t:schema=\"schema\"\n\t\t\t\t\t\t:store=\"store\"\n\t\t\t\t\t\t:surface=\"surface\"\n\t\t\t\t\t\t:integrationContext=\"integrationContext\"\n\t\t\t\t\t\t:cnRegistry=\"cnRegistry\"\n\t\t\t\t\t\t:availableWidgets=\"availableWidgets\" />\n\t\t\t\t</div>\n\t\t\t</section>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport CnDetailWidgetHost from '../CnDetailWidgetHost/CnDetailWidgetHost.vue'\n\n/** Counter for unique heading ids across instances. */\nlet instanceCounter = 0\n\n/**\n * CnSectionsWidget — the `sections` widget: several widgets in one place,\n * each under its own heading.\n *\n * A tab strip binds one widget per tab, and a tab often holds more than one\n * thing: the documents list and the case folder (DqZaak), a portal's details\n * form, house style and sign-in (PtPortalInstellingen). This widget stacks\n * them as sections under headings with a rule between them (the default), or\n * as one card per section (`cards: true`), optionally in a main and a side\n * column (`column: \"side\"` on a section). A single column of separate titled\n * cards in a tab is the tabs widget's own `widgetIds` stack; this widget is\n * for headed sections, and for the two-column card grid a settings tab draws.\n *\n * ```json\n * {\n * \"type\": \"sections\",\n * \"content\": {\n * \"sections\": [\n * { \"title\": \"Documenten\", \"widgetId\": \"case-documents\" },\n * { \"title\": \"Zaakmap\", \"widget\": { \"type\": \"files\" } }\n * ]\n * }\n * }\n * ```\n *\n * A section names its widget inline (`widget`) or by id (`widgetId`, a\n * sibling definition on the same surface). Children render through\n * `CnDetailWidgetHost` in bare mode, the dispatch a tab panel uses, so a child\n * behaves here as it does in a tab. A section whose widget draws nothing\n * shows no heading and takes no space (`hideEmpty`, default true). Stacked\n * children share one cell, so an object list in a section renders every\n * fetched row (`content.fit: false`) unless its definition says otherwise.\n *\n * @spec openspec/changes/abstract-sections-widget/specs/sections-widget/spec.md\n */\nexport default {\n\tname: 'CnSectionsWidget',\n\n\tcomponents: { CnDetailWidgetHost },\n\n\tinject: {\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget configuration.\n\t\t *\n\t\t * - `sections` — `[{ key?, title?, description?, column?, widget? | widgetId? }]`, in order.\n\t\t * `label` is accepted as an alias of `title`; `column` is `main` (default) or `side`.\n\t\t * - `cards` — draw each section as its own card (default false: headings and a rule between sections).\n\t\t * - `hideEmpty` — hide the heading and the space of a section whose widget drew nothing (default true).\n\t\t * - `dividers` — draw a rule between sections (default true). `false` stacks them 8px apart with no rule,\n\t\t * for a row of strips that each say one thing (a record's banners).\n\t\t *\n\t\t * @type {{sections?: Array<object>, cards?: boolean, hideEmpty?: boolean, dividers?: boolean}}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** Sibling widget definitions on the surface, for `widgetId` sections. */\n\t\tavailableWidgets: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** The bound record's id. */\n\t\tobjectId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The loaded record, or null while it is still being fetched. */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The resolved object-type slug. */\n\t\tobjectType: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The resolved JSON Schema object, needed by a `data` child. */\n\t\tschemaObject: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** OpenRegister register slug of the surface. */\n\t\tregister: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** OpenRegister schema slug of the surface. */\n\t\tschema: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The effective object store. */\n\t\tstore: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Rendering surface forwarded to the children. */\n\t\tsurface: {\n\t\t\ttype: String,\n\t\t\tdefault: 'detail-page',\n\t\t},\n\n\t\t/** Object context forwarded to integration children. */\n\t\tintegrationContext: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The consumer's component registry, for custom child widget types. */\n\t\tcnRegistry: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\t},\n\n\tdata() {\n\t\tinstanceCounter += 1\n\t\treturn {\n\t\t\tuid: instanceCounter,\n\t\t\t/**\n\t\t\t * Section key to \"has this section rendered anything\".\n\t\t\t *\n\t\t\t * @type {Record<string, boolean>}\n\t\t\t */\n\t\t\tfilled: {},\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Whether each section is its own card.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tcards() {\n\t\t\treturn this.content?.cards === true\n\t\t},\n\n\t\t/**\n\t\t * Whether the sections stack with no rule between them.\n\t\t *\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/dq-abstract-record-widgets/specs/record-widgets/spec.md#requirement-sections-can-stack-without-dividers\n\t\t */\n\t\tplain() {\n\t\t\treturn !this.cards && this.content?.dividers === false\n\t\t},\n\n\t\t/**\n\t\t * Whether an empty section hides.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\thideEmpty() {\n\t\t\treturn this.content?.hideEmpty !== false\n\t\t},\n\n\t\t/**\n\t\t * The sections to render: a stable key, a translated title, the column\n\t\t * and the widget definition. A section whose widget cannot be found is\n\t\t * dropped (with a development warning for an unknown id).\n\t\t *\n\t\t * @return {Array<{key: string, title: string, description: string, column: string, widget: object}>}\n\t\t * @spec openspec/changes/abstract-sections-widget/specs/sections-widget/spec.md#requirement-a-sections-widget-stacks-configured-widgets-under-headings\n\t\t */\n\t\tsections() {\n\t\t\tconst raw = Array.isArray(this.content?.sections) ? this.content.sections : []\n\t\t\tconst out = []\n\t\t\traw.forEach((entry, index) => {\n\t\t\t\tif (!entry || typeof entry !== 'object') {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tlet widget = entry.widget && typeof entry.widget === 'object' ? entry.widget : null\n\t\t\t\tif (!widget && typeof entry.widgetId === 'string' && entry.widgetId !== '') {\n\t\t\t\t\twidget = this.availableWidgets.find((w) => w && w.id === entry.widgetId) || null\n\t\t\t\t\tif (!widget) {\n\t\t\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\t\t\tconsole.warn(`[CnSectionsWidget] section widgetId \"${entry.widgetId}\" names no widget on this surface; section skipped.`)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (!widget || !widget.type) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tout.push({\n\t\t\t\t\tkey: String(entry.key || widget.id || entry.widgetId || `${widget.type}-${index}`),\n\t\t\t\t\ttitle: this.tr(entry.title || entry.label || ''),\n\t\t\t\t\tdescription: this.tr(entry.description || ''),\n\t\t\t\t\tcolumn: entry.column === 'side' ? 'side' : 'main',\n\t\t\t\t\twidget: this.shared(widget),\n\t\t\t\t})\n\t\t\t})\n\t\t\treturn out\n\t\t},\n\n\t\t/**\n\t\t * Whether any section sits in the side column.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\ttwoColumn() {\n\t\t\treturn this.sections.some((section) => section.column === 'side')\n\t\t},\n\n\t\t/**\n\t\t * The columns to draw, each with its sections in order.\n\t\t *\n\t\t * @return {Array<{id: string, sections: Array<object>}>}\n\t\t * @spec openspec/changes/abstract-sections-widget/specs/sections-widget/spec.md#requirement-a-sections-widget-can-draw-cards-in-a-main-and-a-side-column\n\t\t */\n\t\tcolumns() {\n\t\t\tif (!this.twoColumn) {\n\t\t\t\treturn [{ id: 'main', sections: this.sections }]\n\t\t\t}\n\t\t\treturn [\n\t\t\t\t{ id: 'main', sections: this.sections.filter((section) => section.column === 'main') },\n\t\t\t\t{ id: 'side', sections: this.sections.filter((section) => section.column === 'side') },\n\t\t\t]\n\t\t},\n\t},\n\n\tbeforeUnmount() {\n\t\tthis.stopObserving()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Translate through the host app.\n\t\t *\n\t\t * @param {string} text The text.\n\t\t * @return {string}\n\t\t */\n\t\ttr(text) {\n\t\t\tif (typeof text !== 'string' || text === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text\n\t\t},\n\n\t\t/**\n\t\t * The heading id of a section.\n\t\t *\n\t\t * @param {string} key The section key.\n\t\t * @return {string}\n\t\t */\n\t\theadingId(key) {\n\t\t\treturn `cn-sections-${this.uid}-${key}`.replace(/[^\\w-]/g, '-')\n\t\t},\n\n\t\t/**\n\t\t * A child definition with the cell budget off: stacked children share\n\t\t * one cell, so an object list must not size itself to that cell.\n\t\t *\n\t\t * @param {object} widget The definition.\n\t\t * @return {object}\n\t\t */\n\t\tshared(widget) {\n\t\t\tconst content = widget.content || {}\n\t\t\tif (Object.hasOwn(content, 'fit')) {\n\t\t\t\treturn widget\n\t\t\t}\n\t\t\treturn { ...widget, content: { ...content, fit: false } }\n\t\t},\n\n\t\t/**\n\t\t * Whether a section shows its heading and takes space.\n\t\t *\n\t\t * @param {string} key The section key.\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/abstract-sections-widget/specs/sections-widget/spec.md#requirement-a-section-whose-widget-draws-nothing-stays-silent\n\t\t */\n\t\tisShown(key) {\n\t\t\treturn !this.hideEmpty || this.filled[key] === true\n\t\t},\n\n\t\t/**\n\t\t * Watch one section's host for content, so its heading appears when\n\t\t * the widget renders and stays away when it never does. A\n\t\t * MutationObserver, because most widgets mount empty and fill after a\n\t\t * fetch.\n\t\t *\n\t\t * @param {string} key The section key.\n\t\t * @param {HTMLElement|null} el The host wrapper, or null on teardown.\n\t\t * @return {void}\n\t\t */\n\t\tsetHostRef(key, el) {\n\t\t\tthis.observers = this.observers || {}\n\t\t\tif (this.observers[key]) {\n\t\t\t\tthis.observers[key].disconnect()\n\t\t\t\tdelete this.observers[key]\n\t\t\t}\n\t\t\tif (!el) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// Assign only on a change: `filled` drives the template, so an\n\t\t\t// unconditional write re-renders, mutates, and fires again.\n\t\t\tconst measure = () => {\n\t\t\t\tconst next = this.hasContent(el)\n\t\t\t\tif (this.filled[key] === next) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tthis.filled = { ...this.filled, [key]: next }\n\t\t\t}\n\t\t\tif (typeof MutationObserver === 'function') {\n\t\t\t\tconst observer = new MutationObserver(measure)\n\t\t\t\tobserver.observe(el, { childList: true, subtree: true, characterData: true })\n\t\t\t\tthis.observers[key] = observer\n\t\t\t}\n\t\t\t// Not synchronously: a ref callback runs during the patch.\n\t\t\tthis.$nextTick(measure)\n\t\t},\n\n\t\t/**\n\t\t * Whether a host rendered anything a reader would see: text (an empty\n\t\t * state counts), or a graphical or interactive element.\n\t\t *\n\t\t * @param {HTMLElement} el The host wrapper.\n\t\t * @return {boolean}\n\t\t */\n\t\thasContent(el) {\n\t\t\tif (!el) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif (el.textContent && el.textContent.trim().length > 0) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn el.querySelector('img, svg, canvas, table, input, button, select, textarea') !== null\n\t\t},\n\n\t\t/**\n\t\t * Drop every observer.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tstopObserving() {\n\t\t\tfor (const observer of Object.values(this.observers || {})) {\n\t\t\t\tobserver.disconnect()\n\t\t\t}\n\t\t\tthis.observers = {}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-sections-widget {\n\tmin-width: 0;\n}\n\n.cn-sections-widget__column {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: calc(var(--default-grid-baseline, 4px) * 4);\n\tmin-width: 0;\n}\n\n/* A main and a side column (the settings boards: 1fr and 418px, 20px apart). */\n.cn-sections-widget--two-column {\n\tdisplay: grid;\n\tgrid-template-columns: minmax(0, 1fr) minmax(280px, 418px);\n\tgap: 20px;\n\talign-items: start;\n}\n\n@media (max-width: 1024px) {\n\t.cn-sections-widget--two-column {\n\t\tgrid-template-columns: minmax(0, 1fr);\n\t}\n}\n\n/* A host page may space every <section>; these sections are spaced by the column gap only. */\n.cn-sections-widget__section {\n\tmargin: 0;\n\tmin-width: 0;\n}\n\n/* A rule above a section that follows a shown one. */\n.cn-sections-widget:not(.cn-sections-widget--cards) .cn-sections-widget__section:not(.cn-sections-widget__section--empty) ~ .cn-sections-widget__section {\n\tpadding-top: calc(var(--default-grid-baseline, 4px) * 4);\n\tborder-top: 1px solid var(--color-border);\n}\n\n/* No rule: strips that each say one thing, 8px apart. */\n.cn-sections-widget--plain .cn-sections-widget__column {\n\tgap: calc(var(--default-grid-baseline, 4px) * 2);\n}\n\n.cn-sections-widget.cn-sections-widget--plain .cn-sections-widget__section:not(.cn-sections-widget__section--empty) ~ .cn-sections-widget__section {\n\tpadding-top: 0;\n\tborder-top: 0;\n}\n\n/*\n * An empty section keeps its host mounted (display: none still mounts it, so\n * the widget fetches and can fill later) but takes no space, gap or rule.\n */\n.cn-sections-widget__section--empty {\n\tdisplay: none;\n}\n\n.cn-sections-widget__heading {\n\tmargin: 0 0 calc(var(--default-grid-baseline, 4px) * 2);\n\tfont-size: 16px;\n\tfont-weight: 600;\n\tcolor: var(--color-main-text);\n}\n\n.cn-sections-widget__description {\n\tmargin: calc(var(--default-grid-baseline, 4px) * -1) 0 calc(var(--default-grid-baseline, 4px) * 3);\n\tcolor: var(--color-text-maxcontrast);\n}\n\n/* One card per section. */\n.cn-sections-widget--cards .cn-sections-widget__column {\n\tgap: 20px;\n}\n\n.cn-sections-widget--cards .cn-sections-widget__section {\n\tbackground: var(--color-main-background);\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--cn-board-card-radius, var(--border-radius-large, 12px));\n\tpadding: var(--cn-board-card-padding, 16px 20px);\n}\n\n.cn-sections-widget--cards .cn-sections-widget__heading {\n\tfont-size: 18px;\n\tmargin-bottom: 12px;\n}\n</style>\n"],"names":["CnDetailWidgetHost"],"mappings":"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"}
1
+ {"version":3,"file":"CnSectionsWidget.vue2.js","sources":["../../../../src/components/CnSectionsWidget/CnSectionsWidget.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div\n\t\tclass=\"cn-sections-widget\"\n\t\t:class=\"{\n\t\t\t'cn-sections-widget--cards': cards,\n\t\t\t'cn-sections-widget--two-column': twoColumn,\n\t\t\t'cn-sections-widget--grid': gridColumns > 1,\n\t\t\t'cn-sections-widget--plain': plain,\n\t\t}\"\n\t\tdata-testid=\"cn-sections-widget\">\n\t\t<div\n\t\t\tv-for=\"column in columns\"\n\t\t\t:key=\"column.id\"\n\t\t\tclass=\"cn-sections-widget__column\"\n\t\t\t:class=\"`cn-sections-widget__column--${column.id}`\"\n\t\t\t:style=\"gridColumns > 1 ? { '--cn-sections-columns': gridColumns } : null\">\n\t\t\t<section\n\t\t\t\tv-for=\"section in column.sections\"\n\t\t\t\t:key=\"section.key\"\n\t\t\t\tclass=\"cn-sections-widget__section\"\n\t\t\t\t:class=\"{ 'cn-sections-widget__section--empty': !isShown(section.key) }\"\n\t\t\t\t:aria-labelledby=\"section.title && isShown(section.key) ? headingId(section.key) : undefined\"\n\t\t\t\t:data-testid=\"`cn-section-${section.key}`\">\n\t\t\t\t<!-- The heading waits for content: a widget type that does not\n\t\t\t\t resolve renders nothing, and a heading over nothing is a\n\t\t\t\t label on a void. -->\n\t\t\t\t<h3\n\t\t\t\t\tv-if=\"section.title && isShown(section.key)\"\n\t\t\t\t\t:id=\"headingId(section.key)\"\n\t\t\t\t\tclass=\"cn-sections-widget__heading\">\n\t\t\t\t\t{{ section.title }}\n\t\t\t\t</h3>\n\t\t\t\t<p\n\t\t\t\t\tv-if=\"section.description && isShown(section.key)\"\n\t\t\t\t\tclass=\"cn-sections-widget__description\">\n\t\t\t\t\t{{ section.description }}\n\t\t\t\t</p>\n\t\t\t\t<!-- The host always mounts, also while the section reads as\n\t\t\t\t empty: a widget that has not mounted cannot fetch, so it\n\t\t\t\t could never gain the content that would show it. -->\n\t\t\t\t<div :ref=\"(el) => setHostRef(section.key, el)\" class=\"cn-sections-widget__body\">\n\t\t\t\t\t<CnDetailWidgetHost\n\t\t\t\t\t\t:widget=\"section.widget\"\n\t\t\t\t\t\tchrome=\"bare\"\n\t\t\t\t\t\t:objectId=\"objectId\"\n\t\t\t\t\t\t:object=\"objectData\"\n\t\t\t\t\t\t:objectType=\"objectType\"\n\t\t\t\t\t\t:schemaObject=\"schemaObject\"\n\t\t\t\t\t\t:register=\"register\"\n\t\t\t\t\t\t:schema=\"schema\"\n\t\t\t\t\t\t:store=\"store\"\n\t\t\t\t\t\t:surface=\"surface\"\n\t\t\t\t\t\t:integrationContext=\"integrationContext\"\n\t\t\t\t\t\t:cnRegistry=\"cnRegistry\"\n\t\t\t\t\t\t:availableWidgets=\"availableWidgets\" />\n\t\t\t\t</div>\n\t\t\t</section>\n\t\t</div>\n\t\t<p v-if=\"footnote\" class=\"cn-sections-widget__footnote\" data-testid=\"cn-sections-widget-footnote\">\n\t\t\t{{ footnote }}\n\t\t</p>\n\t</div>\n</template>\n\n<script>\nimport CnDetailWidgetHost from '../CnDetailWidgetHost/CnDetailWidgetHost.vue'\n\n/** Counter for unique heading ids across instances. */\nlet instanceCounter = 0\n\n/**\n * CnSectionsWidget — the `sections` widget: several widgets in one place,\n * each under its own heading.\n *\n * A tab strip binds one widget per tab, and a tab often holds more than one\n * thing: the documents list and the case folder (DqZaak), a portal's details\n * form, house style and sign-in (PtPortalInstellingen). This widget stacks\n * them as sections under headings with a rule between them (the default), or\n * as one card per section (`cards: true`), optionally in a main and a side\n * column (`column: \"side\"` on a section). A single column of separate titled\n * cards in a tab is the tabs widget's own `widgetIds` stack; this widget is\n * for headed sections, and for the two-column card grid a settings tab draws.\n *\n * ```json\n * {\n * \"type\": \"sections\",\n * \"content\": {\n * \"sections\": [\n * { \"title\": \"Documenten\", \"widgetId\": \"case-documents\" },\n * { \"title\": \"Zaakmap\", \"widget\": { \"type\": \"files\" } }\n * ]\n * }\n * }\n * ```\n *\n * A section names its widget inline (`widget`) or by id (`widgetId`, a\n * sibling definition on the same surface). Children render through\n * `CnDetailWidgetHost` in bare mode, the dispatch a tab panel uses, so a child\n * behaves here as it does in a tab. A section whose widget draws nothing\n * shows no heading and takes no space (`hideEmpty`, default true). Stacked\n * children share one cell, so an object list in a section renders every\n * fetched row (`content.fit: false`) unless its definition says otherwise.\n *\n * @spec openspec/changes/abstract-sections-widget/specs/sections-widget/spec.md\n */\nexport default {\n\tname: 'CnSectionsWidget',\n\n\tcomponents: { CnDetailWidgetHost },\n\n\tinject: {\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget configuration.\n\t\t *\n\t\t * - `sections` — `[{ key?, title?, description?, column?, widget? | widgetId? }]`, in order.\n\t\t * `label` is accepted as an alias of `title`; `column` is `main` (default) or `side`.\n\t\t * - `cards` — draw each section as its own card (default false: headings and a rule between sections).\n\t\t * - `hideEmpty` — hide the heading and the space of a section whose widget drew nothing (default true).\n\t\t * - `dividers` — draw a rule between sections (default true). `false` stacks them 8px apart with no rule,\n\t\t * for a row of strips that each say one thing (a record's banners).\n\t\t *\n\t\t * - `columns` — lay the sections out side by side in this many columns (2 to 4), one column on a narrow\n\t\t * screen. Ignored when a section asks for the side column.\n\t\t * - `footnote` — a line of muted text under the sections.\n\t\t *\n\t\t * @type {{sections?: Array<object>, cards?: boolean, hideEmpty?: boolean, dividers?: boolean, columns?: number, footnote?: string}}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** Sibling widget definitions on the surface, for `widgetId` sections. */\n\t\tavailableWidgets: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** The bound record's id. */\n\t\tobjectId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The loaded record, or null while it is still being fetched. */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The resolved object-type slug. */\n\t\tobjectType: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The resolved JSON Schema object, needed by a `data` child. */\n\t\tschemaObject: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** OpenRegister register slug of the surface. */\n\t\tregister: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** OpenRegister schema slug of the surface. */\n\t\tschema: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The effective object store. */\n\t\tstore: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Rendering surface forwarded to the children. */\n\t\tsurface: {\n\t\t\ttype: String,\n\t\t\tdefault: 'detail-page',\n\t\t},\n\n\t\t/** Object context forwarded to integration children. */\n\t\tintegrationContext: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The consumer's component registry, for custom child widget types. */\n\t\tcnRegistry: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\t},\n\n\tdata() {\n\t\tinstanceCounter += 1\n\t\treturn {\n\t\t\tuid: instanceCounter,\n\t\t\t/**\n\t\t\t * Section key to \"has this section rendered anything\".\n\t\t\t *\n\t\t\t * @type {Record<string, boolean>}\n\t\t\t */\n\t\t\tfilled: {},\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Whether each section is its own card.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tcards() {\n\t\t\treturn this.content?.cards === true\n\t\t},\n\n\t\t/**\n\t\t * Whether the sections stack with no rule between them.\n\t\t *\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/dq-abstract-record-widgets/specs/record-widgets/spec.md#requirement-sections-can-stack-without-dividers\n\t\t */\n\t\tplain() {\n\t\t\treturn !this.cards && this.content?.dividers === false\n\t\t},\n\n\t\t/**\n\t\t * Whether an empty section hides.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\thideEmpty() {\n\t\t\treturn this.content?.hideEmpty !== false\n\t\t},\n\n\t\t/**\n\t\t * The sections to render: a stable key, a translated title, the column\n\t\t * and the widget definition. A section whose widget cannot be found is\n\t\t * dropped (with a development warning for an unknown id).\n\t\t *\n\t\t * @return {Array<{key: string, title: string, description: string, column: string, widget: object}>}\n\t\t * @spec openspec/changes/abstract-sections-widget/specs/sections-widget/spec.md#requirement-a-sections-widget-stacks-configured-widgets-under-headings\n\t\t */\n\t\tsections() {\n\t\t\tconst raw = Array.isArray(this.content?.sections) ? this.content.sections : []\n\t\t\tconst out = []\n\t\t\traw.forEach((entry, index) => {\n\t\t\t\tif (!entry || typeof entry !== 'object') {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tlet widget = entry.widget && typeof entry.widget === 'object' ? entry.widget : null\n\t\t\t\tif (!widget && typeof entry.widgetId === 'string' && entry.widgetId !== '') {\n\t\t\t\t\twidget = this.availableWidgets.find((w) => w && w.id === entry.widgetId) || null\n\t\t\t\t\tif (!widget) {\n\t\t\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\t\t\tconsole.warn(`[CnSectionsWidget] section widgetId \"${entry.widgetId}\" names no widget on this surface; section skipped.`)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (!widget || !widget.type) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tout.push({\n\t\t\t\t\tkey: String(entry.key || widget.id || entry.widgetId || `${widget.type}-${index}`),\n\t\t\t\t\ttitle: this.tr(entry.title || entry.label || ''),\n\t\t\t\t\tdescription: this.tr(entry.description || ''),\n\t\t\t\t\tcolumn: entry.column === 'side' ? 'side' : 'main',\n\t\t\t\t\twidget: this.shared(widget),\n\t\t\t\t})\n\t\t\t})\n\t\t\treturn out\n\t\t},\n\n\t\t/**\n\t\t * Whether any section sits in the side column.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\ttwoColumn() {\n\t\t\treturn this.sections.some((section) => section.column === 'side')\n\t\t},\n\n\t\t/**\n\t\t * How many columns the sections are laid out in side by side: the\n\t\t * configured `columns`, capped at four; 1 (stacked) without it or\n\t\t * when a section asks for the side column.\n\t\t *\n\t\t * @return {number}\n\t\t * @spec openspec/changes/sections-card-grid/specs/sections-card-grid/spec.md#requirement-a-sections-widget-can-lay-its-cards-out-in-columns\n\t\t */\n\t\tgridColumns() {\n\t\t\tconst n = Math.floor(Number(this.content?.columns))\n\t\t\tif (!Number.isFinite(n) || n < 2 || this.twoColumn) {\n\t\t\t\treturn 1\n\t\t\t}\n\t\t\treturn Math.min(n, 4)\n\t\t},\n\n\t\t/**\n\t\t * The translated footnote under the sections, or ''.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/sections-card-grid/specs/sections-card-grid/spec.md#requirement-a-sections-widget-can-close-with-a-footnote\n\t\t */\n\t\tfootnote() {\n\t\t\treturn this.tr(this.content?.footnote || '')\n\t\t},\n\n\t\t/**\n\t\t * The columns to draw, each with its sections in order.\n\t\t *\n\t\t * @return {Array<{id: string, sections: Array<object>}>}\n\t\t * @spec openspec/changes/abstract-sections-widget/specs/sections-widget/spec.md#requirement-a-sections-widget-can-draw-cards-in-a-main-and-a-side-column\n\t\t */\n\t\tcolumns() {\n\t\t\tif (!this.twoColumn) {\n\t\t\t\treturn [{ id: 'main', sections: this.sections }]\n\t\t\t}\n\t\t\treturn [\n\t\t\t\t{ id: 'main', sections: this.sections.filter((section) => section.column === 'main') },\n\t\t\t\t{ id: 'side', sections: this.sections.filter((section) => section.column === 'side') },\n\t\t\t]\n\t\t},\n\t},\n\n\tbeforeUnmount() {\n\t\tthis.stopObserving()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Translate through the host app.\n\t\t *\n\t\t * @param {string} text The text.\n\t\t * @return {string}\n\t\t */\n\t\ttr(text) {\n\t\t\tif (typeof text !== 'string' || text === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text\n\t\t},\n\n\t\t/**\n\t\t * The heading id of a section.\n\t\t *\n\t\t * @param {string} key The section key.\n\t\t * @return {string}\n\t\t */\n\t\theadingId(key) {\n\t\t\treturn `cn-sections-${this.uid}-${key}`.replace(/[^\\w-]/g, '-')\n\t\t},\n\n\t\t/**\n\t\t * A child definition with the cell budget off: stacked children share\n\t\t * one cell, so an object list must not size itself to that cell.\n\t\t *\n\t\t * @param {object} widget The definition.\n\t\t * @return {object}\n\t\t */\n\t\tshared(widget) {\n\t\t\tconst content = widget.content || {}\n\t\t\tif (Object.hasOwn(content, 'fit')) {\n\t\t\t\treturn widget\n\t\t\t}\n\t\t\treturn { ...widget, content: { ...content, fit: false } }\n\t\t},\n\n\t\t/**\n\t\t * Whether a section shows its heading and takes space.\n\t\t *\n\t\t * @param {string} key The section key.\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/abstract-sections-widget/specs/sections-widget/spec.md#requirement-a-section-whose-widget-draws-nothing-stays-silent\n\t\t */\n\t\tisShown(key) {\n\t\t\treturn !this.hideEmpty || this.filled[key] === true\n\t\t},\n\n\t\t/**\n\t\t * Watch one section's host for content, so its heading appears when\n\t\t * the widget renders and stays away when it never does. A\n\t\t * MutationObserver, because most widgets mount empty and fill after a\n\t\t * fetch.\n\t\t *\n\t\t * @param {string} key The section key.\n\t\t * @param {HTMLElement|null} el The host wrapper, or null on teardown.\n\t\t * @return {void}\n\t\t */\n\t\tsetHostRef(key, el) {\n\t\t\tthis.observers = this.observers || {}\n\t\t\tif (this.observers[key]) {\n\t\t\t\tthis.observers[key].disconnect()\n\t\t\t\tdelete this.observers[key]\n\t\t\t}\n\t\t\tif (!el) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// Assign only on a change: `filled` drives the template, so an\n\t\t\t// unconditional write re-renders, mutates, and fires again.\n\t\t\tconst measure = () => {\n\t\t\t\tconst next = this.hasContent(el)\n\t\t\t\tif (this.filled[key] === next) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tthis.filled = { ...this.filled, [key]: next }\n\t\t\t}\n\t\t\tif (typeof MutationObserver === 'function') {\n\t\t\t\tconst observer = new MutationObserver(measure)\n\t\t\t\tobserver.observe(el, { childList: true, subtree: true, characterData: true })\n\t\t\t\tthis.observers[key] = observer\n\t\t\t}\n\t\t\t// Not synchronously: a ref callback runs during the patch.\n\t\t\tthis.$nextTick(measure)\n\t\t},\n\n\t\t/**\n\t\t * Whether a host rendered anything a reader would see: text (an empty\n\t\t * state counts), or a graphical or interactive element.\n\t\t *\n\t\t * @param {HTMLElement} el The host wrapper.\n\t\t * @return {boolean}\n\t\t */\n\t\thasContent(el) {\n\t\t\tif (!el) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif (el.textContent && el.textContent.trim().length > 0) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn el.querySelector('img, svg, canvas, table, input, button, select, textarea') !== null\n\t\t},\n\n\t\t/**\n\t\t * Drop every observer.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tstopObserving() {\n\t\t\tfor (const observer of Object.values(this.observers || {})) {\n\t\t\t\tobserver.disconnect()\n\t\t\t}\n\t\t\tthis.observers = {}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-sections-widget {\n\tmin-width: 0;\n}\n\n.cn-sections-widget__column {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: calc(var(--default-grid-baseline, 4px) * 4);\n\tmin-width: 0;\n}\n\n/* A main and a side column (the settings boards: 1fr and 418px, 20px apart). */\n.cn-sections-widget--two-column {\n\tdisplay: grid;\n\tgrid-template-columns: minmax(0, 1fr) minmax(280px, 418px);\n\tgap: 20px;\n\talign-items: start;\n}\n\n@media (max-width: 1024px) {\n\t.cn-sections-widget--two-column {\n\t\tgrid-template-columns: minmax(0, 1fr);\n\t}\n}\n\n/* Sections side by side (`columns`): at most that many to a row, each at\n least 240px wide, so a narrow cell wraps them onto more rows. */\n.cn-sections-widget.cn-sections-widget--grid .cn-sections-widget__column {\n\t--cn-sections-gap: 14px;\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fit, minmax(min(100%, max(240px, calc((100% - (var(--cn-sections-columns, 2) - 1) * var(--cn-sections-gap)) / var(--cn-sections-columns, 2)))), 1fr));\n\tgap: var(--cn-sections-gap);\n\talign-items: start;\n}\n\n.cn-sections-widget__footnote {\n\tmargin: calc(var(--default-grid-baseline, 4px) * 3) 0 0;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 13px;\n}\n\n/* A host page may space every <section>; these sections are spaced by the column gap only. */\n.cn-sections-widget__section {\n\tmargin: 0;\n\tmin-width: 0;\n}\n\n/* A rule above a section that follows a shown one. */\n.cn-sections-widget:not(.cn-sections-widget--cards) .cn-sections-widget__section:not(.cn-sections-widget__section--empty) ~ .cn-sections-widget__section {\n\tpadding-top: calc(var(--default-grid-baseline, 4px) * 4);\n\tborder-top: 1px solid var(--color-border);\n}\n\n/* No rule: strips that each say one thing, 8px apart. */\n.cn-sections-widget--plain .cn-sections-widget__column {\n\tgap: calc(var(--default-grid-baseline, 4px) * 2);\n}\n\n.cn-sections-widget.cn-sections-widget--plain .cn-sections-widget__section:not(.cn-sections-widget__section--empty) ~ .cn-sections-widget__section {\n\tpadding-top: 0;\n\tborder-top: 0;\n}\n\n/*\n * An empty section keeps its host mounted (display: none still mounts it, so\n * the widget fetches and can fill later) but takes no space, gap or rule.\n */\n.cn-sections-widget__section--empty {\n\tdisplay: none;\n}\n\n.cn-sections-widget__heading {\n\tmargin: 0 0 calc(var(--default-grid-baseline, 4px) * 2);\n\tfont-size: 16px;\n\tfont-weight: 600;\n\tcolor: var(--color-main-text);\n}\n\n.cn-sections-widget__description {\n\tmargin: calc(var(--default-grid-baseline, 4px) * -1) 0 calc(var(--default-grid-baseline, 4px) * 3);\n\tcolor: var(--color-text-maxcontrast);\n}\n\n/* One card per section. */\n.cn-sections-widget--cards .cn-sections-widget__column {\n\tgap: 20px;\n}\n\n.cn-sections-widget--cards .cn-sections-widget__section {\n\tbackground: var(--color-main-background);\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--cn-board-card-radius, var(--border-radius-large, 12px));\n\tpadding: var(--cn-board-card-padding, 16px 20px);\n}\n\n.cn-sections-widget--cards .cn-sections-widget__heading {\n\tfont-size: 18px;\n\tmargin-bottom: 12px;\n}\n</style>\n"],"names":["CnDetailWidgetHost"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { resolveComponent, openBlock, createElementBlock, normalizeClass, Fragment, renderList, toDisplayString, createCommentVNode, createElementVNode, createVNode } from 'vue';
1
+ import { resolveComponent, openBlock, createElementBlock, normalizeClass, Fragment, renderList, normalizeStyle, toDisplayString, createCommentVNode, createElementVNode, createVNode } from 'vue';
2
2
 
3
3
  const _hoisted_1 = ["aria-labelledby", "data-testid"];
4
4
  const _hoisted_2 = ["id"];
@@ -6,6 +6,11 @@ const _hoisted_3 = {
6
6
  key: 1,
7
7
  class: "cn-sections-widget__description"
8
8
  };
9
+ const _hoisted_4 = {
10
+ key: 0,
11
+ class: "cn-sections-widget__footnote",
12
+ "data-testid": "cn-sections-widget-footnote"
13
+ };
9
14
 
10
15
  function render(_ctx, _cache, $props, $setup, $data, $options) {
11
16
  const _component_CnDetailWidgetHost = resolveComponent("CnDetailWidgetHost");
@@ -14,6 +19,7 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
14
19
  class: normalizeClass(["cn-sections-widget", {
15
20
  'cn-sections-widget--cards': $options.cards,
16
21
  'cn-sections-widget--two-column': $options.twoColumn,
22
+ 'cn-sections-widget--grid': $options.gridColumns > 1,
17
23
  'cn-sections-widget--plain': $options.plain,
18
24
  }]),
19
25
  "data-testid": "cn-sections-widget"
@@ -21,7 +27,8 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
21
27
  (openBlock(true), createElementBlock(Fragment, null, renderList($options.columns, (column) => {
22
28
  return (openBlock(), createElementBlock("div", {
23
29
  key: column.id,
24
- class: normalizeClass(["cn-sections-widget__column", `cn-sections-widget__column--${column.id}`])
30
+ class: normalizeClass(["cn-sections-widget__column", `cn-sections-widget__column--${column.id}`]),
31
+ style: normalizeStyle($options.gridColumns > 1 ? { '--cn-sections-columns': $options.gridColumns } : null)
25
32
  }, [
26
33
  (openBlock(true), createElementBlock(Fragment, null, renderList(column.sections, (section) => {
27
34
  return (openBlock(), createElementBlock("section", {
@@ -63,8 +70,11 @@ function render(_ctx, _cache, $props, $setup, $data, $options) {
63
70
  ], 512 /* NEED_PATCH */)
64
71
  ], 10 /* CLASS, PROPS */, _hoisted_1))
65
72
  }), 128 /* KEYED_FRAGMENT */))
66
- ], 2 /* CLASS */))
67
- }), 128 /* KEYED_FRAGMENT */))
73
+ ], 6 /* CLASS, STYLE */))
74
+ }), 128 /* KEYED_FRAGMENT */)),
75
+ ($options.footnote)
76
+ ? (openBlock(), createElementBlock("p", _hoisted_4, toDisplayString($options.footnote), 1 /* TEXT */))
77
+ : createCommentVNode("v-if", true)
68
78
  ], 2 /* CLASS */))
69
79
  }
70
80
 
@@ -1 +1 @@
1
- {"version":3,"file":"CnSectionsWidget.vue3.js","sources":["../../../../src/components/CnSectionsWidget/CnSectionsWidget.vue?vue&type=template&id=1bba4b0c&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div\n\t\tclass=\"cn-sections-widget\"\n\t\t:class=\"{\n\t\t\t'cn-sections-widget--cards': cards,\n\t\t\t'cn-sections-widget--two-column': twoColumn,\n\t\t\t'cn-sections-widget--plain': plain,\n\t\t}\"\n\t\tdata-testid=\"cn-sections-widget\">\n\t\t<div\n\t\t\tv-for=\"column in columns\"\n\t\t\t:key=\"column.id\"\n\t\t\tclass=\"cn-sections-widget__column\"\n\t\t\t:class=\"`cn-sections-widget__column--${column.id}`\">\n\t\t\t<section\n\t\t\t\tv-for=\"section in column.sections\"\n\t\t\t\t:key=\"section.key\"\n\t\t\t\tclass=\"cn-sections-widget__section\"\n\t\t\t\t:class=\"{ 'cn-sections-widget__section--empty': !isShown(section.key) }\"\n\t\t\t\t:aria-labelledby=\"section.title && isShown(section.key) ? headingId(section.key) : undefined\"\n\t\t\t\t:data-testid=\"`cn-section-${section.key}`\">\n\t\t\t\t<!-- The heading waits for content: a widget type that does not\n\t\t\t\t resolve renders nothing, and a heading over nothing is a\n\t\t\t\t label on a void. -->\n\t\t\t\t<h3\n\t\t\t\t\tv-if=\"section.title && isShown(section.key)\"\n\t\t\t\t\t:id=\"headingId(section.key)\"\n\t\t\t\t\tclass=\"cn-sections-widget__heading\">\n\t\t\t\t\t{{ section.title }}\n\t\t\t\t</h3>\n\t\t\t\t<p\n\t\t\t\t\tv-if=\"section.description && isShown(section.key)\"\n\t\t\t\t\tclass=\"cn-sections-widget__description\">\n\t\t\t\t\t{{ section.description }}\n\t\t\t\t</p>\n\t\t\t\t<!-- The host always mounts, also while the section reads as\n\t\t\t\t empty: a widget that has not mounted cannot fetch, so it\n\t\t\t\t could never gain the content that would show it. -->\n\t\t\t\t<div :ref=\"(el) => setHostRef(section.key, el)\" class=\"cn-sections-widget__body\">\n\t\t\t\t\t<CnDetailWidgetHost\n\t\t\t\t\t\t:widget=\"section.widget\"\n\t\t\t\t\t\tchrome=\"bare\"\n\t\t\t\t\t\t:objectId=\"objectId\"\n\t\t\t\t\t\t:object=\"objectData\"\n\t\t\t\t\t\t:objectType=\"objectType\"\n\t\t\t\t\t\t:schemaObject=\"schemaObject\"\n\t\t\t\t\t\t:register=\"register\"\n\t\t\t\t\t\t:schema=\"schema\"\n\t\t\t\t\t\t:store=\"store\"\n\t\t\t\t\t\t:surface=\"surface\"\n\t\t\t\t\t\t:integrationContext=\"integrationContext\"\n\t\t\t\t\t\t:cnRegistry=\"cnRegistry\"\n\t\t\t\t\t\t:availableWidgets=\"availableWidgets\" />\n\t\t\t\t</div>\n\t\t\t</section>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport CnDetailWidgetHost from '../CnDetailWidgetHost/CnDetailWidgetHost.vue'\n\n/** Counter for unique heading ids across instances. */\nlet instanceCounter = 0\n\n/**\n * CnSectionsWidget — the `sections` widget: several widgets in one place,\n * each under its own heading.\n *\n * A tab strip binds one widget per tab, and a tab often holds more than one\n * thing: the documents list and the case folder (DqZaak), a portal's details\n * form, house style and sign-in (PtPortalInstellingen). This widget stacks\n * them as sections under headings with a rule between them (the default), or\n * as one card per section (`cards: true`), optionally in a main and a side\n * column (`column: \"side\"` on a section). A single column of separate titled\n * cards in a tab is the tabs widget's own `widgetIds` stack; this widget is\n * for headed sections, and for the two-column card grid a settings tab draws.\n *\n * ```json\n * {\n * \"type\": \"sections\",\n * \"content\": {\n * \"sections\": [\n * { \"title\": \"Documenten\", \"widgetId\": \"case-documents\" },\n * { \"title\": \"Zaakmap\", \"widget\": { \"type\": \"files\" } }\n * ]\n * }\n * }\n * ```\n *\n * A section names its widget inline (`widget`) or by id (`widgetId`, a\n * sibling definition on the same surface). Children render through\n * `CnDetailWidgetHost` in bare mode, the dispatch a tab panel uses, so a child\n * behaves here as it does in a tab. A section whose widget draws nothing\n * shows no heading and takes no space (`hideEmpty`, default true). Stacked\n * children share one cell, so an object list in a section renders every\n * fetched row (`content.fit: false`) unless its definition says otherwise.\n *\n * @spec openspec/changes/abstract-sections-widget/specs/sections-widget/spec.md\n */\nexport default {\n\tname: 'CnSectionsWidget',\n\n\tcomponents: { CnDetailWidgetHost },\n\n\tinject: {\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget configuration.\n\t\t *\n\t\t * - `sections` — `[{ key?, title?, description?, column?, widget? | widgetId? }]`, in order.\n\t\t * `label` is accepted as an alias of `title`; `column` is `main` (default) or `side`.\n\t\t * - `cards` — draw each section as its own card (default false: headings and a rule between sections).\n\t\t * - `hideEmpty` — hide the heading and the space of a section whose widget drew nothing (default true).\n\t\t * - `dividers` — draw a rule between sections (default true). `false` stacks them 8px apart with no rule,\n\t\t * for a row of strips that each say one thing (a record's banners).\n\t\t *\n\t\t * @type {{sections?: Array<object>, cards?: boolean, hideEmpty?: boolean, dividers?: boolean}}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** Sibling widget definitions on the surface, for `widgetId` sections. */\n\t\tavailableWidgets: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** The bound record's id. */\n\t\tobjectId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The loaded record, or null while it is still being fetched. */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The resolved object-type slug. */\n\t\tobjectType: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The resolved JSON Schema object, needed by a `data` child. */\n\t\tschemaObject: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** OpenRegister register slug of the surface. */\n\t\tregister: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** OpenRegister schema slug of the surface. */\n\t\tschema: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The effective object store. */\n\t\tstore: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Rendering surface forwarded to the children. */\n\t\tsurface: {\n\t\t\ttype: String,\n\t\t\tdefault: 'detail-page',\n\t\t},\n\n\t\t/** Object context forwarded to integration children. */\n\t\tintegrationContext: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The consumer's component registry, for custom child widget types. */\n\t\tcnRegistry: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\t},\n\n\tdata() {\n\t\tinstanceCounter += 1\n\t\treturn {\n\t\t\tuid: instanceCounter,\n\t\t\t/**\n\t\t\t * Section key to \"has this section rendered anything\".\n\t\t\t *\n\t\t\t * @type {Record<string, boolean>}\n\t\t\t */\n\t\t\tfilled: {},\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Whether each section is its own card.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tcards() {\n\t\t\treturn this.content?.cards === true\n\t\t},\n\n\t\t/**\n\t\t * Whether the sections stack with no rule between them.\n\t\t *\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/dq-abstract-record-widgets/specs/record-widgets/spec.md#requirement-sections-can-stack-without-dividers\n\t\t */\n\t\tplain() {\n\t\t\treturn !this.cards && this.content?.dividers === false\n\t\t},\n\n\t\t/**\n\t\t * Whether an empty section hides.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\thideEmpty() {\n\t\t\treturn this.content?.hideEmpty !== false\n\t\t},\n\n\t\t/**\n\t\t * The sections to render: a stable key, a translated title, the column\n\t\t * and the widget definition. A section whose widget cannot be found is\n\t\t * dropped (with a development warning for an unknown id).\n\t\t *\n\t\t * @return {Array<{key: string, title: string, description: string, column: string, widget: object}>}\n\t\t * @spec openspec/changes/abstract-sections-widget/specs/sections-widget/spec.md#requirement-a-sections-widget-stacks-configured-widgets-under-headings\n\t\t */\n\t\tsections() {\n\t\t\tconst raw = Array.isArray(this.content?.sections) ? this.content.sections : []\n\t\t\tconst out = []\n\t\t\traw.forEach((entry, index) => {\n\t\t\t\tif (!entry || typeof entry !== 'object') {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tlet widget = entry.widget && typeof entry.widget === 'object' ? entry.widget : null\n\t\t\t\tif (!widget && typeof entry.widgetId === 'string' && entry.widgetId !== '') {\n\t\t\t\t\twidget = this.availableWidgets.find((w) => w && w.id === entry.widgetId) || null\n\t\t\t\t\tif (!widget) {\n\t\t\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\t\t\tconsole.warn(`[CnSectionsWidget] section widgetId \"${entry.widgetId}\" names no widget on this surface; section skipped.`)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (!widget || !widget.type) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tout.push({\n\t\t\t\t\tkey: String(entry.key || widget.id || entry.widgetId || `${widget.type}-${index}`),\n\t\t\t\t\ttitle: this.tr(entry.title || entry.label || ''),\n\t\t\t\t\tdescription: this.tr(entry.description || ''),\n\t\t\t\t\tcolumn: entry.column === 'side' ? 'side' : 'main',\n\t\t\t\t\twidget: this.shared(widget),\n\t\t\t\t})\n\t\t\t})\n\t\t\treturn out\n\t\t},\n\n\t\t/**\n\t\t * Whether any section sits in the side column.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\ttwoColumn() {\n\t\t\treturn this.sections.some((section) => section.column === 'side')\n\t\t},\n\n\t\t/**\n\t\t * The columns to draw, each with its sections in order.\n\t\t *\n\t\t * @return {Array<{id: string, sections: Array<object>}>}\n\t\t * @spec openspec/changes/abstract-sections-widget/specs/sections-widget/spec.md#requirement-a-sections-widget-can-draw-cards-in-a-main-and-a-side-column\n\t\t */\n\t\tcolumns() {\n\t\t\tif (!this.twoColumn) {\n\t\t\t\treturn [{ id: 'main', sections: this.sections }]\n\t\t\t}\n\t\t\treturn [\n\t\t\t\t{ id: 'main', sections: this.sections.filter((section) => section.column === 'main') },\n\t\t\t\t{ id: 'side', sections: this.sections.filter((section) => section.column === 'side') },\n\t\t\t]\n\t\t},\n\t},\n\n\tbeforeUnmount() {\n\t\tthis.stopObserving()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Translate through the host app.\n\t\t *\n\t\t * @param {string} text The text.\n\t\t * @return {string}\n\t\t */\n\t\ttr(text) {\n\t\t\tif (typeof text !== 'string' || text === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text\n\t\t},\n\n\t\t/**\n\t\t * The heading id of a section.\n\t\t *\n\t\t * @param {string} key The section key.\n\t\t * @return {string}\n\t\t */\n\t\theadingId(key) {\n\t\t\treturn `cn-sections-${this.uid}-${key}`.replace(/[^\\w-]/g, '-')\n\t\t},\n\n\t\t/**\n\t\t * A child definition with the cell budget off: stacked children share\n\t\t * one cell, so an object list must not size itself to that cell.\n\t\t *\n\t\t * @param {object} widget The definition.\n\t\t * @return {object}\n\t\t */\n\t\tshared(widget) {\n\t\t\tconst content = widget.content || {}\n\t\t\tif (Object.hasOwn(content, 'fit')) {\n\t\t\t\treturn widget\n\t\t\t}\n\t\t\treturn { ...widget, content: { ...content, fit: false } }\n\t\t},\n\n\t\t/**\n\t\t * Whether a section shows its heading and takes space.\n\t\t *\n\t\t * @param {string} key The section key.\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/abstract-sections-widget/specs/sections-widget/spec.md#requirement-a-section-whose-widget-draws-nothing-stays-silent\n\t\t */\n\t\tisShown(key) {\n\t\t\treturn !this.hideEmpty || this.filled[key] === true\n\t\t},\n\n\t\t/**\n\t\t * Watch one section's host for content, so its heading appears when\n\t\t * the widget renders and stays away when it never does. A\n\t\t * MutationObserver, because most widgets mount empty and fill after a\n\t\t * fetch.\n\t\t *\n\t\t * @param {string} key The section key.\n\t\t * @param {HTMLElement|null} el The host wrapper, or null on teardown.\n\t\t * @return {void}\n\t\t */\n\t\tsetHostRef(key, el) {\n\t\t\tthis.observers = this.observers || {}\n\t\t\tif (this.observers[key]) {\n\t\t\t\tthis.observers[key].disconnect()\n\t\t\t\tdelete this.observers[key]\n\t\t\t}\n\t\t\tif (!el) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// Assign only on a change: `filled` drives the template, so an\n\t\t\t// unconditional write re-renders, mutates, and fires again.\n\t\t\tconst measure = () => {\n\t\t\t\tconst next = this.hasContent(el)\n\t\t\t\tif (this.filled[key] === next) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tthis.filled = { ...this.filled, [key]: next }\n\t\t\t}\n\t\t\tif (typeof MutationObserver === 'function') {\n\t\t\t\tconst observer = new MutationObserver(measure)\n\t\t\t\tobserver.observe(el, { childList: true, subtree: true, characterData: true })\n\t\t\t\tthis.observers[key] = observer\n\t\t\t}\n\t\t\t// Not synchronously: a ref callback runs during the patch.\n\t\t\tthis.$nextTick(measure)\n\t\t},\n\n\t\t/**\n\t\t * Whether a host rendered anything a reader would see: text (an empty\n\t\t * state counts), or a graphical or interactive element.\n\t\t *\n\t\t * @param {HTMLElement} el The host wrapper.\n\t\t * @return {boolean}\n\t\t */\n\t\thasContent(el) {\n\t\t\tif (!el) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif (el.textContent && el.textContent.trim().length > 0) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn el.querySelector('img, svg, canvas, table, input, button, select, textarea') !== null\n\t\t},\n\n\t\t/**\n\t\t * Drop every observer.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tstopObserving() {\n\t\t\tfor (const observer of Object.values(this.observers || {})) {\n\t\t\t\tobserver.disconnect()\n\t\t\t}\n\t\t\tthis.observers = {}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-sections-widget {\n\tmin-width: 0;\n}\n\n.cn-sections-widget__column {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: calc(var(--default-grid-baseline, 4px) * 4);\n\tmin-width: 0;\n}\n\n/* A main and a side column (the settings boards: 1fr and 418px, 20px apart). */\n.cn-sections-widget--two-column {\n\tdisplay: grid;\n\tgrid-template-columns: minmax(0, 1fr) minmax(280px, 418px);\n\tgap: 20px;\n\talign-items: start;\n}\n\n@media (max-width: 1024px) {\n\t.cn-sections-widget--two-column {\n\t\tgrid-template-columns: minmax(0, 1fr);\n\t}\n}\n\n/* A host page may space every <section>; these sections are spaced by the column gap only. */\n.cn-sections-widget__section {\n\tmargin: 0;\n\tmin-width: 0;\n}\n\n/* A rule above a section that follows a shown one. */\n.cn-sections-widget:not(.cn-sections-widget--cards) .cn-sections-widget__section:not(.cn-sections-widget__section--empty) ~ .cn-sections-widget__section {\n\tpadding-top: calc(var(--default-grid-baseline, 4px) * 4);\n\tborder-top: 1px solid var(--color-border);\n}\n\n/* No rule: strips that each say one thing, 8px apart. */\n.cn-sections-widget--plain .cn-sections-widget__column {\n\tgap: calc(var(--default-grid-baseline, 4px) * 2);\n}\n\n.cn-sections-widget.cn-sections-widget--plain .cn-sections-widget__section:not(.cn-sections-widget__section--empty) ~ .cn-sections-widget__section {\n\tpadding-top: 0;\n\tborder-top: 0;\n}\n\n/*\n * An empty section keeps its host mounted (display: none still mounts it, so\n * the widget fetches and can fill later) but takes no space, gap or rule.\n */\n.cn-sections-widget__section--empty {\n\tdisplay: none;\n}\n\n.cn-sections-widget__heading {\n\tmargin: 0 0 calc(var(--default-grid-baseline, 4px) * 2);\n\tfont-size: 16px;\n\tfont-weight: 600;\n\tcolor: var(--color-main-text);\n}\n\n.cn-sections-widget__description {\n\tmargin: calc(var(--default-grid-baseline, 4px) * -1) 0 calc(var(--default-grid-baseline, 4px) * 3);\n\tcolor: var(--color-text-maxcontrast);\n}\n\n/* One card per section. */\n.cn-sections-widget--cards .cn-sections-widget__column {\n\tgap: 20px;\n}\n\n.cn-sections-widget--cards .cn-sections-widget__section {\n\tbackground: var(--color-main-background);\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--cn-board-card-radius, var(--border-radius-large, 12px));\n\tpadding: var(--cn-board-card-padding, 16px 20px);\n}\n\n.cn-sections-widget--cards .cn-sections-widget__heading {\n\tfont-size: 18px;\n\tmargin-bottom: 12px;\n}\n</style>\n"],"names":["_createElementBlock","_normalizeClass","_openBlock","_renderList","_toDisplayString","_createElementVNode"],"mappings":";;;;;;AA0CS,EAAA,KAAC,EAAC,iCAAiC;;;;;;uBA/BzCA;AACD,IAAA,KAAC,kBAAA,oBAAA,EAAA;gCAC+B,QAAA,CAAA,KAAA;qCAAA,QAAA,CAAA,SAAA;gCAAA,QAAA,CAAA,KAAA;;iBAKrB,EAAC,oBAAoB;;sBAC/BA,8CACiB,QAAO,CAAA,OAAA,EAAA,CAAjB,MAAM,KAAA;2BADbA;QAEC,GAAG,EAAE,MAAM,CAAC,EAAE;AACd,QAAA,KAAI,EAACC,cAAA,CAAA,CAAA,4BAA4B,EACK,CAAA,4BAAA,EAAA,MAAM,CAAC,EAAC,CAAA,CAAA,CAAA,CAAA;;AAC9C,SAAAC,SAAA,CAAA,IAAA,CAAA,EAAAF,kBAAA,SACkB,EAAA,IAAA,EAAAG,UAAA,CAAA,MAAM,CAAC,QAAQ,GAA1B,OAAO,KAAA;+BADdH;YAEC,GAAI,EAAC,OAAO,CAAC,GAAG;iBACZ,kBAAC,6BAA4B,EAAA,EAAA,oCAAA,EAAA,CACV,gBAAA,CAAA,OAAA,CAAA,GAAA,CAAA,EAAA,CAAA,CAAA;AACvB,YAAA,iBAAC,EAAA,OAAA,CAAA,KAAA,IAAA,gBAAA,CAAA,OAAA,CAAA,GAAA,IAAA,QAAA,CAAA,SAAA,CAAA,OAAA,CAAA,GAAA,IAAA,SAAA;YACD,aAAW,EAAA,CAAA,WAAA,EAAgB,OAAO,CAAC,GAAG,CAAA,CAAA;;AAKtC,YAAA,CAAA,OAAA,CAAA,KAAA,IAAA,gBAAA,CAAA,OAAA,CAAA,GAAA,CAAA;8BADDA,kBAKG,CAAA,IAAA,EAAA;;AAHD,kBAAA,EAAE,EAAE,QAAA,CAAA,SAAS,CAAC,OAAO,CAAC,GAAG,CAAA;AAC1B,kBAAA,KAAK,EAAC,6BAA6B;AAChC,iBAAA,EAAAI,eAAA,CAAA,OAAO,CAAC,KAAK,CAAA,EAAA,CAAA,oBAAA,UAAA,CAAA;;AAGV,YAAA,CAAA,OAAO,CAAC,WAAW,IAAI,gBAAO,CAAC,OAAO,CAAC,GAAG,CAAA;AADjD,iBAAAF,SAAA,EAAA,EAAAF,kBAAA,CAII,GAJJ,EAAA,UAII,EADCI,eAAA,CAAA,OAAM,CAAC,WAAW,CAAA,EAAA,CAAA,YAAA;;YAKrBC;;cAAI,GAAG,EAAA,CAAG,EAAE,KAAK,QAAA,CAAA,UAAE,CAAA,OAAA,CAAA,GAAA,EAAA,EAAA,CAAA;AAAA,cAAA,KAAA,EAAA,0BAAA;;;gBAElB,MAAM,EAAE,OAAO,CAAC,MAAM;AACvB,gBAAA,MAAM,EAAC,MAAM;AACZ,gBAAA,QAAQ,EAAE,MAAI,CAAA,QAAA;AACd,gBAAA,MAAM,EAAE,MAAI,CAAA,UAAA;AACZ,gBAAA,UAAU,EAAE,MAAI,CAAA,UAAA;AAChB,gBAAA,YAAY,EAAE,MAAY,CAAA,YAAA;AAC1B,gBAAA,QAAQ,EAAE,MAAQ,CAAA,QAAA;AAClB,gBAAA,MAAM,EAAE,MAAM,CAAA,MAAA;AACd,gBAAA,KAAE,EAAA,MAAA,CAAA,KAAA;AACF,gBAAA,OAAK,EAAA,MAAA,CAAA,OAAA;AACL,gBAAA,kBAAA,EAAA,MAAA,CAAA,kBAAA;AACD,gBAAA,UAAA,EAAA,MAAA,CAAA,UAAA;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"CnSectionsWidget.vue3.js","sources":["../../../../src/components/CnSectionsWidget/CnSectionsWidget.vue?vue&type=template&id=28baf01e&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<div\n\t\tclass=\"cn-sections-widget\"\n\t\t:class=\"{\n\t\t\t'cn-sections-widget--cards': cards,\n\t\t\t'cn-sections-widget--two-column': twoColumn,\n\t\t\t'cn-sections-widget--grid': gridColumns > 1,\n\t\t\t'cn-sections-widget--plain': plain,\n\t\t}\"\n\t\tdata-testid=\"cn-sections-widget\">\n\t\t<div\n\t\t\tv-for=\"column in columns\"\n\t\t\t:key=\"column.id\"\n\t\t\tclass=\"cn-sections-widget__column\"\n\t\t\t:class=\"`cn-sections-widget__column--${column.id}`\"\n\t\t\t:style=\"gridColumns > 1 ? { '--cn-sections-columns': gridColumns } : null\">\n\t\t\t<section\n\t\t\t\tv-for=\"section in column.sections\"\n\t\t\t\t:key=\"section.key\"\n\t\t\t\tclass=\"cn-sections-widget__section\"\n\t\t\t\t:class=\"{ 'cn-sections-widget__section--empty': !isShown(section.key) }\"\n\t\t\t\t:aria-labelledby=\"section.title && isShown(section.key) ? headingId(section.key) : undefined\"\n\t\t\t\t:data-testid=\"`cn-section-${section.key}`\">\n\t\t\t\t<!-- The heading waits for content: a widget type that does not\n\t\t\t\t resolve renders nothing, and a heading over nothing is a\n\t\t\t\t label on a void. -->\n\t\t\t\t<h3\n\t\t\t\t\tv-if=\"section.title && isShown(section.key)\"\n\t\t\t\t\t:id=\"headingId(section.key)\"\n\t\t\t\t\tclass=\"cn-sections-widget__heading\">\n\t\t\t\t\t{{ section.title }}\n\t\t\t\t</h3>\n\t\t\t\t<p\n\t\t\t\t\tv-if=\"section.description && isShown(section.key)\"\n\t\t\t\t\tclass=\"cn-sections-widget__description\">\n\t\t\t\t\t{{ section.description }}\n\t\t\t\t</p>\n\t\t\t\t<!-- The host always mounts, also while the section reads as\n\t\t\t\t empty: a widget that has not mounted cannot fetch, so it\n\t\t\t\t could never gain the content that would show it. -->\n\t\t\t\t<div :ref=\"(el) => setHostRef(section.key, el)\" class=\"cn-sections-widget__body\">\n\t\t\t\t\t<CnDetailWidgetHost\n\t\t\t\t\t\t:widget=\"section.widget\"\n\t\t\t\t\t\tchrome=\"bare\"\n\t\t\t\t\t\t:objectId=\"objectId\"\n\t\t\t\t\t\t:object=\"objectData\"\n\t\t\t\t\t\t:objectType=\"objectType\"\n\t\t\t\t\t\t:schemaObject=\"schemaObject\"\n\t\t\t\t\t\t:register=\"register\"\n\t\t\t\t\t\t:schema=\"schema\"\n\t\t\t\t\t\t:store=\"store\"\n\t\t\t\t\t\t:surface=\"surface\"\n\t\t\t\t\t\t:integrationContext=\"integrationContext\"\n\t\t\t\t\t\t:cnRegistry=\"cnRegistry\"\n\t\t\t\t\t\t:availableWidgets=\"availableWidgets\" />\n\t\t\t\t</div>\n\t\t\t</section>\n\t\t</div>\n\t\t<p v-if=\"footnote\" class=\"cn-sections-widget__footnote\" data-testid=\"cn-sections-widget-footnote\">\n\t\t\t{{ footnote }}\n\t\t</p>\n\t</div>\n</template>\n\n<script>\nimport CnDetailWidgetHost from '../CnDetailWidgetHost/CnDetailWidgetHost.vue'\n\n/** Counter for unique heading ids across instances. */\nlet instanceCounter = 0\n\n/**\n * CnSectionsWidget — the `sections` widget: several widgets in one place,\n * each under its own heading.\n *\n * A tab strip binds one widget per tab, and a tab often holds more than one\n * thing: the documents list and the case folder (DqZaak), a portal's details\n * form, house style and sign-in (PtPortalInstellingen). This widget stacks\n * them as sections under headings with a rule between them (the default), or\n * as one card per section (`cards: true`), optionally in a main and a side\n * column (`column: \"side\"` on a section). A single column of separate titled\n * cards in a tab is the tabs widget's own `widgetIds` stack; this widget is\n * for headed sections, and for the two-column card grid a settings tab draws.\n *\n * ```json\n * {\n * \"type\": \"sections\",\n * \"content\": {\n * \"sections\": [\n * { \"title\": \"Documenten\", \"widgetId\": \"case-documents\" },\n * { \"title\": \"Zaakmap\", \"widget\": { \"type\": \"files\" } }\n * ]\n * }\n * }\n * ```\n *\n * A section names its widget inline (`widget`) or by id (`widgetId`, a\n * sibling definition on the same surface). Children render through\n * `CnDetailWidgetHost` in bare mode, the dispatch a tab panel uses, so a child\n * behaves here as it does in a tab. A section whose widget draws nothing\n * shows no heading and takes no space (`hideEmpty`, default true). Stacked\n * children share one cell, so an object list in a section renders every\n * fetched row (`content.fit: false`) unless its definition says otherwise.\n *\n * @spec openspec/changes/abstract-sections-widget/specs/sections-widget/spec.md\n */\nexport default {\n\tname: 'CnSectionsWidget',\n\n\tcomponents: { CnDetailWidgetHost },\n\n\tinject: {\n\t\tcnTranslate: { default: () => (key) => key },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * The widget configuration.\n\t\t *\n\t\t * - `sections` — `[{ key?, title?, description?, column?, widget? | widgetId? }]`, in order.\n\t\t * `label` is accepted as an alias of `title`; `column` is `main` (default) or `side`.\n\t\t * - `cards` — draw each section as its own card (default false: headings and a rule between sections).\n\t\t * - `hideEmpty` — hide the heading and the space of a section whose widget drew nothing (default true).\n\t\t * - `dividers` — draw a rule between sections (default true). `false` stacks them 8px apart with no rule,\n\t\t * for a row of strips that each say one thing (a record's banners).\n\t\t *\n\t\t * - `columns` — lay the sections out side by side in this many columns (2 to 4), one column on a narrow\n\t\t * screen. Ignored when a section asks for the side column.\n\t\t * - `footnote` — a line of muted text under the sections.\n\t\t *\n\t\t * @type {{sections?: Array<object>, cards?: boolean, hideEmpty?: boolean, dividers?: boolean, columns?: number, footnote?: string}}\n\t\t */\n\t\tcontent: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** Sibling widget definitions on the surface, for `widgetId` sections. */\n\t\tavailableWidgets: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** The bound record's id. */\n\t\tobjectId: {\n\t\t\ttype: [String, Number],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The loaded record, or null while it is still being fetched. */\n\t\tobjectData: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The resolved object-type slug. */\n\t\tobjectType: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The resolved JSON Schema object, needed by a `data` child. */\n\t\tschemaObject: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** OpenRegister register slug of the surface. */\n\t\tregister: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** OpenRegister schema slug of the surface. */\n\t\tschema: {\n\t\t\ttype: [String, Object],\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The effective object store. */\n\t\tstore: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** Rendering surface forwarded to the children. */\n\t\tsurface: {\n\t\t\ttype: String,\n\t\t\tdefault: 'detail-page',\n\t\t},\n\n\t\t/** Object context forwarded to integration children. */\n\t\tintegrationContext: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** The consumer's component registry, for custom child widget types. */\n\t\tcnRegistry: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\t},\n\n\tdata() {\n\t\tinstanceCounter += 1\n\t\treturn {\n\t\t\tuid: instanceCounter,\n\t\t\t/**\n\t\t\t * Section key to \"has this section rendered anything\".\n\t\t\t *\n\t\t\t * @type {Record<string, boolean>}\n\t\t\t */\n\t\t\tfilled: {},\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * Whether each section is its own card.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\tcards() {\n\t\t\treturn this.content?.cards === true\n\t\t},\n\n\t\t/**\n\t\t * Whether the sections stack with no rule between them.\n\t\t *\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/dq-abstract-record-widgets/specs/record-widgets/spec.md#requirement-sections-can-stack-without-dividers\n\t\t */\n\t\tplain() {\n\t\t\treturn !this.cards && this.content?.dividers === false\n\t\t},\n\n\t\t/**\n\t\t * Whether an empty section hides.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\thideEmpty() {\n\t\t\treturn this.content?.hideEmpty !== false\n\t\t},\n\n\t\t/**\n\t\t * The sections to render: a stable key, a translated title, the column\n\t\t * and the widget definition. A section whose widget cannot be found is\n\t\t * dropped (with a development warning for an unknown id).\n\t\t *\n\t\t * @return {Array<{key: string, title: string, description: string, column: string, widget: object}>}\n\t\t * @spec openspec/changes/abstract-sections-widget/specs/sections-widget/spec.md#requirement-a-sections-widget-stacks-configured-widgets-under-headings\n\t\t */\n\t\tsections() {\n\t\t\tconst raw = Array.isArray(this.content?.sections) ? this.content.sections : []\n\t\t\tconst out = []\n\t\t\traw.forEach((entry, index) => {\n\t\t\t\tif (!entry || typeof entry !== 'object') {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tlet widget = entry.widget && typeof entry.widget === 'object' ? entry.widget : null\n\t\t\t\tif (!widget && typeof entry.widgetId === 'string' && entry.widgetId !== '') {\n\t\t\t\t\twidget = this.availableWidgets.find((w) => w && w.id === entry.widgetId) || null\n\t\t\t\t\tif (!widget) {\n\t\t\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\t\t\tconsole.warn(`[CnSectionsWidget] section widgetId \"${entry.widgetId}\" names no widget on this surface; section skipped.`)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (!widget || !widget.type) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tout.push({\n\t\t\t\t\tkey: String(entry.key || widget.id || entry.widgetId || `${widget.type}-${index}`),\n\t\t\t\t\ttitle: this.tr(entry.title || entry.label || ''),\n\t\t\t\t\tdescription: this.tr(entry.description || ''),\n\t\t\t\t\tcolumn: entry.column === 'side' ? 'side' : 'main',\n\t\t\t\t\twidget: this.shared(widget),\n\t\t\t\t})\n\t\t\t})\n\t\t\treturn out\n\t\t},\n\n\t\t/**\n\t\t * Whether any section sits in the side column.\n\t\t *\n\t\t * @return {boolean}\n\t\t */\n\t\ttwoColumn() {\n\t\t\treturn this.sections.some((section) => section.column === 'side')\n\t\t},\n\n\t\t/**\n\t\t * How many columns the sections are laid out in side by side: the\n\t\t * configured `columns`, capped at four; 1 (stacked) without it or\n\t\t * when a section asks for the side column.\n\t\t *\n\t\t * @return {number}\n\t\t * @spec openspec/changes/sections-card-grid/specs/sections-card-grid/spec.md#requirement-a-sections-widget-can-lay-its-cards-out-in-columns\n\t\t */\n\t\tgridColumns() {\n\t\t\tconst n = Math.floor(Number(this.content?.columns))\n\t\t\tif (!Number.isFinite(n) || n < 2 || this.twoColumn) {\n\t\t\t\treturn 1\n\t\t\t}\n\t\t\treturn Math.min(n, 4)\n\t\t},\n\n\t\t/**\n\t\t * The translated footnote under the sections, or ''.\n\t\t *\n\t\t * @return {string}\n\t\t * @spec openspec/changes/sections-card-grid/specs/sections-card-grid/spec.md#requirement-a-sections-widget-can-close-with-a-footnote\n\t\t */\n\t\tfootnote() {\n\t\t\treturn this.tr(this.content?.footnote || '')\n\t\t},\n\n\t\t/**\n\t\t * The columns to draw, each with its sections in order.\n\t\t *\n\t\t * @return {Array<{id: string, sections: Array<object>}>}\n\t\t * @spec openspec/changes/abstract-sections-widget/specs/sections-widget/spec.md#requirement-a-sections-widget-can-draw-cards-in-a-main-and-a-side-column\n\t\t */\n\t\tcolumns() {\n\t\t\tif (!this.twoColumn) {\n\t\t\t\treturn [{ id: 'main', sections: this.sections }]\n\t\t\t}\n\t\t\treturn [\n\t\t\t\t{ id: 'main', sections: this.sections.filter((section) => section.column === 'main') },\n\t\t\t\t{ id: 'side', sections: this.sections.filter((section) => section.column === 'side') },\n\t\t\t]\n\t\t},\n\t},\n\n\tbeforeUnmount() {\n\t\tthis.stopObserving()\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Translate through the host app.\n\t\t *\n\t\t * @param {string} text The text.\n\t\t * @return {string}\n\t\t */\n\t\ttr(text) {\n\t\t\tif (typeof text !== 'string' || text === '') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text\n\t\t},\n\n\t\t/**\n\t\t * The heading id of a section.\n\t\t *\n\t\t * @param {string} key The section key.\n\t\t * @return {string}\n\t\t */\n\t\theadingId(key) {\n\t\t\treturn `cn-sections-${this.uid}-${key}`.replace(/[^\\w-]/g, '-')\n\t\t},\n\n\t\t/**\n\t\t * A child definition with the cell budget off: stacked children share\n\t\t * one cell, so an object list must not size itself to that cell.\n\t\t *\n\t\t * @param {object} widget The definition.\n\t\t * @return {object}\n\t\t */\n\t\tshared(widget) {\n\t\t\tconst content = widget.content || {}\n\t\t\tif (Object.hasOwn(content, 'fit')) {\n\t\t\t\treturn widget\n\t\t\t}\n\t\t\treturn { ...widget, content: { ...content, fit: false } }\n\t\t},\n\n\t\t/**\n\t\t * Whether a section shows its heading and takes space.\n\t\t *\n\t\t * @param {string} key The section key.\n\t\t * @return {boolean}\n\t\t * @spec openspec/changes/abstract-sections-widget/specs/sections-widget/spec.md#requirement-a-section-whose-widget-draws-nothing-stays-silent\n\t\t */\n\t\tisShown(key) {\n\t\t\treturn !this.hideEmpty || this.filled[key] === true\n\t\t},\n\n\t\t/**\n\t\t * Watch one section's host for content, so its heading appears when\n\t\t * the widget renders and stays away when it never does. A\n\t\t * MutationObserver, because most widgets mount empty and fill after a\n\t\t * fetch.\n\t\t *\n\t\t * @param {string} key The section key.\n\t\t * @param {HTMLElement|null} el The host wrapper, or null on teardown.\n\t\t * @return {void}\n\t\t */\n\t\tsetHostRef(key, el) {\n\t\t\tthis.observers = this.observers || {}\n\t\t\tif (this.observers[key]) {\n\t\t\t\tthis.observers[key].disconnect()\n\t\t\t\tdelete this.observers[key]\n\t\t\t}\n\t\t\tif (!el) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// Assign only on a change: `filled` drives the template, so an\n\t\t\t// unconditional write re-renders, mutates, and fires again.\n\t\t\tconst measure = () => {\n\t\t\t\tconst next = this.hasContent(el)\n\t\t\t\tif (this.filled[key] === next) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tthis.filled = { ...this.filled, [key]: next }\n\t\t\t}\n\t\t\tif (typeof MutationObserver === 'function') {\n\t\t\t\tconst observer = new MutationObserver(measure)\n\t\t\t\tobserver.observe(el, { childList: true, subtree: true, characterData: true })\n\t\t\t\tthis.observers[key] = observer\n\t\t\t}\n\t\t\t// Not synchronously: a ref callback runs during the patch.\n\t\t\tthis.$nextTick(measure)\n\t\t},\n\n\t\t/**\n\t\t * Whether a host rendered anything a reader would see: text (an empty\n\t\t * state counts), or a graphical or interactive element.\n\t\t *\n\t\t * @param {HTMLElement} el The host wrapper.\n\t\t * @return {boolean}\n\t\t */\n\t\thasContent(el) {\n\t\t\tif (!el) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif (el.textContent && el.textContent.trim().length > 0) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\treturn el.querySelector('img, svg, canvas, table, input, button, select, textarea') !== null\n\t\t},\n\n\t\t/**\n\t\t * Drop every observer.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tstopObserving() {\n\t\t\tfor (const observer of Object.values(this.observers || {})) {\n\t\t\t\tobserver.disconnect()\n\t\t\t}\n\t\t\tthis.observers = {}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-sections-widget {\n\tmin-width: 0;\n}\n\n.cn-sections-widget__column {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: calc(var(--default-grid-baseline, 4px) * 4);\n\tmin-width: 0;\n}\n\n/* A main and a side column (the settings boards: 1fr and 418px, 20px apart). */\n.cn-sections-widget--two-column {\n\tdisplay: grid;\n\tgrid-template-columns: minmax(0, 1fr) minmax(280px, 418px);\n\tgap: 20px;\n\talign-items: start;\n}\n\n@media (max-width: 1024px) {\n\t.cn-sections-widget--two-column {\n\t\tgrid-template-columns: minmax(0, 1fr);\n\t}\n}\n\n/* Sections side by side (`columns`): at most that many to a row, each at\n least 240px wide, so a narrow cell wraps them onto more rows. */\n.cn-sections-widget.cn-sections-widget--grid .cn-sections-widget__column {\n\t--cn-sections-gap: 14px;\n\tdisplay: grid;\n\tgrid-template-columns: repeat(auto-fit, minmax(min(100%, max(240px, calc((100% - (var(--cn-sections-columns, 2) - 1) * var(--cn-sections-gap)) / var(--cn-sections-columns, 2)))), 1fr));\n\tgap: var(--cn-sections-gap);\n\talign-items: start;\n}\n\n.cn-sections-widget__footnote {\n\tmargin: calc(var(--default-grid-baseline, 4px) * 3) 0 0;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 13px;\n}\n\n/* A host page may space every <section>; these sections are spaced by the column gap only. */\n.cn-sections-widget__section {\n\tmargin: 0;\n\tmin-width: 0;\n}\n\n/* A rule above a section that follows a shown one. */\n.cn-sections-widget:not(.cn-sections-widget--cards) .cn-sections-widget__section:not(.cn-sections-widget__section--empty) ~ .cn-sections-widget__section {\n\tpadding-top: calc(var(--default-grid-baseline, 4px) * 4);\n\tborder-top: 1px solid var(--color-border);\n}\n\n/* No rule: strips that each say one thing, 8px apart. */\n.cn-sections-widget--plain .cn-sections-widget__column {\n\tgap: calc(var(--default-grid-baseline, 4px) * 2);\n}\n\n.cn-sections-widget.cn-sections-widget--plain .cn-sections-widget__section:not(.cn-sections-widget__section--empty) ~ .cn-sections-widget__section {\n\tpadding-top: 0;\n\tborder-top: 0;\n}\n\n/*\n * An empty section keeps its host mounted (display: none still mounts it, so\n * the widget fetches and can fill later) but takes no space, gap or rule.\n */\n.cn-sections-widget__section--empty {\n\tdisplay: none;\n}\n\n.cn-sections-widget__heading {\n\tmargin: 0 0 calc(var(--default-grid-baseline, 4px) * 2);\n\tfont-size: 16px;\n\tfont-weight: 600;\n\tcolor: var(--color-main-text);\n}\n\n.cn-sections-widget__description {\n\tmargin: calc(var(--default-grid-baseline, 4px) * -1) 0 calc(var(--default-grid-baseline, 4px) * 3);\n\tcolor: var(--color-text-maxcontrast);\n}\n\n/* One card per section. */\n.cn-sections-widget--cards .cn-sections-widget__column {\n\tgap: 20px;\n}\n\n.cn-sections-widget--cards .cn-sections-widget__section {\n\tbackground: var(--color-main-background);\n\tborder: 1px solid var(--color-border);\n\tborder-radius: var(--cn-board-card-radius, var(--border-radius-large, 12px));\n\tpadding: var(--cn-board-card-padding, 16px 20px);\n}\n\n.cn-sections-widget--cards .cn-sections-widget__heading {\n\tfont-size: 18px;\n\tmargin-bottom: 12px;\n}\n</style>\n"],"names":["_createElementBlock","_normalizeClass","_normalizeStyle","_openBlock","_Fragment","_renderList","_toDisplayString","_createElementVNode"],"mappings":"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"}
@@ -79,6 +79,34 @@ const FLOW_LAYOUT_MARGIN = 60;
79
79
  */
80
80
  const FLOW_LAYOUT_TOP = 96;
81
81
 
82
+ /**
83
+ * Vertical distance between layers when the run reads top to bottom
84
+ * (`direction: 'down'`), in canvas pixels: a step card (~62px) plus room for
85
+ * the line and its arrowhead, as DqFlow draws its steps 100 to 110px apart.
86
+ *
87
+ * @type {number}
88
+ */
89
+ const FLOW_LAYOUT_LAYER_HEIGHT_DOWN = 110;
90
+
91
+ /**
92
+ * Where a node sits, from its layer (run order) and its lane (branch).
93
+ *
94
+ * `right` (default): layers are columns left to right, lanes are rows.
95
+ * `down`: layers are rows top to bottom, lanes are columns.
96
+ *
97
+ * @param {number} layer Zero-based layer.
98
+ * @param {number} lane Zero-based lane.
99
+ * @param {string} direction `right` or `down`.
100
+ * @param {number} top The y of the first layer or row.
101
+ * @return {{x: number, y: number}} The point.
102
+ */
103
+ function cellPoint(layer, lane, direction, top) {
104
+ if (direction === 'down') {
105
+ return { x: FLOW_LAYOUT_MARGIN + (lane * FLOW_LAYOUT_COLUMN_WIDTH), y: top + (layer * FLOW_LAYOUT_LAYER_HEIGHT_DOWN) }
106
+ }
107
+ return { x: FLOW_LAYOUT_MARGIN + (layer * FLOW_LAYOUT_COLUMN_WIDTH), y: top + (lane * FLOW_LAYOUT_ROW_HEIGHT) }
108
+ }
109
+
82
110
  /**
83
111
  * The position a node carries, in either of the two spellings in circulation.
84
112
  *
@@ -324,9 +352,11 @@ function orderRows(nodes, lines, columns) {
324
352
  * @param {Array<object>} nodes The flow's nodes.
325
353
  * @param {Array<{source: string, target: string}>} lines The expanded edges.
326
354
  * @param {Array<string>} startIds The ids a run enters through.
355
+ * @param {object} [options] `{ direction }`: `right` (default) runs left to
356
+ * right, `down` runs top to bottom with branches side by side.
327
357
  * @return {Array<object>} New node objects carrying computed positions.
328
358
  */
329
- function layoutFlowNodes(nodes, lines, startIds) {
359
+ function layoutFlowNodes(nodes, lines, startIds, options = {}) {
330
360
  const list = Array.isArray(nodes) ? nodes : [];
331
361
  if (!list.length) {
332
362
  return []
@@ -336,8 +366,7 @@ function layoutFlowNodes(nodes, lines, startIds) {
336
366
  const rows = orderRows(list, lines, columns);
337
367
 
338
368
  return list.map((node) => {
339
- const x = FLOW_LAYOUT_MARGIN + (columns.get(node.id) * FLOW_LAYOUT_COLUMN_WIDTH);
340
- const y = FLOW_LAYOUT_TOP + (rows.get(node.id) * FLOW_LAYOUT_ROW_HEIGHT);
369
+ const { x, y } = cellPoint(columns.get(node.id), rows.get(node.id), options.direction, FLOW_LAYOUT_TOP);
341
370
  return { ...node, x, y, position: { x, y } }
342
371
  })
343
372
  }
@@ -359,9 +388,10 @@ function layoutFlowNodes(nodes, lines, startIds) {
359
388
  * @param {Array<object>} nodes The flow's nodes, some positioned and some not.
360
389
  * @param {Array<{source: string, target: string}>} lines The expanded edges.
361
390
  * @param {Array<string>} startIds The ids a run enters through.
391
+ * @param {object} [options] `{ direction }`, as {@link layoutFlowNodes}.
362
392
  * @return {Array<object>} The same array shape, loose nodes placed beneath.
363
393
  */
364
- function placeLooseNodes(nodes, lines, startIds) {
394
+ function placeLooseNodes(nodes, lines, startIds, options = {}) {
365
395
  const list = Array.isArray(nodes) ? nodes : [];
366
396
  const loose = list.filter((node) => readNodePoint(node) === null);
367
397
  if (!loose.length) {
@@ -395,11 +425,10 @@ function placeLooseNodes(nodes, lines, startIds) {
395
425
  const row = rowsTaken.get(column) || 0;
396
426
  rowsTaken.set(column, row + 1);
397
427
 
398
- const x = FLOW_LAYOUT_MARGIN + (column * FLOW_LAYOUT_COLUMN_WIDTH);
399
- const y = top + (row * FLOW_LAYOUT_ROW_HEIGHT);
428
+ const { x, y } = cellPoint(column, row, options.direction, top);
400
429
  return { ...node, x, y, position: { x, y } }
401
430
  })
402
431
  }
403
432
 
404
- export { FLOW_LAYOUT_COLUMN_WIDTH, FLOW_LAYOUT_MARGIN, FLOW_LAYOUT_ROW_HEIGHT, FLOW_LAYOUT_TOP, layoutFlowNodes, needsFullLayout, placeLooseNodes, readNodePoint };
433
+ export { FLOW_LAYOUT_COLUMN_WIDTH, FLOW_LAYOUT_LAYER_HEIGHT_DOWN, FLOW_LAYOUT_MARGIN, FLOW_LAYOUT_ROW_HEIGHT, FLOW_LAYOUT_TOP, layoutFlowNodes, needsFullLayout, placeLooseNodes, readNodePoint };
405
434
  //# sourceMappingURL=flowGraphLayout.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"flowGraphLayout.js","sources":["../../../src/composables/flowGraphLayout.js"],"sourcesContent":["/**\n * SPDX-License-Identifier: EUPL-1.2\n * SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>\n */\n\n/**\n * Pure layout for flow graphs whose nodes carry no usable positions.\n *\n * A flow declared in a schema's `x-openregister-flows` (and any generated or\n * imported flow) ships nodes with no coordinates at all. Rendered as-is, all\n * of them land on one point: the canvas shows one node's worth of pixels with\n * seventeen more underneath, and no amount of fit-view or zoom can separate\n * them. The run replay's per-node badges paint correctly — onto a pile nobody\n * can read.\n *\n * The functions here are PURE and deterministic: same nodes, same lines, same\n * start ids — same coordinates, every time. They never touch a store, never\n * read the DOM, and never mutate their inputs, which is what makes them safe\n * to run on a published (graph-locked) flow: the caller decides what to do\n * with the returned array, and a viewer that only renders it has mutated\n * nothing. This is also why the layout lives here and not in CnGraphCanvas:\n * per ADR-065 the canvas renders the geometry it is given and never invents\n * any.\n *\n * The algorithm is a small layered pass, on purpose. dagre and friends are\n * real dependencies with real weight, the in-app CSP allows no CDN fetch, and\n * nothing in the dependency tree ships a layout engine — while the layouts a\n * flow needs (left-to-right, branches fanned out, loops tolerated) fit in the\n * three steps below:\n *\n * 1. LAYER: longest-path columns over the graph with its back edges\n * dropped, so triggers sit left and every forward edge points right.\n * Back edges are found by depth-first search; dropping them is what\n * makes a loop terminate instead of pushing its members ever further\n * right (or hanging).\n * 2. ORDER: one barycenter pass per column — a node sits near the average\n * row of the nodes that lead to it, which keeps parallel branches apart\n * instead of braided.\n * 3. PLACE: fixed spacing on a grid. One column and row per node, so no\n * two nodes can overlap by construction.\n */\n\n/**\n * Horizontal distance between layout columns, in canvas pixels.\n *\n * Sized against the node card (~230px wide) so the edge between two adjacent\n * columns has room for its arrowhead and result badge.\n *\n * @type {number}\n */\nexport const FLOW_LAYOUT_COLUMN_WIDTH = 260\n\n/**\n * Vertical distance between layout rows, in canvas pixels.\n *\n * About a card high plus a gap, for the same reason as the column width.\n *\n * @type {number}\n */\nexport const FLOW_LAYOUT_ROW_HEIGHT = 170\n\n/**\n * Left margin before the first column, in canvas pixels.\n *\n * @type {number}\n */\nexport const FLOW_LAYOUT_MARGIN = 60\n\n/**\n * Top margin above the first row, in canvas pixels.\n *\n * The editor's toolbar FLOATS over the canvas (position: absolute, ~52px\n * tall). A node laid out at a uniform 60px margin landed UNDER it, and an\n * overlaid node is not merely ugly — it is unreachable: the toolbar swallows\n * the pointer. Measured live: Playwright reported `cn-flow-detail__toolbar\n * subtree intercepts pointer events` on a node behind it.\n *\n * @type {number}\n */\nexport const FLOW_LAYOUT_TOP = 96\n\n/**\n * The position a node carries, in either of the two spellings in circulation.\n *\n * The server stores `position: {x, y}`; the editor writes flat `x`/`y` in\n * memory. Reading only one spelling is the bug that once piled every saved\n * flow onto the origin, so both are read here, flat first — the same order\n * `useFlowStore.hasPosition()` and the canvas mapping use.\n *\n * A node AT the origin still counts as positioned: (0, 0) is a legitimate\n * place to park something. Whether a WHOLE GRAPH of identical points counts\n * is a different question, answered by {@link needsFullLayout}.\n *\n * @param {object} node A flow node.\n * @return {{x: number, y: number}|null} Its point, or null when it has none.\n */\nexport function readNodePoint(node) {\n\tconst x = Number(node?.x ?? node?.position?.x)\n\tif (!Number.isFinite(x)) {\n\t\treturn null\n\t}\n\n\tconst y = Number(node?.y ?? node?.position?.y)\n\treturn { x, y: Number.isFinite(y) ? y : 0 }\n}\n\n/**\n * Whether the whole graph needs laying out from scratch.\n *\n * True in exactly two situations:\n *\n * - NO node carries a position. Declared, generated and imported flows all\n * arrive like this.\n * - Every node that does carry a position sits on the SAME point, and at\n * least two do. That is not an arrangement, it is the same pile with\n * coordinates written down — an importer that stamps `{x: 0, y: 0}` on\n * every node produces it, and it must not be mistaken for a deliberate\n * layout. A single node parked at one point (the origin included) stays\n * where its author put it.\n *\n * Anything else — at least two distinct positioned points — is treated as\n * arranged: someone chose those places, and a layout that second-guesses them\n * throws that choice away. Position-less nodes in an arranged flow are the\n * caller's problem; {@link placeLooseNodes} slots them beneath.\n *\n * @param {Array<object>} nodes The flow's nodes.\n * @return {boolean} True when nothing about the current positions is worth keeping.\n */\nexport function needsFullLayout(nodes) {\n\tconst list = Array.isArray(nodes) ? nodes : []\n\tif (!list.length) {\n\t\treturn false\n\t}\n\n\tconst points = []\n\tfor (const node of list) {\n\t\tconst point = readNodePoint(node)\n\t\tif (point !== null) {\n\t\t\tpoints.push(point)\n\t\t}\n\t}\n\n\tif (!points.length) {\n\t\treturn true\n\t}\n\n\treturn points.length >= 2\n\t\t&& points.every((p) => p.x === points[0].x && p.y === points[0].y)\n}\n\n/**\n * Column per node id: longest path from the start nodes, back edges dropped.\n *\n * Depth-first search from the start ids classifies back edges — an edge whose\n * target is still on the current DFS stack closes a loop. Those edges are\n * excluded from layering, which is what lets a flow with a loop (send back to\n * the submitter, wait, check again) lay out finitely: without it, longest\n * path around a cycle is unbounded. The back edge is still DRAWN, of course —\n * only the layering ignores it.\n *\n * Nodes the start ids cannot reach go one column past everything — never at\n * the origin, where they would hide under the entry points.\n *\n * @param {Array<object>} nodes The flow's nodes.\n * @param {Array<{source: string, target: string}>} lines The expanded edges.\n * @param {Array<string>} startIds The ids a run enters through.\n * @return {Map<string, number>} Node id to zero-based column.\n */\nfunction layerColumns(nodes, lines, startIds) {\n\tconst ids = new Set(nodes.map((node) => node.id))\n\tconst outgoing = new Map()\n\tfor (const line of lines) {\n\t\tif (line.source === line.target || !ids.has(line.source) || !ids.has(line.target)) {\n\t\t\tcontinue\n\t\t}\n\t\tif (!outgoing.has(line.source)) {\n\t\t\toutgoing.set(line.source, [])\n\t\t}\n\t\toutgoing.get(line.source).push(line.target)\n\t}\n\n\t// DFS: classify back edges and record a reverse postorder (a topological\n\t// order of the graph minus its back edges). Iterative, because a flow's\n\t// depth is user-controlled and the call stack is not.\n\tconst seen = new Set()\n\tconst onStack = new Set()\n\tconst forward = new Map()\n\tconst postorder = []\n\tfor (const startId of startIds) {\n\t\tif (!ids.has(startId) || seen.has(startId)) {\n\t\t\tcontinue\n\t\t}\n\n\t\tconst stack = [{ id: startId, next: 0 }]\n\t\tseen.add(startId)\n\t\tonStack.add(startId)\n\t\twhile (stack.length) {\n\t\t\tconst frame = stack[stack.length - 1]\n\t\t\tconst targets = outgoing.get(frame.id) || []\n\t\t\tif (frame.next >= targets.length) {\n\t\t\t\tstack.pop()\n\t\t\t\tonStack.delete(frame.id)\n\t\t\t\tpostorder.push(frame.id)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tconst target = targets[frame.next++]\n\t\t\tif (onStack.has(target)) {\n\t\t\t\t// A back edge: drawing it is fine, layering on it is not.\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif (!forward.has(frame.id)) {\n\t\t\t\tforward.set(frame.id, [])\n\t\t\t}\n\t\t\tforward.get(frame.id).push(target)\n\n\t\t\tif (!seen.has(target)) {\n\t\t\t\tseen.add(target)\n\t\t\t\tonStack.add(target)\n\t\t\t\tstack.push({ id: target, next: 0 })\n\t\t\t}\n\t\t}\n\t}\n\n\t// Longest path over the acyclic remainder, in topological order: a node\n\t// sits one column past the furthest node that leads to it.\n\tconst columns = new Map()\n\tfor (const startId of startIds) {\n\t\tif (seen.has(startId)) {\n\t\t\tcolumns.set(startId, 0)\n\t\t}\n\t}\n\tfor (let i = postorder.length - 1; i >= 0; i--) {\n\t\tconst id = postorder[i]\n\t\tconst from = columns.get(id) ?? 0\n\t\tcolumns.set(id, from)\n\t\tfor (const target of (forward.get(id) || [])) {\n\t\t\tif ((columns.get(target) ?? -1) < from + 1) {\n\t\t\t\tcolumns.set(target, from + 1)\n\t\t\t}\n\t\t}\n\t}\n\n\tconst unreachableColumn = (Math.max(-1, ...columns.values()) + 1)\n\tfor (const node of nodes) {\n\t\tif (!columns.has(node.id)) {\n\t\t\tcolumns.set(node.id, unreachableColumn)\n\t\t}\n\t}\n\n\treturn columns\n}\n\n/**\n * Row per node id: barycenter order within each column.\n *\n * One left-to-right pass. A node's sort key is the mean row of the already\n * placed nodes that lead into it, so a branch follows the branch point it\n * came from instead of braiding across the others. Ties — and nodes with no\n * placed predecessor — keep the document's own node order, which is what\n * makes the whole pass deterministic.\n *\n * @param {Array<object>} nodes The flow's nodes, in document order.\n * @param {Array<{source: string, target: string}>} lines The expanded edges.\n * @param {Map<string, number>} columns Node id to column, from layering.\n * @return {Map<string, number>} Node id to zero-based row within its column.\n */\nfunction orderRows(nodes, lines, columns) {\n\tconst incoming = new Map()\n\tfor (const line of lines) {\n\t\tif (line.source === line.target) {\n\t\t\tcontinue\n\t\t}\n\t\tif (!incoming.has(line.target)) {\n\t\t\tincoming.set(line.target, [])\n\t\t}\n\t\tincoming.get(line.target).push(line.source)\n\t}\n\n\tconst byColumn = new Map()\n\tfor (const [index, node] of nodes.entries()) {\n\t\tconst column = columns.get(node.id)\n\t\tif (!byColumn.has(column)) {\n\t\t\tbyColumn.set(column, [])\n\t\t}\n\t\tbyColumn.get(column).push({ id: node.id, index })\n\t}\n\n\tconst rows = new Map()\n\tconst columnKeys = [...byColumn.keys()].sort((a, b) => a - b)\n\tfor (const column of columnKeys) {\n\t\tconst members = byColumn.get(column)\n\t\tconst keyed = members.map((member) => {\n\t\t\tconst placed = (incoming.get(member.id) || [])\n\t\t\t\t.map((source) => rows.get(source))\n\t\t\t\t.filter((row) => row !== undefined)\n\t\t\tconst barycenter = placed.length\n\t\t\t\t? placed.reduce((sum, row) => sum + row, 0) / placed.length\n\t\t\t\t: member.index\n\t\t\treturn { ...member, barycenter }\n\t\t})\n\t\tkeyed.sort((a, b) => (a.barycenter - b.barycenter) || (a.index - b.index))\n\t\tfor (const [row, member] of keyed.entries()) {\n\t\t\trows.set(member.id, row)\n\t\t}\n\t}\n\n\treturn rows\n}\n\n/**\n * Lay every node out on the grid, document order preserved.\n *\n * Coordinates and NOTHING else change — each returned node is a copy of the\n * input node with `x`, `y` and `position` set, in both spellings, because\n * both are in circulation (see {@link readNodePoint}) and a layout that only\n * writes one does not survive the save round trip. The input array and its\n * nodes are never mutated.\n *\n * No two nodes can share a point: every node gets its own (column, row) cell\n * and cells are spaced by fixed offsets.\n *\n * @param {Array<object>} nodes The flow's nodes.\n * @param {Array<{source: string, target: string}>} lines The expanded edges.\n * @param {Array<string>} startIds The ids a run enters through.\n * @return {Array<object>} New node objects carrying computed positions.\n */\nexport function layoutFlowNodes(nodes, lines, startIds) {\n\tconst list = Array.isArray(nodes) ? nodes : []\n\tif (!list.length) {\n\t\treturn []\n\t}\n\n\tconst columns = layerColumns(list, lines, startIds)\n\tconst rows = orderRows(list, lines, columns)\n\n\treturn list.map((node) => {\n\t\tconst x = FLOW_LAYOUT_MARGIN + (columns.get(node.id) * FLOW_LAYOUT_COLUMN_WIDTH)\n\t\tconst y = FLOW_LAYOUT_TOP + (rows.get(node.id) * FLOW_LAYOUT_ROW_HEIGHT)\n\t\treturn { ...node, x, y, position: { x, y } }\n\t})\n}\n\n/**\n * Slot position-less nodes beneath an arranged graph, leaving it untouched.\n *\n * A flow with even one deliberately placed node is treated as arranged, and\n * an arranged flow is never rearranged behind its author's back. But the\n * nodes that DO lack positions still may not pile onto the origin — under\n * the arranged nodes, most likely — so they get the same layered pass as a\n * full layout, shifted below the lowest positioned node. Left-to-right order\n * still follows the run, and rows are counted among the loose nodes only.\n *\n * Positioned nodes are returned exactly as given (the same object, not a\n * copy), which is what \"untouched\" means here: reference equality is the\n * proof no field changed.\n *\n * @param {Array<object>} nodes The flow's nodes, some positioned and some not.\n * @param {Array<{source: string, target: string}>} lines The expanded edges.\n * @param {Array<string>} startIds The ids a run enters through.\n * @return {Array<object>} The same array shape, loose nodes placed beneath.\n */\nexport function placeLooseNodes(nodes, lines, startIds) {\n\tconst list = Array.isArray(nodes) ? nodes : []\n\tconst loose = list.filter((node) => readNodePoint(node) === null)\n\tif (!loose.length) {\n\t\treturn list.slice()\n\t}\n\n\tlet lowest = -Infinity\n\tfor (const node of list) {\n\t\tconst point = readNodePoint(node)\n\t\tif (point !== null && point.y > lowest) {\n\t\t\tlowest = point.y\n\t\t}\n\t}\n\tconst top = Math.max(\n\t\tFLOW_LAYOUT_TOP,\n\t\tlowest === -Infinity ? FLOW_LAYOUT_TOP : lowest + FLOW_LAYOUT_ROW_HEIGHT,\n\t)\n\n\t// Columns come from the WHOLE graph, so a loose node keeps its place in\n\t// the run order; rows count loose nodes only, so the strip below the\n\t// arranged graph stays as short as the loose nodes make it.\n\tconst columns = layerColumns(list, lines, startIds)\n\tconst rowsTaken = new Map()\n\n\treturn list.map((node) => {\n\t\tif (readNodePoint(node) !== null) {\n\t\t\treturn node\n\t\t}\n\n\t\tconst column = columns.get(node.id)\n\t\tconst row = rowsTaken.get(column) || 0\n\t\trowsTaken.set(column, row + 1)\n\n\t\tconst x = FLOW_LAYOUT_MARGIN + (column * FLOW_LAYOUT_COLUMN_WIDTH)\n\t\tconst y = top + (row * FLOW_LAYOUT_ROW_HEIGHT)\n\t\treturn { ...node, x, y, position: { x, y } }\n\t})\n}\n"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACY,MAAC,wBAAwB,GAAG,IAAG;AAC3C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACY,MAAC,sBAAsB,GAAG,IAAG;AACzC;AACA;AACA;AACA;AACA;AACA;AACY,MAAC,kBAAkB,GAAG,GAAE;AACpC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACY,MAAC,eAAe,GAAG,GAAE;AACjC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,aAAa,CAAC,IAAI,EAAE;AACpC,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,IAAI,IAAI,EAAE,QAAQ,EAAE,CAAC,EAAC;AAC/C,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE;AAC1B,EAAE,OAAO,IAAI;AACb,EAAE;AACF;AACA,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,IAAI,IAAI,EAAE,QAAQ,EAAE,CAAC,EAAC;AAC/C,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE;AAC5C,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,eAAe,CAAC,KAAK,EAAE;AACvC,CAAC,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,KAAK,GAAG,GAAE;AAC/C,CAA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"}
1
+ {"version":3,"file":"flowGraphLayout.js","sources":["../../../src/composables/flowGraphLayout.js"],"sourcesContent":["/**\n * SPDX-License-Identifier: EUPL-1.2\n * SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>\n */\n\n/**\n * Pure layout for flow graphs whose nodes carry no usable positions.\n *\n * A flow declared in a schema's `x-openregister-flows` (and any generated or\n * imported flow) ships nodes with no coordinates at all. Rendered as-is, all\n * of them land on one point: the canvas shows one node's worth of pixels with\n * seventeen more underneath, and no amount of fit-view or zoom can separate\n * them. The run replay's per-node badges paint correctly — onto a pile nobody\n * can read.\n *\n * The functions here are PURE and deterministic: same nodes, same lines, same\n * start ids — same coordinates, every time. They never touch a store, never\n * read the DOM, and never mutate their inputs, which is what makes them safe\n * to run on a published (graph-locked) flow: the caller decides what to do\n * with the returned array, and a viewer that only renders it has mutated\n * nothing. This is also why the layout lives here and not in CnGraphCanvas:\n * per ADR-065 the canvas renders the geometry it is given and never invents\n * any.\n *\n * The algorithm is a small layered pass, on purpose. dagre and friends are\n * real dependencies with real weight, the in-app CSP allows no CDN fetch, and\n * nothing in the dependency tree ships a layout engine — while the layouts a\n * flow needs (left-to-right, branches fanned out, loops tolerated) fit in the\n * three steps below:\n *\n * 1. LAYER: longest-path columns over the graph with its back edges\n * dropped, so triggers sit left and every forward edge points right.\n * Back edges are found by depth-first search; dropping them is what\n * makes a loop terminate instead of pushing its members ever further\n * right (or hanging).\n * 2. ORDER: one barycenter pass per column — a node sits near the average\n * row of the nodes that lead to it, which keeps parallel branches apart\n * instead of braided.\n * 3. PLACE: fixed spacing on a grid. One column and row per node, so no\n * two nodes can overlap by construction.\n */\n\n/**\n * Horizontal distance between layout columns, in canvas pixels.\n *\n * Sized against the node card (~230px wide) so the edge between two adjacent\n * columns has room for its arrowhead and result badge.\n *\n * @type {number}\n */\nexport const FLOW_LAYOUT_COLUMN_WIDTH = 260\n\n/**\n * Vertical distance between layout rows, in canvas pixels.\n *\n * About a card high plus a gap, for the same reason as the column width.\n *\n * @type {number}\n */\nexport const FLOW_LAYOUT_ROW_HEIGHT = 170\n\n/**\n * Left margin before the first column, in canvas pixels.\n *\n * @type {number}\n */\nexport const FLOW_LAYOUT_MARGIN = 60\n\n/**\n * Top margin above the first row, in canvas pixels.\n *\n * The editor's toolbar FLOATS over the canvas (position: absolute, ~52px\n * tall). A node laid out at a uniform 60px margin landed UNDER it, and an\n * overlaid node is not merely ugly — it is unreachable: the toolbar swallows\n * the pointer. Measured live: Playwright reported `cn-flow-detail__toolbar\n * subtree intercepts pointer events` on a node behind it.\n *\n * @type {number}\n */\nexport const FLOW_LAYOUT_TOP = 96\n\n/**\n * Vertical distance between layers when the run reads top to bottom\n * (`direction: 'down'`), in canvas pixels: a step card (~62px) plus room for\n * the line and its arrowhead, as DqFlow draws its steps 100 to 110px apart.\n *\n * @type {number}\n */\nexport const FLOW_LAYOUT_LAYER_HEIGHT_DOWN = 110\n\n/**\n * Where a node sits, from its layer (run order) and its lane (branch).\n *\n * `right` (default): layers are columns left to right, lanes are rows.\n * `down`: layers are rows top to bottom, lanes are columns.\n *\n * @param {number} layer Zero-based layer.\n * @param {number} lane Zero-based lane.\n * @param {string} direction `right` or `down`.\n * @param {number} top The y of the first layer or row.\n * @return {{x: number, y: number}} The point.\n */\nfunction cellPoint(layer, lane, direction, top) {\n\tif (direction === 'down') {\n\t\treturn { x: FLOW_LAYOUT_MARGIN + (lane * FLOW_LAYOUT_COLUMN_WIDTH), y: top + (layer * FLOW_LAYOUT_LAYER_HEIGHT_DOWN) }\n\t}\n\treturn { x: FLOW_LAYOUT_MARGIN + (layer * FLOW_LAYOUT_COLUMN_WIDTH), y: top + (lane * FLOW_LAYOUT_ROW_HEIGHT) }\n}\n\n/**\n * The position a node carries, in either of the two spellings in circulation.\n *\n * The server stores `position: {x, y}`; the editor writes flat `x`/`y` in\n * memory. Reading only one spelling is the bug that once piled every saved\n * flow onto the origin, so both are read here, flat first — the same order\n * `useFlowStore.hasPosition()` and the canvas mapping use.\n *\n * A node AT the origin still counts as positioned: (0, 0) is a legitimate\n * place to park something. Whether a WHOLE GRAPH of identical points counts\n * is a different question, answered by {@link needsFullLayout}.\n *\n * @param {object} node A flow node.\n * @return {{x: number, y: number}|null} Its point, or null when it has none.\n */\nexport function readNodePoint(node) {\n\tconst x = Number(node?.x ?? node?.position?.x)\n\tif (!Number.isFinite(x)) {\n\t\treturn null\n\t}\n\n\tconst y = Number(node?.y ?? node?.position?.y)\n\treturn { x, y: Number.isFinite(y) ? y : 0 }\n}\n\n/**\n * Whether the whole graph needs laying out from scratch.\n *\n * True in exactly two situations:\n *\n * - NO node carries a position. Declared, generated and imported flows all\n * arrive like this.\n * - Every node that does carry a position sits on the SAME point, and at\n * least two do. That is not an arrangement, it is the same pile with\n * coordinates written down — an importer that stamps `{x: 0, y: 0}` on\n * every node produces it, and it must not be mistaken for a deliberate\n * layout. A single node parked at one point (the origin included) stays\n * where its author put it.\n *\n * Anything else — at least two distinct positioned points — is treated as\n * arranged: someone chose those places, and a layout that second-guesses them\n * throws that choice away. Position-less nodes in an arranged flow are the\n * caller's problem; {@link placeLooseNodes} slots them beneath.\n *\n * @param {Array<object>} nodes The flow's nodes.\n * @return {boolean} True when nothing about the current positions is worth keeping.\n */\nexport function needsFullLayout(nodes) {\n\tconst list = Array.isArray(nodes) ? nodes : []\n\tif (!list.length) {\n\t\treturn false\n\t}\n\n\tconst points = []\n\tfor (const node of list) {\n\t\tconst point = readNodePoint(node)\n\t\tif (point !== null) {\n\t\t\tpoints.push(point)\n\t\t}\n\t}\n\n\tif (!points.length) {\n\t\treturn true\n\t}\n\n\treturn points.length >= 2\n\t\t&& points.every((p) => p.x === points[0].x && p.y === points[0].y)\n}\n\n/**\n * Column per node id: longest path from the start nodes, back edges dropped.\n *\n * Depth-first search from the start ids classifies back edges — an edge whose\n * target is still on the current DFS stack closes a loop. Those edges are\n * excluded from layering, which is what lets a flow with a loop (send back to\n * the submitter, wait, check again) lay out finitely: without it, longest\n * path around a cycle is unbounded. The back edge is still DRAWN, of course —\n * only the layering ignores it.\n *\n * Nodes the start ids cannot reach go one column past everything — never at\n * the origin, where they would hide under the entry points.\n *\n * @param {Array<object>} nodes The flow's nodes.\n * @param {Array<{source: string, target: string}>} lines The expanded edges.\n * @param {Array<string>} startIds The ids a run enters through.\n * @return {Map<string, number>} Node id to zero-based column.\n */\nfunction layerColumns(nodes, lines, startIds) {\n\tconst ids = new Set(nodes.map((node) => node.id))\n\tconst outgoing = new Map()\n\tfor (const line of lines) {\n\t\tif (line.source === line.target || !ids.has(line.source) || !ids.has(line.target)) {\n\t\t\tcontinue\n\t\t}\n\t\tif (!outgoing.has(line.source)) {\n\t\t\toutgoing.set(line.source, [])\n\t\t}\n\t\toutgoing.get(line.source).push(line.target)\n\t}\n\n\t// DFS: classify back edges and record a reverse postorder (a topological\n\t// order of the graph minus its back edges). Iterative, because a flow's\n\t// depth is user-controlled and the call stack is not.\n\tconst seen = new Set()\n\tconst onStack = new Set()\n\tconst forward = new Map()\n\tconst postorder = []\n\tfor (const startId of startIds) {\n\t\tif (!ids.has(startId) || seen.has(startId)) {\n\t\t\tcontinue\n\t\t}\n\n\t\tconst stack = [{ id: startId, next: 0 }]\n\t\tseen.add(startId)\n\t\tonStack.add(startId)\n\t\twhile (stack.length) {\n\t\t\tconst frame = stack[stack.length - 1]\n\t\t\tconst targets = outgoing.get(frame.id) || []\n\t\t\tif (frame.next >= targets.length) {\n\t\t\t\tstack.pop()\n\t\t\t\tonStack.delete(frame.id)\n\t\t\t\tpostorder.push(frame.id)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tconst target = targets[frame.next++]\n\t\t\tif (onStack.has(target)) {\n\t\t\t\t// A back edge: drawing it is fine, layering on it is not.\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif (!forward.has(frame.id)) {\n\t\t\t\tforward.set(frame.id, [])\n\t\t\t}\n\t\t\tforward.get(frame.id).push(target)\n\n\t\t\tif (!seen.has(target)) {\n\t\t\t\tseen.add(target)\n\t\t\t\tonStack.add(target)\n\t\t\t\tstack.push({ id: target, next: 0 })\n\t\t\t}\n\t\t}\n\t}\n\n\t// Longest path over the acyclic remainder, in topological order: a node\n\t// sits one column past the furthest node that leads to it.\n\tconst columns = new Map()\n\tfor (const startId of startIds) {\n\t\tif (seen.has(startId)) {\n\t\t\tcolumns.set(startId, 0)\n\t\t}\n\t}\n\tfor (let i = postorder.length - 1; i >= 0; i--) {\n\t\tconst id = postorder[i]\n\t\tconst from = columns.get(id) ?? 0\n\t\tcolumns.set(id, from)\n\t\tfor (const target of (forward.get(id) || [])) {\n\t\t\tif ((columns.get(target) ?? -1) < from + 1) {\n\t\t\t\tcolumns.set(target, from + 1)\n\t\t\t}\n\t\t}\n\t}\n\n\tconst unreachableColumn = (Math.max(-1, ...columns.values()) + 1)\n\tfor (const node of nodes) {\n\t\tif (!columns.has(node.id)) {\n\t\t\tcolumns.set(node.id, unreachableColumn)\n\t\t}\n\t}\n\n\treturn columns\n}\n\n/**\n * Row per node id: barycenter order within each column.\n *\n * One left-to-right pass. A node's sort key is the mean row of the already\n * placed nodes that lead into it, so a branch follows the branch point it\n * came from instead of braiding across the others. Ties — and nodes with no\n * placed predecessor — keep the document's own node order, which is what\n * makes the whole pass deterministic.\n *\n * @param {Array<object>} nodes The flow's nodes, in document order.\n * @param {Array<{source: string, target: string}>} lines The expanded edges.\n * @param {Map<string, number>} columns Node id to column, from layering.\n * @return {Map<string, number>} Node id to zero-based row within its column.\n */\nfunction orderRows(nodes, lines, columns) {\n\tconst incoming = new Map()\n\tfor (const line of lines) {\n\t\tif (line.source === line.target) {\n\t\t\tcontinue\n\t\t}\n\t\tif (!incoming.has(line.target)) {\n\t\t\tincoming.set(line.target, [])\n\t\t}\n\t\tincoming.get(line.target).push(line.source)\n\t}\n\n\tconst byColumn = new Map()\n\tfor (const [index, node] of nodes.entries()) {\n\t\tconst column = columns.get(node.id)\n\t\tif (!byColumn.has(column)) {\n\t\t\tbyColumn.set(column, [])\n\t\t}\n\t\tbyColumn.get(column).push({ id: node.id, index })\n\t}\n\n\tconst rows = new Map()\n\tconst columnKeys = [...byColumn.keys()].sort((a, b) => a - b)\n\tfor (const column of columnKeys) {\n\t\tconst members = byColumn.get(column)\n\t\tconst keyed = members.map((member) => {\n\t\t\tconst placed = (incoming.get(member.id) || [])\n\t\t\t\t.map((source) => rows.get(source))\n\t\t\t\t.filter((row) => row !== undefined)\n\t\t\tconst barycenter = placed.length\n\t\t\t\t? placed.reduce((sum, row) => sum + row, 0) / placed.length\n\t\t\t\t: member.index\n\t\t\treturn { ...member, barycenter }\n\t\t})\n\t\tkeyed.sort((a, b) => (a.barycenter - b.barycenter) || (a.index - b.index))\n\t\tfor (const [row, member] of keyed.entries()) {\n\t\t\trows.set(member.id, row)\n\t\t}\n\t}\n\n\treturn rows\n}\n\n/**\n * Lay every node out on the grid, document order preserved.\n *\n * Coordinates and NOTHING else change — each returned node is a copy of the\n * input node with `x`, `y` and `position` set, in both spellings, because\n * both are in circulation (see {@link readNodePoint}) and a layout that only\n * writes one does not survive the save round trip. The input array and its\n * nodes are never mutated.\n *\n * No two nodes can share a point: every node gets its own (column, row) cell\n * and cells are spaced by fixed offsets.\n *\n * @param {Array<object>} nodes The flow's nodes.\n * @param {Array<{source: string, target: string}>} lines The expanded edges.\n * @param {Array<string>} startIds The ids a run enters through.\n * @param {object} [options] `{ direction }`: `right` (default) runs left to\n * right, `down` runs top to bottom with branches side by side.\n * @return {Array<object>} New node objects carrying computed positions.\n */\nexport function layoutFlowNodes(nodes, lines, startIds, options = {}) {\n\tconst list = Array.isArray(nodes) ? nodes : []\n\tif (!list.length) {\n\t\treturn []\n\t}\n\n\tconst columns = layerColumns(list, lines, startIds)\n\tconst rows = orderRows(list, lines, columns)\n\n\treturn list.map((node) => {\n\t\tconst { x, y } = cellPoint(columns.get(node.id), rows.get(node.id), options.direction, FLOW_LAYOUT_TOP)\n\t\treturn { ...node, x, y, position: { x, y } }\n\t})\n}\n\n/**\n * Slot position-less nodes beneath an arranged graph, leaving it untouched.\n *\n * A flow with even one deliberately placed node is treated as arranged, and\n * an arranged flow is never rearranged behind its author's back. But the\n * nodes that DO lack positions still may not pile onto the origin — under\n * the arranged nodes, most likely — so they get the same layered pass as a\n * full layout, shifted below the lowest positioned node. Left-to-right order\n * still follows the run, and rows are counted among the loose nodes only.\n *\n * Positioned nodes are returned exactly as given (the same object, not a\n * copy), which is what \"untouched\" means here: reference equality is the\n * proof no field changed.\n *\n * @param {Array<object>} nodes The flow's nodes, some positioned and some not.\n * @param {Array<{source: string, target: string}>} lines The expanded edges.\n * @param {Array<string>} startIds The ids a run enters through.\n * @param {object} [options] `{ direction }`, as {@link layoutFlowNodes}.\n * @return {Array<object>} The same array shape, loose nodes placed beneath.\n */\nexport function placeLooseNodes(nodes, lines, startIds, options = {}) {\n\tconst list = Array.isArray(nodes) ? nodes : []\n\tconst loose = list.filter((node) => readNodePoint(node) === null)\n\tif (!loose.length) {\n\t\treturn list.slice()\n\t}\n\n\tlet lowest = -Infinity\n\tfor (const node of list) {\n\t\tconst point = readNodePoint(node)\n\t\tif (point !== null && point.y > lowest) {\n\t\t\tlowest = point.y\n\t\t}\n\t}\n\tconst top = Math.max(\n\t\tFLOW_LAYOUT_TOP,\n\t\tlowest === -Infinity ? FLOW_LAYOUT_TOP : lowest + FLOW_LAYOUT_ROW_HEIGHT,\n\t)\n\n\t// Columns come from the WHOLE graph, so a loose node keeps its place in\n\t// the run order; rows count loose nodes only, so the strip below the\n\t// arranged graph stays as short as the loose nodes make it.\n\tconst columns = layerColumns(list, lines, startIds)\n\tconst rowsTaken = new Map()\n\n\treturn list.map((node) => {\n\t\tif (readNodePoint(node) !== null) {\n\t\t\treturn node\n\t\t}\n\n\t\tconst column = columns.get(node.id)\n\t\tconst row = rowsTaken.get(column) || 0\n\t\trowsTaken.set(column, row + 1)\n\n\t\tconst { x, y } = cellPoint(column, row, options.direction, top)\n\t\treturn { ...node, x, y, position: { x, y } }\n\t})\n}\n"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACY,MAAC,wBAAwB,GAAG,IAAG;AAC3C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACY,MAAC,sBAAsB,GAAG,IAAG;AACzC;AACA;AACA;AACA;AACA;AACA;AACY,MAAC,kBAAkB,GAAG,GAAE;AACpC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACY,MAAC,eAAe,GAAG,GAAE;AACjC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACY,MAAC,6BAA6B,GAAG,IAAG;AAChD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAAS,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE;AAChD,CAAC,IAAI,SAAS,KAAK,MAAM,EAAE;AAC3B,EAAE,OAAO,EAAE,CAAC,EAAE,kBAAkB,IAAI,IAAI,GAAG,wBAAwB,CAAC,EAAE,CAAC,EAAE,GAAG,IAAI,KAAK,GAAG,6BAA6B,CAAC,EAAE;AACxH,EAAE;AACF,CAAC,OAAO,EAAE,CAAC,EAAE,kBAAkB,IAAI,KAAK,GAAG,wBAAwB,CAAC,EAAE,CAAC,EAAE,GAAG,IAAI,IAAI,GAAG,sBAAsB,CAAC,EAAE;AAChH,CAAC;AACD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAAS,aAAa,CAAC,IAAI,EAAE;AACpC,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,IAAI,IAAI,EAAE,QAAQ,EAAE,CAAC,EAAC;AAC/C,CAAC,IAAI,CAAC,MAAM,CAAC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"}
@@ -102,6 +102,10 @@ const useFlowStore = defineStore('cnFlow', {
102
102
  flow: emptyFlow(),
103
103
  flows: [],
104
104
 
105
+ // How the automatic layout runs: `right` (left to right) or `down`
106
+ // (top to bottom). Set by the editor that hosts the canvas.
107
+ layoutDirection: 'right',
108
+
105
109
  // Snapshots of `flow`, oldest first, for Ctrl+Z.
106
110
  //
107
111
  // A canvas is direct manipulation: a mis-drag or a mis-aimed Delete
@@ -1660,7 +1664,7 @@ const useFlowStore = defineStore('cnFlow', {
1660
1664
  return
1661
1665
  }
1662
1666
 
1663
- this.flow.nodes = layoutFlowNodes(this.nodes, this.canvasEdges, this.startNodeIds);
1667
+ this.flow.nodes = layoutFlowNodes(this.nodes, this.canvasEdges, this.startNodeIds, { direction: this.layoutDirection });
1664
1668
  this.dirty = true;
1665
1669
  },
1666
1670
 
@@ -1694,12 +1698,12 @@ const useFlowStore = defineStore('cnFlow', {
1694
1698
  }
1695
1699
 
1696
1700
  if (needsFullLayout(nodes)) {
1697
- this.flow.nodes = layoutFlowNodes(nodes, this.canvasEdges, this.startNodeIds);
1701
+ this.flow.nodes = layoutFlowNodes(nodes, this.canvasEdges, this.startNodeIds, { direction: this.layoutDirection });
1698
1702
  return true
1699
1703
  }
1700
1704
 
1701
1705
  if (nodes.some((node) => !this.hasPosition(node))) {
1702
- this.flow.nodes = placeLooseNodes(nodes, this.canvasEdges, this.startNodeIds);
1706
+ this.flow.nodes = placeLooseNodes(nodes, this.canvasEdges, this.startNodeIds, { direction: this.layoutDirection });
1703
1707
  return true
1704
1708
  }
1705
1709