@conduction/nextcloud-vue 2.56.0-beta.21 → 2.56.0-beta.22

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.
@@ -1 +1 @@
1
- {"version":3,"file":"CnGraphCanvas.vue3.js","sources":["../../../../src/components/CnGraphCanvas/CnGraphCanvas.vue?vue&type=template&id=1d8bc107&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>\n - SPDX-License-Identifier: EUPL-1.2\n -->\n<template>\n\t<div class=\"cn-graph-canvas\" @drop=\"onDrop\" @dragover.prevent>\n\t\t<VueFlow\n\t\t\t:nodes=\"nodes\"\n\t\t\t:edges=\"edges\"\n\t\t\t:minZoom=\"minZoom\"\n\t\t\t:maxZoom=\"maxZoom\"\n\t\t\t:fitViewOnInit=\"fitView\"\n\t\t\t:snapToGrid=\"snapToGrid\"\n\t\t\t:snapGrid=\"snapGrid\"\n\t\t\t:nodesDraggable=\"interactive\"\n\t\t\t:nodesConnectable=\"interactive\"\n\t\t\t:elementsSelectable=\"interactive\"\n\t\t\t:connectionMode=\"connectionMode\"\n\t\t\t@nodesChange=\"onNodesChange\"\n\t\t\t@edgesChange=\"onEdgesChange\"\n\t\t\t@connect=\"onConnect\"\n\t\t\t@nodeClick=\"onNodeClick\"\n\t\t\t@edgeClick=\"onEdgeClick\"\n\t\t\t@paneClick=\"onPaneClick\">\n\t\t\t<!-- Per-type node components, Vue Flow's convention. A host that\n\t\t\t registers `type: 'trigger'` gets `#node-trigger`; anything\n\t\t\t unregistered falls back to CnFlowNode, which carries the\n\t\t\t keyboard contract. -->\n\t\t\t<template #node-default=\"nodeProps\">\n\t\t\t\t<!-- `@connect` routes a KEYBOARD-completed connection into the\n\t\t\t\t same handler as a pointer one, so `onConnect` is the single\n\t\t\t\t place a new edge leaves this component regardless of input.\n\t\t\t\t Without it the keyboard path wrote into Vue Flow's internal\n\t\t\t\t graph and the host never heard about it — the edge drew and\n\t\t\t\t the save dropped it. -->\n\t\t\t\t<CnFlowNode\n\t\t\t\t\tv-bind=\"nodeProps\"\n\t\t\t\t\t:deletable=\"!readOnly\"\n\t\t\t\t\t@connect=\"onConnect\"\n\t\t\t\t\t@remove=\"onNodeRemove\">\n\t\t\t\t\t<template #default=\"slotProps\">\n\t\t\t\t\t\t<!-- @slot node The body of a step, rendered inside the\n\t\t\t\t\t\t focusable node wrapper. Receives `{ node }` with the\n\t\t\t\t\t\t node's `id`, `data` and `selected`. The wrapper — and\n\t\t\t\t\t\t with it the keyboard contract and ARIA state — stays\n\t\t\t\t\t\t the canvas's, so a host cannot accidentally replace a\n\t\t\t\t\t\t focusable node with an inert div. -->\n\t\t\t\t\t\t<slot name=\"node\" v-bind=\"slotProps\" />\n\t\t\t\t\t</template>\n\t\t\t\t</CnFlowNode>\n\t\t\t</template>\n\n\t\t\t<!-- Per-type edge components, the same convention as nodes above.\n\t\t\t Registered on `default` because that is the type an edge gets\n\t\t\t when nothing else is asked for — which is every edge, now that\n\t\t\t the ROUTER moved off `type` and into `data.lineType`.\n\n\t\t\t That move is the edge half of the lesson the node slot records:\n\t\t\t `type` selects a COMPONENT, and a router is not a component. So\n\t\t\t long as `type` carried `smoothstep`, Vue Flow answered with its\n\t\t\t own built-in edge and no consumer could add anything to a line\n\t\t\t at all. -->\n\t\t\t<template #edge-default=\"edgeProps\">\n\t\t\t\t<CnFlowEdge\n\t\t\t\t\tv-bind=\"edgeProps\"\n\t\t\t\t\t:lineType=\"edgeProps.data && edgeProps.data.lineType\"\n\t\t\t\t\t:animated=\"!(edgeProps.data && edgeProps.data.animated === false)\"\n\t\t\t\t\t:label-aria-label=\"edgeProps.data && edgeProps.data.labelAriaLabel\"\n\t\t\t\t\t@labelMove=\"onEdgeLabelMove\"\n\t\t\t\t\t@labelClick=\"onEdgeLabelClick\"\n\t\t\t\t\t@labelContext=\"onEdgeLabelContext\">\n\t\t\t\t\t<template #label=\"slotProps\">\n\t\t\t\t\t\t<!-- @slot edge-label The chrome of a connection's\n\t\t\t\t\t\t label. Receives `{ edge }` with the edge's `id`,\n\t\t\t\t\t\t `data` and `selected`. Rendered inside a focusable\n\t\t\t\t\t\t control the canvas owns, so render inert content:\n\t\t\t\t\t\t the arrow keys, the focus ring and the ARIA name\n\t\t\t\t\t\t are already handled. An edge whose host renders\n\t\t\t\t\t\t nothing here draws no label control at all. -->\n\t\t\t\t\t\t<slot name=\"edge-label\" v-bind=\"slotProps\" />\n\t\t\t\t\t</template>\n\n\t\t\t\t\t<template #adornment=\"slotProps\">\n\t\t\t\t\t\t<!-- @slot edge-adornment A host's own controls beside a\n\t\t\t\t\t\t connection's label. Receives `{ edge }`. Unlike\n\t\t\t\t\t\t `edge-label` this is NOT inside the label's button,\n\t\t\t\t\t\t so what goes here may be interactive — a replay's\n\t\t\t\t\t\t payload control is the case it exists for. -->\n\t\t\t\t\t\t<slot name=\"edge-adornment\" v-bind=\"slotProps\" />\n\t\t\t\t\t</template>\n\t\t\t\t</CnFlowEdge>\n\t\t\t</template>\n\n\t\t\t<Background v-if=\"showBackground\" :gap=\"snapGrid[0]\" />\n\t\t\t<MiniMap v-if=\"showMiniMap\" />\n\t\t</VueFlow>\n\n\t\t<!-- OUR OWN CONTROLS, not `@vue-flow/controls`.\n\t\t The library's control bar renders bare <button> elements with an\n\t\t icon and no accessible name, which axe reports as `button-name` at\n\t\t SERIOUS impact — caught by the e2e in this change, not by the unit\n\t\t lane or the gates. Rather than ship a control bar a screen-reader\n\t\t user cannot identify, the buttons are ours and carry real labels. -->\n\t\t<div v-if=\"showControls\" class=\"cn-graph-canvas__controls\">\n\t\t\t<button\n\t\t\t\ttype=\"button\"\n\t\t\t\tclass=\"cn-graph-canvas__control\"\n\t\t\t\t:aria-label=\"t('nextcloud-vue', 'Zoom in')\"\n\t\t\t\t@click=\"zoomIn()\">\n\t\t\t\t+\n\t\t\t</button>\n\t\t\t<button\n\t\t\t\ttype=\"button\"\n\t\t\t\tclass=\"cn-graph-canvas__control\"\n\t\t\t\t:aria-label=\"t('nextcloud-vue', 'Zoom out')\"\n\t\t\t\t@click=\"zoomOut()\">\n\t\t\t\t−\n\t\t\t</button>\n\t\t\t<button\n\t\t\t\ttype=\"button\"\n\t\t\t\tclass=\"cn-graph-canvas__control\"\n\t\t\t\t:aria-label=\"t('nextcloud-vue', 'Fit the whole flow in view')\"\n\t\t\t\t@click=\"fitViewNow()\">\n\t\t\t\t⤢\n\t\t\t</button>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport { Background } from '@vue-flow/background'\nimport { useVueFlow, VueFlow } from '@vue-flow/core'\nimport { MiniMap } from '@vue-flow/minimap'\nimport CnFlowEdge from './CnFlowEdge.vue'\nimport CnFlowNode from './CnFlowNode.vue'\n\n/**\n * A graph canvas built on Vue Flow — the library n8n runs.\n *\n * WHY THIS IS NOT A WRAPPER AROUND THE OLD API\n * --------------------------------------------\n * This component used to be ~1,300 hand-written lines re-implementing pan,\n * zoom, drag, connect, resize and hit-testing on a bare SVG, because the fleet\n * was on Vue 2.7 and every maintained canvas library is Vue-3-only (ADR-065\n * §5). ADR-081 removed that constraint and named this library as the reason for\n * the migration.\n *\n * The props therefore MOVED rather than being translated. The graph page was\n * not in production, so there was no consumer to protect, and a shim can only\n * ever expose what the old API could already express — `fitView`, `<MiniMap>`,\n * `connectionMode` and the node resizer would all have stayed unreachable\n * behind props designed for a hand-drawn SVG.\n *\n * `nodeWidth` / `nodeHeight` are gone entirely. They existed only so hand-drawn\n * edges could guess where a node's centre was, and the old docblock admitted\n * the failure mode: \"set them to match what your node slot actually renders, or\n * edges will attach off-centre\". Vue Flow measures the rendered node, so the\n * whole class of bug went with the props.\n *\n * ⚠️ THE CANVAS STILL NEVER MUTATES THE GRAPH. Vue Flow is perfectly willing to\n * own node positions internally; here it does not. Changes are emitted and the\n * host decides what to persist — the same rule the previous implementation\n * held, and the reason a canvas can be dropped into a read-only surface without\n * auditing it first.\n *\n * ⚠️ KEYBOARD OPERATION LIVES IN {@see CnFlowNode}. Vue Flow is pointer-first;\n * the focus/ARIA/key contract is re-implemented in the node component, not\n * inherited from the library. See that file for why the multi-exit case is the\n * one that matters.\n */\nexport default {\n\tname: 'CnGraphCanvas',\n\n\tcomponents: { VueFlow, Background, MiniMap, CnFlowNode, CnFlowEdge },\n\n\tprops: {\n\t\t/** Nodes in Vue Flow's shape: `{ id, type, position: {x,y}, data }`. */\n\t\tnodes: {\n\t\t\ttype: Array,\n\t\t\trequired: true,\n\t\t},\n\n\t\t/** Edges in Vue Flow's shape: `{ id, source, target }`. */\n\t\tedges: {\n\t\t\ttype: Array,\n\t\t\trequired: true,\n\t\t},\n\n\t\t/**\n\t\t * When true, every interaction is refused — drag, connect AND select.\n\t\t *\n\t\t * Expressed as three Vue Flow flags below. Missing one produces a canvas\n\t\t * that LOOKS locked and is not, so the three are set together and tested\n\t\t * together.\n\t\t */\n\t\treadOnly: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Frame the whole graph on first render. */\n\t\tfitView: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Snap dragged nodes to the grid. */\n\t\tsnapToGrid: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Grid spacing, as Vue Flow's `[x, y]`. */\n\t\tsnapGrid: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [16, 16],\n\t\t},\n\n\t\t/** Minimum zoom. */\n\t\tminZoom: {\n\t\t\ttype: Number,\n\t\t\tdefault: 0.2,\n\t\t},\n\n\t\t/** Maximum zoom. */\n\t\tmaxZoom: {\n\t\t\ttype: Number,\n\t\t\tdefault: 2,\n\t\t},\n\n\t\t/** Vue Flow's connection mode — `loose` or `strict`. */\n\t\tconnectionMode: {\n\t\t\ttype: String,\n\t\t\tdefault: 'loose',\n\t\t},\n\n\t\t/** Draw the dotted background. */\n\t\tshowBackground: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Draw the zoom/fit controls. */\n\t\tshowControls: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Draw the minimap. */\n\t\tshowMiniMap: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\t},\n\n\temits: [\n\t\t'nodes-change',\n\t\t'edges-change',\n\t\t'connect',\n\t\t'node-select',\n\t\t'edge-select',\n\t\t'edge-label-move',\n\t\t'edge-label-click',\n\t\t'edge-label-context',\n\t\t'canvas-click',\n\t\t'canvas-drop',\n\n\t\t'node-remove',\n\n\t\t/**\n\t\t * The canvas zoom changed, by any route — a control here, the host's own\n\t\t * toolbar, the wheel, or a fit-view.\n\t\t *\n\t\t * @type {number}\n\t\t */\n\t\t'zoom-change',\n\t],\n\n\tsetup() {\n\t\t// ⚠️ ALIASED. `fitView` is already a BOOLEAN PROP on this component\n\t\t// (frame the graph on first render), and Vue Flow's composable exports a\n\t\t// FUNCTION of the same name. Returning both puts two `fitView` bindings\n\t\t// on the instance — eslint's `vue/no-dupe-keys` catches it, but the\n\t\t// symptom would have been a fit button that tries to call `true`.\n\t\t// `zoomTo` and `viewport` so a HOST toolbar can drive the zoom and show\n\t\t// the real figure — including one the wheel or a fit-view changed.\n\t\tconst { project, zoomIn, zoomOut, zoomTo, fitView: fitViewNow, viewport } = useVueFlow()\n\n\t\treturn { project, zoomIn, zoomOut, zoomTo, fitViewNow, viewport }\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * @return {boolean} Whether the canvas accepts interaction at all.\n\t\t */\n\t\tinteractive() {\n\t\t\treturn this.readOnly === false\n\t\t},\n\t},\n\n\twatch: {\n\t\t// Reported rather than owned: Vue Flow holds the transform, so a host\n\t\t// that mirrors it in its own toolbar must hear every change, not only\n\t\t// the ones its own buttons caused.\n\t\t'viewport.zoom': {\n\t\t\timmediate: true,\n\t\t\thandler(value) {\n\t\t\t\tthis.$emit('zoom-change', value)\n\t\t\t},\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Forward Vue Flow's node changes.\n\t\t *\n\t\t * @param {Array<object>} changes The changes.\n\t\t * @return {void}\n\t\t */\n\t\tonNodesChange(changes) {\n\t\t\t/**\n\t\t\t * @event nodes-change Vue Flow's node change stream — position,\n\t\t\t * selection, dimensions, removal. The canvas does NOT apply these;\n\t\t\t * the host decides what to persist. Drag frames arrive with\n\t\t\t * `dragging: true`, so a host that writes on every change writes\n\t\t\t * once per animation frame.\n\t\t\t */\n\t\t\tthis.$emit('nodes-change', changes)\n\t\t},\n\n\t\t/**\n\t\t * Forward Vue Flow's edge changes.\n\t\t *\n\t\t * @param {Array<object>} changes The changes.\n\t\t * @return {void}\n\t\t */\n\t\tonEdgesChange(changes) {\n\t\t\t/**\n\t\t\t * @event edges-change Vue Flow's edge change stream, same contract\n\t\t\t * as `nodes-change`: reported, never applied here.\n\t\t\t */\n\t\t\tthis.$emit('edges-change', changes)\n\t\t},\n\n\t\t/**\n\t\t * Forward a new connection.\n\t\t *\n\t\t * @param {object} connection The connection.\n\t\t * @return {void}\n\t\t */\n\t\t/**\n\t\t * Pass a node's removal request up to the host.\n\t\t *\n\t\t * @param {string} id The node that asked to be removed.\n\t\t * @return {void}\n\t\t */\n\t\tonNodeRemove(id) {\n\t\t\t/**\n\t\t\t * @event node-remove The focused node should be removed, by id.\n\t\t\t * Raised by Delete/Backspace on a node. The canvas removes\n\t\t\t * nothing itself — the host owns `nodes` and `edges`, and a node\n\t\t\t * dropped here would leave the edges pointing at it behind.\n\t\t\t * @type {string}\n\t\t\t */\n\t\t\tthis.$emit('node-remove', id)\n\t\t},\n\n\t\tonConnect(connection) {\n\t\t\t/**\n\t\t\t * @event connect A new connection was made, by pointer OR by\n\t\t\t * keyboard. Carries Vue Flow's\n\t\t\t * `{ source, target, sourceHandle, targetHandle }`, with the\n\t\t\t * handles reduced to the PORT the host declared — see\n\t\t\t * `portIdOf` below.\n\t\t\t */\n\t\t\tthis.$emit('connect', {\n\t\t\t\t...connection,\n\t\t\t\tsourceHandle: this.portIdOf(connection?.sourceHandle),\n\t\t\t\ttargetHandle: this.portIdOf(connection?.targetHandle),\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Reduce a HANDLE id to the PORT id the host declared.\n\t\t *\n\t\t * A node draws more handles than it has ports — one exit is offered on\n\t\t * both the right and the bottom edge, one entry on both the left and the\n\t\t * top — because a line has to be able to leave and arrive on the side\n\t\t * the graph reads best from. Vue Flow keys handles by id, so those\n\t\t * cannot share one, and the side is encoded into the id instead\n\t\t * (`yes__right`). See CnFlowNode's `exitHandles`.\n\t\t *\n\t\t * ⚠️ WHICH MEANS THE ENCODED ID MUST NEVER LEAVE THE CANVAS. A branch\n\t\t * recorded as `yes__bottom` names a port the engine has never heard of,\n\t\t * and a flow whose branch ids do not match its catalogue is one the\n\t\t * engine cannot route — it would take the default exit and the canvas\n\t\t * would keep showing the branch the author drew. So both routes into\n\t\t * this method (pointer via <VueFlow>, keyboard via CnFlowNode) are\n\t\t * stripped here, in the one place every connection passes through.\n\t\t *\n\t\t * An entry handle carries no port of its own — a node has exactly one\n\t\t * inbound port — so it reduces to null rather than to the literal `in`.\n\t\t *\n\t\t * @param {string|null|undefined} handleId The handle id Vue Flow reports.\n\t\t * @return {string|null} The port id, or null.\n\t\t */\n\t\tportIdOf(handleId) {\n\t\t\tif (typeof handleId !== 'string') {\n\t\t\t\treturn null\n\t\t\t}\n\n\t\t\tconst cut = handleId.lastIndexOf('__')\n\t\t\tconst port = cut === -1 ? handleId : handleId.slice(0, cut)\n\n\t\t\treturn port === 'in' ? null : port\n\t\t},\n\n\t\t/**\n\t\t * Forward a node click.\n\t\t *\n\t\t * @param {object} event Vue Flow's node event.\n\t\t * @return {void}\n\t\t */\n\t\tonNodeClick(event) {\n\t\t\t/**\n\t\t\t * @event node-select A node was clicked. Payload is Vue Flow's\n\t\t\t * `{ node }`, so a host tracking bare ids reads `event.node.id`.\n\t\t\t */\n\t\t\tthis.$emit('node-select', event)\n\t\t},\n\n\t\t/**\n\t\t * Forward an edge click.\n\t\t *\n\t\t * @param {object} event Vue Flow's edge event.\n\t\t * @return {void}\n\t\t */\n\t\tonEdgeClick(event) {\n\t\t\t/**\n\t\t\t * @event edge-select An edge was clicked.\n\t\t\t */\n\t\t\tthis.$emit('edge-select', event)\n\t\t},\n\n\t\t/**\n\t\t * Forward a label slid along its line.\n\t\t *\n\t\t * @param {{id: string, labelT: number}} payload The edge and its new\n\t\t * fraction, already clamped.\n\t\t * @return {void}\n\t\t */\n\t\tonEdgeLabelMove(payload) {\n\t\t\t/**\n\t\t\t * @event edge-label-move A connection's label was moved, by pointer\n\t\t\t * or by keyboard. Carries `{ id, labelT }`. As everywhere else in\n\t\t\t * this component the change is REPORTED, not applied — the host\n\t\t\t * stores `labelT` and feeds it back down.\n\t\t\t */\n\t\t\tthis.$emit('edge-label-move', payload)\n\t\t},\n\n\t\t/**\n\t\t * Forward an activated label.\n\t\t *\n\t\t * @param {string} id The edge id.\n\t\t * @return {void}\n\t\t */\n\t\tonEdgeLabelClick(id) {\n\t\t\t/**\n\t\t\t * @event edge-label-click A connection's label was activated.\n\t\t\t */\n\t\t\tthis.$emit('edge-label-click', id)\n\t\t},\n\n\t\t/**\n\t\t * Forward a right-click on a label.\n\t\t *\n\t\t * @param {{id: string, event: MouseEvent}} payload The edge and event.\n\t\t * @return {void}\n\t\t */\n\t\tonEdgeLabelContext(payload) {\n\t\t\t/**\n\t\t\t * @event edge-label-context A connection's label was right-clicked.\n\t\t\t * Hosts open the same menu they open for the line itself.\n\t\t\t */\n\t\t\tthis.$emit('edge-label-context', payload)\n\t\t},\n\n\t\t/**\n\t\t * Forward a click on the empty pane.\n\t\t *\n\t\t * @param {object} event The click event.\n\t\t * @return {void}\n\t\t */\n\t\tonPaneClick(event) {\n\t\t\t/**\n\t\t\t * @event canvas-click The empty pane was clicked — hosts use this to\n\t\t\t * clear a selection.\n\t\t\t */\n\t\t\tthis.$emit('canvas-click', event)\n\t\t},\n\n\t\t/**\n\t\t * A palette drop, with the point already converted to canvas space.\n\t\t *\n\t\t * Kept as a Conduction-level event because HTML5 drop plus coordinate\n\t\t * projection is glue every consumer would otherwise write. The\n\t\t * projection is Vue Flow's `project()` rather than the old hand-rolled\n\t\t * `(clientX - rect.left - panOffset.x) / zoom`.\n\t\t *\n\t\t * The canvas does not create the node — the host does, mirroring the\n\t\t * rule that it never mutates positions either.\n\t\t *\n\t\t * @param {DragEvent} event The drop event.\n\t\t * @return {void}\n\t\t */\n\t\tonDrop(event) {\n\t\t\tif (this.readOnly === true) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst bounds = this.$el.getBoundingClientRect()\n\t\t\tconst position = this.project({\n\t\t\t\tx: event.clientX - bounds.left,\n\t\t\t\ty: event.clientY - bounds.top,\n\t\t\t})\n\n\t\t\t/**\n\t\t\t * @event canvas-drop An HTML5 drop landed on the canvas, with\n\t\t\t * `position` already projected into canvas space and the native\n\t\t\t * `event` alongside so the host can read `dataTransfer`. The canvas\n\t\t\t * never creates the node itself.\n\t\t\t */\n\t\t\tthis.$emit('canvas-drop', { position, event })\n\t\t},\n\t},\n}\n</script>\n\n<style>\n/* Vue Flow's own stylesheet, themed through Nextcloud variables. Imported\n un-scoped because it styles elements Vue Flow renders outside this\n component's scope. */\n@import '@vue-flow/core/dist/style.css';\n@import '@vue-flow/core/dist/theme-default.css';\n\n/* ⚠️ EVERY VUE FLOW PLUGIN SHIPS ITS OWN STYLESHEET, AND ONLY CORE'S WAS HERE.\n\n `@vue-flow/node-resizer` was the one that mattered: its stylesheet is what\n gives a resize control `position: absolute` and a 5px handle. Without it the\n handles CnFlowNode renders have no size and no position, so a node that is\n resizable looks exactly like a node that is not — the component was mounted\n and the affordance was invisible.\n\n The minimap's own sheet is small (a background colour and the grab cursors);\n its geometry comes from `.vue-flow__panel` in core, which was already here.\n Imported for the cursors, and before our own rules so the theme below still\n wins. */\n@import '@vue-flow/node-resizer/dist/style.css';\n@import '@vue-flow/minimap/dist/style.css';\n\n.cn-graph-canvas {\n\tposition: relative;\n\twidth: 100%;\n\theight: 100%;\n\tmin-height: 320px;\n}\n\n/* ⚠️ VUE FLOW'S DEFAULT NODE DRAWS A BOX WE NEVER ASKED FOR.\n\n Every node this canvas renders goes through the `#node-default` slot, so\n Vue Flow wraps our CnFlowNode in `.vue-flow__node-default` — and\n `theme-default.css` gives that wrapper a 1px border, a white background and\n 10px of padding. The result was a box around our box: the wrapper's border,\n then `.cn-flow-node`'s own. Nothing failed and nothing warned; a node just\n quietly had one more outline than it was designed to have.\n\n Stripped, not restyled: `.cn-flow-node` is the node's chrome, and it is the\n ONLY border a node draws. `width` stays 150px because Vue Flow measures the\n wrapper to route edges and place handles — dropping it would move every\n node's footprint, which is a layout change, not a fix for a stray border. */\n.cn-graph-canvas .vue-flow__node-default {\n\tpadding: 0;\n\tborder: 0;\n\tborder-radius: 0;\n\tbackground: none;\n\n\t/* The handle colour reaches `.cn-flow-node__handle` through this variable.\n\t Vue Flow's own `.vue-flow__node-default .vue-flow__handle` rule outranks\n\t our single-class one, so a themed handle lost to a hard-coded #1a192b on\n\t every node — setting the variable themes it at the source instead of\n\t fighting the specificity. */\n\t--vf-handle: var(--color-primary-element, #0082c9);\n}\n\n/* The selected/focus/hover states re-assert the border and shadow the rule\n above just removed, so they have to be stripped too — otherwise clicking a\n node brought the extra box back. Selection is shown by `.cn-flow-node--selected`,\n and focus by `.cn-flow-node:focus-visible`, both on the node itself. */\n.cn-graph-canvas .vue-flow__node-default.selected,\n.cn-graph-canvas .vue-flow__node-default.selected:hover,\n.cn-graph-canvas .vue-flow__node-default.selectable:hover,\n.cn-graph-canvas .vue-flow__node-default:focus,\n.cn-graph-canvas .vue-flow__node-default:focus-visible {\n\tborder: 0;\n\tbox-shadow: none;\n}\n\n/* THEME, NOT OVERRIDE. Every colour comes from a Nextcloud variable with a\n fallback, so the canvas follows the user's theme — including dark — instead\n of being the one component that ignores it. */\n.cn-graph-canvas .vue-flow__background {\n\tbackground: var(--color-main-background, #fff);\n}\n\n.cn-graph-canvas .vue-flow__edge-path {\n\tstroke: var(--color-border-dark, #888);\n}\n\n.cn-graph-canvas .vue-flow__edge.selected .vue-flow__edge-path {\n\tstroke: var(--color-primary-element, #0082c9);\n}\n\n/* An edge says which way the data flows, and it can only say it with an\n arrowhead — the direction is the one thing a line cannot express on its own.\n\n ⚠️ `!important` IS LOAD-BEARING HERE, not laziness. Vue Flow writes the\n arrowhead's colour as an INLINE style on the polyline it generates, and an\n inline style beats any class selector. The alternative is passing a colour\n through the marker definition in JS, which would hard-code a hex and take\n the arrowhead out of the theme — it would stay dark grey in dark mode while\n the line it terminates went light. Themed here, it follows the user.\n\n `markerUnits` defaults to `strokeWidth`, so the size set alongside this in\n `useFlowStore.canvasEdges` is what keeps the arrow clear of the target\n node's port handle. */\n.cn-graph-canvas .vue-flow__arrowhead polyline,\n.cn-graph-canvas .vue-flow__arrowhead path {\n\tfill: var(--color-text-maxcontrast, #6b6b6b) !important;\n\tstroke: var(--color-text-maxcontrast, #6b6b6b) !important;\n}\n\n.cn-graph-canvas__controls {\n\tposition: absolute;\n\tbottom: 12px;\n\tleft: 12px;\n\tdisplay: flex;\n\tflex-direction: column;\n\tz-index: 5;\n}\n\n.cn-graph-canvas__control {\n\twidth: 28px;\n\theight: 28px;\n\tborder: 1px solid var(--color-border, #d0d0d0);\n\tbackground: var(--color-main-background, #fff);\n\tcolor: var(--color-main-text, #222);\n\tcursor: pointer;\n}\n\n.cn-graph-canvas__control:focus-visible {\n\toutline: 2px solid var(--color-primary-element, #0082c9);\n\toutline-offset: 1px;\n}\n\n.cn-graph-canvas .vue-flow__minimap {\n\tbackground: var(--color-background-dark, #ededed);\n}\n</style>\n"],"names":["_createElementBlock","_createVNode","_withCtx","_mergeProps","_renderSlot","_createBlock","_openBlock","_createElementVNode"],"mappings":"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"}
1
+ {"version":3,"file":"CnGraphCanvas.vue3.js","sources":["../../../../src/components/CnGraphCanvas/CnGraphCanvas.vue?vue&type=template&id=1d8bc107&lang.js"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>\n - SPDX-License-Identifier: EUPL-1.2\n -->\n<template>\n\t<div class=\"cn-graph-canvas\" @drop=\"onDrop\" @dragover.prevent>\n\t\t<VueFlow\n\t\t\t:nodes=\"nodes\"\n\t\t\t:edges=\"edges\"\n\t\t\t:minZoom=\"minZoom\"\n\t\t\t:maxZoom=\"maxZoom\"\n\t\t\t:fitViewOnInit=\"fitView\"\n\t\t\t:snapToGrid=\"snapToGrid\"\n\t\t\t:snapGrid=\"snapGrid\"\n\t\t\t:nodesDraggable=\"interactive\"\n\t\t\t:nodesConnectable=\"interactive\"\n\t\t\t:elementsSelectable=\"interactive\"\n\t\t\t:connectionMode=\"connectionMode\"\n\t\t\t@nodesChange=\"onNodesChange\"\n\t\t\t@edgesChange=\"onEdgesChange\"\n\t\t\t@connect=\"onConnect\"\n\t\t\t@nodeClick=\"onNodeClick\"\n\t\t\t@edgeClick=\"onEdgeClick\"\n\t\t\t@paneClick=\"onPaneClick\">\n\t\t\t<!-- Per-type node components, Vue Flow's convention. A host that\n\t\t\t registers `type: 'trigger'` gets `#node-trigger`; anything\n\t\t\t unregistered falls back to CnFlowNode, which carries the\n\t\t\t keyboard contract. -->\n\t\t\t<template #node-default=\"nodeProps\">\n\t\t\t\t<!-- `@connect` routes a KEYBOARD-completed connection into the\n\t\t\t\t same handler as a pointer one, so `onConnect` is the single\n\t\t\t\t place a new edge leaves this component regardless of input.\n\t\t\t\t Without it the keyboard path wrote into Vue Flow's internal\n\t\t\t\t graph and the host never heard about it — the edge drew and\n\t\t\t\t the save dropped it. -->\n\t\t\t\t<CnFlowNode\n\t\t\t\t\tv-bind=\"nodeProps\"\n\t\t\t\t\t:deletable=\"!readOnly\"\n\t\t\t\t\t@connect=\"onConnect\"\n\t\t\t\t\t@remove=\"onNodeRemove\"\n\t\t\t\t\t@activate=\"onNodeActivate\"\n\t\t\t\t\t@menu=\"onNodeMenu\">\n\t\t\t\t\t<template #default=\"slotProps\">\n\t\t\t\t\t\t<!-- @slot node The body of a step, rendered inside the\n\t\t\t\t\t\t focusable node wrapper. Receives `{ node }` with the\n\t\t\t\t\t\t node's `id`, `data` and `selected`. The wrapper — and\n\t\t\t\t\t\t with it the keyboard contract and ARIA state — stays\n\t\t\t\t\t\t the canvas's, so a host cannot accidentally replace a\n\t\t\t\t\t\t focusable node with an inert div. -->\n\t\t\t\t\t\t<slot name=\"node\" v-bind=\"slotProps\" />\n\t\t\t\t\t</template>\n\t\t\t\t</CnFlowNode>\n\t\t\t</template>\n\n\t\t\t<!-- Per-type edge components, the same convention as nodes above.\n\t\t\t Registered on `default` because that is the type an edge gets\n\t\t\t when nothing else is asked for — which is every edge, now that\n\t\t\t the ROUTER moved off `type` and into `data.lineType`.\n\n\t\t\t That move is the edge half of the lesson the node slot records:\n\t\t\t `type` selects a COMPONENT, and a router is not a component. So\n\t\t\t long as `type` carried `smoothstep`, Vue Flow answered with its\n\t\t\t own built-in edge and no consumer could add anything to a line\n\t\t\t at all. -->\n\t\t\t<template #edge-default=\"edgeProps\">\n\t\t\t\t<CnFlowEdge\n\t\t\t\t\tv-bind=\"edgeProps\"\n\t\t\t\t\t:lineType=\"edgeProps.data && edgeProps.data.lineType\"\n\t\t\t\t\t:animated=\"!(edgeProps.data && edgeProps.data.animated === false)\"\n\t\t\t\t\t:label-aria-label=\"edgeProps.data && edgeProps.data.labelAriaLabel\"\n\t\t\t\t\t@labelMove=\"onEdgeLabelMove\"\n\t\t\t\t\t@labelClick=\"onEdgeLabelClick\"\n\t\t\t\t\t@labelContext=\"onEdgeLabelContext\">\n\t\t\t\t\t<template #label=\"slotProps\">\n\t\t\t\t\t\t<!-- @slot edge-label The chrome of a connection's\n\t\t\t\t\t\t label. Receives `{ edge }` with the edge's `id`,\n\t\t\t\t\t\t `data` and `selected`. Rendered inside a focusable\n\t\t\t\t\t\t control the canvas owns, so render inert content:\n\t\t\t\t\t\t the arrow keys, the focus ring and the ARIA name\n\t\t\t\t\t\t are already handled. An edge whose host renders\n\t\t\t\t\t\t nothing here draws no label control at all. -->\n\t\t\t\t\t\t<slot name=\"edge-label\" v-bind=\"slotProps\" />\n\t\t\t\t\t</template>\n\n\t\t\t\t\t<template #adornment=\"slotProps\">\n\t\t\t\t\t\t<!-- @slot edge-adornment A host's own controls beside a\n\t\t\t\t\t\t connection's label. Receives `{ edge }`. Unlike\n\t\t\t\t\t\t `edge-label` this is NOT inside the label's button,\n\t\t\t\t\t\t so what goes here may be interactive — a replay's\n\t\t\t\t\t\t payload control is the case it exists for. -->\n\t\t\t\t\t\t<slot name=\"edge-adornment\" v-bind=\"slotProps\" />\n\t\t\t\t\t</template>\n\t\t\t\t</CnFlowEdge>\n\t\t\t</template>\n\n\t\t\t<Background v-if=\"showBackground\" :gap=\"snapGrid[0]\" />\n\t\t\t<MiniMap v-if=\"showMiniMap\" />\n\t\t</VueFlow>\n\n\t\t<!-- OUR OWN CONTROLS, not `@vue-flow/controls`.\n\t\t The library's control bar renders bare <button> elements with an\n\t\t icon and no accessible name, which axe reports as `button-name` at\n\t\t SERIOUS impact — caught by the e2e in this change, not by the unit\n\t\t lane or the gates. Rather than ship a control bar a screen-reader\n\t\t user cannot identify, the buttons are ours and carry real labels. -->\n\t\t<div v-if=\"showControls\" class=\"cn-graph-canvas__controls\">\n\t\t\t<button\n\t\t\t\ttype=\"button\"\n\t\t\t\tclass=\"cn-graph-canvas__control\"\n\t\t\t\t:aria-label=\"t('nextcloud-vue', 'Zoom in')\"\n\t\t\t\t@click=\"zoomIn()\">\n\t\t\t\t+\n\t\t\t</button>\n\t\t\t<button\n\t\t\t\ttype=\"button\"\n\t\t\t\tclass=\"cn-graph-canvas__control\"\n\t\t\t\t:aria-label=\"t('nextcloud-vue', 'Zoom out')\"\n\t\t\t\t@click=\"zoomOut()\">\n\t\t\t\t−\n\t\t\t</button>\n\t\t\t<button\n\t\t\t\ttype=\"button\"\n\t\t\t\tclass=\"cn-graph-canvas__control\"\n\t\t\t\t:aria-label=\"t('nextcloud-vue', 'Fit the whole flow in view')\"\n\t\t\t\t@click=\"fitViewNow()\">\n\t\t\t\t⤢\n\t\t\t</button>\n\t\t</div>\n\t</div>\n</template>\n\n<script>\nimport { Background } from '@vue-flow/background'\nimport { useVueFlow, VueFlow } from '@vue-flow/core'\nimport { MiniMap } from '@vue-flow/minimap'\nimport CnFlowEdge from './CnFlowEdge.vue'\nimport CnFlowNode from './CnFlowNode.vue'\n\n/**\n * A graph canvas built on Vue Flow — the library n8n runs.\n *\n * WHY THIS IS NOT A WRAPPER AROUND THE OLD API\n * --------------------------------------------\n * This component used to be ~1,300 hand-written lines re-implementing pan,\n * zoom, drag, connect, resize and hit-testing on a bare SVG, because the fleet\n * was on Vue 2.7 and every maintained canvas library is Vue-3-only (ADR-065\n * §5). ADR-081 removed that constraint and named this library as the reason for\n * the migration.\n *\n * The props therefore MOVED rather than being translated. The graph page was\n * not in production, so there was no consumer to protect, and a shim can only\n * ever expose what the old API could already express — `fitView`, `<MiniMap>`,\n * `connectionMode` and the node resizer would all have stayed unreachable\n * behind props designed for a hand-drawn SVG.\n *\n * `nodeWidth` / `nodeHeight` are gone entirely. They existed only so hand-drawn\n * edges could guess where a node's centre was, and the old docblock admitted\n * the failure mode: \"set them to match what your node slot actually renders, or\n * edges will attach off-centre\". Vue Flow measures the rendered node, so the\n * whole class of bug went with the props.\n *\n * ⚠️ THE CANVAS STILL NEVER MUTATES THE GRAPH. Vue Flow is perfectly willing to\n * own node positions internally; here it does not. Changes are emitted and the\n * host decides what to persist — the same rule the previous implementation\n * held, and the reason a canvas can be dropped into a read-only surface without\n * auditing it first.\n *\n * ⚠️ KEYBOARD OPERATION LIVES IN {@see CnFlowNode}. Vue Flow is pointer-first;\n * the focus/ARIA/key contract is re-implemented in the node component, not\n * inherited from the library. See that file for why the multi-exit case is the\n * one that matters.\n */\nexport default {\n\tname: 'CnGraphCanvas',\n\n\tcomponents: { VueFlow, Background, MiniMap, CnFlowNode, CnFlowEdge },\n\n\tprops: {\n\t\t/** Nodes in Vue Flow's shape: `{ id, type, position: {x,y}, data }`. */\n\t\tnodes: {\n\t\t\ttype: Array,\n\t\t\trequired: true,\n\t\t},\n\n\t\t/** Edges in Vue Flow's shape: `{ id, source, target }`. */\n\t\tedges: {\n\t\t\ttype: Array,\n\t\t\trequired: true,\n\t\t},\n\n\t\t/**\n\t\t * When true, every interaction is refused — drag, connect AND select.\n\t\t *\n\t\t * Expressed as three Vue Flow flags below. Missing one produces a canvas\n\t\t * that LOOKS locked and is not, so the three are set together and tested\n\t\t * together.\n\t\t */\n\t\treadOnly: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Frame the whole graph on first render. */\n\t\tfitView: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Snap dragged nodes to the grid. */\n\t\tsnapToGrid: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Grid spacing, as Vue Flow's `[x, y]`. */\n\t\tsnapGrid: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [16, 16],\n\t\t},\n\n\t\t/** Minimum zoom. */\n\t\tminZoom: {\n\t\t\ttype: Number,\n\t\t\tdefault: 0.2,\n\t\t},\n\n\t\t/** Maximum zoom. */\n\t\tmaxZoom: {\n\t\t\ttype: Number,\n\t\t\tdefault: 2,\n\t\t},\n\n\t\t/** Vue Flow's connection mode — `loose` or `strict`. */\n\t\tconnectionMode: {\n\t\t\ttype: String,\n\t\t\tdefault: 'loose',\n\t\t},\n\n\t\t/** Draw the dotted background. */\n\t\tshowBackground: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Draw the zoom/fit controls. */\n\t\tshowControls: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/** Draw the minimap. */\n\t\tshowMiniMap: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\t},\n\n\temits: [\n\t\t'nodes-change',\n\t\t'edges-change',\n\t\t'connect',\n\t\t'node-select',\n\t\t'edge-select',\n\t\t'edge-label-move',\n\t\t'edge-label-click',\n\t\t'edge-label-context',\n\t\t'canvas-click',\n\t\t'canvas-drop',\n\n\t\t'node-remove',\n\t\t'node-activate',\n\t\t'node-menu',\n\n\t\t/**\n\t\t * The canvas zoom changed, by any route — a control here, the host's own\n\t\t * toolbar, the wheel, or a fit-view.\n\t\t *\n\t\t * @type {number}\n\t\t */\n\t\t'zoom-change',\n\t],\n\n\tsetup() {\n\t\t// ⚠️ ALIASED. `fitView` is already a BOOLEAN PROP on this component\n\t\t// (frame the graph on first render), and Vue Flow's composable exports a\n\t\t// FUNCTION of the same name. Returning both puts two `fitView` bindings\n\t\t// on the instance — eslint's `vue/no-dupe-keys` catches it, but the\n\t\t// symptom would have been a fit button that tries to call `true`.\n\t\t// `zoomTo` and `viewport` so a HOST toolbar can drive the zoom and show\n\t\t// the real figure — including one the wheel or a fit-view changed.\n\t\tconst { project, zoomIn, zoomOut, zoomTo, fitView: fitViewNow, viewport } = useVueFlow()\n\n\t\treturn { project, zoomIn, zoomOut, zoomTo, fitViewNow, viewport }\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * @return {boolean} Whether the canvas accepts interaction at all.\n\t\t */\n\t\tinteractive() {\n\t\t\treturn this.readOnly === false\n\t\t},\n\t},\n\n\twatch: {\n\t\t// Reported rather than owned: Vue Flow holds the transform, so a host\n\t\t// that mirrors it in its own toolbar must hear every change, not only\n\t\t// the ones its own buttons caused.\n\t\t'viewport.zoom': {\n\t\t\timmediate: true,\n\t\t\thandler(value) {\n\t\t\t\tthis.$emit('zoom-change', value)\n\t\t\t},\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Forward Vue Flow's node changes.\n\t\t *\n\t\t * @param {Array<object>} changes The changes.\n\t\t * @return {void}\n\t\t */\n\t\tonNodesChange(changes) {\n\t\t\t/**\n\t\t\t * @event nodes-change Vue Flow's node change stream — position,\n\t\t\t * selection, dimensions, removal. The canvas does NOT apply these;\n\t\t\t * the host decides what to persist. Drag frames arrive with\n\t\t\t * `dragging: true`, so a host that writes on every change writes\n\t\t\t * once per animation frame.\n\t\t\t */\n\t\t\tthis.$emit('nodes-change', changes)\n\t\t},\n\n\t\t/**\n\t\t * Forward Vue Flow's edge changes.\n\t\t *\n\t\t * @param {Array<object>} changes The changes.\n\t\t * @return {void}\n\t\t */\n\t\tonEdgesChange(changes) {\n\t\t\t/**\n\t\t\t * @event edges-change Vue Flow's edge change stream, same contract\n\t\t\t * as `nodes-change`: reported, never applied here.\n\t\t\t */\n\t\t\tthis.$emit('edges-change', changes)\n\t\t},\n\n\t\t/**\n\t\t * Forward a new connection.\n\t\t *\n\t\t * @param {object} connection The connection.\n\t\t * @return {void}\n\t\t */\n\t\t/**\n\t\t * Pass a node's removal request up to the host.\n\t\t *\n\t\t * @param {string} id The node that asked to be removed.\n\t\t * @return {void}\n\t\t */\n\t\tonNodeRemove(id) {\n\t\t\t/**\n\t\t\t * @event node-remove The focused node should be removed, by id.\n\t\t\t * Raised by Delete/Backspace on a node. The canvas removes\n\t\t\t * nothing itself — the host owns `nodes` and `edges`, and a node\n\t\t\t * dropped here would leave the edges pointing at it behind.\n\t\t\t * @type {string}\n\t\t\t */\n\t\t\tthis.$emit('node-remove', id)\n\t\t},\n\n\t\tonConnect(connection) {\n\t\t\t/**\n\t\t\t * @event connect A new connection was made, by pointer OR by\n\t\t\t * keyboard. Carries Vue Flow's\n\t\t\t * `{ source, target, sourceHandle, targetHandle }`, with the\n\t\t\t * handles reduced to the PORT the host declared — see\n\t\t\t * `portIdOf` below.\n\t\t\t */\n\t\t\tthis.$emit('connect', {\n\t\t\t\t...connection,\n\t\t\t\tsourceHandle: this.portIdOf(connection?.sourceHandle),\n\t\t\t\ttargetHandle: this.portIdOf(connection?.targetHandle),\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Reduce a HANDLE id to the PORT id the host declared.\n\t\t *\n\t\t * A node draws more handles than it has ports — one exit is offered on\n\t\t * both the right and the bottom edge, one entry on both the left and the\n\t\t * top — because a line has to be able to leave and arrive on the side\n\t\t * the graph reads best from. Vue Flow keys handles by id, so those\n\t\t * cannot share one, and the side is encoded into the id instead\n\t\t * (`yes__right`). See CnFlowNode's `exitHandles`.\n\t\t *\n\t\t * ⚠️ WHICH MEANS THE ENCODED ID MUST NEVER LEAVE THE CANVAS. A branch\n\t\t * recorded as `yes__bottom` names a port the engine has never heard of,\n\t\t * and a flow whose branch ids do not match its catalogue is one the\n\t\t * engine cannot route — it would take the default exit and the canvas\n\t\t * would keep showing the branch the author drew. So both routes into\n\t\t * this method (pointer via <VueFlow>, keyboard via CnFlowNode) are\n\t\t * stripped here, in the one place every connection passes through.\n\t\t *\n\t\t * An entry handle carries no port of its own — a node has exactly one\n\t\t * inbound port — so it reduces to null rather than to the literal `in`.\n\t\t *\n\t\t * @param {string|null|undefined} handleId The handle id Vue Flow reports.\n\t\t * @return {string|null} The port id, or null.\n\t\t */\n\t\tportIdOf(handleId) {\n\t\t\tif (typeof handleId !== 'string') {\n\t\t\t\treturn null\n\t\t\t}\n\n\t\t\tconst cut = handleId.lastIndexOf('__')\n\t\t\tconst port = cut === -1 ? handleId : handleId.slice(0, cut)\n\n\t\t\treturn port === 'in' ? null : port\n\t\t},\n\n\t\t/**\n\t\t * Forward Enter or Space on a node: the keyboard double-click.\n\t\t *\n\t\t * @param {string} id The node id.\n\t\t * @return {void}\n\t\t */\n\t\tonNodeActivate(id) {\n\t\t\tif (this.readOnly === true) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t/**\n\t\t\t * @event node-activate Enter or Space on a node. Payload is the id.\n\t\t\t */\n\t\t\tthis.$emit('node-activate', id)\n\t\t},\n\n\t\t/**\n\t\t * Forward Shift+F10 or the menu key on a node: the keyboard click menu.\n\t\t *\n\t\t * @param {object} request `{ id, clientX, clientY }`, a point at the node.\n\t\t * @return {void}\n\t\t */\n\t\tonNodeMenu(request) {\n\t\t\tif (this.readOnly === true) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t/**\n\t\t\t * @event node-menu Shift+F10 or the menu key on a node. Payload is\n\t\t\t * `{ id, clientX, clientY }`.\n\t\t\t */\n\t\t\tthis.$emit('node-menu', request)\n\t\t},\n\n\t\t/**\n\t\t * Forward a node click.\n\t\t *\n\t\t * @param {object} event Vue Flow's node event.\n\t\t * @return {void}\n\t\t */\n\t\tonNodeClick(event) {\n\t\t\t/**\n\t\t\t * @event node-select A node was clicked. Payload is Vue Flow's\n\t\t\t * `{ node }`, so a host tracking bare ids reads `event.node.id`.\n\t\t\t */\n\t\t\tthis.$emit('node-select', event)\n\t\t},\n\n\t\t/**\n\t\t * Forward an edge click.\n\t\t *\n\t\t * @param {object} event Vue Flow's edge event.\n\t\t * @return {void}\n\t\t */\n\t\tonEdgeClick(event) {\n\t\t\t/**\n\t\t\t * @event edge-select An edge was clicked.\n\t\t\t */\n\t\t\tthis.$emit('edge-select', event)\n\t\t},\n\n\t\t/**\n\t\t * Forward a label slid along its line.\n\t\t *\n\t\t * @param {{id: string, labelT: number}} payload The edge and its new\n\t\t * fraction, already clamped.\n\t\t * @return {void}\n\t\t */\n\t\tonEdgeLabelMove(payload) {\n\t\t\t/**\n\t\t\t * @event edge-label-move A connection's label was moved, by pointer\n\t\t\t * or by keyboard. Carries `{ id, labelT }`. As everywhere else in\n\t\t\t * this component the change is REPORTED, not applied — the host\n\t\t\t * stores `labelT` and feeds it back down.\n\t\t\t */\n\t\t\tthis.$emit('edge-label-move', payload)\n\t\t},\n\n\t\t/**\n\t\t * Forward an activated label.\n\t\t *\n\t\t * @param {string} id The edge id.\n\t\t * @return {void}\n\t\t */\n\t\tonEdgeLabelClick(id) {\n\t\t\t/**\n\t\t\t * @event edge-label-click A connection's label was activated.\n\t\t\t */\n\t\t\tthis.$emit('edge-label-click', id)\n\t\t},\n\n\t\t/**\n\t\t * Forward a right-click on a label.\n\t\t *\n\t\t * @param {{id: string, event: MouseEvent}} payload The edge and event.\n\t\t * @return {void}\n\t\t */\n\t\tonEdgeLabelContext(payload) {\n\t\t\t/**\n\t\t\t * @event edge-label-context A connection's label was right-clicked.\n\t\t\t * Hosts open the same menu they open for the line itself.\n\t\t\t */\n\t\t\tthis.$emit('edge-label-context', payload)\n\t\t},\n\n\t\t/**\n\t\t * Forward a click on the empty pane.\n\t\t *\n\t\t * @param {object} event The click event.\n\t\t * @return {void}\n\t\t */\n\t\tonPaneClick(event) {\n\t\t\t/**\n\t\t\t * @event canvas-click The empty pane was clicked — hosts use this to\n\t\t\t * clear a selection.\n\t\t\t */\n\t\t\tthis.$emit('canvas-click', event)\n\t\t},\n\n\t\t/**\n\t\t * A palette drop, with the point already converted to canvas space.\n\t\t *\n\t\t * Kept as a Conduction-level event because HTML5 drop plus coordinate\n\t\t * projection is glue every consumer would otherwise write. The\n\t\t * projection is Vue Flow's `project()` rather than the old hand-rolled\n\t\t * `(clientX - rect.left - panOffset.x) / zoom`.\n\t\t *\n\t\t * The canvas does not create the node — the host does, mirroring the\n\t\t * rule that it never mutates positions either.\n\t\t *\n\t\t * @param {DragEvent} event The drop event.\n\t\t * @return {void}\n\t\t */\n\t\tonDrop(event) {\n\t\t\tif (this.readOnly === true) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst bounds = this.$el.getBoundingClientRect()\n\t\t\tconst position = this.project({\n\t\t\t\tx: event.clientX - bounds.left,\n\t\t\t\ty: event.clientY - bounds.top,\n\t\t\t})\n\n\t\t\t/**\n\t\t\t * @event canvas-drop An HTML5 drop landed on the canvas, with\n\t\t\t * `position` already projected into canvas space and the native\n\t\t\t * `event` alongside so the host can read `dataTransfer`. The canvas\n\t\t\t * never creates the node itself.\n\t\t\t */\n\t\t\tthis.$emit('canvas-drop', { position, event })\n\t\t},\n\t},\n}\n</script>\n\n<style>\n/* Vue Flow's own stylesheet, themed through Nextcloud variables. Imported\n un-scoped because it styles elements Vue Flow renders outside this\n component's scope. */\n@import '@vue-flow/core/dist/style.css';\n@import '@vue-flow/core/dist/theme-default.css';\n\n/* ⚠️ EVERY VUE FLOW PLUGIN SHIPS ITS OWN STYLESHEET, AND ONLY CORE'S WAS HERE.\n\n `@vue-flow/node-resizer` was the one that mattered: its stylesheet is what\n gives a resize control `position: absolute` and a 5px handle. Without it the\n handles CnFlowNode renders have no size and no position, so a node that is\n resizable looks exactly like a node that is not — the component was mounted\n and the affordance was invisible.\n\n The minimap's own sheet is small (a background colour and the grab cursors);\n its geometry comes from `.vue-flow__panel` in core, which was already here.\n Imported for the cursors, and before our own rules so the theme below still\n wins. */\n@import '@vue-flow/node-resizer/dist/style.css';\n@import '@vue-flow/minimap/dist/style.css';\n\n.cn-graph-canvas {\n\tposition: relative;\n\twidth: 100%;\n\theight: 100%;\n\tmin-height: 320px;\n}\n\n/* ⚠️ VUE FLOW'S DEFAULT NODE DRAWS A BOX WE NEVER ASKED FOR.\n\n Every node this canvas renders goes through the `#node-default` slot, so\n Vue Flow wraps our CnFlowNode in `.vue-flow__node-default` — and\n `theme-default.css` gives that wrapper a 1px border, a white background and\n 10px of padding. The result was a box around our box: the wrapper's border,\n then `.cn-flow-node`'s own. Nothing failed and nothing warned; a node just\n quietly had one more outline than it was designed to have.\n\n Stripped, not restyled: `.cn-flow-node` is the node's chrome, and it is the\n ONLY border a node draws. `width` stays 150px because Vue Flow measures the\n wrapper to route edges and place handles — dropping it would move every\n node's footprint, which is a layout change, not a fix for a stray border. */\n.cn-graph-canvas .vue-flow__node-default {\n\tpadding: 0;\n\tborder: 0;\n\tborder-radius: 0;\n\tbackground: none;\n\n\t/* The handle colour reaches `.cn-flow-node__handle` through this variable.\n\t Vue Flow's own `.vue-flow__node-default .vue-flow__handle` rule outranks\n\t our single-class one, so a themed handle lost to a hard-coded #1a192b on\n\t every node — setting the variable themes it at the source instead of\n\t fighting the specificity. */\n\t--vf-handle: var(--color-primary-element, #0082c9);\n}\n\n/* The selected/focus/hover states re-assert the border and shadow the rule\n above just removed, so they have to be stripped too — otherwise clicking a\n node brought the extra box back. Selection is shown by `.cn-flow-node--selected`,\n and focus by `.cn-flow-node:focus-visible`, both on the node itself. */\n.cn-graph-canvas .vue-flow__node-default.selected,\n.cn-graph-canvas .vue-flow__node-default.selected:hover,\n.cn-graph-canvas .vue-flow__node-default.selectable:hover,\n.cn-graph-canvas .vue-flow__node-default:focus,\n.cn-graph-canvas .vue-flow__node-default:focus-visible {\n\tborder: 0;\n\tbox-shadow: none;\n}\n\n/* THEME, NOT OVERRIDE. Every colour comes from a Nextcloud variable with a\n fallback, so the canvas follows the user's theme — including dark — instead\n of being the one component that ignores it. */\n.cn-graph-canvas .vue-flow__background {\n\tbackground: var(--color-main-background, #fff);\n}\n\n.cn-graph-canvas .vue-flow__edge-path {\n\tstroke: var(--color-border-dark, #888);\n}\n\n.cn-graph-canvas .vue-flow__edge.selected .vue-flow__edge-path {\n\tstroke: var(--color-primary-element, #0082c9);\n}\n\n/* An edge says which way the data flows, and it can only say it with an\n arrowhead — the direction is the one thing a line cannot express on its own.\n\n ⚠️ `!important` IS LOAD-BEARING HERE, not laziness. Vue Flow writes the\n arrowhead's colour as an INLINE style on the polyline it generates, and an\n inline style beats any class selector. The alternative is passing a colour\n through the marker definition in JS, which would hard-code a hex and take\n the arrowhead out of the theme — it would stay dark grey in dark mode while\n the line it terminates went light. Themed here, it follows the user.\n\n `markerUnits` defaults to `strokeWidth`, so the size set alongside this in\n `useFlowStore.canvasEdges` is what keeps the arrow clear of the target\n node's port handle. */\n.cn-graph-canvas .vue-flow__arrowhead polyline,\n.cn-graph-canvas .vue-flow__arrowhead path {\n\tfill: var(--color-text-maxcontrast, #6b6b6b) !important;\n\tstroke: var(--color-text-maxcontrast, #6b6b6b) !important;\n}\n\n.cn-graph-canvas__controls {\n\tposition: absolute;\n\tbottom: 12px;\n\tleft: 12px;\n\tdisplay: flex;\n\tflex-direction: column;\n\tz-index: 5;\n}\n\n.cn-graph-canvas__control {\n\twidth: 28px;\n\theight: 28px;\n\tborder: 1px solid var(--color-border, #d0d0d0);\n\tbackground: var(--color-main-background, #fff);\n\tcolor: var(--color-main-text, #222);\n\tcursor: pointer;\n}\n\n.cn-graph-canvas__control:focus-visible {\n\toutline: 2px solid var(--color-primary-element, #0082c9);\n\toutline-offset: 1px;\n}\n\n.cn-graph-canvas .vue-flow__minimap {\n\tbackground: var(--color-background-dark, #ededed);\n}\n</style>\n"],"names":["_createElementBlock","_createVNode","_withCtx","_mergeProps","_renderSlot","_createBlock","_openBlock","_createElementVNode"],"mappings":"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"}
@@ -231610,6 +231610,19 @@ var script$22 = {
231610
231610
  * edges, its undo stack, its persistence — so nothing is removed here.
231611
231611
  */
231612
231612
  'remove',
231613
+
231614
+ /**
231615
+ * @event activate Enter or Space on the focused node: open this step.
231616
+ * Carries the node id. The pointer equivalent is a double-click.
231617
+ */
231618
+ 'activate',
231619
+
231620
+ /**
231621
+ * @event menu Shift+F10 or the menu key on the focused node: show this
231622
+ * step's actions. Carries `{ id, clientX, clientY }`, a point at the
231623
+ * node, so a host positions the menu as it would for a click.
231624
+ */
231625
+ 'menu',
231613
231626
  ],
231614
231627
 
231615
231628
  setup() {
@@ -231864,7 +231877,8 @@ var script$22 = {
231864
231877
  * The keyboard contract.
231865
231878
  *
231866
231879
  * Arrow keys move; `c` arms an exit and, on a second node, completes the
231867
- * connection; `Escape` cancels. Repeated `c` on the SAME node steps
231880
+ * connection; `Escape` cancels. Enter or Space opens the step, Shift+F10
231881
+ * or the menu key opens its actions. Repeated `c` on the SAME node steps
231868
231882
  * through its exits rather than re-arming the first — the behaviour a
231869
231883
  * mouse gets for free by pointing.
231870
231884
  *
@@ -231913,6 +231927,27 @@ var script$22 = {
231913
231927
  return
231914
231928
  }
231915
231929
 
231930
+ // Opening the step, and its menu, from the keyboard. Only for a key
231931
+ // pressed ON the node: a port inside it is a control of its own.
231932
+ if (event.target === event.currentTarget) {
231933
+ if (event.key === 'Enter' || event.key === ' ') {
231934
+ event.preventDefault();
231935
+ this.$emit('activate', this.id);
231936
+ return
231937
+ }
231938
+
231939
+ if ((event.key === 'F10' && event.shiftKey === true) || event.key === 'ContextMenu') {
231940
+ event.preventDefault();
231941
+ const rect = event.currentTarget.getBoundingClientRect();
231942
+ this.$emit('menu', {
231943
+ id: this.id,
231944
+ clientX: rect.left + rect.width / 2,
231945
+ clientY: rect.bottom,
231946
+ });
231947
+ return
231948
+ }
231949
+ }
231950
+
231916
231951
  // Delete and Backspace both, because which one removes a thing is a
231917
231952
  // platform habit rather than a preference: Backspace is the delete
231918
231953
  // key on a Mac keyboard, and a user who learned one does not think
@@ -232270,6 +232305,8 @@ var script$21 = {
232270
232305
  'canvas-drop',
232271
232306
 
232272
232307
  'node-remove',
232308
+ 'node-activate',
232309
+ 'node-menu',
232273
232310
 
232274
232311
  /**
232275
232312
  * The canvas zoom changed, by any route — a control here, the host's own
@@ -232419,6 +232456,41 @@ var script$21 = {
232419
232456
  return port === 'in' ? null : port
232420
232457
  },
232421
232458
 
232459
+ /**
232460
+ * Forward Enter or Space on a node: the keyboard double-click.
232461
+ *
232462
+ * @param {string} id The node id.
232463
+ * @return {void}
232464
+ */
232465
+ onNodeActivate(id) {
232466
+ if (this.readOnly === true) {
232467
+ return
232468
+ }
232469
+
232470
+ /**
232471
+ * @event node-activate Enter or Space on a node. Payload is the id.
232472
+ */
232473
+ this.$emit('node-activate', id);
232474
+ },
232475
+
232476
+ /**
232477
+ * Forward Shift+F10 or the menu key on a node: the keyboard click menu.
232478
+ *
232479
+ * @param {object} request `{ id, clientX, clientY }`, a point at the node.
232480
+ * @return {void}
232481
+ */
232482
+ onNodeMenu(request) {
232483
+ if (this.readOnly === true) {
232484
+ return
232485
+ }
232486
+
232487
+ /**
232488
+ * @event node-menu Shift+F10 or the menu key on a node. Payload is
232489
+ * `{ id, clientX, clientY }`.
232490
+ */
232491
+ this.$emit('node-menu', request);
232492
+ },
232493
+
232422
232494
  /**
232423
232495
  * Forward a node click.
232424
232496
  *
@@ -232583,13 +232655,15 @@ function render$21(_ctx, _cache, $props, $setup, $data, $options) {
232583
232655
  vue$1.createVNode(_component_CnFlowNode, vue$1.mergeProps(nodeProps, {
232584
232656
  deletable: !$props.readOnly,
232585
232657
  onConnect: $options.onConnect,
232586
- onRemove: $options.onNodeRemove
232658
+ onRemove: $options.onNodeRemove,
232659
+ onActivate: $options.onNodeActivate,
232660
+ onMenu: $options.onNodeMenu
232587
232661
  }), {
232588
232662
  default: vue$1.withCtx((slotProps) => [
232589
232663
  vue$1.renderSlot(_ctx.$slots, "node", vue$1.normalizeProps(vue$1.guardReactiveProps(slotProps)))
232590
232664
  ]),
232591
232665
  _: 3 /* FORWARDED */
232592
- }, 16 /* FULL_PROPS */, ["deletable", "onConnect", "onRemove"])
232666
+ }, 16 /* FULL_PROPS */, ["deletable", "onConnect", "onRemove", "onActivate", "onMenu"])
232593
232667
  ]),
232594
232668
  "edge-default": vue$1.withCtx((edgeProps) => [
232595
232669
  vue$1.createVNode(_component_CnFlowEdge, vue$1.mergeProps(edgeProps, {
@@ -234359,6 +234433,31 @@ var script$1$ = {
234359
234433
  this.openNodeMenu({ item: id, event: mouse });
234360
234434
  },
234361
234435
 
234436
+ /**
234437
+ * Enter or Space on a step: open its editor, as a double-click does.
234438
+ *
234439
+ * @param {string} id The step id.
234440
+ * @return {void}
234441
+ */
234442
+ onNodeActivate(id) {
234443
+ this.store.selectedNodeId = id;
234444
+ this.store.editingNodeId = id;
234445
+ },
234446
+
234447
+ /**
234448
+ * Shift+F10 or the menu key on a step: its menu, at the step.
234449
+ *
234450
+ * @param {object} request The node's request.
234451
+ * @param {string} request.id The step id.
234452
+ * @param {number} request.clientX Where to place the menu.
234453
+ * @param {number} request.clientY Where to place the menu.
234454
+ * @return {void}
234455
+ */
234456
+ onNodeMenu({ id, clientX, clientY }) {
234457
+ this.store.selectedNodeId = id;
234458
+ this.openNodeMenu({ item: id, event: { clientX, clientY } });
234459
+ },
234460
+
234362
234461
  /**
234363
234462
  * Place a palette node where it was dropped.
234364
234463
  *
@@ -234973,6 +235072,8 @@ function render$1$(_ctx, _cache, $props, $setup, $data, $options) {
234973
235072
  maxZoom: $data.maxZoom,
234974
235073
  onZoomChange: _cache[8] || (_cache[8] = $event => ($data.zoom = $event)),
234975
235074
  onNodeSelect: $options.onNodeSelect,
235075
+ onNodeActivate: $options.onNodeActivate,
235076
+ onNodeMenu: $options.onNodeMenu,
234976
235077
  onEdgeSelect: $options.onEdgeSelect,
234977
235078
  onEdgeLabelClick: $options.onEdgeLabelClick,
234978
235079
  onEdgeLabelContext: $options.onEdgeLabelContext,
@@ -235018,7 +235119,7 @@ function render$1$(_ctx, _cache, $props, $setup, $data, $options) {
235018
235119
  : vue$1.createCommentVNode("v-if", true)
235019
235120
  ]),
235020
235121
  _: 1 /* STABLE */
235021
- }, 8 /* PROPS */, ["nodes", "edges", "minZoom", "maxZoom", "onNodeSelect", "onEdgeSelect", "onEdgeLabelClick", "onEdgeLabelContext", "onEdgeLabelMove", "onCanvasClick", "onNodesChange", "onCanvasDrop"]),
235122
+ }, 8 /* PROPS */, ["nodes", "edges", "minZoom", "maxZoom", "onNodeSelect", "onNodeActivate", "onNodeMenu", "onEdgeSelect", "onEdgeLabelClick", "onEdgeLabelContext", "onEdgeLabelMove", "onCanvasClick", "onNodesChange", "onCanvasDrop"]),
235022
235123
  vue$1.createVNode(_component_CnContextMenu, {
235023
235124
  open: $setup.nodeMenuOpen,
235024
235125
  "onUpdate:open": _cache[11] || (_cache[11] = $event => (($setup.nodeMenuOpen) = $event)),
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: package/dist/nextcloud-vue.cjs.js.map
3
- size: 35684577 bytes
4
- sha256: 40220f92c6defbd478644408d9d23a88047c47798fb52eefee28e6d70e0fbc9a
3
+ size: 35692438 bytes
4
+ sha256: 9054c705b23062b8dca68969cf561ce87cf95a1f2c6dea28e8e5cb78a2ca437c
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conduction/nextcloud-vue",
3
- "version": "2.56.0-beta.21",
3
+ "version": "2.56.0-beta.22",
4
4
  "description": "Shared Vue component library for Conduction Nextcloud apps — complements @nextcloud/vue with higher-level components, OpenRegister integration, and NL Design System support",
5
5
  "homepage": "https://github.com/ConductionNL/nextcloud-vue#readme",
6
6
  "bugs": {
@@ -0,0 +1,206 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ */
5
+
6
+ /**
7
+ * Scores every component under src/components on how much it relies on
8
+ * OpenRegister and writes the result to docs/components/_generated/reliance.json
9
+ * for the documentation site.
10
+ *
11
+ * Two numbers per component:
12
+ * - direct: lines inside the component's own directory that reference
13
+ * OpenRegister (store imports, API paths, composables, the register and
14
+ * schema selector).
15
+ * - transitive: files outside the directory, reached by following the actual
16
+ * import statements, that carry such a reference. A wrapper with no
17
+ * reference of its own still counts the children it cannot render without,
18
+ * while importing the widget registry alone does not drag in every widget.
19
+ *
20
+ * The tier follows the sum: none (0), light (1 to 3), medium (4 to 10) and
21
+ * heavy (11 and more). A component with tier none does not touch OpenRegister
22
+ * itself nor through anything it imports.
23
+ *
24
+ * Usage: node scripts/openregister-reliance.js
25
+ */
26
+
27
+ const fs = require('fs')
28
+ const path = require('path')
29
+
30
+ /** Patterns that mark a direct OpenRegister reliance; each hit counts once per line. */
31
+ const DIRECT_MARKERS = [
32
+ /\/apps\/openregister/,
33
+ /\bopenregister\b/i,
34
+ /useObjectStore|useRegisterStore|useSchemaStore|createObjectStore|createCrudStore/,
35
+ /from '[^']*\/store(\/index\.js)?'/,
36
+ /from '[^']*\/store\/[^']+'/,
37
+ /objectService|ObjectService/,
38
+ /CnRegisterSchemaSelect/,
39
+ ]
40
+
41
+ const SOURCE_FILE = /\.(vue|js|ts|mjs)$/
42
+ const TEST_FILE = /\.(spec|test)\./
43
+
44
+ /**
45
+ * @param {string} dir directory to walk
46
+ * @return {string[]} source files below it, tests excluded
47
+ */
48
+ function walk(dir) {
49
+ const files = []
50
+ for (const entry of fs.readdirSync(dir)) {
51
+ const full = path.join(dir, entry)
52
+ if (fs.statSync(full).isDirectory()) {
53
+ if (entry !== 'node_modules' && entry !== '__tests__') {
54
+ files.push(...walk(full))
55
+ }
56
+ } else if (SOURCE_FILE.test(entry) && !TEST_FILE.test(entry)) {
57
+ files.push(full)
58
+ }
59
+ }
60
+ return files
61
+ }
62
+
63
+ /**
64
+ * @param {string} source file contents
65
+ * @return {number} lines with a direct marker, comments excluded
66
+ */
67
+ function countDirect(source) {
68
+ let count = 0
69
+ for (const line of source.split('\n')) {
70
+ const trimmed = line.trimStart()
71
+ if (trimmed.startsWith('*') || trimmed.startsWith('//') || trimmed.startsWith('/*')) {
72
+ continue
73
+ }
74
+ if (DIRECT_MARKERS.some((marker) => marker.test(line))) {
75
+ count++
76
+ }
77
+ }
78
+ return count
79
+ }
80
+
81
+ /**
82
+ * Resolves a relative import to a file on disk the way the bundler does.
83
+ *
84
+ * @param {string} fromFile importing file
85
+ * @param {string} specifier the relative import path
86
+ * @return {string|null}
87
+ */
88
+ function resolveImport(fromFile, specifier) {
89
+ const base = path.resolve(path.dirname(fromFile), specifier)
90
+ const candidates = [base, base + '.js', base + '.mjs', base + '.ts', base + '.vue', path.join(base, 'index.js'), path.join(base, 'index.mjs'), path.join(base, 'index.ts')]
91
+ return candidates.find((candidate) => fs.existsSync(candidate) && fs.statSync(candidate).isFile()) ?? null
92
+ }
93
+
94
+ /**
95
+ * @param {string} source file contents
96
+ * @return {string[]} relative import specifiers, static and dynamic
97
+ */
98
+ function relativeImports(source) {
99
+ return [...source.matchAll(/(?:from\s+|import\s*\(\s*|import\s+)'(\.{1,2}\/[^']+)'/g)].map((match) => match[1])
100
+ }
101
+
102
+ /**
103
+ * @param {number} score direct lines plus transitive files
104
+ * @return {'none'|'light'|'medium'|'heavy'}
105
+ */
106
+ function tierOf(score) {
107
+ if (score === 0) {
108
+ return 'none'
109
+ }
110
+ if (score <= 3) {
111
+ return 'light'
112
+ }
113
+ if (score <= 10) {
114
+ return 'medium'
115
+ }
116
+ return 'heavy'
117
+ }
118
+
119
+ /**
120
+ * @param {string} componentName e.g. CnDeleteDialog
121
+ * @return {string} the docs page slug, e.g. cn-delete-dialog
122
+ */
123
+ function pageSlug(componentName) {
124
+ return componentName.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()
125
+ }
126
+
127
+ /**
128
+ * @param {string} srcDir the src directory holding components/
129
+ * @param {string|null} docsComponentsDir where the component pages live, to link only existing pages
130
+ * @return {Record<string, {direct: number, transitive: number, tier: string, via: string[], page: string|null}>}
131
+ */
132
+ function computeReliance(srcDir, docsComponentsDir = null) {
133
+ const componentsDir = path.join(srcDir, 'components')
134
+ const files = new Map()
135
+ for (const file of walk(srcDir)) {
136
+ const source = fs.readFileSync(file, 'utf8')
137
+ files.set(file, {
138
+ direct: countDirect(source),
139
+ imports: relativeImports(source).map((specifier) => resolveImport(file, specifier)).filter(Boolean),
140
+ })
141
+ }
142
+
143
+ const reachable = (startFiles) => {
144
+ const own = new Set(startFiles)
145
+ const seen = new Set()
146
+ const queue = [...startFiles]
147
+ while (queue.length > 0) {
148
+ const file = queue.pop()
149
+ for (const next of (files.get(file) ?? { imports: [] }).imports) {
150
+ if (!own.has(next) && !seen.has(next)) {
151
+ seen.add(next)
152
+ queue.push(next)
153
+ }
154
+ }
155
+ }
156
+ return [...seen]
157
+ }
158
+
159
+ const result = {}
160
+ for (const entry of fs.readdirSync(componentsDir).sort()) {
161
+ const dir = path.join(componentsDir, entry)
162
+ if (!fs.statSync(dir).isDirectory() || !/^Cn[A-Z]/.test(entry)) {
163
+ continue
164
+ }
165
+ const ownFiles = walk(dir)
166
+ const direct = ownFiles.reduce((sum, file) => sum + (files.get(file)?.direct ?? 0), 0)
167
+ const relyingFiles = reachable(ownFiles).filter((file) => (files.get(file)?.direct ?? 0) > 0)
168
+ const via = new Set(relyingFiles
169
+ .filter((file) => file.startsWith(componentsDir + path.sep))
170
+ .map((file) => file.slice(componentsDir.length + 1).split(path.sep)[0])
171
+ .filter((name) => name !== entry))
172
+ const slug = pageSlug(entry)
173
+ const hasPage = docsComponentsDir !== null && fs.existsSync(path.join(docsComponentsDir, slug + '.md'))
174
+ result[entry] = {
175
+ direct,
176
+ transitive: relyingFiles.length,
177
+ tier: tierOf(direct + relyingFiles.length),
178
+ via: [...via].sort(),
179
+ page: hasPage ? slug : null,
180
+ }
181
+ }
182
+ return result
183
+ }
184
+
185
+ /**
186
+ * @param {string} repoRoot the repository root
187
+ * @return {{outFile: string, components: object}}
188
+ */
189
+ function writeReliance(repoRoot) {
190
+ const components = computeReliance(path.join(repoRoot, 'src'), path.join(repoRoot, 'docs/components'))
191
+ const outFile = path.join(repoRoot, 'docs/components/_generated/reliance.json')
192
+ fs.mkdirSync(path.dirname(outFile), { recursive: true })
193
+ fs.writeFileSync(outFile, JSON.stringify({ generatedBy: 'scripts/openregister-reliance.js', components }, null, '\t') + '\n')
194
+ return { outFile, components }
195
+ }
196
+
197
+ module.exports = { computeReliance, countDirect, tierOf, pageSlug, writeReliance }
198
+
199
+ if (require.main === module) {
200
+ const { outFile, components } = writeReliance(path.resolve(__dirname, '..'))
201
+ const tiers = {}
202
+ for (const item of Object.values(components)) {
203
+ tiers[item.tier] = (tiers[item.tier] ?? 0) + 1
204
+ }
205
+ console.log(`wrote ${path.relative(process.cwd(), outFile)}: ${Object.keys(components).length} components`, tiers)
206
+ }
@@ -200,6 +200,8 @@
200
200
  :maxZoom="maxZoom"
201
201
  @zoomChange="zoom = $event"
202
202
  @nodeSelect="onNodeSelect"
203
+ @nodeActivate="onNodeActivate"
204
+ @nodeMenu="onNodeMenu"
203
205
  @edgeSelect="onEdgeSelect"
204
206
  @edgeLabelClick="onEdgeLabelClick"
205
207
  @edgeLabelContext="onEdgeLabelContext"
@@ -1713,6 +1715,31 @@ export default {
1713
1715
  this.openNodeMenu({ item: id, event: mouse })
1714
1716
  },
1715
1717
 
1718
+ /**
1719
+ * Enter or Space on a step: open its editor, as a double-click does.
1720
+ *
1721
+ * @param {string} id The step id.
1722
+ * @return {void}
1723
+ */
1724
+ onNodeActivate(id) {
1725
+ this.store.selectedNodeId = id
1726
+ this.store.editingNodeId = id
1727
+ },
1728
+
1729
+ /**
1730
+ * Shift+F10 or the menu key on a step: its menu, at the step.
1731
+ *
1732
+ * @param {object} request The node's request.
1733
+ * @param {string} request.id The step id.
1734
+ * @param {number} request.clientX Where to place the menu.
1735
+ * @param {number} request.clientY Where to place the menu.
1736
+ * @return {void}
1737
+ */
1738
+ onNodeMenu({ id, clientX, clientY }) {
1739
+ this.store.selectedNodeId = id
1740
+ this.openNodeMenu({ item: id, event: { clientX, clientY } })
1741
+ },
1742
+
1716
1743
  /**
1717
1744
  * Place a palette node where it was dropped.
1718
1745
  *
@@ -224,6 +224,19 @@ export default {
224
224
  * edges, its undo stack, its persistence — so nothing is removed here.
225
225
  */
226
226
  'remove',
227
+
228
+ /**
229
+ * @event activate Enter or Space on the focused node: open this step.
230
+ * Carries the node id. The pointer equivalent is a double-click.
231
+ */
232
+ 'activate',
233
+
234
+ /**
235
+ * @event menu Shift+F10 or the menu key on the focused node: show this
236
+ * step's actions. Carries `{ id, clientX, clientY }`, a point at the
237
+ * node, so a host positions the menu as it would for a click.
238
+ */
239
+ 'menu',
227
240
  ],
228
241
 
229
242
  setup() {
@@ -478,7 +491,8 @@ export default {
478
491
  * The keyboard contract.
479
492
  *
480
493
  * Arrow keys move; `c` arms an exit and, on a second node, completes the
481
- * connection; `Escape` cancels. Repeated `c` on the SAME node steps
494
+ * connection; `Escape` cancels. Enter or Space opens the step, Shift+F10
495
+ * or the menu key opens its actions. Repeated `c` on the SAME node steps
482
496
  * through its exits rather than re-arming the first — the behaviour a
483
497
  * mouse gets for free by pointing.
484
498
  *
@@ -527,6 +541,27 @@ export default {
527
541
  return
528
542
  }
529
543
 
544
+ // Opening the step, and its menu, from the keyboard. Only for a key
545
+ // pressed ON the node: a port inside it is a control of its own.
546
+ if (event.target === event.currentTarget) {
547
+ if (event.key === 'Enter' || event.key === ' ') {
548
+ event.preventDefault()
549
+ this.$emit('activate', this.id)
550
+ return
551
+ }
552
+
553
+ if ((event.key === 'F10' && event.shiftKey === true) || event.key === 'ContextMenu') {
554
+ event.preventDefault()
555
+ const rect = event.currentTarget.getBoundingClientRect()
556
+ this.$emit('menu', {
557
+ id: this.id,
558
+ clientX: rect.left + rect.width / 2,
559
+ clientY: rect.bottom,
560
+ })
561
+ return
562
+ }
563
+ }
564
+
530
565
  // Delete and Backspace both, because which one removes a thing is a
531
566
  // platform habit rather than a preference: Backspace is the delete
532
567
  // key on a Mac keyboard, and a user who learned one does not think