diff --git a/.obsidian/community-plugins.json b/.obsidian/community-plugins.json index 0e083ffd..b477314f 100644 --- a/.obsidian/community-plugins.json +++ b/.obsidian/community-plugins.json @@ -1,3 +1,5 @@ [ - "editor-width-slider" + "editor-width-slider", + "better-embedded-canvas", + "advanced-canvas" ] \ No newline at end of file diff --git a/.obsidian/plugins/advanced-canvas/main.js b/.obsidian/plugins/advanced-canvas/main.js new file mode 100644 index 00000000..9180bb75 --- /dev/null +++ b/.obsidian/plugins/advanced-canvas/main.js @@ -0,0 +1,7770 @@ +/* +THIS IS A GENERATED/BUNDLED FILE BY ESBUILD +if you want to view the source, please visit the github repository of this plugin +*/ + +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); + +// src/main.ts +var main_exports = {}; +__export(main_exports, { + default: () => AdvancedCanvasPlugin +}); +module.exports = __toCommonJS(main_exports); +var import_obsidian21 = require("obsidian"); + +// src/utils/icons-helper.ts +var import_obsidian = require("obsidian"); +var CUSTOM_ICONS = { + "shape-pill": ``, + "shape-parallelogram": ``, + "shape-predefined-process": ` + + + + + + `, + "shape-document": ``, + "shape-database": ` + + + + + `, + "border-solid": ``, + "border-dashed": ``, + "border-dotted": ``, + "path-solid": ``, + "path-dotted": ``, + "path-short-dashed": ``, + "path-long-dashed": ``, + "arrow-triangle": ``, + "arrow-triangle-outline": ``, + "arrow-thin-triangle": ``, + "arrow-halved-triangle": ``, + "arrow-diamond": ``, + "arrow-diamond-outline": ``, + "arrow-circle": ``, + "arrow-circle-outline": ``, + "pathfinding-method-bezier": ``, + "pathfinding-method-square": ``, + "arrows-selected": ` + + + + + + + + + + `, + "arrow-right-selected": ` + + + + + + + + `, + "arrow-left-selected": ` + + + + + + + + ` +}; +var IconsHelper = class { + static addIcons() { + for (const [id, svg] of Object.entries(CUSTOM_ICONS)) { + (0, import_obsidian.addIcon)(id, svg); + } + } +}; + +// src/utils/debug-helper.ts +var DebugHelper = class { + constructor(plugin) { + this.logging = true; + this.nodeAddedCount = 0; + this.nodeChangedCount = 0; + this.edgeAddedCount = 0; + this.edgeChangedCount = 0; + this.plugin = plugin; + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:canvas-changed", + (_canvas) => { + this.nodeAddedCount = 0; + this.nodeChangedCount = 0; + this.edgeAddedCount = 0; + this.edgeChangedCount = 0; + } + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:node-added", + (_canvas, _node) => { + if (this.logging) console.count("\u{1F7E2} NodeAdded"); + this.nodeAddedCount++; + } + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:node-changed", + (_canvas, _node) => { + if (this.logging) console.count("\u{1F7E1} NodeChanged"); + this.nodeChangedCount++; + } + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:edge-added", + (_canvas, _edge) => { + if (this.logging) console.count("\u{1F7E2} EdgeAdded"); + this.edgeAddedCount++; + } + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:edge-changed", + (_canvas, _edge) => { + if (this.logging) console.count("\u{1F7E1} EdgeChanged"); + this.edgeChangedCount++; + } + )); + } + resetEfficiency() { + this.nodeAddedCount = 0; + this.nodeChangedCount = 0; + this.edgeAddedCount = 0; + this.edgeChangedCount = 0; + } + logEfficiency() { + const canvas = this.plugin.getCurrentCanvas(); + if (!canvas) return; + console.log("NodeAdded Efficiency:", this.nodeAddedCount / canvas.nodes.size); + console.log("NodeChanged Efficiency:", this.nodeChangedCount / canvas.nodes.size); + console.log("EdgeAdded Efficiency:", this.edgeAddedCount / canvas.edges.size); + console.log("EdgeChanged Efficiency:", this.edgeChangedCount / canvas.edges.size); + } + static markBBox(canvas, bbox, duration = -1) { + const node = canvas.createTextNode({ + pos: { x: bbox.minX, y: bbox.minY }, + size: { width: bbox.maxX - bbox.minX, height: bbox.maxY - bbox.minY }, + text: "", + focus: false + }); + node.setData({ + ...node.getData(), + id: "debug-bbox", + color: "1", + styleAttributes: { + border: "invisible" + } + }); + if (duration >= 0) { + window.setTimeout(() => { + canvas.removeNode(node); + }, duration); + } + } +}; + +// src/settings.ts +var import_obsidian4 = require("obsidian"); + +// src/utils/bbox-helper.ts +var BBoxHelper = class { + static combineBBoxes(bboxes) { + let minX = Infinity; + let minY = Infinity; + let maxX = -Infinity; + let maxY = -Infinity; + for (const bbox of bboxes) { + minX = Math.min(minX, bbox.minX); + minY = Math.min(minY, bbox.minY); + maxX = Math.max(maxX, bbox.maxX); + maxY = Math.max(maxY, bbox.maxY); + } + return { minX, minY, maxX, maxY }; + } + static scaleBBox(bbox, scale) { + const diffX = (scale - 1) * (bbox.maxX - bbox.minX); + const diffY = (scale - 1) * (bbox.maxY - bbox.minY); + return { + minX: bbox.minX - diffX / 2, + maxX: bbox.maxX + diffX / 2, + minY: bbox.minY - diffY / 2, + maxY: bbox.maxY + diffY / 2 + }; + } + static isColliding(bbox1, bbox2) { + return bbox1.minX <= bbox2.maxX && bbox1.maxX >= bbox2.minX && bbox1.minY <= bbox2.maxY && bbox1.maxY >= bbox2.minY; + } + static insideBBox(position, bbox, canTouchEdge) { + if ("x" in position) { + const x = position.x, y = position.y; + return canTouchEdge ? x >= bbox.minX && x <= bbox.maxX && y >= bbox.minY && y <= bbox.maxY : x > bbox.minX && x < bbox.maxX && y > bbox.minY && y < bbox.maxY; + } + return canTouchEdge ? position.minX >= bbox.minX && position.maxX <= bbox.maxX && position.minY >= bbox.minY && position.maxY <= bbox.maxY : position.minX > bbox.minX && position.maxX < bbox.maxX && position.minY > bbox.minY && position.maxY < bbox.maxY; + } + static enlargeBBox(bbox, padding) { + return { + minX: bbox.minX - padding, + minY: bbox.minY - padding, + maxX: bbox.maxX + padding, + maxY: bbox.maxY + padding + }; + } + static moveInDirection(position, side, distance) { + switch (side) { + case "top": + return { x: position.x, y: position.y - distance }; + case "right": + return { x: position.x + distance, y: position.y }; + case "bottom": + return { x: position.x, y: position.y + distance }; + case "left": + return { x: position.x - distance, y: position.y }; + } + } + static getCenterOfBBoxSide(bbox, side) { + switch (side) { + case "top": + return { x: (bbox.minX + bbox.maxX) / 2, y: bbox.minY }; + case "right": + return { x: bbox.maxX, y: (bbox.minY + bbox.maxY) / 2 }; + case "bottom": + return { x: (bbox.minX + bbox.maxX) / 2, y: bbox.maxY }; + case "left": + return { x: bbox.minX, y: (bbox.minY + bbox.maxY) / 2 }; + } + } + static getSideVector(side) { + switch (side) { + case "top": + return { x: 0, y: 1 }; + case "right": + return { x: 1, y: 0 }; + case "bottom": + return { x: 0, y: -1 }; + case "left": + return { x: -1, y: 0 }; + default: + return { x: 0, y: 0 }; + } + } + static getOppositeSide(side) { + switch (side) { + case "top": + return "bottom"; + case "right": + return "left"; + case "bottom": + return "top"; + case "left": + return "right"; + } + } + static isHorizontal(side) { + return side === "left" || side === "right"; + } + static direction(side) { + return side === "right" || side === "bottom" ? 1 : -1; + } +}; + +// src/utils/canvas-helper.ts +var import_obsidian2 = require("obsidian"); +var _CanvasHelper = class _CanvasHelper { + static canvasCommand(plugin, check, run) { + return (checking) => { + const canvas = plugin.getCurrentCanvas(); + if (checking) return canvas !== null && check(canvas); + if (canvas) run(canvas); + return true; + }; + } + static createControlMenuButton(menuOption) { + const quickSetting = activeDocument.createElement("div"); + if (menuOption.id) quickSetting.id = menuOption.id; + quickSetting.classList.add("canvas-control-item"); + (0, import_obsidian2.setIcon)(quickSetting, menuOption.icon); + (0, import_obsidian2.setTooltip)(quickSetting, menuOption.label, { placement: "left" }); + quickSetting.addEventListener("click", () => { + var _a; + return (_a = menuOption.callback) == null ? void 0 : _a.call(menuOption); + }); + return quickSetting; + } + static addControlMenuButton(controlGroup, element) { + var _a; + if (element.id) (_a = controlGroup.querySelector(`#${element.id}`)) == null ? void 0 : _a.remove(); + controlGroup.appendChild(element); + } + static createCardMenuOption(canvas, menuOption, previewNodeSize, onPlaced, onRightClick) { + const menuOptionElement = activeDocument.createElement("div"); + if (menuOption.id) menuOptionElement.id = menuOption.id; + menuOptionElement.classList.add("canvas-card-menu-button"); + menuOptionElement.classList.add("mod-draggable"); + (0, import_obsidian2.setIcon)(menuOptionElement, menuOption.icon); + (0, import_obsidian2.setTooltip)(menuOptionElement, menuOption.label, { placement: "top" }); + menuOptionElement.addEventListener("click", (_e) => { + onPlaced(canvas, this.getCenterCoordinates(canvas, previewNodeSize())); + }); + if (onRightClick) menuOptionElement.addEventListener("contextmenu", (e) => { + e.preventDefault(); + onRightClick(e); + }); + menuOptionElement.addEventListener("pointerdown", (e) => { + canvas.dragTempNode(e, previewNodeSize(), (pos) => { + canvas.deselectAll(); + onPlaced(canvas, pos); + }); + }); + return menuOptionElement; + } + static addCardMenuOption(canvas, element) { + var _a; + if (element.id) (_a = canvas == null ? void 0 : canvas.cardMenuEl.querySelector(`#${element.id}`)) == null ? void 0 : _a.remove(); + canvas == null ? void 0 : canvas.cardMenuEl.appendChild(element); + } + static createPopupMenuOption(menuOption) { + const menuOptionElement = activeDocument.createElement("button"); + if (menuOption.id) menuOptionElement.id = menuOption.id; + menuOptionElement.classList.add("clickable-icon"); + (0, import_obsidian2.setIcon)(menuOptionElement, menuOption.icon); + (0, import_obsidian2.setTooltip)(menuOptionElement, menuOption.label, { placement: "top" }); + menuOptionElement.addEventListener("click", () => { + var _a; + return (_a = menuOption.callback) == null ? void 0 : _a.call(menuOption); + }); + return menuOptionElement; + } + static createExpandablePopupMenuOption(menuOption, subMenuOptions) { + const menuOptionElement = this.createPopupMenuOption({ + ...menuOption, + callback: () => { + var _a, _b, _c; + const submenuId = `${menuOption.id}-submenu`; + if (menuOptionElement.classList.contains("is-active")) { + menuOptionElement.classList.remove("is-active"); + (_b = (_a = menuOptionElement.parentElement) == null ? void 0 : _a.querySelector(`#${submenuId}`)) == null ? void 0 : _b.remove(); + return; + } + menuOptionElement.classList.add("is-active"); + const submenu = (_c = menuOptionElement.parentElement) == null ? void 0 : _c.createDiv(); + if (!submenu) return; + submenu.id = submenuId; + submenu.classList.add("canvas-submenu"); + for (const subMenuOption of subMenuOptions) { + const subMenuOptionElement = this.createPopupMenuOption(subMenuOption); + submenu.appendChild(subMenuOptionElement); + } + } + }); + return menuOptionElement; + } + static addPopupMenuOption(canvas, element, index = -1) { + var _a; + const popupMenuEl = (_a = canvas == null ? void 0 : canvas.menu) == null ? void 0 : _a.menuEl; + if (!popupMenuEl) return; + if (element.id) { + const optionToReplace = popupMenuEl.querySelector(`#${element.id}`); + if (optionToReplace && index === -1) index = Array.from(popupMenuEl.children).indexOf(optionToReplace) - 1; + optionToReplace == null ? void 0 : optionToReplace.remove(); + } + const sisterElement = index >= 0 ? popupMenuEl.children[index] : popupMenuEl.children[popupMenuEl.children.length + index]; + popupMenuEl.insertAfter(element, sisterElement); + } + static getCenterCoordinates(canvas, nodeSize) { + const viewBounds = canvas.getViewportBBox(); + return { + x: (viewBounds.minX + viewBounds.maxX) / 2 - nodeSize.width / 2, + y: (viewBounds.minY + viewBounds.maxY) / 2 - nodeSize.height / 2 + }; + } + static getBBox(canvasElements) { + const bBoxes = canvasElements.map((element) => { + if ("getBBox" in element) return element.getBBox(); + if (element.x !== void 0 && element.y !== void 0 && element.width !== void 0 && element.height !== void 0) + return { minX: element.x, minY: element.y, maxX: element.x + element.width, maxY: element.y + element.height }; + return null; + }).filter((bbox) => bbox !== null); + return BBoxHelper.combineBBoxes(bBoxes); + } + static getSmallestAllowedZoomBBox(canvas, bbox) { + if (canvas.screenshotting) return bbox; + if (canvas.canvasRect.width === 0 || canvas.canvasRect.height === 0) return bbox; + const widthZoom = canvas.canvasRect.width / (bbox.maxX - bbox.minX); + const heightZoom = canvas.canvasRect.height / (bbox.maxY - bbox.minY); + const requiredZoom = Math.min(widthZoom, heightZoom); + if (requiredZoom > _CanvasHelper.MAX_ALLOWED_ZOOM) { + const scaleFactor = requiredZoom / _CanvasHelper.MAX_ALLOWED_ZOOM; + return BBoxHelper.scaleBBox(bbox, scaleFactor); + } + return bbox; + } + static addStyleAttributesToPopup(plugin, canvas, styleAttributes, currentStyleAttributes, setStyleAttribute) { + if (!plugin.settings.getSetting("combineCustomStylesInDropdown")) + this.addStyleAttributesButtons(canvas, styleAttributes, currentStyleAttributes, setStyleAttribute); + else this.addStyleAttributesDropdownMenu(canvas, styleAttributes, currentStyleAttributes, setStyleAttribute); + } + static addStyleAttributesButtons(canvas, stylableAttributes, currentStyleAttributes, setStyleAttribute) { + var _a; + for (const stylableAttribute of stylableAttributes) { + const selectedStyle = (_a = stylableAttribute.options.find((option) => currentStyleAttributes[stylableAttribute.key] === option.value)) != null ? _a : stylableAttribute.options.find((value) => value.value === null); + if (!selectedStyle) { + console.warn(`No "null" style option found for stylable attribute "${stylableAttribute.key}"`); + continue; + } + const menuOption = _CanvasHelper.createExpandablePopupMenuOption({ + id: `menu-option-${stylableAttribute.key}`, + label: stylableAttribute.label, + icon: selectedStyle.icon + }, stylableAttribute.options.map((styleOption) => ({ + label: styleOption.label, + icon: styleOption.icon, + callback: () => { + setStyleAttribute(stylableAttribute, styleOption.value); + currentStyleAttributes[stylableAttribute.key] = styleOption.value; + (0, import_obsidian2.setIcon)(menuOption, styleOption.icon); + menuOption.dispatchEvent(new Event("click")); + } + }))); + _CanvasHelper.addPopupMenuOption(canvas, menuOption); + } + } + static addStyleAttributesDropdownMenu(canvas, stylableAttributes, currentStyleAttributes, setStyleAttribute) { + var _a, _b; + const STYLE_MENU_ID = "style-menu"; + const STYLE_MENU_DROPDOWN_ID = "style-menu-dropdown"; + const STYLE_MENU_DROPDOWN_SUBMENU_ID = "style-menu-dropdown-submenu"; + const popupMenuElement = (_a = canvas == null ? void 0 : canvas.menu) == null ? void 0 : _a.menuEl; + if (!popupMenuElement) return; + (_b = popupMenuElement.querySelector(`#${STYLE_MENU_ID}`)) == null ? void 0 : _b.remove(); + const styleMenuButtonElement = popupMenuElement.createEl("button"); + styleMenuButtonElement.id = STYLE_MENU_ID; + styleMenuButtonElement.classList.add("clickable-icon"); + (0, import_obsidian2.setIcon)(styleMenuButtonElement, "paintbrush"); + (0, import_obsidian2.setTooltip)(styleMenuButtonElement, "Style", { placement: "top" }); + styleMenuButtonElement.addEventListener("click", () => { + var _a2, _b2, _c; + const isOpen = styleMenuButtonElement.classList.toggle("has-active-menu"); + if (!isOpen) { + (_a2 = popupMenuElement.querySelector(`#${STYLE_MENU_DROPDOWN_ID}`)) == null ? void 0 : _a2.remove(); + (_b2 = popupMenuElement.querySelector(`#${STYLE_MENU_DROPDOWN_SUBMENU_ID}`)) == null ? void 0 : _b2.remove(); + return; + } + const styleMenuDropdownElement = popupMenuElement.createDiv(); + styleMenuDropdownElement.id = STYLE_MENU_DROPDOWN_ID; + styleMenuDropdownElement.classList.add("menu"); + styleMenuDropdownElement.style.position = "absolute"; + styleMenuDropdownElement.style.maxHeight = "initial"; + styleMenuDropdownElement.style.top = `${popupMenuElement.getBoundingClientRect().height}px`; + const canvasWrapperCenterX = canvas.wrapperEl.getBoundingClientRect().left + canvas.wrapperEl.getBoundingClientRect().width / 2; + const leftPosition = styleMenuButtonElement.getBoundingClientRect().left - popupMenuElement.getBoundingClientRect().left; + const rightPosition = popupMenuElement.getBoundingClientRect().right - styleMenuButtonElement.getBoundingClientRect().right; + if (popupMenuElement.getBoundingClientRect().left + leftPosition < canvasWrapperCenterX) + styleMenuDropdownElement.style.left = `${leftPosition}px`; + else styleMenuDropdownElement.style.right = `${rightPosition}px`; + for (const stylableAttribute of stylableAttributes) { + const stylableAttributeElement = styleMenuDropdownElement.createDiv(); + stylableAttributeElement.classList.add("menu-item"); + stylableAttributeElement.classList.add("tappable"); + const iconElement = stylableAttributeElement.createDiv(); + iconElement.classList.add("menu-item-icon"); + let selectedStyle = (_c = stylableAttribute.options.find((option) => currentStyleAttributes[stylableAttribute.key] === option.value)) != null ? _c : stylableAttribute.options.find((value) => value.value === null); + if (!selectedStyle) continue; + (0, import_obsidian2.setIcon)(iconElement, selectedStyle.icon); + const labelElement = stylableAttributeElement.createDiv(); + labelElement.classList.add("menu-item-title"); + labelElement.textContent = stylableAttribute.label; + const expandIconElement = stylableAttributeElement.createDiv(); + expandIconElement.classList.add("menu-item-icon"); + (0, import_obsidian2.setIcon)(expandIconElement, "chevron-right"); + stylableAttributeElement.addEventListener("pointerenter", () => { + stylableAttributeElement.classList.add("selected"); + }); + stylableAttributeElement.addEventListener("pointerleave", () => { + stylableAttributeElement.classList.remove("selected"); + }); + stylableAttributeElement.addEventListener("click", () => { + var _a3; + (_a3 = popupMenuElement.querySelector(`#${STYLE_MENU_DROPDOWN_SUBMENU_ID}`)) == null ? void 0 : _a3.remove(); + const styleMenuDropdownSubmenuElement = popupMenuElement.createDiv(); + styleMenuDropdownSubmenuElement.id = STYLE_MENU_DROPDOWN_SUBMENU_ID; + styleMenuDropdownSubmenuElement.classList.add("menu"); + styleMenuDropdownSubmenuElement.style.position = "absolute"; + styleMenuDropdownSubmenuElement.style.maxHeight = "initial"; + const topOffset = parseFloat(window.getComputedStyle(styleMenuDropdownElement).getPropertyValue("padding-top")) + (styleMenuDropdownElement.offsetHeight - styleMenuDropdownElement.clientHeight) / 2; + styleMenuDropdownSubmenuElement.style.top = `${stylableAttributeElement.getBoundingClientRect().top - topOffset - popupMenuElement.getBoundingClientRect().top}px`; + const leftPosition2 = styleMenuDropdownElement.getBoundingClientRect().right - popupMenuElement.getBoundingClientRect().left; + const rightPosition2 = popupMenuElement.getBoundingClientRect().right - styleMenuDropdownElement.getBoundingClientRect().left; + if (popupMenuElement.getBoundingClientRect().left + leftPosition2 < canvasWrapperCenterX) + styleMenuDropdownSubmenuElement.style.left = `${leftPosition2}px`; + else styleMenuDropdownSubmenuElement.style.right = `${rightPosition2}px`; + for (const styleOption of stylableAttribute.options) { + const styleMenuDropdownSubmenuOptionElement = this.createDropdownOptionElement({ + label: styleOption.label, + icon: styleOption.icon, + callback: () => { + setStyleAttribute(stylableAttribute, styleOption.value); + currentStyleAttributes[stylableAttribute.key] = styleOption.value; + selectedStyle = styleOption; + (0, import_obsidian2.setIcon)(iconElement, styleOption.icon); + styleMenuDropdownSubmenuElement.remove(); + } + }); + if (selectedStyle === styleOption) { + styleMenuDropdownSubmenuOptionElement.classList.add("mod-selected"); + const selectedIconElement = styleMenuDropdownSubmenuOptionElement.createDiv(); + selectedIconElement.classList.add("menu-item-icon"); + selectedIconElement.classList.add("mod-selected"); + (0, import_obsidian2.setIcon)(selectedIconElement, "check"); + } + styleMenuDropdownSubmenuElement.appendChild(styleMenuDropdownSubmenuOptionElement); + } + }); + } + }); + } + static createDropdownOptionElement(menuOption) { + const menuDropdownOptionElement = activeDocument.createElement("div"); + menuDropdownOptionElement.classList.add("menu-item"); + menuDropdownOptionElement.classList.add("tappable"); + const iconElement = menuDropdownOptionElement.createDiv(); + iconElement.classList.add("menu-item-icon"); + (0, import_obsidian2.setIcon)(iconElement, menuOption.icon); + const labelElement = menuDropdownOptionElement.createDiv(); + labelElement.classList.add("menu-item-title"); + labelElement.textContent = menuOption.label; + menuDropdownOptionElement.addEventListener("pointerenter", () => { + menuDropdownOptionElement.classList.add("selected"); + }); + menuDropdownOptionElement.addEventListener("pointerleave", () => { + menuDropdownOptionElement.classList.remove("selected"); + }); + menuDropdownOptionElement.addEventListener("click", () => { + var _a; + (_a = menuOption.callback) == null ? void 0 : _a.call(menuOption); + }); + return menuDropdownOptionElement; + } + static createDropdownSeparatorElement() { + const separatorElement = activeDocument.createElement("div"); + separatorElement.classList.add("menu-separator"); + return separatorElement; + } + static alignToGrid(value, gridSize = this.GRID_SIZE) { + return Math.round(value / gridSize) * gridSize; + } + static getBestSideForFloatingEdge(sourcePos, target) { + const targetBBox = target.getBBox(); + const possibleSides = ["top", "right", "bottom", "left"]; + const possibleTargetPos = possibleSides.map((side) => [side, BBoxHelper.getCenterOfBBoxSide(targetBBox, side)]); + let bestSide = null; + let bestDistance = Infinity; + for (const [side, pos] of possibleTargetPos) { + const distance = Math.sqrt(Math.pow(sourcePos.x - pos.x, 2) + Math.pow(sourcePos.y - pos.y, 2)); + if (distance < bestDistance) { + bestDistance = distance; + bestSide = side; + } + } + return bestSide; + } + static selectEdgesForNodes(canvas, direction) { + const selection = canvas.getSelectionData(); + if (selection.nodes.length === 0) return; + const edges = /* @__PURE__ */ new Set(); + for (const nodeData of selection.nodes) { + const node = canvas.nodes.get(nodeData.id); + if (!node) continue; + for (const edge of canvas.getEdgesForNode(node)) { + switch (direction) { + case "connected": + edges.add(edge); + break; + case "incoming": + if (edge.to.node === node) edges.add(edge); + break; + case "outgoing": + if (edge.from.node === node) edges.add(edge); + break; + } + } + } + canvas.updateSelection(() => { + canvas.selection = edges; + }); + } +}; +_CanvasHelper.GRID_SIZE = 20; +_CanvasHelper.MAX_ALLOWED_ZOOM = 1; +var CanvasHelper = _CanvasHelper; + +// src/canvas-extensions/canvas-extension.ts +var CanvasExtension = class { + constructor(plugin) { + this.plugin = plugin; + const isEnabled = this.isEnabled(); + if (!(isEnabled === true || this.plugin.settings.getSetting(isEnabled))) + return; + this.init(); + } +}; + +// src/utils/svg-path-helper.ts +var SvgPathHelper = class { + static smoothenPathArray(positions, tension) { + let newPositions = [...positions]; + if (positions.length <= 2) return newPositions; + newPositions = [positions[0]]; + for (let i = 1; i < positions.length - 2; i++) { + const p1 = positions[i]; + const p2 = positions[i + 1]; + const p3 = positions[i + 2]; + const t1 = (1 - tension) / 2; + const t2 = 1 - t1; + const x = t2 * t2 * t2 * p1.x + 3 * t2 * t2 * t1 * p2.x + 3 * t2 * t1 * t1 * p3.x + t1 * t1 * t1 * p2.x; + const y = t2 * t2 * t2 * p1.y + 3 * t2 * t2 * t1 * p2.y + 3 * t2 * t1 * t1 * p3.y + t1 * t1 * t1 * p2.y; + newPositions.push({ x, y }); + } + const lastPoint = positions[positions.length - 1]; + newPositions.push(lastPoint); + return newPositions; + } + static pathArrayToSvgPath(positions) { + for (let i = 0; i < positions.length - 2; i++) { + const p1 = positions[i]; + const p2 = positions[i + 1]; + const p3 = positions[i + 2]; + const currentDirection = { + x: p2.x - p1.x, + y: p2.y - p1.y + }; + const nextDirection = { + x: p3.x - p2.x, + y: p3.y - p2.y + }; + if (currentDirection.x !== nextDirection.x && currentDirection.y !== nextDirection.y) continue; + positions.splice(i + 1, 1); + i--; + } + return positions.map( + (position, index) => `${index === 0 ? "M" : "L"} ${position.x} ${position.y}` + ).join(" "); + } + static pathArrayToRoundedSvgPath(pathArray, targetRadius) { + if (pathArray.length < 3) + return this.pathArrayToSvgPath(pathArray); + pathArray = pathArray.filter((position, index) => { + if (index === 0) return true; + const previous = pathArray[index - 1]; + return !(position.x === previous.x && position.y === previous.y); + }); + const commands = []; + commands.push(`M ${pathArray[0].x} ${pathArray[0].y}`); + for (let i = 1; i < pathArray.length - 1; i++) { + const previous = pathArray[i - 1]; + const current = pathArray[i]; + const next = pathArray[i + 1]; + const prevDelta = { x: current.x - previous.x, y: current.y - previous.y }; + const nextDelta = { x: next.x - current.x, y: next.y - current.y }; + const prevLen = Math.sqrt(prevDelta.x * prevDelta.x + prevDelta.y * prevDelta.y); + const nextLen = Math.sqrt(nextDelta.x * nextDelta.x + nextDelta.y * nextDelta.y); + const prevUnit = prevLen ? { x: prevDelta.x / prevLen, y: prevDelta.y / prevLen } : { x: 0, y: 0 }; + const nextUnit = nextLen ? { x: nextDelta.x / nextLen, y: nextDelta.y / nextLen } : { x: 0, y: 0 }; + let dot = prevUnit.x * nextUnit.x + prevUnit.y * nextUnit.y; + dot = Math.max(-1, Math.min(1, dot)); + const angle = Math.acos(dot); + if (angle < 0.01 || Math.abs(Math.PI - angle) < 0.01) { + commands.push(`L ${current.x} ${current.y}`); + continue; + } + const desiredOffset = targetRadius * Math.tan(angle / 2); + const d = Math.min(desiredOffset, prevLen / 2, nextLen / 2); + const effectiveRadius = d / Math.tan(angle / 2); + const firstAnchor = { + x: current.x - prevUnit.x * d, + y: current.y - prevUnit.y * d + }; + const secondAnchor = { + x: current.x + nextUnit.x * d, + y: current.y + nextUnit.y * d + }; + commands.push(`L ${firstAnchor.x} ${firstAnchor.y}`); + const cross = prevDelta.x * nextDelta.y - prevDelta.y * nextDelta.x; + const sweepFlag = cross < 0 ? 0 : 1; + commands.push(`A ${effectiveRadius} ${effectiveRadius} 0 0 ${sweepFlag} ${secondAnchor.x} ${secondAnchor.y}`); + } + const last = pathArray[pathArray.length - 1]; + commands.push(`L ${last.x} ${last.y}`); + return commands.join(" "); + } +}; + +// src/canvas-extensions/advanced-styles/edge-pathfinding-methods/edge-pathfinding-method.ts +var EdgePathfindingMethod = class { + constructor(plugin, canvas, fromNodeBBox, fromPos, fromBBoxSidePos, fromSide, toNodeBBox, toPos, toBBoxSidePos, toSide) { + this.plugin = plugin; + this.canvas = canvas; + this.fromNodeBBox = fromNodeBBox; + this.fromPos = fromPos; + this.fromBBoxSidePos = fromBBoxSidePos; + this.fromSide = fromSide; + this.toNodeBBox = toNodeBBox; + this.toPos = toPos; + this.toBBoxSidePos = toBBoxSidePos; + this.toSide = toSide; + } +}; + +// src/canvas-extensions/advanced-styles/edge-pathfinding-methods/pathfinding-a-star.ts +var MAX_MS_CALCULATION = 100; +var BASIC_DIRECTIONS = [ + { dx: 1, dy: 0 }, + { dx: -1, dy: 0 }, + { dx: 0, dy: 1 }, + { dx: 0, dy: -1 } +]; +var DIAGONAL_DIRECTIONS = [ + { dx: 1, dy: 1 }, + { dx: -1, dy: 1 }, + { dx: 1, dy: -1 }, + { dx: -1, dy: -1 } +]; +var DIAGONAL_COST = Math.sqrt(2); +var ROUND_PATH_RADIUS = 5; +var SMOOTHEN_PATH_TENSION = 0.2; +var Node = class { + constructor(x, y) { + this.x = x; + this.y = y; + this.gCost = 0; + this.hCost = 0; + this.fCost = 0; + this.parent = null; + } + // Only check for x and y, not gCost, hCost, fCost, or parent + inList(nodes) { + return nodes.some((n) => n.x === this.x && n.y === this.y); + } +}; +var EdgePathfindingAStar = class extends EdgePathfindingMethod { + getPath() { + const nodeBBoxes = [...this.canvas.nodes.values()].filter((node) => { + const nodeData = node.getData(); + if (nodeData.portal === true) return false; + const nodeBBox = node.getBBox(); + const nodeContainsFromPos = BBoxHelper.insideBBox(this.fromPos, nodeBBox, true); + const nodeContainsToPos = BBoxHelper.insideBBox(this.toPos, nodeBBox, true); + return !nodeContainsFromPos && !nodeContainsToPos; + }).map((node) => node.getBBox()); + const fromPosWithMargin = BBoxHelper.moveInDirection(this.fromPos, this.fromSide, 10); + const toPosWithMargin = BBoxHelper.moveInDirection(this.toPos, this.toSide, 10); + const allowDiagonal = this.plugin.settings.getSetting("edgeStylePathfinderAllowDiagonal"); + const pathArray = this.aStarAlgorithm(fromPosWithMargin, toPosWithMargin, nodeBBoxes, CanvasHelper.GRID_SIZE / 2, allowDiagonal); + if (!pathArray) return null; + pathArray.splice(0, 0, this.fromPos); + pathArray.splice(pathArray.length, 0, this.toPos); + let svgPath; + const roundPath = this.plugin.settings.getSetting("edgeStylePathfinderPathRounded"); + if (roundPath) { + if (allowDiagonal) + svgPath = SvgPathHelper.pathArrayToSvgPath(SvgPathHelper.smoothenPathArray(pathArray, SMOOTHEN_PATH_TENSION)); + else + svgPath = SvgPathHelper.pathArrayToRoundedSvgPath(pathArray, ROUND_PATH_RADIUS); + } else svgPath = SvgPathHelper.pathArrayToSvgPath(pathArray); + return { + svgPath, + center: pathArray[Math.floor(pathArray.length / 2)], + rotateArrows: false + }; + } + aStarAlgorithm(fromPos, toPos, obstacles, gridResolution, allowDiagonal) { + const start = new Node( + Math.floor(fromPos.x / gridResolution) * gridResolution, + Math.floor(fromPos.y / gridResolution) * gridResolution + ); + if (this.fromSide === "right" && fromPos.x !== start.x) start.x += gridResolution; + if (this.fromSide === "bottom" && fromPos.y !== start.y) start.y += gridResolution; + const end = new Node( + Math.floor(toPos.x / gridResolution) * gridResolution, + Math.floor(toPos.y / gridResolution) * gridResolution + ); + if (this.toSide === "right" && toPos.x !== end.x) end.x += gridResolution; + if (this.toSide === "bottom" && toPos.y !== end.y) end.y += gridResolution; + if (this.isInsideObstacle(start, obstacles) || this.isInsideObstacle(end, obstacles)) return null; + const openSet = [start]; + const closedSet = []; + const startTimestamp = performance.now(); + while (openSet.length > 0) { + let current = null; + let lowestFCost = Infinity; + for (const node of openSet) { + if (node.fCost < lowestFCost) { + current = node; + lowestFCost = node.fCost; + } + } + if (performance.now() - startTimestamp > MAX_MS_CALCULATION) + return null; + if (!current) + return null; + openSet.splice(openSet.indexOf(current), 1); + closedSet.push(current); + if (current.x === end.x && current.y === end.y) + return [fromPos, ...this.reconstructPath(current), toPos].map((node) => ({ x: node.x, y: node.y })); + if (!(current.x === start.x && current.y === start.y) && this.isTouchingObstacle(current, obstacles)) + continue; + for (const neighbor of this.getPossibleNeighbors(current, obstacles, gridResolution, allowDiagonal)) { + if (neighbor.inList(closedSet)) + continue; + const tentativeGCost = current.gCost + (allowDiagonal ? this.getMovementCost({ + dx: neighbor.x - current.x, + dy: neighbor.y - current.y + }) : 1); + if (!neighbor.inList(openSet) || tentativeGCost < neighbor.gCost) { + neighbor.parent = current; + neighbor.gCost = tentativeGCost; + neighbor.hCost = this.heuristic(neighbor, end); + neighbor.fCost = neighbor.gCost + neighbor.hCost; + openSet.push(neighbor); + } + } + } + return null; + } + // Manhattan distance + heuristic(node, end) { + return Math.abs(node.x - end.x) + Math.abs(node.y - end.y); + } + // Define a function to check if a position isn't inside any obstacle + isTouchingObstacle(node, obstacles) { + return obstacles.some((obstacle) => BBoxHelper.insideBBox(node, obstacle, true)); + } + isInsideObstacle(node, obstacles) { + return obstacles.some((obstacle) => BBoxHelper.insideBBox(node, obstacle, false)); + } + // Define a function to calculate movement cost based on direction + getMovementCost(direction) { + return direction.dx !== 0 && direction.dy !== 0 ? DIAGONAL_COST : 1; + } + getPossibleNeighbors(node, obstacles, gridResolution, allowDiagonal) { + const neighbors = []; + const availableDirections = allowDiagonal ? [...BASIC_DIRECTIONS, ...DIAGONAL_DIRECTIONS] : BASIC_DIRECTIONS; + for (const direction of availableDirections) { + const neighbor = new Node( + node.x + direction.dx * gridResolution, + node.y + direction.dy * gridResolution + ); + neighbor.gCost = node.gCost + this.getMovementCost(direction); + if (this.isInsideObstacle(neighbor, obstacles)) continue; + neighbors.push(neighbor); + } + return neighbors; + } + reconstructPath(node) { + const path = []; + while (node) { + path.push(node); + node = node.parent; + } + return path.reverse(); + } +}; + +// src/canvas-extensions/advanced-styles/edge-pathfinding-methods/pathfinding-direct.ts +var EdgePathfindingDirect = class extends EdgePathfindingMethod { + getPath() { + return { + svgPath: SvgPathHelper.pathArrayToSvgPath([this.fromPos, this.toPos]), + center: { + x: (this.fromPos.x + this.toPos.x) / 2, + y: (this.fromPos.y + this.toPos.y) / 2 + }, + rotateArrows: true + }; + } +}; + +// src/canvas-extensions/advanced-styles/edge-pathfinding-methods/pathfinding-square.ts +var ROUNDED_EDGE_RADIUS = 5; +var EdgePathfindingSquare = class extends EdgePathfindingMethod { + getPath() { + const pathArray = []; + let center = { + x: (this.fromPos.x + this.toPos.x) / 2, + y: (this.fromPos.y + this.toPos.y) / 2 + }; + const idealCenter = BBoxHelper.isHorizontal(this.fromSide) ? { + x: this.toBBoxSidePos.x, + y: this.fromBBoxSidePos.y + } : { + x: this.fromBBoxSidePos.x, + y: this.toBBoxSidePos.y + }; + const isPathCollidingAtFrom = this.fromSide === "top" && idealCenter.y > this.fromPos.y || this.fromSide === "bottom" && idealCenter.y < this.fromPos.y || this.fromSide === "left" && idealCenter.x > this.fromPos.x || this.fromSide === "right" && idealCenter.x < this.fromPos.x; + const isPathCollidingAtTo = this.toSide === "top" && idealCenter.y > this.toPos.y || this.toSide === "bottom" && idealCenter.y < this.toPos.y || this.toSide === "left" && idealCenter.x > this.toPos.x || this.toSide === "right" && idealCenter.x < this.toPos.x; + if (this.fromSide === this.toSide) { + const uPath = this.getUPath(this.fromPos, this.toPos, this.fromSide, this.toSide); + pathArray.push(...uPath.pathArray); + center = uPath.center; + } else if (BBoxHelper.isHorizontal(this.fromSide) === BBoxHelper.isHorizontal(this.toSide)) { + let zPath; + if (!isPathCollidingAtFrom || !isPathCollidingAtTo) { + zPath = this.getZPath(this.fromPos, this.toPos, this.fromSide, this.toSide); + pathArray.push(...zPath.pathArray); + } else { + const fromDirection = BBoxHelper.direction(this.fromSide); + const firstFromDetourPoint = BBoxHelper.isHorizontal(this.fromSide) ? { + x: CanvasHelper.alignToGrid(this.fromBBoxSidePos.x + fromDirection * CanvasHelper.GRID_SIZE), + y: this.fromBBoxSidePos.y + } : { + x: this.fromBBoxSidePos.x, + y: CanvasHelper.alignToGrid(this.fromBBoxSidePos.y + fromDirection * CanvasHelper.GRID_SIZE) + }; + const toDirection = BBoxHelper.direction(this.toSide); + const firstToDetourPoint = BBoxHelper.isHorizontal(this.toSide) ? { + x: CanvasHelper.alignToGrid(this.toBBoxSidePos.x + toDirection * CanvasHelper.GRID_SIZE), + y: this.toBBoxSidePos.y + } : { + x: this.toBBoxSidePos.x, + y: CanvasHelper.alignToGrid(this.toBBoxSidePos.y + toDirection * CanvasHelper.GRID_SIZE) + }; + const newFromSide = BBoxHelper.isHorizontal(this.fromSide) ? firstFromDetourPoint.y < this.fromPos.y ? "top" : "bottom" : firstFromDetourPoint.x < firstToDetourPoint.x ? "right" : "left"; + zPath = this.getZPath(firstFromDetourPoint, firstToDetourPoint, newFromSide, BBoxHelper.getOppositeSide(newFromSide)); + pathArray.push(this.fromPos); + pathArray.push(...zPath.pathArray); + pathArray.push(this.toPos); + } + center = zPath.center; + } else { + if (isPathCollidingAtFrom || isPathCollidingAtTo) { + if (isPathCollidingAtFrom && isPathCollidingAtTo) { + const direction = BBoxHelper.direction(this.fromSide); + let firstFromDetourPoint; + let secondFromDetourPoint; + if (BBoxHelper.isHorizontal(this.fromSide)) { + const combinedBBoxes = BBoxHelper.combineBBoxes([this.fromNodeBBox, this.toNodeBBox]); + firstFromDetourPoint = { + x: CanvasHelper.alignToGrid((direction > 0 ? combinedBBoxes.maxX : combinedBBoxes.minX) + direction * CanvasHelper.GRID_SIZE), + y: this.fromBBoxSidePos.y + }; + secondFromDetourPoint = { + x: firstFromDetourPoint.x, + y: BBoxHelper.getCenterOfBBoxSide(this.fromNodeBBox, this.toSide).y + }; + } else { + const combinedBBoxes = BBoxHelper.combineBBoxes([this.fromNodeBBox, this.toNodeBBox]); + firstFromDetourPoint = { + x: this.fromBBoxSidePos.x, + y: CanvasHelper.alignToGrid((direction > 0 ? combinedBBoxes.maxY : combinedBBoxes.minY) + direction * CanvasHelper.GRID_SIZE) + }; + secondFromDetourPoint = { + x: BBoxHelper.getCenterOfBBoxSide(this.fromNodeBBox, this.toSide).x, + y: firstFromDetourPoint.y + }; + } + const uPath = this.getUPath(secondFromDetourPoint, this.toPos, this.toSide, this.toSide); + pathArray.push(this.fromPos); + pathArray.push(firstFromDetourPoint); + pathArray.push(...uPath.pathArray); + center = pathArray[Math.floor(pathArray.length / 2)]; + } else { + if (isPathCollidingAtFrom) { + const direction = BBoxHelper.direction(this.fromSide); + const firstFromDetourPoint = BBoxHelper.isHorizontal(this.fromSide) ? { + x: CanvasHelper.alignToGrid(this.fromBBoxSidePos.x + direction * CanvasHelper.GRID_SIZE), + y: this.fromBBoxSidePos.y + } : { + x: this.fromBBoxSidePos.x, + y: CanvasHelper.alignToGrid(this.fromBBoxSidePos.y + direction * CanvasHelper.GRID_SIZE) + }; + const useUPath = BBoxHelper.isHorizontal(this.fromSide) ? this.toPos.y > BBoxHelper.getCenterOfBBoxSide(this.fromNodeBBox, BBoxHelper.getOppositeSide(this.toSide)).y === BBoxHelper.direction(this.toSide) > 0 : this.toPos.x > BBoxHelper.getCenterOfBBoxSide(this.fromNodeBBox, BBoxHelper.getOppositeSide(this.toSide)).x === BBoxHelper.direction(this.toSide) > 0; + const connectionSide = useUPath ? this.toSide : BBoxHelper.getOppositeSide(this.toSide); + const secondFromDetourPoint = BBoxHelper.isHorizontal(this.fromSide) ? { + x: firstFromDetourPoint.x, + y: BBoxHelper.getCenterOfBBoxSide(this.fromNodeBBox, connectionSide).y + } : { + x: BBoxHelper.getCenterOfBBoxSide(this.fromNodeBBox, connectionSide).x, + y: firstFromDetourPoint.y + }; + const path = useUPath ? this.getUPath(secondFromDetourPoint, this.toPos, this.toSide, this.toSide) : this.getZPath(secondFromDetourPoint, this.toPos, this.toSide, this.toSide); + pathArray.push(this.fromPos); + pathArray.push(firstFromDetourPoint); + pathArray.push(...path.pathArray); + center = path.center; + } + if (isPathCollidingAtTo) { + const direction = BBoxHelper.direction(this.toSide); + const firstToDetourPoint = BBoxHelper.isHorizontal(this.toSide) ? { + x: CanvasHelper.alignToGrid(this.toBBoxSidePos.x + direction * CanvasHelper.GRID_SIZE), + y: this.toBBoxSidePos.y + } : { + x: this.toBBoxSidePos.x, + y: CanvasHelper.alignToGrid(this.toBBoxSidePos.y + direction * CanvasHelper.GRID_SIZE) + }; + const useUPath = BBoxHelper.isHorizontal(this.toSide) ? this.fromPos.y > BBoxHelper.getCenterOfBBoxSide(this.toNodeBBox, BBoxHelper.getOppositeSide(this.fromSide)).y === BBoxHelper.direction(this.fromSide) > 0 : this.fromPos.x > BBoxHelper.getCenterOfBBoxSide(this.toNodeBBox, BBoxHelper.getOppositeSide(this.fromSide)).x === BBoxHelper.direction(this.fromSide) > 0; + const connectionSide = useUPath ? this.fromSide : BBoxHelper.getOppositeSide(this.fromSide); + const secondToDetourPoint = BBoxHelper.isHorizontal(this.toSide) ? { + x: firstToDetourPoint.x, + y: BBoxHelper.getCenterOfBBoxSide(this.toNodeBBox, connectionSide).y + } : { + x: BBoxHelper.getCenterOfBBoxSide(this.toNodeBBox, connectionSide).x, + y: firstToDetourPoint.y + }; + const path = useUPath ? this.getUPath(this.fromPos, secondToDetourPoint, this.fromSide, this.fromSide) : this.getZPath(this.fromPos, secondToDetourPoint, this.fromSide, this.fromSide); + pathArray.push(...path.pathArray); + pathArray.push(secondToDetourPoint); + pathArray.push(firstToDetourPoint); + pathArray.push(this.toPos); + center = path.center; + } + } + } else { + pathArray.push( + this.fromPos, + idealCenter, + this.toPos + ); + center = { + x: pathArray[1].x, + y: pathArray[1].y + }; + } + } + const svgPath = this.plugin.settings.getSetting("edgeStyleSquarePathRounded") ? SvgPathHelper.pathArrayToRoundedSvgPath(pathArray, ROUNDED_EDGE_RADIUS) : SvgPathHelper.pathArrayToSvgPath(pathArray); + return { svgPath, center, rotateArrows: false }; + } + getUPath(fromPos, toPos, fromSide, toSide) { + const direction = BBoxHelper.direction(fromSide); + if (BBoxHelper.isHorizontal(fromSide)) { + const xExtremum = direction > 0 ? Math.max(fromPos.x, toPos.x) : Math.min(fromPos.x, toPos.x); + const x = CanvasHelper.alignToGrid(xExtremum + direction * CanvasHelper.GRID_SIZE); + return { + pathArray: [ + fromPos, + { x, y: fromPos.y }, + { x, y: toPos.y }, + toPos + ], + center: { + x, + y: (fromPos.y + toPos.y) / 2 + } + }; + } else { + const yExtremum = direction > 0 ? Math.max(fromPos.y, toPos.y) : Math.min(fromPos.y, toPos.y); + const y = CanvasHelper.alignToGrid(yExtremum + direction * CanvasHelper.GRID_SIZE); + return { + pathArray: [ + fromPos, + { x: fromPos.x, y }, + { x: toPos.x, y }, + toPos + ], + center: { + x: (fromPos.x + toPos.x) / 2, + y + } + }; + } + } + getZPath(fromPos, toPos, fromSide, toSide) { + if (BBoxHelper.isHorizontal(fromSide)) { + const midX = fromPos.x + (toPos.x - fromPos.x) / 2; + return { + pathArray: [ + fromPos, + { x: midX, y: fromPos.y }, + { x: midX, y: toPos.y }, + toPos + ], + center: { + x: midX, + y: (fromPos.y + toPos.y) / 2 + } + }; + } else { + const midY = fromPos.y + (toPos.y - fromPos.y) / 2; + return { + pathArray: [ + fromPos, + { x: fromPos.x, y: midY }, + { x: toPos.x, y: midY }, + toPos + ], + center: { + x: (fromPos.x + toPos.x) / 2, + y: midY + } + }; + } + } +}; + +// src/utils/text-helper.ts +var TextHelper = class { + static toCamelCase(str) { + return str.replace(/-./g, (x) => x[1].toUpperCase()); + } + static toTitleCase(str) { + return str.toLowerCase().split(" ").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" "); + } +}; + +// src/canvas-extensions/advanced-styles/style-config.ts +function styleAttributeValidator(json) { + var _a; + const hasKey = json.key !== void 0; + const hasLabel = json.label !== void 0; + const hasOptions = Array.isArray(json.options); + if (!hasKey) console.error('Style attribute is missing the "key" property'); + if (!hasLabel) console.error('Style attribute is missing the "label" property'); + if (!hasOptions) console.error('Style attribute is missing the "options" property or it is not an array'); + json.key = TextHelper.toCamelCase(json.key); + let optionsValid = true; + let hasDefault = false; + for (const option of json.options) { + const hasIcon = option.icon !== void 0; + const hasLabel2 = option.label !== void 0; + const hasValue = option.value !== void 0; + if (!hasIcon) console.error(`Style attribute option (${(_a = option.value) != null ? _a : option.label}) is missing the "icon" property`); + if (!hasLabel2) console.error(`Style attribute option (${option.value}) is missing the "label" property`); + if (!hasValue) console.error(`Style attribute option (${option.label}) is missing the "value" property`); + if (!hasIcon || !hasLabel2 || !hasValue) optionsValid = false; + if (option.value === null) hasDefault = true; + } + if (!hasDefault) console.error('Style attribute is missing a default option (option with a "value" of null)'); + const isValid = hasKey && hasLabel && hasOptions && optionsValid && hasDefault; + return isValid ? json : null; +} +var BUILTIN_NODE_STYLE_ATTRIBUTES = [ + { + key: "textAlign", + label: "Text Alignment", + nodeTypes: ["text"], + options: [ + { + icon: "align-left", + label: "Left", + value: null + }, + { + icon: "align-center", + label: "Center", + value: "center" + }, + { + icon: "align-right", + label: "Right", + value: "right" + } + ] + }, + { + key: "shape", + label: "Shape", + nodeTypes: ["text"], + options: [ + { + icon: "rectangle-horizontal", + label: "Round Rectangle", + value: null + }, + { + icon: "shape-pill", + label: "Pill", + value: "pill" + }, + { + icon: "diamond", + label: "Diamond", + value: "diamond" + }, + { + icon: "shape-parallelogram", + label: "Parallelogram", + value: "parallelogram" + }, + { + icon: "circle", + label: "Circle", + value: "circle" + }, + { + icon: "shape-predefined-process", + label: "Predefined Process", + value: "predefined-process" + }, + { + icon: "shape-document", + label: "Document", + value: "document" + }, + { + icon: "shape-database", + label: "Database", + value: "database" + } + ] + }, + { + key: "border", + label: "Border", + options: [ + { + icon: "border-solid", + label: "Solid", + value: null + }, + { + icon: "border-dashed", + label: "Dashed", + value: "dashed" + }, + { + icon: "border-dotted", + label: "Dotted", + value: "dotted" + }, + { + icon: "eye-off", + label: "Invisible", + value: "invisible" + } + ] + } +]; +var BUILTIN_EDGE_STYLE_ATTRIBUTES = [ + { + key: "path", + label: "Path Style", + options: [ + { + icon: "path-solid", + label: "Solid", + value: null + }, + { + icon: "path-dotted", + label: "Dotted", + value: "dotted" + }, + { + icon: "path-short-dashed", + label: "Short Dashed", + value: "short-dashed" + }, + { + icon: "path-long-dashed", + label: "Long Dashed", + value: "long-dashed" + } + ] + }, + { + key: "arrow", + label: "Arrow Style", + options: [ + { + icon: "arrow-triangle", + label: "Triangle", + value: null + }, + { + icon: "arrow-triangle-outline", + label: "Triangle Outline", + value: "triangle-outline" + }, + { + icon: "arrow-thin-triangle", + label: "Thin Triangle", + value: "thin-triangle" + }, + { + icon: "arrow-halved-triangle", + label: "Halved Triangle", + value: "halved-triangle" + }, + { + icon: "arrow-diamond", + label: "Diamond", + value: "diamond" + }, + { + icon: "arrow-diamond-outline", + label: "Diamond Outline", + value: "diamond-outline" + }, + { + icon: "arrow-circle", + label: "Circle", + value: "circle" + }, + { + icon: "arrow-circle-outline", + label: "Circle Outline", + value: "circle-outline" + }, + { + icon: "tally-1", + label: "Blunt", + value: "blunt" + } + ] + }, + { + key: "pathfindingMethod", + label: "Pathfinding Method", + options: [ + { + icon: "pathfinding-method-bezier", + label: "Bezier", + value: null + }, + { + icon: "slash", + label: "Direct", + value: "direct" + }, + { + icon: "pathfinding-method-square", + label: "Square", + value: "square" + }, + { + icon: "map", + label: "A*", + value: "a-star" + } + ] + } +]; + +// src/managers/css-styles-config-manager.ts +var import_obsidian3 = require("obsidian"); +var CssStylesConfigManager = class { + constructor(plugin, trigger, validate) { + this.plugin = plugin; + this.validate = validate; + this.cachedConfig = null; + this.configRegex = new RegExp(`\\/\\*\\s*@${trigger}\\s*\\n([\\s\\S]*?)\\*\\/`, "g"); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "css-change", + () => { + this.cachedConfig = null; + } + )); + } + getStyles() { + if (this.cachedConfig) return this.cachedConfig; + this.cachedConfig = []; + const styleSheets = activeDocument.styleSheets; + for (let i = 0; i < styleSheets.length; i++) { + const sheet = styleSheets.item(i); + if (!sheet) continue; + const styleSheetConfigs = this.parseStyleConfigsFromCSS(sheet); + for (const config of styleSheetConfigs) { + const validConfig = this.validate(config); + if (!validConfig) continue; + this.cachedConfig.push(validConfig); + } + } + return this.cachedConfig; + } + parseStyleConfigsFromCSS(sheet) { + var _a, _b; + const textContent = (_b = (_a = sheet == null ? void 0 : sheet.ownerNode) == null ? void 0 : _a.textContent) == null ? void 0 : _b.trim(); + if (!textContent) return []; + const configs = []; + const matches = textContent.matchAll(this.configRegex); + for (const match of matches) { + const yamlString = match[1]; + if (!yamlString) continue; + const configYaml = (0, import_obsidian3.parseYaml)(yamlString); + configs.push(configYaml); + } + return configs; + } +}; + +// src/canvas-extensions/advanced-styles/edge-styles.ts +var GET_EDGE_CSS_STYLES_MANAGER = (plugin) => new CssStylesConfigManager(plugin, "advanced-canvas-edge-style", styleAttributeValidator); +var EDGE_PATHFINDING_METHODS = { + "direct": EdgePathfindingDirect, + "square": EdgePathfindingSquare, + "a-star": EdgePathfindingAStar +}; +var MAX_LIVE_UPDATE_SELECTION_SIZE = 5; +var EdgeStylesExtension = class extends CanvasExtension { + isEnabled() { + return "edgesStylingFeatureEnabled"; + } + init() { + this.cssStylesManager = GET_EDGE_CSS_STYLES_MANAGER(this.plugin); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:popup-menu-created", + (canvas) => this.onPopupMenuCreated(canvas) + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:edge-changed", + (canvas, edge) => this.onEdgeChanged(canvas, edge) + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:edge-center-requested", + (canvas, edge, center) => this.onEdgeCenterRequested(canvas, edge, center) + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:node-added", + (canvas, node) => { + if (canvas.dirty.size > 1 && !canvas.isPasting) return; + this.updateAllEdgesInArea(canvas, node.getBBox()); + } + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:node-moved", + // Only update edges this way if a node got moved with the arrow keys + (canvas, node, keyboard) => node.initialized && keyboard ? this.updateAllEdgesInArea(canvas, node.getBBox()) : void 0 + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:node-removed", + (canvas, node) => this.updateAllEdgesInArea(canvas, node.getBBox()) + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:dragging-state-changed", + (canvas, isDragging) => { + if (isDragging) return; + const selectedNodes = canvas.getSelectionData().nodes.map((nodeData) => canvas.nodes.get(nodeData.id)).filter((node) => node !== void 0); + const selectedNodeBBoxes = selectedNodes.map((node) => node.getBBox()); + const selectedNodeBBox = BBoxHelper.combineBBoxes(selectedNodeBBoxes); + this.updateAllEdgesInArea(canvas, selectedNodeBBox); + } + )); + } + // Skip if isDragging and setting isn't enabled and not connecting an edge + shouldUpdateEdge(canvas) { + return !canvas.isDragging || this.plugin.settings.getSetting("edgeStyleUpdateWhileDragging") || canvas.canvasEl.hasClass("is-connecting"); + } + onPopupMenuCreated(canvas) { + var _a; + const selectedEdges = [...canvas.selection].filter((item) => item.path !== void 0); + if (canvas.readonly || selectedEdges.length === 0 || selectedEdges.length !== canvas.selection.size) + return; + CanvasHelper.addStyleAttributesToPopup( + this.plugin, + canvas, + [ + ...BUILTIN_EDGE_STYLE_ATTRIBUTES, + /* Legacy */ + ...this.plugin.settings.getSetting("customEdgeStyleAttributes"), + ...this.cssStylesManager.getStyles() + ], + (_a = selectedEdges[0].getData().styleAttributes) != null ? _a : {}, + (attribute, value) => this.setStyleAttributeForSelection(canvas, attribute, value) + ); + } + setStyleAttributeForSelection(canvas, attribute, value) { + const selectedEdges = [...canvas.selection].filter((item) => item.path !== void 0); + for (const edge of selectedEdges) { + const edgeData = edge.getData(); + edge.setData({ + ...edgeData, + styleAttributes: { + ...edgeData.styleAttributes, + [attribute.key]: value + } + }); + } + canvas.pushHistory(canvas.getData()); + } + updateAllEdgesInArea(canvas, bbox) { + if (!this.shouldUpdateEdge(canvas)) return; + for (const edge of canvas.edges.values()) { + if (!BBoxHelper.isColliding(edge.getBBox(), bbox)) continue; + canvas.markDirty(edge); + } + } + onEdgeChanged(canvas, edge) { + var _a, _b, _c, _d, _e, _f, _g; + if (!canvas.dirty.has(edge) && !canvas.selection.has(edge)) return; + if (!this.shouldUpdateEdge(canvas)) { + const tooManySelected = canvas.selection.size > MAX_LIVE_UPDATE_SELECTION_SIZE; + if (tooManySelected) return; + const groupNodesSelected = [...canvas.selection].some((item) => { + var _a2; + return ((_a2 = item.getData()) == null ? void 0 : _a2.type) === "group"; + }); + if (groupNodesSelected) return; + } + const edgeData = edge.getData(); + if (!edge.bezier) return; + edge.center = void 0; + edge.updatePath(); + const pathfindingMethod = (_a = edgeData.styleAttributes) == null ? void 0 : _a.pathfindingMethod; + if (pathfindingMethod && pathfindingMethod in EDGE_PATHFINDING_METHODS) { + const fromNodeBBox = edge.from.node.getBBox(); + const fromBBoxSidePos = BBoxHelper.getCenterOfBBoxSide(fromNodeBBox, edge.from.side); + const fromPos = edge.from.end === "none" ? fromBBoxSidePos : edge.bezier.from; + const toNodeBBox = edge.to.node.getBBox(); + const toBBoxSidePos = BBoxHelper.getCenterOfBBoxSide(toNodeBBox, edge.to.side); + const toPos = edge.to.end === "none" ? toBBoxSidePos : edge.bezier.to; + const PathfindingConstructor = EDGE_PATHFINDING_METHODS[pathfindingMethod]; + const path = new PathfindingConstructor( + this.plugin, + canvas, + fromNodeBBox, + fromPos, + fromBBoxSidePos, + edge.from.side, + toNodeBBox, + toPos, + toBBoxSidePos, + edge.to.side + ).getPath(); + if (!path) return; + edge.center = path.center; + edge.path.interaction.setAttr("d", path == null ? void 0 : path.svgPath); + edge.path.display.setAttr("d", path == null ? void 0 : path.svgPath); + } + (_b = edge.labelElement) == null ? void 0 : _b.render(); + const arrowPolygonPoints = this.getArrowPolygonPoints((_c = edgeData.styleAttributes) == null ? void 0 : _c.arrow); + if ((_d = edge.fromLineEnd) == null ? void 0 : _d.el) (_e = edge.fromLineEnd.el.querySelector("polygon")) == null ? void 0 : _e.setAttribute("points", arrowPolygonPoints); + if ((_f = edge.toLineEnd) == null ? void 0 : _f.el) (_g = edge.toLineEnd.el.querySelector("polygon")) == null ? void 0 : _g.setAttribute("points", arrowPolygonPoints); + } + onEdgeCenterRequested(_canvas, edge, center) { + var _a, _b, _c, _d; + center.x = (_b = (_a = edge.center) == null ? void 0 : _a.x) != null ? _b : center.x; + center.y = (_d = (_c = edge.center) == null ? void 0 : _c.y) != null ? _d : center.y; + } + getArrowPolygonPoints(arrowStyle) { + if (arrowStyle === "halved-triangle") + return `-2,0 7.5,12 -2,12`; + else if (arrowStyle === "thin-triangle") + return `0,0 7,10 0,0 0,10 0,0 -7,10`; + else if (arrowStyle === "diamond" || arrowStyle === "diamond-outline") + return `0,0 5,10 0,20 -5,10`; + else if (arrowStyle === "circle" || arrowStyle === "circle-outline") + return `0 0, 4.95 1.8, 7.5 6.45, 6.6 11.7, 2.7 15, -2.7 15, -6.6 11.7, -7.5 6.45, -4.95 1.8`; + else if (arrowStyle === "blunt") + return `-10,8 10,8 10,6 -10,6`; + else + return `0,0 6.5,10.4 -6.5,10.4`; + } +}; + +// src/canvas-extensions/advanced-styles/node-styles.ts +var GET_NODE_CSS_STYLES_MANAGER = (plugin) => new CssStylesConfigManager(plugin, "advanced-canvas-node-style", styleAttributeValidator); +var NodeStylesExtension = class extends CanvasExtension { + isEnabled() { + return "nodeStylingFeatureEnabled"; + } + init() { + this.cssStylesManager = GET_NODE_CSS_STYLES_MANAGER(this.plugin); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:popup-menu-created", + (canvas) => this.onPopupMenuCreated(canvas) + )); + } + onPopupMenuCreated(canvas) { + var _a; + const selectionNodeData = canvas.getSelectionData().nodes; + if (canvas.readonly || selectionNodeData.length === 0 || selectionNodeData.length !== canvas.selection.size) + return; + const selectedNodeTypes = new Set(selectionNodeData.map((node) => node.type)); + const availableNodeStyles = [ + ...BUILTIN_NODE_STYLE_ATTRIBUTES, + /* Legacy */ + ...this.plugin.settings.getSetting("customNodeStyleAttributes"), + ...this.cssStylesManager.getStyles() + ].filter((style) => !style.nodeTypes || style.nodeTypes.some((type) => selectedNodeTypes.has(type))); + CanvasHelper.addStyleAttributesToPopup( + this.plugin, + canvas, + availableNodeStyles, + (_a = selectionNodeData[0].styleAttributes) != null ? _a : {}, + (attribute, value) => this.setStyleAttributeForSelection(canvas, attribute, value) + ); + } + setStyleAttributeForSelection(canvas, attribute, value) { + const selectionNodeData = canvas.getSelectionData().nodes; + for (const nodeData of selectionNodeData) { + const node = canvas.nodes.get(nodeData.id); + if (!node) continue; + if (attribute.nodeTypes && !attribute.nodeTypes.includes(nodeData.type)) continue; + node.setData({ + ...nodeData, + styleAttributes: { + ...nodeData.styleAttributes, + [attribute.key]: value + } + }); + } + canvas.pushHistory(canvas.getData()); + } +}; + +// src/canvas-extensions/variable-breakpoint-canvas-extension.ts +var VARIABLE_BREAKPOINT_CSS_VAR = "--variable-breakpoint"; +var VariableBreakpointCanvasExtension = class extends CanvasExtension { + isEnabled() { + return "variableBreakpointFeatureEnabled"; + } + init() { + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:node-breakpoint-changed", + (canvas, node, breakpointRef) => this.onNodeBreakpointChanged(canvas, node, breakpointRef) + )); + } + onNodeBreakpointChanged(canvas, node, breakpointRef) { + if (!node.initialized) return; + if (node.breakpoint === void 0) { + const computedStyle = window.getComputedStyle(node.nodeEl); + const variableBreakpointString = computedStyle.getPropertyValue(VARIABLE_BREAKPOINT_CSS_VAR); + let numberBreakpoint; + if (variableBreakpointString.length > 0 && !isNaN(numberBreakpoint = parseFloat(variableBreakpointString))) + node.breakpoint = numberBreakpoint; + else node.breakpoint = null; + } + if (node.breakpoint === null) return; + breakpointRef.value = canvas.zoom > node.breakpoint; + } +}; + +// src/settings.ts +var README_URL = "https://github.com/Developer-Mike/obsidian-advanced-canvas?tab=readme-ov-file"; +var KOFI_PAGE_URL = "https://ko-fi.com/X8X27IA08"; +var KOFI_BADGE_URI = "data:image/webp;base64,UklGRrosAABXRUJQVlA4TK4sAAAv1wNDEL/CoJEkRXUCbvwrekfM/BYQspGkHsCNw/nbvcAzahtJkue7R/GnubUAykDaNvFv9r2CqU3bgHHKGHIH7H9DeOynEYZHCKFOj1neMfXZ0SmmUzuYgs6P2cH0fjuY11JBq5hO7ejVDqZTnWJ29Op+1twlRYq6rzLHZ6dIkSJFCnjb/mlP41jbjKzG2JjQKAiRUTrz/JCnNasnK3MmnnWm07aORtgyyHpA3/+r2BiOqvpXifW0bRH9h4ZtO9DqlUuZ7LSRz/d9JOv8Ofs/iSZZzKPZdHr9ykynsyheLEGwfD6k6WTvcCZ7h/M/ZfHNZ9ejcOBthqPJLJaMLokmw8DraK6m8fJ/tMJGk5FXbvfL/7NYgjyYXQXEg5nE/zP12uw6GPCaYBQlrD5vRzzHchX9VwTLOJpcj4bhixmOriazeIFImh44snA0mkzni1MR8SQcyJjhZMF1XCPGQwmvk/9qlDKhZ1kyjWFOVvNn0tT7yE5An2AgacIoYQjPflwjQ4IvkyRZxHE8j17MbLpvJtdSZnrARHsmfjHPR7a0rJRBp+liKvEYXp9yHslzZpc31zF1TeYkpfTksYijaPZyuhi9EKPBQJV5Ia1HL6ecaB7Hiigl8fQSXC/gi7HwBKkPitLlWPl/FsgdiZ6TSBw9VyqvhuHAGBM+n12ms7neU0t8hU7TLd8O94qWE26FowTHXomHktQH+tstF9Hs+uqZFjDQBKOraRQvDStmwgi+xhlGJ9ka9sryM+kjeYvLV/ZhQtkY3UQNdzoZs38kVwk8cXqdnJhr4l97DJBpwwTxtclwYKZRy52WSZFv4aucYXRarkmnqxlG/pmBfdyzZ22fPjCj2QIZiyH4mT8ZydGMJxEiplwlna6WVygH8hmUz6BHTHg9hwJIITBjKsckP+qr5cmDxet8he2ZAFWchwm0wMH2qgCkx3IEfuafB8IJ8MRYIHhoAtybYxYhCozqjt1Gl77IQjq1DJcce52Uiz8PDTrUIgA7joU4W9m+NWktQyDMA+wz/wzh2x+dMPhMC2kawB3Hol/j1it8mmGTdMkIhMlzsuiqahIt4S2SIuBeNCOMqN9i19XmMCXM7DTB54HlZG4iWZ/vyZUIxwLUvcHJ0yA5VYL10cJTkzyJArwF4tYSydMTIIwVopO027WvzK5LwfD6iLpUnAnLWJM8bd7u8/3DB617x69O6yepF7/AK93V22Ll7o4aty7KZiePtK0eDh9Stt7WLAfzmYjv6bSywDr6zz3ZgEBeJ8ZbLQLW3F64O5rJ1ts2FfSp1pnfwbjHlqGEwPHtN2mbaGGDVPcGr3V+dpLFv3vJ7UxmXXUiaNekQ3GPHZlX02ucSd1agUsW2zVVuS2Ksmw4ypKRTK0z3e0f2basyUeWnBKWK7Nv3R2vWdWdwBrZUFdGnJzJXjdvBTCmlzJPx0qZFZ2mm7ETIGm9XXGWVtenlU2f/Hw48j/vGsCRzHRrB6Tdntm1B0xTs5n2iOn2jSEii7f0CpsATRckrDZ9WvsmwNPn5c8Z8zr0SrplOxBXi3stxCupXde2dV2VZVEUD+v1yjmX3eGa7PmoVuv1+oXuLav6RdwBUbGOmANRM3smk+JGr5hJwil+6/+3Tk8mW++tga/sWKmQh47ihRpH2rV1VRbF2rk7E8zzGebhpXrbdjp4WiEJFe1MmlWUPzg+YMlnK+Ln7/25BydCAxOGNYA89MSAirmkdTtKOmVQrmYXI5bFwzMZYJjJVutt1e5EQkH4dfRyZt0Rjvu5HONak1nik0BeTj5ZtyuMgq2jouQ/kIrg4KhrdfX2WeRgqFk9VNwyzXAB4Fdnogku+hyjjHGpJyanghoMS0kA7llCHUcMYdP6sGaAqUG3TYqnEBZKp5bMn4ShM1dax1UX7MdNQInoE1JJuSVapGXEYvn4yla/1DIK2oT9HtkqKDshmcYj3+fceP7di97HFZGHtgJL6CnBCpna3xG27b2ZRD9Rb6jFiT4JSJZt6STQvP7y5bxm/QixDFY9l2Aqlp0cp2rH78w4wq/uTDV8KoGimiNjipXJ8XyiVgAWz+UJE3v6TAXrWLqjNWiEdLr0xpyF7dsrZl3zLGL7MOf49UFwiVoBjio8XWLYOcwkzlHgQKTTqb/AgXGtP4JvO/FlhFJlq44DjDxtPQuVXseIT3QCHVl9+DBQ3i/0FjjQ2b79ZDiivhahIxv+qlrK+m4onNt5rweC4owLck2Fs3GWcgYecogR+3rlM+pbgFTZHhm1FVYw5OKsz/2wrBxTtsaUxk8FOJMm7IX8VT/R35TuQpQBLV8cOKXKpMcRErCFTt0PHi6iM/S6IBIvZ7KH3q6WUowZUUsbuV0Aa52706KN6FuSxTbtURfTWYpxJvt7pwWv2wknN0yBbu2FixNEHb2EF/scdTGdyIMzyaAd0POeYcIqM3fyao6ACb48KaIa0yy646EKAxjJxEcRhvwx977nkJPvU0uzVjFTwPaUQKQ5f60pMnOcCuOQLDE/fuR96bnjjnzsHaO4LBQywRd+x3Fq7NOqSNjdwW0Ek3K8MLY/fhVt+eY+LZHQsv0a2N7d++HEcDunK1l3GEdRMTCgIoI1XQsdr3IdJMSkCZFUUqIFpBVPC8XV1CkhRL32hiP+IiZB4sOdeQa1ZQBXM50hXn2pItoTEW9Jb6hjE6tS7egaMW855Ii9GkXHJj1fEFzBTSrTFG+jp10YFqu4nDO/u4N94ZplnxKsr9JP+bMp9s2mPGpqX1OVPmZTvDL5nnHPBm1h6xV4DMjezMiykFMwyFv/QOqqlxmKvI7a7HeMJy/P3Kf7YlNWL72Ne/Uujtsl+u6lG/SX802euwx0uWKoNHXCKWVH11wAk5v3s6te1rR6lUlgqA6/1HRE+x4ka7i8KKtm1w2MMOmubaqyyJ3YQm+nMycQkoAvNKApb8iYC1+bZQ/EAuKHQAnuUFASMQbgTGb5pmq6gV1m12zyDCWykE5dSROShfeJHZRSvGAYm+fkehQeLVnD4ctQV+2+243Q30+ROSCjx90xOOQK00SfNvXhfS0Lytxx65pjlwCyxHG4yT3lbW+yiCZXDBeaGV5ZPGD6yAKNrw4fA7Jb89AHRZ0OTDVdfS8vPw+Wvtsw/FVYie6Kr43wMp7wDuMxGM1iUzH0TY13/YREkCbKG2t82Ppbv+eUYdNy4Rg0aXqRXA+4CW/uNTJ6saFwGt9HYt43kKaJsvII1W/t7o6pD1mqE4AKJBtUGWfNoOm4jOl3xNIHN0Mw9t6np+Dt0tfSge2mzXn6kKU5MSG09I6obXM78oVGDvj0m8fS9wNzKrFTECSW+ZDzQyEMD15whPF3/MocmNP/1qvOCfvKBeAIwsIoMS40govDOO2gIxKkibJmYKmR7XymJTkhKkO38JL84Lb8xxzDo23BR55YacOro0XWhxnSYnY94ctxVWUhr4DwfTkArI8BpnFu56/as5yFiMP9Mbbz01Zda6bDlQ5obYjI1+XqJagHd9oSlN0i2LS54mwXIK8EsHw9hMfeXJnZ0FByffBwsbUhRvzl3dPAToPRrejOv53opR82CHDH1VaB976AHphnGNkpNO/UditQAbNrphSaqKbbfvF0IupuC99YHQQ81SpBe7gO0AffZuX2ozo0B6odwNoQlO2cZHvIpPcSu0yPSy+QhCaQnKRC67D6FjZuO3XoXqf2DFgbgrC9I+3RT2Yj4AQzP1QYgGx03ykeS7oxe8vHjEax57CvDCgaAo+kq0P3lCkFnABYD/IwgxgqtFbPmX3KMLk6VIcKH+z+jeeeyGgFTl+tguTGOwwgtllslQ+13Q7MfUNr3D6wibo9ISjD1H0XiJ9t0/KhBQEgI2mlfiScduiky25jkmz3CCIbQsihHx4Y/v5eimyFcN3cDRYqqB76tJuRFZ6hDduNTPHDXbwn/iKHlJIwe810GfOF7oETZybYM4thpUA4NwELG7YdVjp1HfcrDcHtOVPLyxRTrCS3hrsm7AkQKhLdhk0DrbxLeGXiWcXtV5ennrXW9YqD61f6Xvz+NvaH2rpevR7S7l6HmKnFocYm6K4WQoQlpXjnlpq7vnVZGKMtHVDngBUcXdrJfQttVawy0l02VG1q69sO/ZCx+8ER7Dtxdg2hcejACzuC23+mkt7MkTi1DrhoYhyaJ95QOIPYIql5C+sy5twNYIKKCDyMmiAz70HjhCVUGRC9XsIWTWw32rBiPv+VfGPPnBnUo1qlDE9+PWHiJahkLyRrXWdtM0VfFc/10TXjfRfVskT0DqadEZhfJeCTyQy9bWdg9FMDYn30TKwZGhf2+owQcuWdMk2m5w9/WdlDLVnUVyxogHrUWd1pfYv0SfCIV3Vn3XIJ+QrGBS4qsRiJV8G5ZJUS7cTw5Z+//+HHn25ubn784fs/f8WIr755/+OPz3/tTz++/+Z3XwihlBiHaSnKzN7AVP6EqFKGxKJhEL2PHctiTMidrvVUx/ioQ8qH3ELt/dX3Nx9vj5iPP339JQN+9/fjf+6/fvyzwEudJ+dNKpJMUci7tlO6T7uGYDW2hi1LLVJzmfaXj1BHEe2DlSC5F8AXf/9w62l++jPxz/3+xv/P5cajDAOr4HoBmpjcPULtw72mY+WtKSE3epBVxAwCZENSxxUblFd+9/MtxfzyNeUk9JH25/JiJVNHb7mEi9DEJFThVUutiUdj1DJTEFK0f9EN/6tkN2b9kFvuyvxeWU9b8Y3mu701lBXK9yGyudUT0kH58Q6gXNPKTB0onGmNvv47oBzGNa0lrHeGSnnxXNgzmB+8z0IMJyI+SJWfD3KS89KiUXywCSv0jYQz1rknckiM6/HUDmMAZAlNlogQ8st/3d7eipwZ/vbxlsf8nQ1Sd/FRyQV8QAGkH8/YmHV5BokVezZq1pi+/5wtDtJxTGlE0Y5XG8afhucyv/75CP5xu2d4PDkhUOp3cgHtGYbXlstX60g4FSJQqeWWFQcqzTCeqwMHIjA8hlZeUvOPW07z9UH8eHvLuHzFBpkw3jmM4YpJ9vilLHE+gMuJKWZoRFyFvMSkLj6RBpnQpnDXqXM1240JjziJopIPP97ymm8P8N9vblnNr1+LI0v56fiajJ05YAm0c4rZCp/piwtzz3R/ZCVlkX/KDo/U6H5z4K7v+FdIKSHfS/k0PyIqb1ENrxVZ/Zn/72WBlLOROqE2WiHa8q3O4MlfWlr93IS+Eh3WKTsFua/itM59v8PNXlWCjfQ652QnN3yv8puvXp2G+M3vpJFRsplUBagyBsXABrnBkyERzIRrbMoaVnbHaxVZXnLwB8JECi2/5WS+Sf29v34lXdFu/dFK6fltzH2vR/U8w6EBrcg6bIhH2zZluTdmwpptcFcLOnOyEMWfbkXMzTPn/lbE/PIFGWJJvZMSnOdGBdI/NuaHlBKGa452X6Zo92p0wFy9mjPFBKHzpRahMY8qw6tjNOBExyMv/0UGt//+5Uehv/dGGCv2v9eZGlfFYFcmjF03Jk4BH8P4toIR6/C2LjHRqgI9Bmz8eCtlfrmVMt/K4i7ljiqzNK4tj7VgCZeEKT7lnk7BpdQlwWoAWGKx8fWtOvPrl7LYvYCVPUvmspgSiMfqWji9llJzvzqcxCJqbiUADLGi4PHTZ25kURMoeRm0psTAaNXswOcVXwWi0zJH9DgHhR0WHP+41Wh+J4qSwHmTQW0IdgxvpLA+whMhYcwOIDMmHGBKTlozLDi+vFWJG1FswVAaGkOpUjmHxD4GGwrTVQyqvmpah99BdCYNln10OpIowLCxU7+ttKVJW7DEm6LG0esVDhLkAQ16hyIBj1+U4sYO5FbgfmCU+ny2I+2y/AxxbA48FYu8D3CxPJMHRyxaHTOwsrLxWOfM+UAWkDPV9XZ5dBJK+wCEyR7Ax41a/GAGMjvUVhlPe1ZEPhxhUsUvOx2oWfQZBjME0EOrxa9fWLGbEffYzGBLu/iIgJ4H7re65zD4HAAM23o35VBhobMBLUMg5Sc7baRxsjdrbeCoTNgNPsiNYvwghwYLOyO10dUEMuK6cmESBwN0lDZElIDjo2L8Aqi/uhJBa0sPDsRbq+snL/8BgPULKNr90PjjrWbzBQPY2484kfYmtYm2s4DtoMz0E7ju+SbAwfeq8RcprMHah1Ym9nq656fc63RCtz/HdW3bSUV+jtOfw3+oxndSqNiL07UN6quyQ+CqTGAonkJ4mdzO4zPfxRAGGDeq8SMd7IRPK4NCO3KWsseZINfvGxSAPkXmWXEiz6DTiAcbGXxQjf8vhDV/ebplv8iI2qbV1t3rf9UzeGjZx+MkOYVArmbwwuwv8VY1fhFCjda/cW6BtbG/iUwi/3X7ldzXtzrMPWeKKtKKWz9sq6ZN5fGUiU6U47KXcCsDJ5DMGiq0K6Mb5EIM7LVrymLl/OdT8DkTi+fhVCqtEAfcJRmPE54ox88EUdPwziiwdHoANPwPp/xynUyurU7LNaGzq0efGzLVPsV8623TiaDkSmEN2+TCDvsbW5pmi7XNTVw34HLNcPCGwrTa8mfg9mmlbQ74kDqbfuLaZdTbcLScJ9RtuW7L5drhcHCtRMbAxMwV0zZRdDwSEoyJcmTo9bUP4ToJSv7eKrhuYJjrnYAxc7t74hvoh6qJomO0IBPlKIaKht8YBSeTB1shyVBn1xu9Hmu1JVijXoHQ1bhrwh8Foj5DxdKg7JITpWG7/crOG79PZUrUlgr91qZOWvm/wfQBYDgtmmatjY/Bhti2b+eLyeGrJbHCYn2YWmbwME6oOrozzg6cLdjiKuK5nUWz1vZITxpbHHFqsBS6CtqWiP/chzldJq4khhlSSzWVaNWnVaZrJVgX8S7D5hmkj1YxXiqkE/JUCAUR/0+3XjrjNPXlVSpXolaKwXm3I3ZOF1Xlz3pAXrqV5tNnA1hvbP0v7iAyXp3CsnZktGL/9Q7Zp02dilaAd2QYpzG2F9qUwNCtYVqT2trUeBUzl5IjnuI0j/xGNX5PnjoV3t9+wok1gKq1WrtCS1xBdY3dZ0TqzLqiU412QyGI+K1qvFNxxzBbOlRbiwe0VkF/b2uT6j9ZB8ehzFJq4IhTL9vZ1a/aB6gWeoTvd8cPvSPB8U5I5m12DvtkVdmlK7CFK05i4i0PCeFGMX5CQsnXDwFwkhWa9GmOATLZt7nFZvN3mV0sSllZBnFzDjRMhPeK8Q1Sb1aSHc+XGnVWTdyb73tMHXbV6NEumeKjMA+QNq0eMdAwEf5o8yuMqbqzBikznEUosGtPGXRTG2GCHYt+WRM3F6zddnfkF3rglS2p/yxjcPfAi51Nk3oL3hlGaxKZlOHxgnY0lZjED7k/jJKc/4CDSrb7zlJ/92Tmzjhqh12SFza1jG196Te0wCOR4QO0Vm1+BYOdcHfbpWU9++c2ocJmKGY2KQ1sJVWoWPlsryNlRT0gbxq7zSvhqVmgsKUJr5KbeS9sAMtkk06nDo+N2tLmjyrgzBvVwKadv8Gx0p1tFwqLHRtfYfsuc2gqJLVLppiCakRNSB0mVlIXVfiN1r0V2LvaJty1QsqrRuUubCqkMQQNXHpY014YMY8j2CKVmH9qsOsBI2x6q0er0GBz1JxNo+Ct8JSKU5Lk/JJ5HMEWBX6nwN51nLDprVZWocJug5dp51YylcewczWxoDT87uSqtzdWu02GcO2nQjFBqeWuKMwl0VPlykA85XELy9qZURqSpXI3wvNHo92SrCtqGLKOK6R9AWtssbwpjWwA56/J8yLcEsO3Tzkci91uTR+SjHqIcxwaCHza1Luu69oK69oc4Ao7rU2j4D1eLQN9YUhqSObkKrh/NNitMvp1b03DXYUm0Ge5oY22QCzKS2i0ZqEDqQBSAi6YL3jH4VirutquGfTJOGGV14N8bbSDvXegpufRABcnCUUxvRAsld7pUnb7IO52jywDA+OEVV45Yr8RGxQap4cB+N3dkzTd/LAhqVaZouz2F/E1Hk4nK6zyyiA7Ai8xsD0UBBpNv6n2OdKO516oRW88+4OV+7RrpvbVvLDKS/6ggJeif7E+mhxgA14L3Aao48EfTfRrtxlbdyj8MEgrpiNBtPZzj5W9mr7cWCNKPf3IuEuaQozoHSr/h3UNTbqq4COJGwnhu0FaMa0whX4cWDPb5bBF+51JKHxD4p3wQ4nb7Obqpe8/DfHrDpuXaVYvVqz54GlgAaQqKRwJF4BMqhBqLj60DKRQvrMEpSzLk7UJmITgvGbCu4/a/YhpCcJen1szbLQtDTWSH55aNXTLdGdT3861VEgB3eqE0hPqgzBb+mszNGEaUDtIyDCmpWiJywksoRRiHHQVOjPqssQrFKwoLU4SP2wow2+0Isx3TC0jQe30zB6sBCHaziaFQgFNOuX91tLdkaZsIuCkGjO+Wu4NPP6dq2Ukpg1hb+0Mi6YBqUA29G8ViYlcQfMwN0eg/ayPyKH0FJxLTP9Mx4Z3H8DxM9PBp6AZNKEN8sci4XkBUqwN0G96q4Mskg0SnG+Fa6asJ4IJJaqUpM5pQQQfvuBqzi1qSX4m4W4tCx6aEP6mMjPkryG16ZUKm6VzTZqKDSevz/lHCxRhNpDX0rug3Tqp7l0AsWY4XUNhg8zxHuQ9oZXOWsBfkqIqAMat/bUB+m054uBIwoQKi4TnD8j3/wWFFvBLwN1EYX8+YGNIutRIhTBa1veo+Miotir1DeW0jzY+o8GmXXLBPenoFHQujvKRpVZE2h2lXbB/DBTKji1hPOrql4+/ZVQ+FwpGbJc1pQ7eSfsu+tAigOYg6eeVRrjHq9fYPwYKZccUQ6fzvUYrzhNZ7aj3b0GF9l3wvFUA1ak8Br9hg3uchNXwI0SMWQOuB8qXDy8W+JWsJN9vDXmKENTvgganIQjq95ZbeItGKXopkpC6TCxAhqP+XnWT0AxuDrfC5Mw4rLDl5hpuxnyD5nfsmKyIBbLh3MobAlNh2Q64F0CL+4prGe/JZGIU7io0tGxiDpwDy6H5Gyv9TBd6IgWj5hakgB3MbKp/rbcDhw37zIyxPjArU/3dg7PScBs4zj6UDssOsvqd7bBulQZXCtagIyZdG2yQwdzIzW8/wOBf74AHfeE4hn8Y8Uyti/53kKLS9Q5Ys/PI9CCvWrQ8ITN3MxomvIRjy7+8Q1n+SZotR+lyO3/+KHnt2cwfJr1DdQe7+QiAlPXXJQCYErNuZcE6ZvsaWiaJ/FARIuU3v/kZpuWY+DZhGpfY7VlEhyl9rNWwwz7tQLNCKezb4vIX8T6NUguWngmGeAuE6qL/+DX/oTpGH07LMNg2k96hkSXQwV3nO1l1yHNX2GKEEhHugCOlZZNz3hzZiSx/+YAgLsfbzjet2DPmMzcMazA+4prkAvdCUX8SyrZMfVuWcqqscCe5AkrPxhO0Fza0TWVR/Ow9ybSCxZi7L9tUcqIrYj9kVjhgGb8h4AFClYN0D4nsPhVfIuoK6OThOcZ2nZgRCdoW7dYm3wv3tYSznRdl7XHLg3Q1GCY4Kxxbr0dAMQ3fBUOzT5smZf8d03StKcWU+ntqZISdhcw3H3CDsXKpUoz560g4YAnT10HIq0DlQyXDTfbltKqA+/RSNAhdjaIQSiNQtEsOD+oMshdwP/TLO5m3/UFoopdXOMzG6dkZCTZ1G+BDqUsvEhbrag97vc2XxSGzKcu6BvhZQNvNJ3Cf8uVzMQET3s0slfsav5Plv2PortbcCOa+c+FDgUVarIW8eAhX6tuq/9Epmo1EUuH8WST1oCUi9kOaEe8xWMWb3exf+gspdIfyyPQYcd/ZhLM9XO7fdqg3yZQODDAl3Feev/r5Qg2B7089VUD+aizvWX353s2L94wxE9UTypxUGQEuOUkDdiuI7SNQ5hvYCyqT4fVvDGsNLj3JaBLpJurJ6ivmYmyHMf8cklnldCg3cFo1bV1hSafW/YCs2FT1gaf7xiREvjuDpoX3fL54ruP1XbLP+Vx1Z5aTmfVbrJuACmb+BywCNdISldfy7obJF9AtOKVqscgcDZrlrKxCrQ+YaWVrGWhtRFuOO0EDlaF//DPoTUR0OM/s39gLzZ1ZzsYq7PoBpRScJsy4T2id+Df57p9kWTmku2Zbmy6l5kDULKe2CgMFuOhHdJ6gb9YUR9781d/zX3/AvecX6R9MjqFDB0tMhfuBmBoyiM9mdiHHB30zzaiO9HuS+XRw35VD+hn0ZubdHJezMOAW3zfZe+yq4FFbiMg3MNFB3Onnd8BVtVo2HnUNv8HS+YNNaNB9Z0xPY17TVzjClloGng66+uEfjnHkb573EocTHCvWcLaUnutru6jryiak2DhYLkQ0z7YXwND9yMTz0Av4itm7f3vl8fHm/TtsDYcnasEyieV/8LSVXbdivzMJq4EUJOpwyRgarQFIGRD7fuQq2gu8+8Mf3sHcRoTUoIouoiXCz3X6YBg7LOvVV4JJ4pFKLnoNLN2FDX35X7b+gJtrCTcY03yYG5zY5RQWoYXl0vlyZmLvlZ1a4X+BjvlRqRH7ufTJIFT87gXx4SdlV0w3Bo+gknP0xiazS2Gck+pwsechdQC1ajEo6Lf8WGBuJ+sM/R0X754IU3APhNqwuVNIDUIx8L0OfBJdiJH9DCXDFBOpfjie6SyfsDtrc+DuaHUVrIeF59LanyshN1IpffL68SN+w/7aJc9zKB1hYkDHcIpOUq54ZsYy5h9ba2UMSgE5SIL1iyLuLCOxKyNjjQ9PkvzlRcBC8V+OZ/NEta7DgEO4g441U3YaeTO3jfRqaFQSkEu6yxrLNE5SteDuV8Ojm6t4PpuOLy7enJ8Hr3Po+fn5m4vL8WQaRYvXpYFmOCLrkDJYkgodyzMup7wzMcLOaXLPInMnOyM3UcntQuXvgZKhSv/fTKIlW1ti1Ef+hMtJMwt/hOzUET90tgRsgqIbUtZvgXkOoMVrpBIOspSexktNbYm3XA8/XOQvtzIwzANNCsLzI03ZoRBT5pBpLc2M2VvnU+e48ATkgy/D3JZYz6O84q3FuzKmm1p2DsgTjEtvTbtBQYsZ8cYW7PB/HRQ9nc0XflDWlrj2zFCMTktKtZ151mH+pmLnwFyYapBDVp9bA3XSZzrxAFlKK2tL3HgWlL4OexnoLFhqER3Da7N+HLF60PZe4Frrw58JqZhS+eVmZW2JW88c5en472JZ5DWZowGZfsfAVF0DlfdTmxyn9kf6CRDR+IGpLTF6dw6sTkOciwDLrEsx3bMpcFQtVN5/skn69yS6N16VrYQspZW1JeasSc397+iIuNhl7zqiiyY4X+Ews3LIzdIOdNjPYCl/Z/MaSukEWzHdsT7SdCOCF20H7VagrdsEU1pToGQ3KKc9HLFtMV9ofWYWahWltB822GpuMffTHNfeGbAIUHA57s8VuER/Fh4KSFq7NMNVtK1AgJ15ltLcIhtIwX1COqEKnNzcTim2d/SzHGzyPYfdMQObISd3hNCzzA3PtGKHKCiMdf2ulCXvkyOVBEK/0lClEd2agZ+Cm3wvYXdcoRXq7WHHMvda7w/VBtmIi0lkAy02D3mZzDyl11Yfdo6D44vLHJrA7rhGG9PzkznjhH5S9eTiYgiwJTIaVy6GFptfkB8aT1/2hilRVCxfN3CpFMFqphdoFOLamp5knL+rF04WlV8pzXq8sE+Yk8hzaLmhf8IKg4mo6YQJbvOFDVRNLYdMcbl+1Lp+Jl0mAbYG5dEFctukjP9hsR3J9cy61uNZZZJP/irwz3baQNXUugwxB7cmqLhpesYFFQiwd0wPpmeCHNXa/4mc5NqFuW74pMtXphnMNbJ6RYpVU6sg+d4b/fvoilmBAJuhlCbdyCIItgKPqOpsIDjQZAdypXT5uX5ugX9pX+JdW2DR6zkiiZhm+t9C1aN/xiTAxotrzlXydMj93XBoBcnN8u2qhd+L54JNyH60u7bAyv5dhlg2N7r3UecyCbABlczYSp4MAzvvhjuMXvyVwzXySu2YxDj+mICzf2p9F+oTOLPeq95H2IVVdcCjvpd8Pzw8bshLpnJQMNOsa9R4nPhd6C6Bfu/mAJOs2wImumJghtdWmnBDVXVI8WYjhvFBAcANOeV+RHIlyi9Xd4j1eYC70EVvvzBwCGWpnxdU7bPV65cqcGVKSIdGfV8wPg8buNqrc/5HGlhEE/KhgfJpngcYKG9n+L///gatptZA6kxsjIlxECvCHE6AzVi9PIftgqmVKjHjAcBylz2CeKQvY+FGsBMFybOFq6ltIJk6K514Iko7NQvON2h7zhhFNjnqKyEnwSTPmHqod9I+9UPGO3QBzEB8ZF2awdXUVohssk5lmC21QqZZcF6jfZQxY/4tUV+5ZKq60p4EY+ZK3YPYL6DS1dzP5nnWavqN+IeIp/9y4Jn54Y6lVRnIhifk+xyshWjN+Fj1LlBfqAVruEuwx5Z1/Vi1HfOTbu9FH4i2eIl1PGANNV5NrYXUQqkUSsyJlJvuqFIdQusAVHLeiWWu1088ONotjq3WRVk3/jOcnnZdW1fbYuWEjmnP6imSYt9NvJpaBdmXR2nGrFNHCh/8FKAKlmOlvkeYulMr2QcYO6LR5jlECXLQQPx8B/CWZ/wBOeD1LTeQ1b9HjYowFjysuBHWt3rPSn3PMDe3wk13ptROzWwxFdd/DQK85LxFzHg5BFRHXlEfigf1O9ZYaSRmpijFt6UFyGNVD2gB8l3WDZd6FCwGGaBsOV0hIlW0PA0s8KPd2C1WGiEcMCL9t5J/3sBH5vymcZTf7oUndrhItdxhaueE4b3osfJ+Rqkmrpip7zHiZi2vNLgcDgwMlVUDPZZjLKui9V/M5X7gv2ix8n5WqSZWzLyjCLFe1/Geos1+Aq7zjsUq2swlR1OBuWwHhEWHtcOP8qxcqRBuLT1RAbahn4M90/KFAb4sVtNmK5hEiIs4G1Pp+9QIusQqVBNVCLdGeIE778d0nn+xzDPveCzsJpbTDUCXVnHlLaPNLM2ZTnOOk0xqduHWTMkkb12fwS2DUjX7nccq22zlXMwlo/YRjZ0kOoTUYJdqYsdeo1kGaE7nvS2zmLRTVqbqLNPQGO+lxgIJutzTnxATpZKRuSkPwFya4YzMRYCiLPGuwiS3ENXeFKJMB/os09AYu0zWRZvDgFptcuGYMB6lL6Ds3PlhKNHYOnVoV2ECXQa1tvcougG+lWsbXyEQy+wpX/SuK0qF1bKJNQ+YYIuiijCj6uL4VhWxRCuLM9hlUCsKkG061v+nIuzAbLugXKGllDsebUpN25SYHOx5IBRzEBZoIqMIngu1toX4Otn/VUzaQlF9ngFjJTVFEK+ccZSaToL80OfpWjLxZv5z1FnIZOflAKiq2OApIky4Hg0biagF7/ffgspj4E0xCWV7wEK9ZjwaLF8Aq1AiqmJIAGPvAgJHxyiUbzmrPeps0yL+L45AfhUl8Hj3SxnyQxFTJt8NAKxGiaiKawzHMDTaCrP4nAds5U6JGjyvU3Oe9MtlgKBEVgtebxzqgskpxixl5C3ffyaU3p3ZGKGQg3L2rz7fo5CU0ufpJGRM9Xj32J/lVcp5O2aqSh7/BfkukUqp297A131w9YDJWrSZOvn8HHPIbAjtAzDstfyJdszb22Ge4fCAPB7uJEPksqF0rs9qmX4kV6Dnqa50wiEssHK5usukq2oXPDIbhMV1yNWqKXMSbUuAl1zB/b/9E8YKU6ik+NztYK+PltVSxyNF3WX37cAgyzzgQWmaLpL9J0RK7xkrdOQSVD4Ue0W0LnIxmv9+c+BnnFYXDKPHi84xMa3lVlAHBrUrnJxkREVswMVhLWtDcTlWydcfKHiHssuJTDJtOJ4Tb4hnMCqY6XzeJymeTo5kHl6xrMiep6jzEcM+myAQVj+DtckkabqI7ZCFxSBb4Eb05NuNZJwJk01xn+e5O272HiH1sqoPPx2QKnzBa0Rd5aQqwsnZmEf9KCNMeZj81KLkTHQcY9Vs+GKNcOSoupxSTPPxdkWZpXnLrDnYM/83ZcHMpv+6mKbIKCQzraI2ZivTeR8KsVHIejW28v4TaeqAioq1jjHW8Ryay5fWpZPS3osxOinsCkERB9apeh4CQ7Mv7r9l1q7a5O7QZHxw1GxCXtborn59VC6nyyuSJBR4kKWK/JM7/Fy/F75PDjpDyzE7HKIggvYMfQHItpKAQUVUhO6bYpO9Pf9sjc7Yaj+MuTAR+WpTnnzNxpXxvgiMHP1mzZP+G3LzNDIy5yJIkBJoU3zyqWLtJSiOPm9s3kngZtAVRghmZoL/RGE8SW+m8MmitfZfRiltq02Rv5h7nnnnmrH/R+1wHHW5f9R79Bjntwq+U28/R77LEAsT/v+6KXbyIwl1YWLQI15/BVbrVJJA9XUoG9mwchGb8P/FBFWuLuW0FPhKtOVEEwE3N+npE8dKJAleAjWDYxKFBqwZ8f9AhZdCEfP+5Ehq0cZDNcdLdTEdcUJ95wZRqH3NChMBFjxwUQyPMmW0CDytMhPVxfSUEQZ0RRSFutfsWNDoSbxQYSKWWHrqX/FyBPPJCgmwTOWERhcqee8Ta56n6ggrheCFChN9EtIwNu6/mEF1MT1VJ+Aw9t9sMpwaVArNA+AUEoWYB5SM0fcyblGdn6fGyi2XmoIHl3E/JdKLJUlJATTpJRd9osbAzxBXnZ+nIHuo+pImoY7a/Myu1JoMhRnluCerC9L3FIfAa323OlwqJynH+mQX47AXfFSWAMEcly06IaeUC8i1HlzGS+VnM53Vq8X0DSzZ/+xjYSpFljAnE1SNlMUYfa2XqLopQG15bDMvNIkAo30zia1MqlPoBJKMUTVSkmkII+801kx15+cotFpwuYjGAm9DmyoDS8ufMXThmoxhW9fNxwDh1Sm1geRp1fk5GRsuuFzG00vpoOfjA/NiYusKNMmcRCHmETHMRwzTlE1ZnEJ05+d5aHsFaxnPJhcCuwZvxtNo8RzW7HQaQvMf55ewKS+JLgORqpmRPgQRI39+1k2oXFuQEf1nwuT8nEYWnF9cjqfRXG18YEmFI9OfTw9nRLAy/WIuKEmZXQS8dFxkfxKJLnSX0lHYK8zQZeJnlv3z/+Y3H4c0ocJC/qje4MorFjMWnkxwOZn3RVKJx3yyXfVH/V8Jk8x9OBvnl5NoAXtU55dAfJDF/KXeFnjRBm/Gk8gsbosk3cVB0yGriDJUuy6mi7P/kpjlIp5HeyZeJIiZIY73jmm+SEDz2uLluPaPLI6TXiqt6HMddT6eJQpolclFQKpi/Y+PiZ8Jr4vz4BBFdDmZaarkLObPjK83R45ahOo7Aw=="; +var DEFAULT_SETTINGS_VALUES = { + nodeTypeOnDoubleClick: "text", + alignNewNodesToGrid: true, + defaultTextNodeDimensions: [260, 60], + defaultFileNodeDimensions: [400, 400], + minNodeSize: 60, + maxNodeWidth: -1, + disableFontSizeRelativeToZoom: false, + canvasMetadataCompatibilityEnabled: true, + enableSingleNodeLinks: true, + enableSingleNodePopupReferenceCopy: false, + combineCustomStylesInDropdown: false, + nodeStylingFeatureEnabled: true, + customNodeStyleAttributes: [], + defaultTextNodeColor: 0, + defaultTextNodeStyleAttributes: {}, + nodeTemplates: [], + edgesStylingFeatureEnabled: true, + customEdgeStyleAttributes: [], + inheritEdgeColorFromNode: false, + defaultEdgeColor: 0, + defaultEdgeLineDirection: "unidirectional", + defaultEdgeStyleAttributes: {}, + edgeStyleUpdateWhileDragging: false, + edgeStyleSquarePathRounded: true, + edgeStylePathfinderAllowDiagonal: false, + edgeStylePathfinderPathRounded: true, + variableBreakpointFeatureEnabled: false, + zOrderingControlFeatureEnabled: false, + zOrderingControlShowOneLayerShiftOptions: false, + aspectRatioControlFeatureEnabled: false, + commandsFeatureEnabled: true, + zoomToClonedNode: true, + cloneNodeMargin: 20, + expandNodeStepSize: 20, + nativeFileSearchEnabled: true, + floatingEdgeFeatureEnabled: true, + allowFloatingEdgeCreation: false, + newEdgeFromSideFloating: false, + flipEdgeFeatureEnabled: true, + betterExportFeatureEnabled: true, + betterReadonlyEnabled: false, + hideBackgroundGridWhenInReadonly: true, + disableNodePopup: false, + disableZoom: false, + disablePan: false, + autoResizeNodeFeatureEnabled: false, + autoResizeNodeEnabledByDefault: false, + autoResizeNodeMaxHeight: -1, + autoResizeNodeSnapToGrid: true, + collapsibleGroupsFeatureEnabled: true, + collapsedGroupPreviewOnDrag: true, + focusModeFeatureEnabled: false, + presentationFeatureEnabled: true, + showSetStartNodeInPopup: false, + defaultSlideDimensions: [1200, 675], + wrapInSlidePadding: 20, + resetViewportOnPresentationEnd: true, + useArrowKeysToChangeSlides: true, + usePgUpPgDownKeysToChangeSlides: true, + zoomToSlideWithoutPadding: true, + useUnclampedZoomWhilePresenting: false, + fullscreenPresentationEnabled: true, + slideTransitionAnimationDuration: 0.5, + slideTransitionAnimationIntensity: 1.25, + canvasEncapsulationEnabled: false, + portalsFeatureEnabled: true, + showEdgesIntoDisabledPortals: true, + autoFileNodeEdgesFeatureEnabled: false, + autoFileNodeEdgesFrontmatterKey: "canvas-edges", + edgeHighlightEnabled: false, + highlightIncomingEdges: false, + edgeSelectionEnabled: false, + selectEdgeByDirection: false +}; +var SETTINGS = { + general: { + label: "General", + description: "General settings of the Advanced Canvas plugin.", + disableToggle: true, + children: { + nodeTypeOnDoubleClick: { + label: "Node type on double click", + description: "The type of node that will be created when double clicking on the canvas.", + type: "dropdown", + options: { + "text": "Text", + "file": "File" + } + }, + alignNewNodesToGrid: { + label: "Always align new nodes to grid", + description: "When enabled, new nodes will be aligned to the grid.", + type: "boolean" + }, + defaultTextNodeDimensions: { + label: "Default text node dimensions", + description: "The default dimensions of a text node.", + type: "dimension", + parse: (value) => { + const width = Math.max(1, parseInt(value[0]) || 0); + const height = Math.max(1, parseInt(value[1]) || 0); + return [width, height]; + } + }, + defaultFileNodeDimensions: { + label: "Default file node dimensions", + description: "The default dimensions of a file node.", + type: "dimension", + parse: (value) => { + const width = Math.max(1, parseInt(value[0]) || 0); + const height = Math.max(1, parseInt(value[1]) || 0); + return [width, height]; + } + }, + minNodeSize: { + label: "Minimum node size", + description: "The minimum size (either width or height) of a node.", + type: "number", + parse: (value) => Math.max(1, parseInt(value) || 0) + }, + maxNodeWidth: { + label: "Maximum node width", + description: "The maximum width of a node. Set to -1 for no limit.", + type: "number", + parse: (value) => Math.max(-1, parseInt(value) || 0) + }, + disableFontSizeRelativeToZoom: { + label: "Disable font size relative to zoom", + description: "When enabled, the font size of e.g. group node titles and edge labels will not increase when zooming out.", + type: "boolean" + } + } + }, + commandsFeatureEnabled: { + label: "Extended commands", + description: "Add more commands to the canvas.", + infoSection: "canvas-commands", + children: { + zoomToClonedNode: { + label: "Zoom to cloned node", + description: "When enabled, the canvas will zoom to the cloned node.", + type: "boolean" + }, + cloneNodeMargin: { + label: "Clone node margin", + description: "The margin between the cloned node and the source node.", + type: "number", + parse: (value) => Math.max(0, parseInt(value) || 0) + }, + expandNodeStepSize: { + label: "Expand node step size", + description: "The step size for expanding the node.", + type: "number", + parse: (value) => Math.max(1, parseInt(value) || 0) + } + } + }, + canvasMetadataCompatibilityEnabled: { + label: "Enable .canvas metadata cache compatibility", + description: "Make .canvas files compatible with the backlinks and outgoing links feature and show the connections in the graph view.", + infoSection: "full-metadata-cache-support", + children: { + enableSingleNodeLinks: { + label: "Enable support for linking to a node using a [[wikilink]]", + description: 'When enabled, you can link and embed a node using [[canvas-file#node-id]] (Use the "Copy wikilink to node" command to get an id).', + type: "boolean" + }, + enableSingleNodePopupReferenceCopy: { + label: "Show button to copy node [[wikilink]]", + description: "When enabled, the node popup will show a button to copy the [[wikilink]] of the node for easy reference in other notes.", + type: "boolean" + } + } + }, + nativeFileSearchEnabled: { + label: "Native-like file search", + description: "When enabled, the file search will be done using the native Obsidian file search.", + infoSection: "native-like-file-search", + children: {} + }, + autoFileNodeEdgesFeatureEnabled: { + label: "Auto file node edges", + description: "Automatically create edges between file nodes based their frontmatter links.", + infoSection: "auto-file-node-edges", + children: { + autoFileNodeEdgesFrontmatterKey: { + label: "Frontmatter key name", + description: "The frontmatter key to fetch the outgoing edges from. (Keep the default to ensure best compatibility.)", + type: "text", + parse: (value) => value.trim() || "canvas-edges" + } + } + }, + portalsFeatureEnabled: { + label: "Portals", + description: "Create portals to other canvases.", + infoSection: "portals", + children: { + showEdgesIntoDisabledPortals: { + label: "Show edges into disabled portals", + description: "When enabled, edges into disabled portals will be shown.", + type: "boolean" + } + } + }, + collapsibleGroupsFeatureEnabled: { + label: "Collapsible groups", + description: "Group nodes can be collapsed and expanded to keep the canvas organized.", + infoSection: "collapsible-groups", + children: { + collapsedGroupPreviewOnDrag: { + label: "Collapsed group preview on drag", + description: "When enabled, a group that is collapsed show its border while dragging a node.", + type: "boolean" + } + } + }, + combineCustomStylesInDropdown: { + label: "Combine custom styles", + description: "Combine all style attributes of Advanced Canvas in a single dropdown.", + children: {} + }, + nodeStylingFeatureEnabled: { + label: "Node styling", + description: "Style your nodes with different shapes and borders.", + infoSection: "node-styles", + children: { + customNodeStyleAttributes: { + label: "Custom node style settings", + description: "Add custom style settings for nodes. (Go to GitHub for more information)", + type: "button", + onClick: () => { + const anchor = activeDocument.createElement("a"); + anchor.href = "https://github.com/Developer-Mike/obsidian-advanced-canvas/blob/main/README.md#custom-styles"; + anchor.target = "_blank"; + anchor.click(); + } + }, + defaultTextNodeColor: { + label: "Default text node color", + description: "The default color of a text node. The default range is from 0 to 6, where 0 is no color. The range can be extended by using the Custom Colors feature of Advanced Canvas.", + type: "number", + parse: (value) => Math.max(0, parseInt(value) || 0) + }, + defaultTextNodeStyleAttributes: { + label: "Default text node style attributes", + type: "styles", + getParameters(settingsManager) { + return [ + ...BUILTIN_NODE_STYLE_ATTRIBUTES, + /* BUILTINS */ + ...settingsManager.nodeCssStylesManager.getStyles(), + /* CUSTOM CSS STYLES */ + ...settingsManager.getSetting("customNodeStyleAttributes") + /* LEGACY CUSTOM STYLES */ + ].filter((setting) => { + var _a; + return setting.nodeTypes === void 0 || ((_a = setting.nodeTypes) == null ? void 0 : _a.includes("text")); + }); + } + } + } + }, + edgesStylingFeatureEnabled: { + label: "Edges styling", + description: "Style your edges with different path styles.", + infoSection: "edge-styles", + children: { + customEdgeStyleAttributes: { + label: "Custom edge style settings", + description: "Add custom style settings for edges. (Go to GitHub for more information)", + type: "button", + onClick: () => { + const anchor = activeDocument.createElement("a"); + anchor.href = "https://github.com/Developer-Mike/obsidian-advanced-canvas/blob/main/README.md#custom-styles"; + anchor.target = "_blank"; + anchor.click(); + } + }, + inheritEdgeColorFromNode: { + label: "Inherit edge color from node", + description: "When creating a new edge by dragging from a node, the edge will inherit the color of the node it is dragged from.", + type: "boolean" + }, + defaultEdgeColor: { + label: "Default edge color", + description: "The default color of an edge. The default range is from 0 to 6, where 0 is no color. The range can be extended by using the Custom Colors feature of Advanced Canvas.", + type: "number", + parse: (value) => Math.max(0, parseInt(value) || 0) + }, + defaultEdgeLineDirection: { + label: "Default edge line direction", + description: "The default line direction of an edge.", + type: "dropdown", + options: { + "nondirectional": "Nondirectional", + "unidirectional": "Unidirectional", + "bidirectional": "Bidirectional" + } + }, + defaultEdgeStyleAttributes: { + label: "Default edge style attributes", + type: "styles", + getParameters(settingsManager) { + return [ + ...BUILTIN_EDGE_STYLE_ATTRIBUTES, + /* BUILTINS */ + ...settingsManager.edgeCssStylesManager.getStyles(), + /* CUSTOM CSS STYLES */ + ...settingsManager.getSetting("customEdgeStyleAttributes") + /* LEGACY CUSTOM STYLES */ + ]; + } + }, + edgeStyleUpdateWhileDragging: { + label: "Update edge style while dragging (Can be very slow)", + description: "When enabled, the edge style will be updated while dragging an edge.", + type: "boolean" + }, + edgeStyleSquarePathRounded: { + label: "Square path rounded", + description: "When enabled, the square path's corners will be rounded.", + type: "boolean" + }, + edgeStylePathfinderAllowDiagonal: { + label: "A* allow diagonal", + description: "When enabled, the A* path style will allow diagonal paths.", + type: "boolean" + }, + edgeStylePathfinderPathRounded: { + label: "A* rounded path", + description: "When enabled, the A* path style will be rounded.", + type: "boolean" + } + } + }, + floatingEdgeFeatureEnabled: { + label: "Floating edges (auto edge side)", + description: "Floating edges are automatically placed on the most suitable side of the node.", + infoSection: "floating-edges-automatic-edge-side", + children: { + allowFloatingEdgeCreation: { + label: "Allow floating edges creation", + description: "Allow floating edges creation by dragging the edge over the target node without placing it over a specific side connection point. (If disabled, floating edges can only be created and used by other Advanced Canvas features.)", + type: "boolean" + }, + newEdgeFromSideFloating: { + label: "New edge from side floating", + description: 'When enabled, the "from" side of the edge will always be floating.', + type: "boolean" + } + } + }, + flipEdgeFeatureEnabled: { + label: "Flip edges", + description: "Flip the direction of edges using the popup menu.", + infoSection: "flip-edge", + children: {} + }, + presentationFeatureEnabled: { + label: "Presentations", + description: "Create a presentation from your canvas.", + infoSection: "presentation-mode", + children: { + showSetStartNodeInPopup: { + label: 'Show "Set Start Node" in node popup', + description: "If turned off, you can still set the start node using the corresponding command.", + type: "boolean" + }, + defaultSlideDimensions: { + label: "Default slide dimensions", + description: "The default dimensions of a slide.", + type: "dimension", + parse: (value) => { + const width = Math.max(1, parseInt(value[0]) || 0); + const height = Math.max(1, parseInt(value[1]) || 0); + return [width, height]; + } + }, + wrapInSlidePadding: { + label: "Wrap in slide padding", + description: "The padding of the slide when wrapping the canvas in a slide.", + type: "number", + parse: (value) => Math.max(0, parseInt(value) || 0) + }, + resetViewportOnPresentationEnd: { + label: "Reset viewport on presentation end", + description: "When enabled, the viewport will be reset to the original position after the presentation ends.", + type: "boolean" + }, + useArrowKeysToChangeSlides: { + label: "Use arrow keys to change slides", + description: "When enabled, you can use the arrow keys to change slides in presentation mode.", + type: "boolean" + }, + usePgUpPgDownKeysToChangeSlides: { + label: "Use PgUp/PgDown keys to change slides", + description: "When enabled, you can use the PgUp/PgDown keys to change slides in presentation mode (Makes the presentation mode compatible with most presentation remotes).", + type: "boolean" + }, + zoomToSlideWithoutPadding: { + label: "Zoom to slide without padding", + description: "When enabled, the canvas will zoom to the slide without padding.", + type: "boolean" + }, + useUnclampedZoomWhilePresenting: { + label: "Use unclamped zoom while presenting", + description: "When enabled, the zoom will not be clamped while presenting.", + type: "boolean" + }, + fullscreenPresentationEnabled: { + label: "Enter fullscreen while presenting", + description: "When enabled, presentations automatically request fullscreen. Disable to keep Obsidian windowed during presentations.", + type: "boolean" + }, + slideTransitionAnimationDuration: { + label: "Slide transition animation duration", + description: "The duration of the slide transition animation in seconds. Set to 0 to disable the animation.", + type: "number", + parse: (value) => Math.max(0, parseFloat(value) || 0) + }, + slideTransitionAnimationIntensity: { + label: "Slide transition animation intensity", + description: "The intensity of the slide transition animation. The higher the value, the more the canvas will zoom out before zooming in on the next slide.", + type: "number", + parse: (value) => Math.max(0, parseFloat(value) || 0) + } + } + }, + zOrderingControlFeatureEnabled: { + label: "Z ordering controls", + description: "Change the persistent z-index of nodes using the context menu.", + children: { + zOrderingControlShowOneLayerShiftOptions: { + label: "Show one layer shift options", + description: "When enabled, you can move nodes one layer forward or backward.", + type: "boolean" + } + } + }, + aspectRatioControlFeatureEnabled: { + label: "Aspect ratio control", + description: "Change the aspect ratio of nodes using the context menu.", + children: {} + }, + variableBreakpointFeatureEnabled: { + label: "Variable breakpoint", + description: `Change the zoom breakpoint (the zoom level at which the nodes won't render their content anymore) on a per-node basis using the ${VARIABLE_BREAKPOINT_CSS_VAR} CSS variable.`, + infoSection: "variable-breakpoints", + children: {} + }, + autoResizeNodeFeatureEnabled: { + label: "Auto resize node", + description: "Automatically resize the height of a node to fit the content.", + infoSection: "auto-node-resizing", + children: { + autoResizeNodeEnabledByDefault: { + label: "Enable auto resize by default", + description: "When enabled, the auto resize feature will be enabled by default for all nodes.", + type: "boolean" + }, + autoResizeNodeMaxHeight: { + label: "Max height", + description: "The maximum height of the node when auto resizing (-1 for unlimited).", + type: "number", + parse: (value) => { + var _a; + return Math.max(-1, (_a = parseInt(value)) != null ? _a : -1); + } + }, + autoResizeNodeSnapToGrid: { + label: "Snap to grid", + description: "When enabled, the height of the node will snap to the grid.", + type: "boolean" + } + } + }, + canvasEncapsulationEnabled: { + label: "Canvas encapsulation", + description: "Encapsulate a selection of nodes and edges into a new canvas using the context menu.", + infoSection: "encapsulate-selection", + children: {} + }, + betterReadonlyEnabled: { + label: "Better readonly", + description: "Improve the readonly mode.", + infoSection: "better-readonly", + children: { + hideBackgroundGridWhenInReadonly: { + label: "Hide background grid when in readonly", + description: "When enabled, the background grid will be hidden when in readonly mode.", + type: "boolean" + } + } + }, + edgeHighlightEnabled: { + label: "Edge highlight", + description: "Highlight outgoing (and optionally incoming) edges of a selected node.", + infoSection: "edge-highlight", + children: { + highlightIncomingEdges: { + label: "Highlight incoming edges", + description: "When enabled, incoming edges will also be highlighted.", + type: "boolean" + } + } + }, + edgeSelectionEnabled: { + label: "Edge selection", + description: "Select edges connected to the selected node(s) using the popup menu.", + infoSection: "edge-selection", + children: { + selectEdgeByDirection: { + label: "Select edge by direction", + description: "Select incoming or outgoing edges using separate popup menu items.", + type: "boolean" + } + } + }, + focusModeFeatureEnabled: { + label: "Focus mode", + description: "Focus on a single node and blur all other nodes.", + infoSection: "focus-mode", + children: {} + } +}; +var SettingsManager = class { + constructor(plugin) { + this.plugin = plugin; + this.nodeCssStylesManager = GET_NODE_CSS_STYLES_MANAGER(plugin); + this.edgeCssStylesManager = GET_EDGE_CSS_STYLES_MANAGER(plugin); + } + async loadSettings() { + this.settings = Object.assign({}, DEFAULT_SETTINGS_VALUES, await this.plugin.loadData()); + this.plugin.app.workspace.trigger("advanced-canvas:settings-changed"); + } + async saveSettings() { + await this.plugin.saveData(this.settings); + } + getSetting(key) { + return this.settings[key]; + } + async setSetting(data) { + this.settings = Object.assign(this.settings, data); + await this.saveSettings(); + this.plugin.app.workspace.trigger("advanced-canvas:settings-changed"); + } + addSettingsTab() { + this.settingsTab = new AdvancedCanvasPluginSettingTab(this.plugin, this); + this.plugin.addSettingTab(this.settingsTab); + } +}; +var AdvancedCanvasPluginSettingTab = class extends import_obsidian4.PluginSettingTab { + constructor(plugin, settingsManager) { + super(plugin.app, plugin); + this.settingsManager = settingsManager; + } + display() { + const { containerEl } = this; + containerEl.empty(); + containerEl.classList.add("ac-settings"); + this.createKofiBanner(containerEl); + for (const [headingId, heading] of Object.entries(SETTINGS)) { + this.createFeatureHeading( + containerEl, + heading.label, + heading.description, + heading.infoSection, + heading.disableToggle ? null : headingId + ); + const settingsHeaderChildrenContainerEl = containerEl.createDiv(); + settingsHeaderChildrenContainerEl.classList.add("settings-header-children"); + settingsHeaderChildrenContainerEl.createSpan(); + for (const [settingId, setting] of Object.entries(heading.children)) { + if (!(settingId in DEFAULT_SETTINGS_VALUES)) continue; + switch (setting.type) { + case "text": + this.createTextSetting(settingsHeaderChildrenContainerEl, settingId, setting); + break; + case "number": + this.createNumberSetting(settingsHeaderChildrenContainerEl, settingId, setting); + break; + case "dimension": + this.createDimensionSetting(settingsHeaderChildrenContainerEl, settingId, setting); + break; + case "boolean": + this.createBooleanSetting(settingsHeaderChildrenContainerEl, settingId, setting); + break; + case "dropdown": + this.createDropdownSetting(settingsHeaderChildrenContainerEl, settingId, setting); + break; + case "button": + this.createButtonSetting(settingsHeaderChildrenContainerEl, settingId, setting); + break; + case "styles": + this.createStylesSetting(settingsHeaderChildrenContainerEl, settingId, setting); + break; + } + } + } + } + createFeatureHeading(containerEl, label, description, infoSection, settingsKey) { + const setting = new import_obsidian4.Setting(containerEl).setHeading().setClass("ac-settings-heading").setName(label).setDesc(description); + if (infoSection !== void 0) { + setting.addExtraButton( + (button) => button.setTooltip("Open GitHub documentation").setIcon("info").onClick(async () => { + const anchor = activeDocument.createElement("a"); + anchor.href = `${README_URL}#${infoSection}`; + anchor.target = "_blank"; + anchor.click(); + }) + ); + } + if (settingsKey !== null) { + setting.addToggle( + (toggle) => toggle.setTooltip("Requires a reload to take effect.").setValue(this.settingsManager.getSetting(settingsKey)).onChange(async (value) => { + await this.settingsManager.setSetting({ [settingsKey]: value }); + new import_obsidian4.Notice("Reload obsidian to apply the changes."); + }) + ); + } + return setting; + } + createTextSetting(containerEl, settingId, setting) { + new import_obsidian4.Setting(containerEl).setName(setting.label).setDesc(setting.description).addText( + (text) => text.setValue(this.settingsManager.getSetting(settingId)).onChange(async (value) => { + await this.settingsManager.setSetting({ [settingId]: setting.parse ? setting.parse(value) : value }); + }) + ); + } + createNumberSetting(containerEl, settingId, setting) { + new import_obsidian4.Setting(containerEl).setName(setting.label).setDesc(setting.description).addText( + (text) => text.setValue(JSON.stringify(this.settingsManager.getSetting(settingId))).onChange(async (value) => { + await this.settingsManager.setSetting({ [settingId]: setting.parse(value) }); + }) + ); + } + createDimensionSetting(containerEl, settingId, setting) { + let text1; + let text2; + new import_obsidian4.Setting(containerEl).setName(setting.label).setDesc(setting.description).addText((text) => { + text1 = text.setValue(this.settingsManager.getSetting(settingId)[0].toString()).onChange(async (value) => await this.settingsManager.setSetting({ [settingId]: setting.parse([value, text2.getValue()]) })); + }).addText((text) => { + text2 = text.setValue(this.settingsManager.getSetting(settingId)[1].toString()).onChange(async (value) => await this.settingsManager.setSetting({ [settingId]: setting.parse([text1.getValue(), value]) })); + }); + } + createBooleanSetting(containerEl, settingId, setting) { + new import_obsidian4.Setting(containerEl).setName(setting.label).setDesc(setting.description).addToggle( + (toggle) => toggle.setValue(this.settingsManager.getSetting(settingId)).onChange(async (value) => { + await this.settingsManager.setSetting({ [settingId]: value }); + }) + ); + } + createDropdownSetting(containerEl, settingId, setting) { + new import_obsidian4.Setting(containerEl).setName(setting.label).setDesc(setting.description).addDropdown( + (dropdown) => dropdown.addOptions(setting.options).setValue(this.settingsManager.getSetting(settingId)).onChange(async (value) => { + await this.settingsManager.setSetting({ [settingId]: value }); + }) + ); + } + createButtonSetting(containerEl, settingId, setting) { + new import_obsidian4.Setting(containerEl).setName(setting.label).setDesc(setting.description).addButton( + (button) => button.setButtonText("Open").onClick(() => setting.onClick()) + ); + } + createStylesSetting(containerEl, settingId, setting) { + const nestedContainerEl = containerEl.createEl("details"); + nestedContainerEl.classList.add("setting-item"); + const summaryEl = nestedContainerEl.createEl("summary"); + summaryEl.textContent = setting.label; + for (const styleAttribute of setting.getParameters(this.settingsManager)) { + new import_obsidian4.Setting(nestedContainerEl).setName(styleAttribute.label).addDropdown( + (dropdown) => { + var _a; + return dropdown.addOptions(Object.fromEntries(styleAttribute.options.map((option) => [option.value, option.value === null ? `${option.label} (default)` : option.label]))).setValue((_a = this.settingsManager.getSetting(settingId)[styleAttribute.key]) != null ? _a : "null").onChange(async (value) => { + const newValue = this.settingsManager.getSetting(settingId); + if (value === "null") delete newValue[styleAttribute.key]; + else newValue[styleAttribute.key] = value; + await this.settingsManager.setSetting({ + [settingId]: newValue + }); + }); + } + ); + } + } + createKofiBanner(containerEl) { + const banner = containerEl.createDiv(); + banner.classList.add("kofi-banner"); + const title = banner.createSpan(); + title.classList.add("ac-kofi-banner-title"); + title.textContent = "Support the development of Advanced Canvas"; + const koFiButton = banner.createEl("a"); + koFiButton.classList.add("ac-kofi-button"); + koFiButton.href = KOFI_PAGE_URL; + koFiButton.target = "_blank"; + const koFiImage = koFiButton.createEl("img"); + koFiImage.src = KOFI_BADGE_URI; + } +}; + +// src/managers/windows-manager.ts +var WindowsManager = class { + constructor(plugin) { + this.windows = [window]; + this.plugin = plugin; + this.plugin.registerEvent(this.plugin.app.workspace.on( + "window-open", + (_win, window2) => this.windows.push(window2) + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "window-close", + (_win, window2) => this.windows = this.windows.filter((w) => w !== window2) + )); + } +}; + +// src/patchers/canvas-patcher.ts +var import_view = require("@codemirror/view"); + +// node_modules/monkey-around/mjs/index.js +function around(obj, factories) { + const removers = Object.keys(factories).map((key) => around1(obj, key, factories[key])); + return removers.length === 1 ? removers[0] : function() { + removers.forEach((r) => r()); + }; +} +function around1(obj, method, createWrapper) { + const original = obj[method], hadOwn = obj.hasOwnProperty(method); + let current = createWrapper(original); + if (original) + Object.setPrototypeOf(current, original); + Object.setPrototypeOf(wrapper, current); + obj[method] = wrapper; + return remove; + function wrapper(...args) { + if (current === original && obj[method] === wrapper) + remove(); + return current.apply(this, args); + } + function remove() { + if (obj[method] === wrapper) { + if (hadOwn) + obj[method] = original; + else + delete obj[method]; + } + if (current === original) + return; + current = original; + Object.setPrototypeOf(wrapper, original || Function); + } +} + +// src/patchers/canvas-patcher.ts +var import_obsidian5 = require("obsidian"); + +// node_modules/tiny-jsonc/dist/index.js +var stringOrCommentRe = /("(?:\\?[^])*?")|(\/\/.*)|(\/\*[^]*?\*\/)/g; +var stringOrTrailingCommaRe = /("(?:\\?[^])*?")|(,\s*)(?=]|})/g; +var JSONC = { + parse: (text) => { + text = String(text); + try { + return JSON.parse(text); + } catch (e) { + return JSON.parse(text.replace(stringOrCommentRe, "$1").replace(stringOrTrailingCommaRe, "$1")); + } + } +}; +var dist_default = JSONC; + +// src/patchers/patcher.ts +var Patcher = class _Patcher { + constructor(plugin) { + this.plugin = plugin; + void this.patch(); + } + static async waitForMapValueLookup(map, viewType, patch) { + return new Promise((resolve) => { + const uninstaller = around(map, { + [viewType]: (next) => function(...args) { + const view = next.call(this, ...args); + patch(view); + const patchedView = next.call(this, ...args); + uninstaller(); + resolve(patchedView); + return patchedView; + } + }); + }); + } + static async waitForViewRequest(plugin, viewType, patch) { + return this.waitForMapValueLookup(plugin.app.viewRegistry.viewByType, viewType, patch); + } + static OverrideExisting(fn) { + return Object.assign(fn, { __overrideExisting: true }); + } + static patchThisAndPrototype(plugin, object, patches, uninstallers) { + _Patcher.patch(plugin, object, patches, false, uninstallers); + return _Patcher.patchPrototype(plugin, object, patches, uninstallers); + } + static patchPrototype(plugin, target, patches, uninstallers) { + return _Patcher.patch(plugin, target, patches, true, uninstallers); + } + static patch(plugin, object, patches, prototype = false, uninstallers) { + if (!object) return null; + const target = prototype ? object.constructor.prototype : object; + for (const key of Object.keys(patches)) { + const patch = patches[key]; + if (patch == null ? void 0 : patch.__overrideExisting) { + if (typeof target[key] !== "function") + throw new Error(`Method ${String(key)} does not exist on target`); + } + } + const uninstaller = around(target, patches); + if (uninstallers) uninstallers.push(uninstaller); + plugin.register(uninstaller); + return object; + } + static async patchOnce(plugin, object, patches) { + const uninstallers = []; + const value = await new Promise( + (resolve) => this.patch(plugin, object, patches(resolve), false, uninstallers) + ); + for (const uninstall of uninstallers) uninstall(); + return value; + } + static tryPatchWorkspacePrototype(plugin, getTarget, patches, uninstallers) { + return new Promise((resolve) => { + const result = _Patcher.patchPrototype(plugin, getTarget(), patches, uninstallers); + if (result) { + resolve(result); + return; + } + const listener = plugin.app.workspace.on("layout-change", () => { + const result2 = _Patcher.patchPrototype(plugin, getTarget(), patches, uninstallers); + if (result2) { + plugin.app.workspace.offref(listener); + resolve(result2); + } + }); + plugin.registerEvent(listener); + }); + } +}; + +// src/utils/migration-helper.ts +var CURRENT_SPEC_VERSION = "1.0-1.0"; +var _MigrationHelper = class _MigrationHelper { + static needsMigration(canvas) { + var _a; + return ((_a = canvas.metadata) == null ? void 0 : _a.version) !== CURRENT_SPEC_VERSION; + } + static migrate(canvas) { + var _a, _b; + let version = (_b = (_a = canvas.metadata) == null ? void 0 : _a.version) != null ? _b : "undefined"; + if (version === CURRENT_SPEC_VERSION) return canvas; + while (version !== CURRENT_SPEC_VERSION) { + const migrationFunction = _MigrationHelper.MIGRATIONS[version]; + if (!migrationFunction) { + console.error(`No migration function found for version ${version}. Critical error!`); + break; + } + const { version: newVersion, canvas: migratedCanvas } = migrationFunction(canvas); + version = newVersion; + canvas = migratedCanvas; + if (!canvas.metadata) canvas.metadata = { version, frontmatter: {} }; + else canvas.metadata.version = version; + } + return canvas; + } +}; +_MigrationHelper.MIGRATIONS = { + undefined: (canvas) => { + var _a, _b, _c, _d; + const TARGET_SPEC_VERSION = "1.0-1.0"; + let startNode; + const globalInterdimensionalEdges = {}; + for (const node of (_a = canvas.nodes) != null ? _a : []) { + node.dynamicHeight = node.autoResizeHeight; + delete node.autoResizeHeight; + node.ratio = node.sideRatio; + delete node.sideRatio; + node.collapsed = node.isCollapsed; + delete node.isCollapsed; + if (node.portalToFile) { + node.portal = true; + delete node.portalToFile; + } + if (node.isStartNode) { + startNode = node.id; + delete node.isStartNode; + } + if (node.edgesToNodeFromPortal) { + const edgesToNodeFromPortal = node.edgesToNodeFromPortal; + for (const [portalId, edges] of Object.entries(edgesToNodeFromPortal)) { + if (!(portalId in globalInterdimensionalEdges)) globalInterdimensionalEdges[portalId] = []; + for (const edge of edges) { + if (edge.fromNode !== node.id) edge.fromNode = `${portalId}-${edge.fromNode}`; + if (edge.toNode !== node.id) edge.toNode = `${portalId}-${edge.toNode}`; + } + (_b = globalInterdimensionalEdges[portalId]) == null ? void 0 : _b.push(...edges); + } + delete node.edgesToNodeFromPortal; + } + } + for (const node of (_c = canvas.nodes) != null ? _c : []) { + if (!(node.id in globalInterdimensionalEdges)) continue; + node.interdimensionalEdges = globalInterdimensionalEdges[node.id]; + } + (_d = canvas.metadata) != null ? _d : canvas.metadata = { + version: TARGET_SPEC_VERSION, + frontmatter: {}, + startNode + }; + return { version: TARGET_SPEC_VERSION, canvas }; + } +}; +var MigrationHelper = _MigrationHelper; + +// src/patchers/canvas-patcher.ts +var CanvasPatcher = class extends Patcher { + async patch() { + const loadedCanvasViewLeafs = this.plugin.app.workspace.getLeavesOfType("canvas").filter((leaf) => !(0, import_obsidian5.requireApiVersion)("1.7.2") || !leaf.isDeferred); + if (loadedCanvasViewLeafs.length > 0) { + console.debug(`Patching and reloading loaded canvas views (Count: ${loadedCanvasViewLeafs.length})`); + this.patchCanvas(loadedCanvasViewLeafs.first().view); + for (const leaf of loadedCanvasViewLeafs) leaf.rebuildView(); + } else { + await Patcher.waitForViewRequest(this.plugin, "canvas", (view) => this.patchCanvas(view)); + console.debug(`Patched canvas view on first request`); + } + } + patchCanvas(view) { + const that = this; + Patcher.patchPrototype(this.plugin, view, { + setEphemeralState: Patcher.OverrideExisting((next) => function(state) { + var _a, _b, _c; + if (state == null ? void 0 : state.subpath) { + const nodeId = state.subpath.replace(/^#/, ""); + const node = this.canvas.nodes.get(nodeId); + if (node) { + this.canvas.selectOnly(node); + this.canvas.zoomToSelection(); + return; + } + } + if (((_b = (_a = state.match) == null ? void 0 : _a.matches) == null ? void 0 : _b[0]) && !((_c = state.match) == null ? void 0 : _c.nodeId)) { + const match = state.match.matches[0]; + const elementType = match[0] === 0 ? "nodes" : "edges"; + const elementIndex = match[1]; + const element = elementType === "nodes" ? Array.from(this.canvas.nodes.values())[elementIndex] : Array.from(this.canvas.edges.values())[elementIndex]; + if (element) { + this.canvas.selectOnly(element); + this.canvas.zoomToSelection(); + return; + } + } + return next.call(this, state); + }), + setViewData: Patcher.OverrideExisting((next) => function(json, ...args) { + json = json !== "" ? json : '{"nodes": [], "edges": []}'; + try { + const canvasData = dist_default.parse(json); + if (MigrationHelper.needsMigration(canvasData)) { + if (this.file) that.plugin.createFileSnapshot(this.file.path, json); + json = JSON.stringify(MigrationHelper.migrate(canvasData)); + } + } catch (e) { + console.error("Failed to migrate canvas data:", e); + } + let result; + try { + result = next.call(this, json, ...args); + } catch (e) { + console.error("Invalid JSON, repairing through Advanced Canvas:", e); + if (this.file) that.plugin.createFileSnapshot(this.file.path, json); + json = JSON.stringify(dist_default.parse(json), null, 2); + result = next.call(this, json, ...args); + } + that.plugin.app.workspace.trigger("advanced-canvas:canvas-changed", this.canvas); + return result; + }), + close: Patcher.OverrideExisting((next) => function(...args) { + that.plugin.app.workspace.trigger("advanced-canvas:canvas-view-unloaded:before", this); + return next.call(this, ...args); + }) + }); + Patcher.patchPrototype(this.plugin, view.canvas, { + markViewportChanged: Patcher.OverrideExisting((next) => function(...args) { + that.plugin.app.workspace.trigger("advanced-canvas:viewport-changed:before", this); + const result = next.call(this, ...args); + that.plugin.app.workspace.trigger("advanced-canvas:viewport-changed:after", this); + return result; + }), + markMoved: Patcher.OverrideExisting((next) => function(node) { + const result = next.call(this, node); + if (!this.viewportChanged) { + if (node.prevX !== node.x || node.prevY !== node.y) + that.plugin.app.workspace.trigger("advanced-canvas:node-moved", this, node, !this.isDragging); + if (node.prevWidth !== node.width || node.prevHeight !== node.height) + that.plugin.app.workspace.trigger("advanced-canvas:node-resized", this, node); + } + node.prevX = node.x; + node.prevY = node.y; + node.prevWidth = node.width; + node.prevHeight = node.height; + return result; + }), + onDoubleClick: Patcher.OverrideExisting((next) => function(event) { + const preventDefault = { value: false }; + that.plugin.app.workspace.trigger("advanced-canvas:double-click", this, event, preventDefault); + if (!preventDefault.value) next.call(this, event); + }), + setDragging: Patcher.OverrideExisting((next) => function(dragging) { + const result = next.call(this, dragging); + that.plugin.app.workspace.trigger("advanced-canvas:dragging-state-changed", this, dragging); + return result; + }), + // OBSIDIAN-FIX + cloneData: Patcher.OverrideExisting((next) => function(elements, shift) { + const result = next.call(this, elements, shift); + elements.nodes = elements.nodes.map((nodeData) => JSON.parse(JSON.stringify(nodeData))); + elements.edges = elements.edges.map((edgeData) => JSON.parse(JSON.stringify(edgeData))); + return result; + }), + getContainingNodes: Patcher.OverrideExisting((next) => function(bbox) { + const result = next.call(this, bbox); + that.plugin.app.workspace.trigger("advanced-canvas:containing-nodes-requested", this, bbox, result); + return result; + }), + updateSelection: Patcher.OverrideExisting((next) => function(update) { + const oldSelection = new Set(this.selection); + const result = next.call(this, update); + that.plugin.app.workspace.trigger("advanced-canvas:selection-changed", this, oldSelection, (update2) => next.call(this, update2)); + return result; + }), + createTextNode: Patcher.OverrideExisting((next) => function(...args) { + const node = next.call(this, ...args); + that.plugin.app.workspace.trigger("advanced-canvas:node-created", this, node); + return node; + }), + createFileNode: Patcher.OverrideExisting((next) => function(...args) { + const node = next.call(this, ...args); + that.plugin.app.workspace.trigger("advanced-canvas:node-created", this, node); + return node; + }), + createFileNodes: Patcher.OverrideExisting((next) => function(...args) { + const nodes = next.call(this, ...args); + nodes.forEach((node) => that.plugin.app.workspace.trigger("advanced-canvas:node-created", this, node)); + return nodes; + }), + createGroupNode: Patcher.OverrideExisting((next) => function(...args) { + const node = next.call(this, ...args); + that.plugin.app.workspace.trigger("advanced-canvas:node-created", this, node); + return node; + }), + createLinkNode: Patcher.OverrideExisting((next) => function(...args) { + const node = next.call(this, ...args); + that.plugin.app.workspace.trigger("advanced-canvas:node-created", this, node); + return node; + }), + addNode: Patcher.OverrideExisting((next) => function(node) { + that.patchNode(node); + return next.call(this, node); + }), + addEdge: Patcher.OverrideExisting((next) => function(edge) { + that.patchEdge(edge); + if (!this.viewportChanged) that.plugin.app.workspace.trigger("advanced-canvas:edge-created", this, edge); + return next.call(this, edge); + }), + removeNode: Patcher.OverrideExisting((next) => function(node) { + const result = next.call(this, node); + if (!this.isClearing) that.plugin.app.workspace.trigger("advanced-canvas:node-removed", this, node); + return result; + }), + removeEdge: Patcher.OverrideExisting((next) => function(edge) { + const result = next.call(this, edge); + if (!this.isClearing) that.plugin.app.workspace.trigger("advanced-canvas:edge-removed", this, edge); + return result; + }), + handleCopy: Patcher.OverrideExisting((next) => function(...args) { + this.isCopying = true; + const result = next.call(this, ...args); + this.isCopying = false; + return result; + }), + handlePaste: Patcher.OverrideExisting((next) => function(...args) { + this.isPasting = true; + const result = next.call(this, ...args); + this.isPasting = false; + return result; + }), + getSelectionData: Patcher.OverrideExisting((next) => function(...args) { + const result = next.call(this, ...args); + if (this.isCopying) that.plugin.app.workspace.trigger("advanced-canvas:copy", this, result); + return result; + }), + zoomToBbox: Patcher.OverrideExisting((next) => function(bbox) { + that.plugin.app.workspace.trigger("advanced-canvas:zoom-to-bbox:before", this, bbox); + const result = next.call(this, bbox); + that.plugin.app.workspace.trigger("advanced-canvas:zoom-to-bbox:after", this, bbox); + return result; + }), + // Custom + zoomToRealBbox: (_next) => function(bbox) { + if (this.canvasRect.width === 0 || this.canvasRect.height === 0) return; + that.plugin.app.workspace.trigger("advanced-canvas:zoom-to-bbox:before", this, bbox); + const widthZoom = this.canvasRect.width / (bbox.maxX - bbox.minX); + const heightZoom = this.canvasRect.height / (bbox.maxY - bbox.minY); + const zoom = this.screenshotting ? Math.min(widthZoom, heightZoom) : Math.clamp(Math.min(widthZoom, heightZoom), -4, 1); + this.tZoom = Math.log2(zoom); + this.zoomCenter = null; + this.tx = (bbox.minX + bbox.maxX) / 2; + this.ty = (bbox.minY + bbox.maxY) / 2; + this.markViewportChanged(); + that.plugin.app.workspace.trigger("advanced-canvas:zoom-to-bbox:after", this, bbox); + }, + setReadonly: Patcher.OverrideExisting((next) => function(readonly) { + const result = next.call(this, readonly); + that.plugin.app.workspace.trigger("advanced-canvas:readonly-changed", this, readonly); + return result; + }), + undo: Patcher.OverrideExisting((next) => function(...args) { + const result = next.call(this, ...args); + this.importData(this.getData(), true); + that.plugin.app.workspace.trigger("advanced-canvas:undo", this); + return result; + }), + redo: Patcher.OverrideExisting((next) => function(...args) { + const result = next.call(this, ...args); + this.importData(this.getData(), true); + that.plugin.app.workspace.trigger("advanced-canvas:redo", this); + return result; + }), + clear: Patcher.OverrideExisting((next) => function(...args) { + this.isClearing = true; + const result = next.call(this, ...args); + this.isClearing = false; + return result; + }), + /*setData: Patcher.OverrideExisting(next => function (...args: any): void { + // + const result = next.call(this, ...args) + // + return result + }),*/ + getData: Patcher.OverrideExisting((next) => function(...args) { + const result = next.call(this, ...args); + that.plugin.app.workspace.trigger("advanced-canvas:data-requested", this, result); + return result; + }), + importData: Patcher.OverrideExisting((next) => function(data, clearCanvas, silent) { + var _a; + const targetFilePath = (_a = this.view.file) == null ? void 0 : _a.path; + const setData = (data2) => { + if (!this.view.file || this.view.file.path !== targetFilePath) return; + this.importData(data2, true, true); + }; + if (!silent) that.plugin.app.workspace.trigger("advanced-canvas:data-loaded:before", this, data, setData); + const result = next.call(this, data, clearCanvas); + if (!silent) that.plugin.app.workspace.trigger("advanced-canvas:data-loaded:after", this, data, setData); + return result; + }), + requestSave: Patcher.OverrideExisting((next) => function(...args) { + that.plugin.app.workspace.trigger("advanced-canvas:canvas-saved:before", this); + const result = next.call(this, ...args); + that.plugin.app.workspace.trigger("advanced-canvas:canvas-saved:after", this); + return result; + }) + }); + Patcher.patchPrototype(this.plugin, view.canvas.menu, { + render: Patcher.OverrideExisting((next) => function(...args) { + const result = next.call(this, ...args); + that.plugin.app.workspace.trigger("advanced-canvas:popup-menu-created", this.canvas); + next.call(this); + return result; + }) + }); + Patcher.patchPrototype(this.plugin, view.canvas.nodeInteractionLayer, { + setTarget: Patcher.OverrideExisting((next) => function(node) { + const result = next.call(this, node); + that.plugin.app.workspace.trigger("advanced-canvas:node-interaction", this.canvas, node); + return result; + }) + }); + this.plugin.registerEditorExtension([import_view.EditorView.updateListener.of((update) => { + if (!update.docChanged) return; + const editor = update.state.field(import_obsidian5.editorInfoField); + const node = editor.node; + if (!node) return; + that.plugin.app.workspace.trigger("advanced-canvas:node-text-content-changed", node.canvas, node, update); + })]); + } + patchNode(node) { + const that = this; + Patcher.patch(this.plugin, node, { + setData: Patcher.OverrideExisting((next) => function(data, addHistory) { + const result = next.call(this, data); + if (node.initialized && !node.isDirty) { + node.isDirty = true; + that.plugin.app.workspace.trigger("advanced-canvas:node-changed", this.canvas, node); + delete node.isDirty; + } + this.canvas.data = this.canvas.getData(); + if (this.initialized) this.canvas.view.requestSave(); + if (addHistory) this.canvas.pushHistory(this.canvas.data); + return result; + }), + setZIndex: (_next) => function(value) { + this.setData({ + ...this.getData(), + zIndex: value + }, true); + this.updateZIndex(); + }, + updateZIndex: Patcher.OverrideExisting((next) => function() { + const persistentZIndex = this.getData().zIndex; + if (persistentZIndex === void 0) return next.call(this); + this.canvas.zIndexCounter = Math.max(this.canvas.zIndexCounter, persistentZIndex); + this.renderZIndex(); + }), + renderZIndex: Patcher.OverrideExisting((next) => function() { + const persistentZIndex = this.getData().zIndex; + if (persistentZIndex === void 0) return next.call(this); + this.zIndex = persistentZIndex; + if (this.canvas.selection.size === 1 && this.canvas.selection.has(this)) + this.zIndex = this.canvas.zIndexCounter + 1; + this.nodeEl.style.zIndex = this.zIndex.toString(); + }), + setIsEditing: Patcher.OverrideExisting((next) => function(editing, ...args) { + const result = next.call(this, editing, ...args); + that.plugin.app.workspace.trigger("advanced-canvas:node-editing-state-changed", this.canvas, node, editing); + return result; + }), + updateBreakpoint: Patcher.OverrideExisting((next) => function(breakpoint) { + const breakpointRef = { value: breakpoint }; + that.plugin.app.workspace.trigger("advanced-canvas:node-breakpoint-changed", this.canvas, node, breakpointRef); + return next.call(this, breakpointRef.value); + }), + getBBox: Patcher.OverrideExisting((next) => function(...args) { + const result = next.call(this, ...args); + that.plugin.app.workspace.trigger("advanced-canvas:node-bbox-requested", this.canvas, node, result); + return result; + }), + onConnectionPointerdown: Patcher.OverrideExisting((next) => function(e, side) { + const addEdgeEventRef = that.plugin.app.workspace.on("advanced-canvas:edge-added", (_canvas, edge) => { + that.plugin.app.workspace.trigger("advanced-canvas:edge-connection-dragging:before", this.canvas, edge, e, true, "to"); + that.plugin.app.workspace.offref(addEdgeEventRef); + activeDocument.addEventListener("pointerup", (e2) => { + that.plugin.app.workspace.trigger("advanced-canvas:edge-connection-dragging:after", this.canvas, edge, e2, true, "to"); + }, { once: true }); + }); + const result = next.call(this, e, side); + return result; + }), + // File nodes + setFile: (next) => function(...args) { + const result = next.call(this, ...args); + that.plugin.app.workspace.trigger("advanced-canvas:node-changed", this.canvas, this); + return result; + }, + setFilePath: (next) => function(...args) { + const result = next.call(this, ...args); + that.plugin.app.workspace.trigger("advanced-canvas:node-changed", this.canvas, this); + return result; + } + }); + this.runAfterInitialized(node, () => { + this.plugin.app.workspace.trigger("advanced-canvas:node-added", node.canvas, node); + this.plugin.app.workspace.trigger("advanced-canvas:node-changed", node.canvas, node); + }); + } + patchEdge(edge) { + const that = this; + Patcher.patch(this.plugin, edge, { + setData: Patcher.OverrideExisting((next) => function(data, addHistory) { + const result = next.call(this, data); + if (this.initialized && !this.isDirty) { + this.isDirty = true; + that.plugin.app.workspace.trigger("advanced-canvas:edge-changed", this.canvas, this); + delete this.isDirty; + } + this.canvas.data = this.canvas.getData(); + if (this.initialized) this.canvas.view.requestSave(); + if (addHistory) this.canvas.pushHistory(this.canvas.getData()); + return result; + }), + render: Patcher.OverrideExisting((next) => function(...args) { + const result = next.call(this, ...args); + that.plugin.app.workspace.trigger("advanced-canvas:edge-changed", this.canvas, this); + return result; + }), + getCenter: Patcher.OverrideExisting((next) => function(...args) { + const result = next.call(this, ...args); + that.plugin.app.workspace.trigger("advanced-canvas:edge-center-requested", this.canvas, this, result); + return result; + }), + onConnectionPointerdown: Patcher.OverrideExisting((next) => function(e) { + const cancelRef = { value: false }; + that.plugin.app.workspace.trigger("advanced-canvas:edge-connection-try-dragging:before", this.canvas, this, e, cancelRef); + if (cancelRef.value) return; + const previousEnds = { from: this.from, to: this.to }; + const result = next.call(this, e); + const eventPos = this.canvas.posFromEvt(e); + const fromPos = BBoxHelper.getCenterOfBBoxSide(this.from.node.getBBox(), this.from.side); + const toPos = BBoxHelper.getCenterOfBBoxSide(this.to.node.getBBox(), this.to.side); + const draggingSide = Math.hypot(eventPos.x - fromPos.x, eventPos.y - fromPos.y) > Math.hypot(eventPos.x - toPos.x, eventPos.y - toPos.y) ? "to" : "from"; + that.plugin.app.workspace.trigger("advanced-canvas:edge-connection-dragging:before", this.canvas, this, e, false, draggingSide, previousEnds); + activeDocument.addEventListener("pointerup", (e2) => { + that.plugin.app.workspace.trigger("advanced-canvas:edge-connection-dragging:after", this.canvas, this, e2, false, draggingSide, previousEnds); + }, { once: true }); + return result; + }) + }); + this.runAfterInitialized(edge, () => { + this.plugin.app.workspace.trigger("advanced-canvas:edge-added", edge.canvas, edge); + }); + } + runAfterInitialized(canvasElement, onReady) { + if (canvasElement.initialized) { + onReady(); + return; + } + const that = this; + const uninstall = around(canvasElement, { + initialize: (next) => function(...args) { + const result = next.call(this, ...args); + onReady(); + uninstall(); + return result; + } + }); + that.plugin.register(uninstall); + } +}; + +// src/patchers/link-suggestions-patcher.ts +var import_obsidian6 = require("obsidian"); +var LinkSuggestionsPatcher = class extends Patcher { + async patch() { + var _a; + if (!this.plugin.settings.getSetting("enableSingleNodeLinks")) return; + const suggestManager = (_a = this.plugin.app.workspace.editorSuggest.suggests.find((s) => s.suggestManager)) == null ? void 0 : _a.suggestManager; + if (!suggestManager) return console.warn("LinkSuggestionsPatcher: No suggest manager found."); + Patcher.patchThisAndPrototype(this.plugin, suggestManager, { + getHeadingSuggestions: Patcher.OverrideExisting((next) => async function(context, path, subpath) { + const result = await next.call(this, context, path, subpath); + if (!path.endsWith(".canvas")) return result; + const currentFilePath = this.getSourcePath(); + const targetFile = this.app.metadataCache.getFirstLinkpathDest(path, currentFilePath); + if (!targetFile) return result; + if (!(targetFile instanceof import_obsidian6.TFile) || targetFile.extension !== "canvas") + return result; + const fileCache = this.app.metadataCache.getFileCache(targetFile); + if (!fileCache) return result; + const canvasNodeCaches = fileCache.nodes; + if (!canvasNodeCaches) return result; + for (const nodeId of Object.keys(canvasNodeCaches)) { + if (nodeId === subpath) continue; + const suggestion = { + file: targetFile, + heading: nodeId, + level: 1, + matches: [], + path, + subpath: `#${nodeId}`, + score: 0, + type: "heading" + }; + result.push(suggestion); + } + return result; + }) + }); + } +}; + +// src/advanced-canvas-embed.ts +var import_obsidian7 = require("obsidian"); +var AdvancedCanvasEmbed = class extends import_obsidian7.Component { + constructor(context, file, subpath) { + super(); + this.onModifyCallback = (file) => { + if (file.path !== this.file.path) return; + void this.loadFile(); + }; + this.context = context; + this.file = file; + this.subpath = subpath; + if (!subpath) console.warn("AdvancedCanvasEmbed: No subpath provided. This embed will not work as expected."); + } + onload() { + this.context.app.vault.on("modify", this.onModifyCallback); + } + onunload() { + this.context.app.vault.off("modify", this.onModifyCallback); + } + async loadFile() { + if (!this.subpath) return; + const nodeId = this.subpath.replace(/^#/, ""); + const canvasContent = await this.context.app.vault.cachedRead(this.file); + if (!canvasContent) return console.warn("AdvancedCanvasEmbed: No canvas content found."); + const canvasJson = JSON.parse(canvasContent); + const canvasNode = canvasJson.nodes.find((node) => node.id === nodeId); + if (!canvasNode) { + this.context.containerEl.classList.add("mod-empty"); + this.context.containerEl.textContent = "Node not found"; + return; + } + let nodeContent = ""; + if (canvasNode.type === "text") nodeContent = canvasNode.text; + else if (canvasNode.type === "group") nodeContent = `**Group Node:** ${canvasNode.label}`; + else if (canvasNode.type === "file") nodeContent = `**File Node:** ${canvasNode.file}`; + this.context.containerEl.empty(); + this.context.containerEl.classList.add("ac-card-embed"); + if (canvasNode.color !== void 0) { + this.context.containerEl.classList.add("is-themed"); + this.context.containerEl.classList.add(`mod-canvas-color-${canvasNode.color}`); + } + const markdownContainer = this.context.containerEl.createSpan(); + markdownContainer.classList.add("markdown-content"); + await import_obsidian7.MarkdownRenderer.render(this.context.app, nodeContent, markdownContainer, this.file.path, this); + } +}; + +// src/patchers/embed-patcher.ts +var EmbedPatcher = class extends Patcher { + async patch() { + if (!this.plugin.settings.getSetting("enableSingleNodeLinks")) return; + Patcher.patch(this.plugin, this.plugin.app.embedRegistry.embedByExtension, { + canvas: (next) => function(context, file, subpath) { + if (subpath) return new AdvancedCanvasEmbed(context, file, subpath); + return next.call(this, context, file, subpath); + } + }); + } +}; + +// src/patchers/metadata-cache-patcher.ts +var import_obsidian8 = require("obsidian"); + +// src/utils/filepath-helper.ts +var FilepathHelper = class { + static extension(path) { + var _a; + return (path == null ? void 0 : path.includes(".")) ? (_a = path == null ? void 0 : path.split(".")) == null ? void 0 : _a.pop() : void 0; + } +}; + +// src/utils/hash-helper.ts +var HashHelper = class _HashHelper { + static async getBytesHash(bytes) { + const cryptoBytes = await crypto.subtle.digest("SHA-256", new Uint8Array(bytes)); + return _HashHelper.arrayBufferToHexString(cryptoBytes); + } + static arrayBufferToHexString(buffer) { + const uint8Array = new Uint8Array(buffer); + const hexArray = []; + for (const byte of uint8Array) { + hexArray.push((byte >>> 4).toString(16)); + hexArray.push((byte & 15).toString(16)); + } + return hexArray.join(""); + } +}; + +// src/utils/task-queue.ts +var TaskQueue = class { + constructor() { + this.running = false; + this.queue = []; + this.onFinished = () => { + }; + } + async add(task) { + return new Promise((resolve) => { + this.queue.push([resolve, task]); + if (!this.running) void this.run(); + }); + } + setOnFinished(callback) { + this.onFinished = callback; + } + async run() { + var _a; + this.running = true; + while (this.queue.length > 0) { + const [resolver, task] = (_a = this.queue.shift()) != null ? _a : [void 0, void 0]; + await (task == null ? void 0 : task()); + resolver == null ? void 0 : resolver(); + } + this.running = false; + this.onFinished(); + } +}; + +// src/patchers/metadata-cache-patcher.ts +var MetadataCachePatcher = class extends Patcher { + async patch() { + if (!this.plugin.settings.getSetting("canvasMetadataCompatibilityEnabled")) return; + Patcher.patchPrototype(this.plugin, this.plugin.app.metadataCache, { + getCache: Patcher.OverrideExisting((next) => function(filepath, ...args) { + var _a; + if (FilepathHelper.extension(filepath) === "canvas") { + if (!Object.prototype.hasOwnProperty.call(this.fileCache, filepath)) + return null; + const hash = (_a = this.fileCache[filepath]) == null ? void 0 : _a.hash; + return hash && this.metadataCache[hash] || null; + } + return next.call(this, filepath, ...args); + }), + computeFileMetadataAsync: Patcher.OverrideExisting((next) => async function(file, ...args) { + if (file instanceof import_obsidian8.TFile && (file == null ? void 0 : file.extension) === "canvas") + return CanvasMetadataHandler.computeCanvasFileMetadataAsync.call(this, file); + return next.call(this, file, ...args); + }), + resolveLinks: Patcher.OverrideExisting((next) => async function(filepath) { + const result = next.call(this, filepath); + if (FilepathHelper.extension(filepath) === "canvas") + await CanvasMetadataHandler.resolveCanvasLinks.call(this, filepath); + return result; + }) + }); + } +}; +var _CanvasMetadataHandler = class _CanvasMetadataHandler { + static async computeCanvasFileMetadataAsync(file) { + this.uniqueFileLookup.add(file.name.toLowerCase(), file); + let isStale = true; + const cache2 = this.fileCache[file.path]; + if (!cache2) this.saveFileCache(file.path, { mtime: 0, size: 0, hash: "" }); + else { + const unchanged = cache2.mtime === file.stat.mtime && cache2.size === file.stat.size; + const hasMetadataCache = cache2.hash && Object.prototype.hasOwnProperty.call(this.metadataCache, cache2.hash); + if (unchanged && hasMetadataCache) + isStale = false; + } + if (isStale) { + _CanvasMetadataHandler.linkResolveQueue.setOnFinished(() => this.trigger("finished")); + await _CanvasMetadataHandler.metadataQueue.add( + () => _CanvasMetadataHandler.updateMetadataCache.call(this, file) + ); + } + _CanvasMetadataHandler.linkResolveQueue.setOnFinished(() => this.trigger("resolved")); + await _CanvasMetadataHandler.linkResolveQueue.add( + () => _CanvasMetadataHandler.resolveCanvasLinks.call(this, file.path) + ); + } + static async updateMetadataCache(file) { + const bytes = await this.vault.readBinary(file); + const data = new TextDecoder().decode(new Uint8Array(bytes)); + const hash = await HashHelper.getBytesHash(bytes); + const cache2 = { + mtime: file.stat.mtime, + size: file.stat.size, + hash + }; + this.saveFileCache(file.path, cache2); + let metadata = this.metadataCache[cache2.hash]; + if (metadata) return this.trigger( + "changed", + file, + data, + metadata + ); + const slowIndexingTimeout = window.setTimeout(() => { + new import_obsidian8.Notice(`Canvas indexing taking a long time for file ${file.path}`); + }, 1e4); + try { + metadata = await _CanvasMetadataHandler.computeCanvasMetadataAsync.call(this, data); + } finally { + window.clearTimeout(slowIndexingTimeout); + } + if (metadata) { + this.saveMetaCache(hash, metadata); + this.trigger("changed", file, data, metadata); + } else { + console.error("Canvas metadata failed to parse", file); + } + } + static async computeCanvasMetadataAsync(data) { + var _a, _b, _c, _d, _e; + const content = JSON.parse(data || "{}"); + const metadata = { + v: 1 + }; + const frontmatter = (_a = content.metadata) == null ? void 0 : _a.frontmatter; + metadata.frontmatterPosition = { + start: { line: 0, col: 0, offset: 0 }, + end: { line: 0, col: 0, offset: 0 } + }; + metadata.frontmatter = frontmatter; + metadata.frontmatterLinks = []; + for (const [key, value] of Object.entries(frontmatter != null ? frontmatter : {})) { + const getLinks = (value2) => value2.map((v) => { + if (!v.startsWith("[[") || !v.endsWith("]]")) return null; + const [link, ...aliases] = v.slice(2, -2).split("|"); + return { + key, + displayText: aliases.length > 0 ? aliases.join("|") : link, + link: link != null ? link : v, + original: v + }; + }).filter((v) => v !== null); + if (typeof value === "string") (_b = metadata.frontmatterLinks) == null ? void 0 : _b.push(...getLinks([value])); + else if (Array.isArray(value)) (_c = metadata.frontmatterLinks) == null ? void 0 : _c.push(...getLinks(value)); + } + metadata.nodes = {}; + metadata.links = []; + metadata.embeds = []; + await Promise.all(((_d = content.nodes) != null ? _d : []).map(async (node, index) => { + var _a2, _b2; + if (node.type !== "text") return; + const text = node.text; + const buffer = new TextEncoder().encode(text).buffer; + const nodeMetadata = await this.computeMetadataAsync(buffer); + if (!nodeMetadata) return; + metadata.nodes[node.id] = nodeMetadata; + metadata.links.push(...((_a2 = nodeMetadata.links) != null ? _a2 : []).map((link) => ({ + ...link, + position: { + nodeId: node.id, + start: { line: 0, col: 1, offset: 0 }, + // 0 for node + end: { line: 0, col: 1, offset: index } + // index of node + } + }))); + metadata.embeds.push(...((_b2 = nodeMetadata.embeds) != null ? _b2 : []).map((embed2) => ({ + ...embed2, + position: { + nodeId: node.id, + start: { line: 0, col: 1, offset: 0 }, + // 0 for node + end: { line: 0, col: 1, offset: index } + // index of node + } + }))); + })); + for (const [index, node] of ((_e = content.nodes) != null ? _e : []).entries()) { + if (node.type !== "file") continue; + const file = node.file; + if (!file) continue; + metadata.embeds.push({ + link: file, + original: file, + displayText: file, + position: { + start: { line: 0, col: 1, offset: 0 }, + // 0 for nodes + end: { line: 0, col: 1, offset: index } + // index of node + } + }); + } + return metadata; + } + static async resolveCanvasLinks(filepath) { + var _a, _b, _c, _d, _e; + const file = this.vault.getAbstractFileByPath(filepath); + if (!(file instanceof import_obsidian8.TFile)) return; + const metadata = this.getFileCache(file); + const references = [...(_a = metadata == null ? void 0 : metadata.links) != null ? _a : [], ...(_b = metadata == null ? void 0 : metadata.embeds) != null ? _b : []]; + const referenceLinks = references.map((ref) => ref.link).sort(); + const resolvedLinks = {}; + const unresolvedLinks = {}; + for (const link of referenceLinks) { + const resolved = this.getFirstLinkpathDest(link, filepath); + if (resolved) { + (_d = resolvedLinks[_c = resolved.path]) != null ? _d : resolvedLinks[_c] = 0; + resolvedLinks[resolved.path]++; + } else { + const strippedLink = link.endsWith(".md") ? link.slice(0, -3) : link; + (_e = unresolvedLinks[strippedLink]) != null ? _e : unresolvedLinks[strippedLink] = 0; + unresolvedLinks[strippedLink]++; + } + } + this.resolvedLinks[filepath] = resolvedLinks; + this.unresolvedLinks[filepath] = unresolvedLinks; + await sleep(1); + this.trigger("resolve", file); + } +}; +_CanvasMetadataHandler.metadataQueue = new TaskQueue(); +_CanvasMetadataHandler.linkResolveQueue = new TaskQueue(); +var CanvasMetadataHandler = _CanvasMetadataHandler; + +// src/patchers/backlinks-patcher.ts +var import_obsidian9 = require("obsidian"); +var BacklinksPatcher = class extends Patcher { + constructor() { + super(...arguments); + this.isRecomputingBacklinks = false; + } + async patch() { + if (!this.plugin.settings.getSetting("canvasMetadataCompatibilityEnabled")) return; + const that = this; + await Patcher.waitForViewRequest(this.plugin, "backlink", (view) => { + Patcher.patchPrototype(this.plugin, view.backlink, { + recomputeBacklink: Patcher.OverrideExisting((next) => function(file) { + that.isRecomputingBacklinks = true; + const result = next.call(this, file); + that.isRecomputingBacklinks = false; + return result; + }) + }); + }); + Patcher.patchPrototype(this.plugin, this.plugin.app.vault, { + recurseChildrenAC: (_next) => function(origin, traverse) { + for (let stack = [origin]; stack.length > 0; ) { + const current = stack.pop(); + if (current) { + traverse(current); + if (current instanceof import_obsidian9.TFolder) stack = stack.concat(current.children); + } + } + }, + getMarkdownFiles: Patcher.OverrideExisting((next) => function() { + if (!that.isRecomputingBacklinks) return next.call(this); + const files = []; + const root = this.getRoot(); + this.recurseChildrenAC(root, (child) => { + if (child instanceof import_obsidian9.TFile && (child.extension === "md" || child.extension === "canvas")) { + files.push(child); + } + }); + return files; + }) + }); + } +}; + +// src/patchers/outgoing-links-patcher.ts +var OutgoingLinksPatcher = class extends Patcher { + async patch() { + if (!this.plugin.settings.getSetting("canvasMetadataCompatibilityEnabled")) return; + await Patcher.waitForViewRequest(this.plugin, "outgoing-link", (view) => { + Patcher.patchPrototype(this.plugin, view.outgoingLink, { + recomputeLinks: Patcher.OverrideExisting((next) => function(...args) { + var _a; + const isCanvas = ((_a = this.file) == null ? void 0 : _a.extension) === "canvas"; + if (isCanvas) this.file.extension = "md"; + const result = next.call(this, ...args); + if (isCanvas) this.file.extension = "canvas"; + return result; + }), + recomputeUnlinked: Patcher.OverrideExisting((next) => function(...args) { + var _a; + const isCanvas = ((_a = this.file) == null ? void 0 : _a.extension) === "canvas"; + if (isCanvas) this.file.extension = "md"; + const result = next.call(this, ...args); + if (isCanvas) this.file.extension = "canvas"; + return result; + }) + }); + }); + } +}; + +// src/patchers/file-manager-patcher.ts +var FileManagerPatcher = class extends Patcher { + async patch() { + if (!this.plugin.settings.getSetting("canvasMetadataCompatibilityEnabled")) return; + const that = this; + Patcher.patch(this.plugin, this.plugin.app.fileManager, { + processFrontMatter: Patcher.OverrideExisting((next) => async function(file, fn, options) { + if ((file == null ? void 0 : file.extension) === "canvas") { + that.plugin.app.vault.process(file, (data) => { + const content = JSON.parse(data); + fn(content.metadata.frontmatter); + return JSON.stringify(content, null, 2); + }).catch(() => console.error("Failed to update metadata object in canvas file.")); + return; + } + return next.call(this, file, fn, options); + }) + }); + } +}; + +// src/patchers/properties-patcher.ts +var PropertiesPatcher = class extends Patcher { + async patch() { + if (!this.plugin.settings.getSetting("canvasMetadataCompatibilityEnabled")) return; + if (!this.plugin.app.viewRegistry.viewByType["file-properties"]) return; + await Patcher.waitForViewRequest(this.plugin, "file-properties", (view) => { + Patcher.patchPrototype(this.plugin, view, { + isSupportedFile: Patcher.OverrideExisting((next) => function(file) { + if ((file == null ? void 0 : file.extension) === "canvas") return true; + return next.call(this, file); + }), + updateFrontmatter: Patcher.OverrideExisting((next) => function(file, content) { + var _a, _b, _c; + if ((file == null ? void 0 : file.extension) === "canvas") { + let frontmatter; + try { + frontmatter = (_c = (_b = (_a = JSON.parse(content)) == null ? void 0 : _a.metadata) == null ? void 0 : _b.frontmatter) != null ? _c : {}; + } catch (e) { + frontmatter = {}; + } + this.rawFrontmatter = JSON.stringify(frontmatter, null, 2); + this.frontmatter = frontmatter; + return frontmatter; + } + return next.call(this, file, content); + }), + saveFrontmatter: Patcher.OverrideExisting((next) => function(frontmatter) { + var _a; + if (((_a = this.file) == null ? void 0 : _a.extension) === "canvas") { + if (this.file !== this.modifyingFile) return; + this.app.vault.process(this.file, (data) => { + const content = JSON.parse(data); + if (content == null ? void 0 : content.metadata) content.metadata.frontmatter = frontmatter; + return JSON.stringify(content, null, 2); + }).catch(() => console.error("Failed to update metadata object in canvas file.")); + return; + } + return next.call(this, frontmatter); + }) + }); + }); + } +}; + +// src/patchers/search-patcher.ts +var SearchPatcher = class extends Patcher { + async patch() { + if (!this.plugin.settings.getSetting("canvasMetadataCompatibilityEnabled")) return; + const that = this; + await Patcher.waitForViewRequest(this.plugin, "search", (view) => { + const uninstallers = []; + Patcher.patchThisAndPrototype(this.plugin, view, { + startSearch: (next) => function(...args) { + const result = next.call(this, ...args); + if (this.searchQuery) { + that.patchSearchQuery(this.searchQuery); + uninstallers.forEach((uninstall) => uninstall()); + } + return result; + } + }, uninstallers); + }); + } + patchSearchQuery(searchQuery) { + Patcher.patchThisAndPrototype(this.plugin, searchQuery, { + _match: Patcher.OverrideExisting((next) => function(data) { + var _a, _b; + const isCanvas = (_b = (_a = data.strings.filepath) == null ? void 0 : _a.endsWith(".canvas")) != null ? _b : false; + if (isCanvas && !data.cache) + data.cache = this.app.metadataCache.getCache(data.strings.filepath); + return next.call(this, data); + }) + }); + } +}; + +// src/patchers/search-command-patcher.ts +var import_obsidian10 = require("obsidian"); +var SearchCommandPatcher = class extends Patcher { + async patch() { + if (!this.plugin.settings.getSetting("nativeFileSearchEnabled")) return; + const that = this; + Patcher.patch(this.plugin, this.plugin.app.commands.commands["editor:open-search"], { + checkCallback: Patcher.OverrideExisting((next) => function(checking) { + if (that.plugin.app.workspace.activeEditor) return next.call(this, checking); + const activeCanvasView = that.plugin.getCurrentCanvasView(); + if (!activeCanvasView) return next.call(this, checking); + if (checking) return true; + if (!activeCanvasView.canvas.searchEl) new CanvasSearchView(activeCanvasView); + return true; + }) + }); + } +}; +var CanvasSearchView = class { + constructor(view) { + this.searchMatches = []; + this.matchIndex = 0; + this.view = view; + this.createSearchView(); + } + createSearchView() { + this.containerEl = this.view.canvas.wrapperEl.createDiv(); + this.containerEl.className = "document-search-container"; + const documentSearch = this.containerEl.createDiv(); + documentSearch.className = "document-search"; + const searchInputContainer = documentSearch.createDiv(); + searchInputContainer.className = "search-input-container document-search-input"; + this.searchInput = searchInputContainer.createEl("input"); + this.searchInput.type = "text"; + this.searchInput.placeholder = "Find..."; + this.searchInput.addEventListener("keydown", (e) => this.onKeyDown(e)); + this.searchInput.addEventListener("input", () => this.onInput()); + this.searchCount = searchInputContainer.createDiv(); + this.searchCount.className = "document-search-count"; + this.searchCount.style.display = "none"; + this.searchCount.textContent = "0 / 0"; + const documentSearchButtons = documentSearch.createDiv(); + documentSearchButtons.className = "document-search-buttons"; + const previousButton = documentSearchButtons.createEl("button"); + previousButton.className = "clickable-icon document-search-button"; + previousButton.setAttribute("aria-label", "Previous\nShift + F3"); + previousButton.setAttribute("data-tooltip-position", "top"); + (0, import_obsidian10.setIcon)(previousButton, "arrow-up"); + previousButton.addEventListener("click", () => this.changeMatch(this.matchIndex - 1)); + const nextButton = documentSearchButtons.createEl("button"); + nextButton.className = "clickable-icon document-search-button"; + nextButton.setAttribute("aria-label", "Next\nF3"); + nextButton.setAttribute("data-tooltip-position", "top"); + (0, import_obsidian10.setIcon)(nextButton, "arrow-down"); + nextButton.addEventListener("click", () => this.changeMatch(this.matchIndex + 1)); + const closeButton = documentSearch.createEl("button"); + closeButton.className = "clickable-icon document-search-close-button"; + closeButton.setAttribute("aria-label", "Exit search"); + closeButton.setAttribute("data-tooltip-position", "top"); + (0, import_obsidian10.setIcon)(closeButton, "x"); + closeButton.addEventListener("click", () => this.close()); + this.view.canvas.searchEl = this.containerEl; + this.searchInput.focus(); + } + onKeyDown(e) { + if (e.key === "Enter" || e.key === "F3") + this.changeMatch(this.matchIndex + (e.shiftKey ? -1 : 1)); + else if (e.key === "Escape") + this.close(); + } + onInput() { + const hasQuery = this.searchInput.value.length > 0; + this.searchCount.style.display = hasQuery ? "block" : "none"; + if (!hasQuery) this.searchMatches = []; + else { + this.searchMatches = Array.from(this.view.canvas.nodes.values()).map((node) => { + const nodeData = node.getData(); + let content = void 0; + if (nodeData.type === "text") content = nodeData.text; + else if (nodeData.type === "group") content = nodeData.label; + else if (nodeData.type === "file") content = node.child.data; + if (!content) return null; + const matches = []; + const regex = new RegExp(this.searchInput.value, "gi"); + let match; + while ((match = regex.exec(content)) !== null) { + matches.push([match.index, match.index + match[0].length]); + } + return { nodeId: node.id, content, matches }; + }).filter((match) => match && match.matches.length > 0); + } + this.changeMatch(0); + } + changeMatch(index) { + if (this.searchMatches.length === 0) this.matchIndex = -1; + else { + if (index < 0) index += this.searchMatches.length; + this.matchIndex = index % this.searchMatches.length; + } + const match = this.searchMatches[this.matchIndex]; + if (match) this.goToMatch(match); + this.searchCount.textContent = `${this.matchIndex + 1} / ${this.searchMatches.length}`; + } + goToMatch(match) { + this.view.setEphemeralState({ match }); + } + close() { + this.containerEl.remove(); + this.view.canvas.searchEl = void 0; + } +}; + +// src/canvas-extensions/metadata-canvas-extension.ts +var import_obsidian11 = require("obsidian"); +var MetadataCanvasExtension = class extends CanvasExtension { + constructor() { + super(...arguments); + this.canvasCssclassesCache = /* @__PURE__ */ new Map(); + } + isEnabled() { + return true; + } + init() { + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:canvas-changed", + (canvas) => this.onCanvasChanged(canvas) + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:canvas-metadata-changed", + (canvas) => this.onMetadataChanged(canvas) + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:canvas-view-unloaded:before", + (view) => this.onCanvasViewUnloaded(view) + )); + } + onCanvasChanged(canvas) { + var _a; + const metadata = (_a = canvas.data) == null ? void 0 : _a.metadata; + if (!metadata || metadata.version !== CURRENT_SPEC_VERSION) + return void new import_obsidian11.Notice("Metadata node not found or version mismatch. Should have been migrated (but wasn't)."); + const that = this; + const validator = { + get(target, key) { + if (typeof target[key] === "object" && target[key] !== null) + return new Proxy(target[key], validator); + else return target[key]; + }, + set(target, key, value) { + target[key] = value; + that.plugin.app.workspace.trigger("advanced-canvas:canvas-metadata-changed", canvas); + canvas.requestSave(); + return true; + } + }; + canvas.metadata = new Proxy(metadata, validator); + this.plugin.app.workspace.trigger("advanced-canvas:canvas-metadata-changed", canvas); + } + onMetadataChanged(canvas) { + var _a, _b, _c; + const oldCssClasses = this.canvasCssclassesCache.get(canvas.view); + if (oldCssClasses) canvas.wrapperEl.classList.remove(...oldCssClasses); + const currentClasses = (_c = (_b = (_a = canvas.metadata) == null ? void 0 : _a.frontmatter) == null ? void 0 : _b.cssclasses) != null ? _c : []; + this.canvasCssclassesCache.set(canvas.view, currentClasses); + if (currentClasses.length > 0) canvas.wrapperEl.classList.add(...currentClasses); + } + onCanvasViewUnloaded(view) { + this.canvasCssclassesCache.delete(view); + } +}; + +// src/utils/modal-helper.ts +var import_obsidian12 = require("obsidian"); +var AbstractSelectionModal = class extends import_obsidian12.FuzzySuggestModal { + constructor(app, placeholder, suggestions, arbitrary = false) { + super(app); + this.suggestions = suggestions; + this.arbitrary = arbitrary; + this.setPlaceholder(placeholder); + this.setInstructions([{ + command: "\u2191\u2193", + purpose: "to navigate" + }, { + command: "esc", + purpose: "to dismiss" + }]); + } + getItems() { + return this.suggestions; + } + getItemText(item) { + return item; + } + getSuggestions(query) { + const suggestions = super.getSuggestions(query); + if (this.arbitrary && query.length > 0 && !suggestions.some((s) => s.item === query)) + suggestions.splice(0, 0, { item: query, match: { score: 1, matches: [[0, query.length]] } }); + return suggestions; + } + onChooseItem(_item, _evt) { + } + awaitInput() { + return new Promise((resolve, _reject) => { + this.onChooseItem = (item) => resolve(item); + this.open(); + }); + } +}; +var FileNameModal = class extends import_obsidian12.SuggestModal { + constructor(app, parentPath, fileExtension) { + super(app); + this.parentPath = parentPath.replace(/^\//, "").replace(/\/$/, ""); + this.fileExtension = fileExtension; + } + getSuggestions(query) { + const queryWithoutExtension = query.replace(new RegExp(`\\.${this.fileExtension}$`), ""); + if (queryWithoutExtension === "") return []; + const queryWithExtension = queryWithoutExtension + "." + this.fileExtension; + const suggestions = [queryWithExtension]; + if (this.parentPath.length > 0) suggestions.splice(0, 0, `${this.parentPath}/${queryWithExtension}`); + return suggestions.filter((s) => this.app.vault.getAbstractFileByPath(s) === null); + } + renderSuggestion(text, el) { + el.setText(text); + } + onChooseSuggestion(_text, _evt) { + } + awaitInput() { + return new Promise((resolve, _reject) => { + this.onChooseSuggestion = (text) => { + resolve(text); + }; + this.open(); + }); + } +}; +var FileSelectModal = class extends import_obsidian12.SuggestModal { + constructor(app, extensionsRegex, suggestNewFile = false) { + super(app); + this.files = this.app.vault.getFiles().map((file) => file.path).filter((path) => { + var _a; + return (_a = FilepathHelper.extension(path)) == null ? void 0 : _a.match(extensionsRegex != null ? extensionsRegex : /.*/); + }); + this.suggestNewFile = suggestNewFile; + this.setPlaceholder("Type to search..."); + this.setInstructions([{ + command: "\u2191\u2193", + purpose: "to navigate" + }, { + command: "\u21B5", + purpose: "to open" + }, { + command: "shift \u21B5", + purpose: "to create" + }, { + command: "esc", + purpose: "to dismiss" + }]); + this.scope.register(["Shift"], "Enter", (e) => { + this.onChooseSuggestion(this.inputEl.value, e); + this.close(); + }); + } + getSuggestions(query) { + const suggestions = this.files.filter((path) => path.toLowerCase().includes(query.toLowerCase())); + if (suggestions.length === 0 && this.suggestNewFile) suggestions.push(query); + return suggestions; + } + renderSuggestion(path, el) { + const simplifiedPath = path.replace(/\.md$/, ""); + el.setText(simplifiedPath); + } + onChooseSuggestion(_path, _evt) { + } + awaitInput() { + return new Promise((resolve, _reject) => { + this.onChooseSuggestion = (path, _evt) => { + const file = this.app.vault.getAbstractFileByPath(path); + if (file instanceof import_obsidian12.TFile) + return resolve(file); + if (!this.suggestNewFile) return; + if (FilepathHelper.extension(path) === void 0) path += ".md"; + const newFile = this.app.vault.create(path, ""); + resolve(newFile); + }; + this.open(); + }); + } +}; + +// src/canvas-extensions/node-ratio-canvas-extension.ts +var NodeRatioCanvasExtension = class extends CanvasExtension { + isEnabled() { + return true; + } + init() { + this.plugin.registerEvent(this.plugin.app.workspace.on( + "canvas:node-menu", + (menu, node) => this.onNodeMenu(menu, node) + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:node-resized", + (canvas, node) => this.onNodeResized(canvas, node) + )); + } + onNodeMenu(menu, node) { + if (!this.plugin.settings.getSetting("aspectRatioControlFeatureEnabled")) return; + menu.addItem((item) => { + item.setTitle("Set Aspect Ratio").setIcon("aspect-ratio").onClick(async () => { + const NO_RATIO = "No ratio enforcement"; + const newRatioString = await new AbstractSelectionModal(this.plugin.app, "Enter aspect ratio (width:height)", ["16:9", "4:3", "3:2", "1:1", NO_RATIO]).awaitInput(); + const nodeData = node.getData(); + if (newRatioString === NO_RATIO) { + node.setData({ + ...nodeData, + ratio: void 0 + }); + return; + } + const [width, height] = newRatioString.split(":").map(Number); + if (width && height) { + node.setData({ + ...nodeData, + ratio: width / height + }); + node.setData({ + ...node.getData(), + width: nodeData.height * (width / height) + }); + } + }); + }); + } + onNodeResized(_canvas, node) { + const nodeData = node.getData(); + if (!nodeData.ratio) return; + const nodeBBox = node.getBBox(); + const nodeSize = { + width: nodeBBox.maxX - nodeBBox.minX, + height: nodeBBox.maxY - nodeBBox.minY + }; + const nodeAspectRatio = nodeSize.width / nodeSize.height; + if (nodeAspectRatio < nodeData.ratio) + nodeSize.width = nodeSize.height * nodeData.ratio; + else nodeSize.height = nodeSize.width / nodeData.ratio; + node.setData({ + ...nodeData, + width: nodeSize.width, + height: nodeSize.height + }); + } +}; + +// src/canvas-extensions/group-canvas-extension.ts +var GROUP_NODE_SIZE = { width: 300, height: 300 }; +var GROUP_NODE_PADDING = 20; +var GroupCanvasExtension = class extends CanvasExtension { + isEnabled() { + return true; + } + init() { + this.plugin.addCommand({ + id: "create-group-around-selection", + name: "Group selected nodes", + checkCallback: CanvasHelper.canvasCommand( + this.plugin, + (canvas) => canvas.selection.size > 0, + (canvas) => this.createGroupAroundSelection(canvas) + ) + }); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:canvas-changed", + (canvas) => { + CanvasHelper.addCardMenuOption( + canvas, + CanvasHelper.createCardMenuOption( + canvas, + { + id: "create-group", + label: "Drag to add group", + icon: "group" + }, + () => GROUP_NODE_SIZE, + (canvas2, pos) => { + canvas2.createGroupNode({ + pos, + size: GROUP_NODE_SIZE + }); + } + ) + ); + } + )); + } + createGroupAroundSelection(canvas) { + const bbox = BBoxHelper.combineBBoxes( + Array.from(canvas.selection.values()).map((e) => e.getBBox()) + ); + const paddedBBox = BBoxHelper.enlargeBBox(bbox, GROUP_NODE_PADDING); + canvas.createGroupNode({ + pos: { x: paddedBBox.minX, y: paddedBBox.minY }, + size: { + width: paddedBBox.maxX - paddedBBox.minX, + height: paddedBBox.maxY - paddedBBox.minY + } + }); + } +}; + +// src/canvas-extensions/node-templates-canvas-extension.ts +var import_obsidian13 = require("obsidian"); +var TEMPLATE_NODE_BUTTON_ID_PREFIX = "create-template-node-"; +var NodeTemplatesCanvasExtension = class extends CanvasExtension { + isEnabled() { + return true; + } + init() { + this.plugin.addCommand({ + id: "save-node-as-template", + name: "Save node as template", + checkCallback: CanvasHelper.canvasCommand( + this.plugin, + (canvas) => canvas.getSelectionData().nodes.length === 1, + (canvas) => void this.saveNodeAsTemplate(canvas) + ) + }); + this.registeredNodeTemplateCommandIds = []; + this.registerNodeTemplateCommands(); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:canvas-changed", + (canvas) => this.onCardMenuCreated(canvas) + )); + } + registerNodeTemplateCommands() { + for (const commandId of this.registeredNodeTemplateCommandIds) + this.plugin.removeCommand(commandId); + this.registeredNodeTemplateCommandIds = []; + const templates = this.plugin.settings.getSetting("nodeTemplates"); + for (let i = 0; i < templates.length; i++) { + const template = templates[i]; + const commandId = `create-template-node-${i}`; + this.plugin.addCommand({ + id: commandId, + name: "Create template node " + (template.label ? `"${template.label}"` : i + 1), + checkCallback: CanvasHelper.canvasCommand( + this.plugin, + (_) => true, + (canvas) => { + const center = canvas.posCenter(); + void this.createNodeFromTemplate( + canvas, + template, + { + x: center.x - template.width / 2, + y: center.y - template.height / 2 + } + ); + } + ) + }); + this.registeredNodeTemplateCommandIds.push(commandId); + } + } + onCardMenuCreated(canvas) { + var _a; + const existingButtons = canvas.cardMenuEl.querySelectorAll(`[id^="${TEMPLATE_NODE_BUTTON_ID_PREFIX}"]`); + existingButtons.forEach((button) => button.remove()); + const templates = this.plugin.settings.getSetting("nodeTemplates"); + for (let i = 0; i < templates.length; i++) { + const template = templates[i]; + CanvasHelper.addCardMenuOption( + canvas, + CanvasHelper.createCardMenuOption( + canvas, + { + id: `${TEMPLATE_NODE_BUTTON_ID_PREFIX}${i}`, + label: "Drag to add template node " + (template.label ? `"${template.label}"` : i + 1), + icon: (_a = template.icon) != null ? _a : "book-dashed" + }, + () => ({ width: template.width, height: template.height }), + (canvas2, pos) => void this.createNodeFromTemplate(canvas2, template, pos), + (e) => this.createTemplateContextMenu(e) + ) + ); + } + } + async createNodeFromTemplate(canvas, template, pos) { + var _a; + const creationOptions = { + pos, + size: { + width: template.width, + height: template.height + } + }; + let node; + if (template.type === "text") node = canvas.createTextNode(creationOptions); + else if (template.type === "file") { + let tfile; + if (template.path) { + const abstractFile = this.plugin.app.vault.getAbstractFileByPath(template.path); + if (abstractFile instanceof import_obsidian13.TFile) tfile = abstractFile; + } + tfile != null ? tfile : tfile = await new FileSelectModal(this.plugin.app, void 0, true).awaitInput(); + node = canvas.createFileNode({ ...creationOptions, file: tfile }); + } else if (template.type === "group") node = canvas.createGroupNode(creationOptions); + else if (template.type === "link") node = canvas.createLinkNode({ ...creationOptions, url: template.url }); + else throw new Error(`Unknown template type: ${template.type}`); + const data = node.getData(); + node.setData( + { + ...data, + color: (_a = template.color) != null ? _a : data.color, + styleAttributes: { + ...data.styleAttributes, + ...template.styleAttributes + } + }, + false + /* addHistory */ + ); + } + createTemplateContextMenu(e) { + const menu = new import_obsidian13.Menu(); + menu.addItem( + (item) => item.setTitle("Remove").setIcon("trash").onClick(async () => { + const buttonEl = e.target; + const index = parseInt(buttonEl.id.replace(TEMPLATE_NODE_BUTTON_ID_PREFIX, "")); + const templates = this.plugin.settings.getSetting("nodeTemplates"); + templates.splice(index, 1); + await this.plugin.settings.setSetting({ nodeTemplates: templates }); + const canvas = this.plugin.getCurrentCanvas(); + if (canvas) this.onCardMenuCreated(canvas); + }) + ); + menu.showAtMouseEvent(e); + } + async saveNodeAsTemplate(canvas) { + const selectedNodeData = canvas.getSelectionData().nodes[0]; + const icon = await new IconModal(this.plugin.app).promise; + if (!icon) { + new import_obsidian13.Notice("No icon selected, template creation cancelled."); + return; + } + const label = await new AbstractSelectionModal(this.plugin.app, "Set template label (optional)", [], true).awaitInput(); + await this.plugin.settings.setSetting({ + nodeTemplates: [ + ...this.plugin.settings.getSetting("nodeTemplates"), + { + icon, + label: label != null ? label : void 0, + type: selectedNodeData.type, + width: selectedNodeData.width, + height: selectedNodeData.height, + color: selectedNodeData.color, + styleAttributes: selectedNodeData.styleAttributes, + path: selectedNodeData.type === "file" ? selectedNodeData.file : void 0, + url: selectedNodeData.type === "link" ? selectedNodeData.url : void 0 + } + ] + }); + this.registerNodeTemplateCommands(); + this.onCardMenuCreated(canvas); + } +}; +var IconModal = class extends import_obsidian13.FuzzySuggestModal { + constructor(app) { + super(app); + this.setPlaceholder("Set template icon"); + } + getItems() { + return (0, import_obsidian13.getIconIds)(); + } + getItemText(item) { + return item; + } + renderSuggestion(item, el) { + el.classList.add("icon-modal-suggestion"); + el.createEl("span", { cls: "icon-modal-suggestion-icon" }, (iconEl) => { + (0, import_obsidian13.setIcon)(iconEl, item.item); + }); + el.createEl("span", { + text: item.item.replace("lucide-", ""), + cls: "icon-modal-suggestion-id" + }); + } + onChooseItem(_item, _evt) { + } + get promise() { + return new Promise((resolve, _reject) => { + this.onChooseItem = (item, _evt) => { + resolve(item); + }; + this.onClose = () => window.setTimeout(() => { + resolve(null); + }, 10); + this.open(); + }); + } +}; + +// src/canvas-extensions/presentation-canvas-extension.ts +var import_obsidian14 = require("obsidian"); +var START_SLIDE_NAME = "Start Slide"; +var DEFAULT_SLIDE_NAME = "New Slide"; +var PresentationCanvasExtension = class extends CanvasExtension { + constructor() { + super(...arguments); + this.savedViewport = { x: 0, y: 0, zoom: 1 }; + this.isPresentationMode = false; + this.visitedNodeIds = []; + this.fullscreenModalObserver = null; + this.presentationUsesFullscreen = false; + } + isEnabled() { + return "presentationFeatureEnabled"; + } + init() { + this.plugin.registerEvent(this.plugin.app.workspace.on( + "canvas:selection-menu", + (menu, canvas) => { + menu.addItem( + (item) => item.setTitle("Wrap in slide").setIcon("gallery-vertical").onClick(() => this.addSlide( + canvas, + void 0, + BBoxHelper.enlargeBBox(BBoxHelper.combineBBoxes( + [...canvas.selection.values()].map((element) => element.getBBox()) + ), this.plugin.settings.getSetting("wrapInSlidePadding")) + )) + ); + } + )); + this.plugin.addCommand({ + id: "create-new-slide", + name: "Create new slide", + checkCallback: CanvasHelper.canvasCommand( + this.plugin, + (canvas) => !canvas.readonly && !this.isPresentationMode, + (canvas) => this.addSlide(canvas) + ) + }); + this.plugin.addCommand({ + id: "set-start-node", + name: "Set start node", + checkCallback: CanvasHelper.canvasCommand( + this.plugin, + (canvas) => !canvas.readonly && !this.isPresentationMode && canvas.getSelectionData().nodes.length === 1, + (canvas) => this.setStartNode(canvas, canvas.nodes.get(canvas.getSelectionData().nodes[0].id)) + ) + }); + this.plugin.addCommand({ + id: "start-presentation", + name: "Start presentation", + checkCallback: CanvasHelper.canvasCommand( + this.plugin, + (_canvas) => !this.isPresentationMode, + (canvas) => void this.startPresentation(canvas) + ) + }); + this.plugin.addCommand({ + id: "continue-presentation", + name: "Continue presentation", + checkCallback: CanvasHelper.canvasCommand( + this.plugin, + (_canvas) => !this.isPresentationMode, + (canvas) => void this.startPresentation(canvas, true) + ) + }); + this.plugin.addCommand({ + id: "end-presentation", + name: "End presentation", + checkCallback: CanvasHelper.canvasCommand( + this.plugin, + (_canvas) => this.isPresentationMode, + (canvas) => this.endPresentation(canvas) + ) + }); + this.plugin.addCommand({ + id: "previous-node", + name: "Previous node", + checkCallback: CanvasHelper.canvasCommand( + this.plugin, + (_canvas) => this.isPresentationMode, + (canvas) => this.previousNode(canvas) + ) + }); + this.plugin.addCommand({ + id: "next-node", + name: "Next node", + checkCallback: CanvasHelper.canvasCommand( + this.plugin, + (_canvas) => this.isPresentationMode, + (canvas) => this.nextNode(canvas) + ) + }); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:canvas-changed", + (canvas) => this.onCanvasChanged(canvas) + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:popup-menu-created", + (canvas) => this.onPopupMenuCreated(canvas) + )); + } + onCanvasChanged(canvas) { + CanvasHelper.addCardMenuOption( + canvas, + CanvasHelper.createCardMenuOption( + canvas, + { + id: "new-slide", + label: "Drag to add slide", + icon: "gallery-vertical" + }, + () => this.getDefaultSlideSize(), + (canvas2, pos) => this.addSlide(canvas2, pos) + ) + ); + } + onPopupMenuCreated(canvas) { + if (!this.plugin.settings.getSetting("showSetStartNodeInPopup")) return; + const selectedNodesData = canvas.getSelectionData().nodes; + if (canvas.readonly || selectedNodesData.length !== 1 || canvas.selection.size > 1) return; + const selectedNode = canvas.nodes.get(selectedNodesData[0].id); + if (!selectedNode) return; + CanvasHelper.addPopupMenuOption( + canvas, + CanvasHelper.createPopupMenuOption({ + id: "start-node", + label: "Set as start slide", + icon: "play", + callback: () => this.setStartNode(canvas, selectedNode) + }) + ); + } + setStartNode(canvas, node) { + if (!node) return; + canvas.metadata["startNode"] = node.getData().id; + } + getDefaultSlideSize() { + const slideSize = this.plugin.settings.getSetting("defaultSlideDimensions"); + return { width: slideSize[0], height: slideSize[1] }; + } + getSlideAspectRatio() { + const slideSize = this.getDefaultSlideSize(); + return slideSize.width / slideSize.height; + } + addSlide(canvas, pos, bbox) { + const isStartNode = canvas.metadata["startNode"] === void 0; + const slideSize = this.getDefaultSlideSize(); + const slideAspectRatio = this.getSlideAspectRatio(); + if (bbox) { + const bboxWidth = bbox.maxX - bbox.minX; + const bboxHeight = bbox.maxY - bbox.minY; + if (bboxWidth / bboxHeight > slideAspectRatio) { + slideSize.width = bboxWidth; + slideSize.height = bboxWidth / slideAspectRatio; + } else { + slideSize.height = bboxHeight; + slideSize.width = bboxHeight * slideAspectRatio; + } + pos = { + x: bbox.minX, + y: bbox.minY + }; + } + if (!pos) pos = CanvasHelper.getCenterCoordinates(canvas, this.getDefaultSlideSize()); + const groupNode = canvas.createGroupNode({ + pos, + size: slideSize, + label: isStartNode ? START_SLIDE_NAME : DEFAULT_SLIDE_NAME, + focus: false + }); + groupNode.setData({ + ...groupNode.getData(), + ratio: slideAspectRatio + }); + if (isStartNode) canvas.metadata["startNode"] = groupNode.getData().id; + } + async animateNodeTransition(canvas, fromNode, toNode) { + const removePadding = this.plugin.settings.getSetting("zoomToSlideWithoutPadding"); + const animationDurationMs = this.plugin.settings.getSetting("slideTransitionAnimationDuration") * 1e3; + const toNodeBBox = CanvasHelper.getSmallestAllowedZoomBBox(canvas, toNode.getBBox()); + const toNodeBBoxPadded = removePadding ? toNodeBBox : BBoxHelper.enlargeBBox(toNodeBBox, 50); + if (animationDurationMs > 0 && fromNode) { + const animationIntensity = this.plugin.settings.getSetting("slideTransitionAnimationIntensity"); + const fromNodeBBox = CanvasHelper.getSmallestAllowedZoomBBox(canvas, fromNode.getBBox()); + const fromNodeBBoxPadded = removePadding ? fromNodeBBox : BBoxHelper.enlargeBBox(fromNodeBBox, 50); + const currentNodeBBoxEnlarged = BBoxHelper.scaleBBox(fromNodeBBoxPadded, animationIntensity); + canvas.zoomToRealBbox(currentNodeBBoxEnlarged); + await sleep(animationDurationMs / 2); + if (fromNode.getData().id !== toNode.getData().id) { + const nextNodeBBoxEnlarged = BBoxHelper.scaleBBox(toNodeBBoxPadded, animationIntensity); + canvas.zoomToRealBbox(nextNodeBBoxEnlarged); + await sleep(animationDurationMs / 2); + } + } + canvas.zoomToRealBbox(toNodeBBoxPadded); + } + async startPresentation(canvas, tryContinue = false) { + if (!tryContinue || this.visitedNodeIds.length === 0) { + const startNode2 = canvas.metadata["startNode"] && canvas.nodes.get(canvas.metadata["startNode"]); + if (!startNode2) { + new import_obsidian14.Notice("No start node found. Please mark a node as a start node through the popup menu."); + return; + } + this.visitedNodeIds = [startNode2.getData().id]; + } + this.savedViewport = { + x: canvas.tx, + y: canvas.ty, + zoom: canvas.tZoom + }; + const shouldEnterFullscreen = this.plugin.settings.getSetting("fullscreenPresentationEnabled"); + this.presentationUsesFullscreen = shouldEnterFullscreen; + canvas.wrapperEl.focus(); + canvas.wrapperEl.classList.add("presentation-mode"); + if (shouldEnterFullscreen) { + try { + await canvas.wrapperEl.requestFullscreen(); + } catch (e) { + this.presentationUsesFullscreen = false; + } + } + canvas.setReadonly(true); + if (this.plugin.settings.getSetting("useUnclampedZoomWhilePresenting")) + canvas.screenshotting = true; + canvas.wrapperEl.onkeydown = (e) => { + if (e.key === "Escape") { + e.preventDefault(); + e.stopPropagation(); + this.endPresentation(canvas); + return; + } + if (this.plugin.settings.getSetting("useArrowKeysToChangeSlides")) { + if (e.key === "ArrowRight") this.nextNode(canvas); + else if (e.key === "ArrowLeft") this.previousNode(canvas); + } + if (this.plugin.settings.getSetting("usePgUpPgDownKeysToChangeSlides")) { + if (e.key === "PageDown") this.nextNode(canvas); + else if (e.key === "PageUp") this.previousNode(canvas); + } + }; + if (this.presentationUsesFullscreen) { + this.fullscreenModalObserver = new MutationObserver((mutationRecords) => { + mutationRecords.forEach((mutationRecord) => { + mutationRecord.addedNodes.forEach((node) => { + var _a; + activeDocument.body.removeChild(node); + (_a = activeDocument.fullscreenElement) == null ? void 0 : _a.appendChild(node); + }); + }); + const inputField = activeDocument.querySelector(".prompt-input"); + if (inputField) inputField.focus(); + }); + this.fullscreenModalObserver.observe(activeDocument.body, { childList: true }); + canvas.wrapperEl.onfullscreenchange = (_e) => { + if (activeDocument.fullscreenElement) return; + this.endPresentation(canvas); + }; + } + this.isPresentationMode = true; + if (this.presentationUsesFullscreen) + await sleep(500); + const startNodeId = this.visitedNodeIds.first(); + if (!startNodeId) return; + const startNode = canvas.nodes.get(startNodeId); + if (!startNode) return; + void this.animateNodeTransition(canvas, void 0, startNode); + } + endPresentation(canvas) { + var _a; + if (!this.isPresentationMode) return; + if (this.presentationUsesFullscreen) { + (_a = this.fullscreenModalObserver) == null ? void 0 : _a.disconnect(); + this.fullscreenModalObserver = null; + canvas.wrapperEl.onfullscreenchange = null; + if (activeDocument.fullscreenElement) void activeDocument.exitFullscreen(); + } + canvas.wrapperEl.onkeydown = null; + canvas.setReadonly(false); + if (this.plugin.settings.getSetting("useUnclampedZoomWhilePresenting")) + canvas.screenshotting = false; + canvas.wrapperEl.classList.remove("presentation-mode"); + if (this.plugin.settings.getSetting("resetViewportOnPresentationEnd")) + canvas.setViewport(this.savedViewport.x, this.savedViewport.y, this.savedViewport.zoom); + this.isPresentationMode = false; + this.presentationUsesFullscreen = false; + } + nextNode(canvas) { + var _a, _b; + const fromNodeId = this.visitedNodeIds.last(); + if (!fromNodeId) return; + const fromNode = canvas.nodes.get(fromNodeId); + if (!fromNode) return; + const outgoingEdges = canvas.getEdgesForNode(fromNode).filter((edge) => edge.from.node.getData().id === fromNodeId); + let toNode = (_a = outgoingEdges.first()) == null ? void 0 : _a.to.node; + if (outgoingEdges.length > 1) { + const sortedEdges = outgoingEdges.sort((a, b) => { + if (!a.label) return 1; + if (!b.label) return -1; + return a.label.localeCompare(b.label); + }); + const traversedEdgesCount = this.visitedNodeIds.filter((visitedNodeId) => visitedNodeId === fromNodeId).length - 1; + const nextEdge = sortedEdges[traversedEdgesCount]; + toNode = (_b = nextEdge == null ? void 0 : nextEdge.to) == null ? void 0 : _b.node; + } + if (toNode) { + this.visitedNodeIds.push(toNode.getData().id); + void this.animateNodeTransition(canvas, fromNode, toNode); + } else { + void this.animateNodeTransition(canvas, fromNode, fromNode); + } + } + previousNode(canvas) { + const fromNodeId = this.visitedNodeIds.pop(); + if (!fromNodeId) return; + const fromNode = canvas.nodes.get(fromNodeId); + if (!fromNode) return; + const toNodeId = this.visitedNodeIds.last(); + let toNode = toNodeId ? canvas.nodes.get(toNodeId) : null; + if (!toNode) { + toNode = fromNode; + this.visitedNodeIds.push(fromNodeId); + } + void this.animateNodeTransition(canvas, fromNode, toNode); + } +}; + +// src/canvas-extensions/z-ordering-canvas-extension.ts +var ZOrderingCanvasExtension = class extends CanvasExtension { + isEnabled() { + return "zOrderingControlFeatureEnabled"; + } + init() { + this.plugin.registerEvent(this.plugin.app.workspace.on( + "canvas:node-menu", + (menu, node) => this.nodeContextMenu(node, menu) + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "canvas:selection-menu", + (menu, canvas) => this.selectionContextMenu(canvas, menu) + )); + } + nodeContextMenu(node, menu) { + this.addZOrderingContextMenuItems(node.canvas, [node], menu); + } + selectionContextMenu(canvas, menu) { + const selectedNodes = canvas.getSelectionData().nodes.map((nodeData) => canvas.nodes.get(nodeData.id)).filter((node) => node !== void 0); + this.addZOrderingContextMenuItems(canvas, selectedNodes, menu); + } + addZOrderingContextMenuItems(canvas, nodes, menu) { + menu.addSeparator(); + if (this.plugin.settings.getSetting("zOrderingControlShowOneLayerShiftOptions") && nodes.length === 1) { + menu.addItem((item) => { + item.setTitle("Move one layer forward"); + item.setIcon("arrow-up"); + item.onClick(() => this.moveOneLayer(canvas, nodes.first(), true)); + }); + menu.addItem((item) => { + item.setTitle("Move one layer backward"); + item.setIcon("arrow-down"); + item.onClick(() => this.moveOneLayer(canvas, nodes.first(), false)); + }); + } + menu.addItem((item) => { + item.setTitle("Bring to Front"); + item.setIcon("bring-to-front"); + item.onClick(() => this.moveMaxLayers(canvas, nodes, true)); + }); + menu.addItem((item) => { + item.setTitle("Send to Back"); + item.setIcon("send-to-back"); + item.onClick(() => this.moveMaxLayers(canvas, nodes, false)); + }); + if (nodes.some((node) => node.getData().zIndex !== void 0)) { + menu.addItem((item) => { + item.setTitle("Remove persistent z-index"); + item.setIcon("pin-off"); + item.onClick(() => this.removePersistentZIndexes(canvas, nodes)); + }); + } + menu.addSeparator(); + } + moveOneLayer(canvas, selectedNode, forward) { + const selectedNodeBBox = selectedNode.getBBox(); + const collidingNodes = [...canvas.nodes.values()].filter((node) => BBoxHelper.isColliding(selectedNodeBBox, node.getBBox())).filter((node) => node !== selectedNode); + const nearestZIndexNode = collidingNodes.sort((a, b) => forward ? a.zIndex - b.zIndex : b.zIndex - a.zIndex).filter((node) => forward ? node.zIndex > selectedNode.zIndex : node.zIndex < selectedNode.zIndex).first(); + if (nearestZIndexNode === void 0) return; + const targetZIndex = nearestZIndexNode.zIndex; + this.setNodesZIndex([nearestZIndexNode], selectedNode.zIndex); + this.setNodesZIndex([selectedNode], targetZIndex); + } + moveMaxLayers(canvas, selectedNodes, forward) { + const targetZIndex = forward ? Math.max(...this.getAllZIndexes(canvas)) + 1 : Math.min(...this.getAllZIndexes(canvas)) - selectedNodes.length; + this.setNodesZIndex(selectedNodes, targetZIndex); + } + removePersistentZIndexes(_canvas, nodes) { + for (const node of nodes) node.setZIndex(void 0); + } + setNodesZIndex(nodes, zIndex) { + const sortedNodes = nodes.sort((a, b) => a.zIndex - b.zIndex); + for (let i = 0; i < sortedNodes.length; i++) { + const node = sortedNodes[i]; + const finalZIndex = zIndex + i; + node.setZIndex(finalZIndex); + } + } + getAllZIndexes(canvas) { + return [...canvas.nodes.values()].map((n) => n.zIndex); + } +}; + +// src/canvas-extensions/better-readonly-canvas-extension.ts +var BetterReadonlyCanvasExtension = class extends CanvasExtension { + constructor() { + super(...arguments); + this.isMovingToBBox = false; + } + isEnabled() { + return "betterReadonlyEnabled"; + } + init() { + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:popup-menu-created", + (canvas) => this.updatePopupMenu(canvas) + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:viewport-changed:before", + (canvas) => this.onBeforeViewPortChanged(canvas) + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:zoom-to-bbox:before", + () => this.isMovingToBBox = true + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:readonly-changed", + (canvas, _readonly) => { + this.updatePopupMenu(canvas); + this.updateLockedZoom(canvas); + this.updateLockedPan(canvas); + } + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:canvas-changed", + (canvas) => this.addQuickSettings(canvas) + )); + } + onBeforeViewPortChanged(canvas) { + var _a, _b, _c, _d, _e, _f; + if (this.isMovingToBBox) { + this.isMovingToBBox = false; + this.updateLockedZoom(canvas); + this.updateLockedPan(canvas); + return; + } + if (!canvas.readonly) return; + if (this.plugin.settings.getSetting("disableZoom")) { + canvas.zoom = (_a = canvas.lockedZoom) != null ? _a : canvas.zoom; + canvas.tZoom = (_b = canvas.lockedZoom) != null ? _b : canvas.tZoom; + } + if (this.plugin.settings.getSetting("disablePan")) { + canvas.x = (_c = canvas.lockedX) != null ? _c : canvas.x; + canvas.tx = (_d = canvas.lockedX) != null ? _d : canvas.tx; + canvas.y = (_e = canvas.lockedY) != null ? _e : canvas.y; + canvas.ty = (_f = canvas.lockedY) != null ? _f : canvas.ty; + } + } + addQuickSettings(canvas) { + var _a; + const settingsContainer = (_a = canvas.quickSettingsButton) == null ? void 0 : _a.parentElement; + if (!settingsContainer) return; + CanvasHelper.addControlMenuButton( + settingsContainer, + this.createToggle({ + id: "disable-node-popup", + label: "Disable node popup", + icon: "arrow-up-right-from-circle", + callback: () => this.updatePopupMenu(canvas) + }, "disableNodePopup") + ); + CanvasHelper.addControlMenuButton( + settingsContainer, + this.createToggle({ + id: "disable-zoom", + label: "Disable zoom", + icon: "zoom-in", + callback: () => this.updateLockedZoom(canvas) + }, "disableZoom") + ); + CanvasHelper.addControlMenuButton( + settingsContainer, + this.createToggle({ + id: "disable-pan", + label: "Disable pan", + icon: "move", + callback: () => this.updateLockedPan(canvas) + }, "disablePan") + ); + } + createToggle(menuOption, settingKey) { + const toggle = CanvasHelper.createControlMenuButton({ + ...menuOption, + callback: () => void (async () => { + var _a; + const newValue = !this.plugin.settings.getSetting(settingKey); + await this.plugin.settings.setSetting({ [settingKey]: newValue }); + toggle.dataset.toggled = JSON.stringify(this.plugin.settings.getSetting(settingKey)); + (_a = menuOption.callback) == null ? void 0 : _a.call(this); + })() + }); + toggle.classList.add("show-while-readonly"); + toggle.dataset.toggled = JSON.stringify(this.plugin.settings.getSetting(settingKey)); + return toggle; + } + updatePopupMenu(canvas) { + const hidden = canvas.readonly && this.plugin.settings.getSetting("disableNodePopup"); + canvas.menu.menuEl.style.visibility = hidden ? "hidden" : "visible"; + } + updateLockedZoom(canvas) { + canvas.lockedZoom = canvas.tZoom; + } + updateLockedPan(canvas) { + canvas.lockedX = canvas.tx; + canvas.lockedY = canvas.ty; + } +}; + +// src/canvas-extensions/encapsulate-canvas-extension.ts +var ENCAPSULATED_FILE_NODE_SIZE = { width: 300, height: 300 }; +var EncapsulateCanvasExtension = class extends CanvasExtension { + isEnabled() { + return "canvasEncapsulationEnabled"; + } + init() { + this.plugin.addCommand({ + id: "encapsulate-selection", + name: "Encapsulate selection", + checkCallback: CanvasHelper.canvasCommand( + this.plugin, + (canvas) => !canvas.readonly && canvas.selection.size > 0, + (canvas) => void this.encapsulateSelection(canvas) + ) + }); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "canvas:selection-menu", + (menu, canvas) => { + menu.addItem( + (item) => item.setTitle("Encapsulate").setIcon("file-plus").onClick(() => this.encapsulateSelection(canvas)) + ); + } + )); + } + async encapsulateSelection(canvas) { + var _a, _b, _c, _d; + const selection = canvas.getSelectionData(); + const canvasSettings = this.plugin.app.internalPlugins.plugins.canvas.instance.options; + const defaultNewCanvasLocation = canvasSettings.newFileLocation; + let targetFolderPath = this.plugin.app.vault.getRoot().path; + if (defaultNewCanvasLocation === "current") targetFolderPath = (_c = (_b = (_a = canvas.view.file) == null ? void 0 : _a.parent) == null ? void 0 : _b.path) != null ? _c : targetFolderPath; + else if (defaultNewCanvasLocation === "folder") targetFolderPath = (_d = canvasSettings.newFileFolderPath) != null ? _d : targetFolderPath; + const targetFilePath = await new FileNameModal( + this.plugin.app, + targetFolderPath, + "canvas" + ).awaitInput(); + const newFileData = { nodes: selection.nodes, edges: selection.edges }; + const file = await this.plugin.app.vault.create(targetFilePath, JSON.stringify(newFileData, null, 2)); + for (const nodeData of selection.nodes) { + const node = canvas.nodes.get(nodeData.id); + if (node) canvas.removeNode(node); + } + canvas.createFileNode({ + pos: { + x: selection.center.x - ENCAPSULATED_FILE_NODE_SIZE.width / 2, + y: selection.center.y - ENCAPSULATED_FILE_NODE_SIZE.height / 2 + }, + size: ENCAPSULATED_FILE_NODE_SIZE, + file + }); + } +}; + +// src/canvas-extensions/commands-canvas-extension.ts +var import_obsidian16 = require("obsidian"); + +// src/canvas-extensions/copy-node-reference-canvas-extension.ts +var import_obsidian15 = require("obsidian"); +var CopyNodeReferenceCanvasExtension = class _CopyNodeReferenceCanvasExtension extends CanvasExtension { + isEnabled() { + return "enableSingleNodePopupReferenceCopy"; + } + init() { + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:popup-menu-created", + (canvas) => this.onPopupMenuCreated(canvas) + )); + } + onPopupMenuCreated(canvas) { + var _a; + const popupMenuEl = (_a = canvas == null ? void 0 : canvas.menu) == null ? void 0 : _a.menuEl; + if (!popupMenuEl) return; + const selectionData = canvas.getSelectionData(); + if (selectionData.nodes.length !== 1 && selectionData.edges.length === 0) return; + const menuOption = { + id: "node-popup-menu-option-copy-reference", + label: "Copy wikilink to node", + icon: "link", + callback: () => _CopyNodeReferenceCanvasExtension.copyWikilinkToNode(canvas.view.file, selectionData.nodes[0]) + }; + const popupMenuOption = CanvasHelper.createPopupMenuOption(menuOption); + CanvasHelper.addPopupMenuOption(canvas, popupMenuOption); + } + static copyWikilinkToNode(file, nodeData) { + const nodeTypeString = TextHelper.toTitleCase(nodeData.type); + const wikilink = `[[${file.path}#${nodeData.id}|${file.name} (${nodeTypeString} node)]]`; + navigator.clipboard.writeText(wikilink).then( + () => new import_obsidian15.Notice("Copied wikilink to node to clipboard.", 2e3) + ).catch(() => new import_obsidian15.Notice("Failed to copy wikilink to node to clipboard.", 2e3)); + } +}; + +// src/canvas-extensions/commands-canvas-extension.ts +var DIRECTIONS = ["up", "down", "left", "right"]; +var CommandsCanvasExtension = class extends CanvasExtension { + isEnabled() { + return "commandsFeatureEnabled"; + } + init() { + this.plugin.addCommand({ + id: "toggle-readonly", + name: "Toggle readonly", + checkCallback: CanvasHelper.canvasCommand( + this.plugin, + (_canvas) => true, + (canvas) => canvas.setReadonly(!canvas.readonly) + ) + }); + this.plugin.addCommand({ + id: "create-text-node", + name: "Create text node", + checkCallback: CanvasHelper.canvasCommand( + this.plugin, + (canvas) => !canvas.readonly, + (canvas) => this.createTextNode(canvas) + ) + }); + this.plugin.addCommand({ + id: "create-file-node", + name: "Create file node", + checkCallback: CanvasHelper.canvasCommand( + this.plugin, + (canvas) => !canvas.readonly, + (canvas) => void this.createFileNode(canvas) + ) + }); + this.plugin.addCommand({ + id: "select-all-edges", + name: "Select all edges", + checkCallback: CanvasHelper.canvasCommand( + this.plugin, + (_canvas) => true, + (canvas) => canvas.updateSelection( + () => canvas.selection = new Set(canvas.edges.values()) + ) + ) + }); + this.plugin.addCommand({ + id: "zoom-to-selection", + name: "Zoom to selection", + checkCallback: CanvasHelper.canvasCommand( + this.plugin, + (canvas) => canvas.selection.size > 0, + (canvas) => canvas.zoomToSelection() + ) + }); + this.plugin.addCommand({ + id: "zoom-to-fit", + name: "Zoom to fit", + checkCallback: CanvasHelper.canvasCommand( + this.plugin, + (_canvas) => true, + (canvas) => canvas.zoomToFit() + ) + }); + for (const direction of DIRECTIONS) { + this.plugin.addCommand({ + id: `clone-node-${direction}`, + name: `Clone node ${direction}`, + checkCallback: CanvasHelper.canvasCommand( + this.plugin, + (canvas) => { + var _a; + return !canvas.readonly && canvas.selection.size === 1 && ((_a = canvas.selection.values().next().value) == null ? void 0 : _a.getData().type) === "text"; + }, + (canvas) => this.cloneNode(canvas, direction) + ) + }); + this.plugin.addCommand({ + id: `expand-node-${direction}`, + name: `Expand node ${direction}`, + checkCallback: CanvasHelper.canvasCommand( + this.plugin, + (canvas) => !canvas.readonly && canvas.selection.size === 1, + (canvas) => this.expandNode(canvas, direction) + ) + }); + this.plugin.addCommand({ + id: `navigate-${direction}`, + name: `Navigate ${direction}`, + checkCallback: CanvasHelper.canvasCommand( + this.plugin, + (canvas) => canvas.getSelectionData().nodes.length === 1, + (canvas) => this.navigate(canvas, direction) + ) + }); + } + this.plugin.addCommand({ + id: "flip-selection-horizontally", + name: "Flip selection horizontally", + checkCallback: CanvasHelper.canvasCommand( + this.plugin, + (canvas) => !canvas.readonly && canvas.selection.size > 0, + (canvas) => this.flipSelection( + canvas, + true + ) + ) + }); + this.plugin.addCommand({ + id: "flip-selection-vertically", + name: "Flip selection vertically", + checkCallback: CanvasHelper.canvasCommand( + this.plugin, + (canvas) => !canvas.readonly && canvas.selection.size > 0, + (canvas) => this.flipSelection(canvas, false) + ) + }); + this.plugin.addCommand({ + id: "select-connected-edges", + name: "Select connected edges", + checkCallback: CanvasHelper.canvasCommand( + this.plugin, + (canvas) => canvas.selection.size > 0, + (canvas) => CanvasHelper.selectEdgesForNodes(canvas, "connected") + ) + }); + this.plugin.addCommand({ + id: "select-incoming-edges", + name: "Select incoming edges", + checkCallback: CanvasHelper.canvasCommand( + this.plugin, + (canvas) => canvas.selection.size > 0, + (canvas) => CanvasHelper.selectEdgesForNodes(canvas, "incoming") + ) + }); + this.plugin.addCommand({ + id: "select-outgoing-edges", + name: "Select outgoing edges", + checkCallback: CanvasHelper.canvasCommand( + this.plugin, + (canvas) => canvas.selection.size > 0, + (canvas) => CanvasHelper.selectEdgesForNodes(canvas, "outgoing") + ) + }); + this.plugin.addCommand({ + id: "swap-nodes", + name: "Swap nodes", + checkCallback: CanvasHelper.canvasCommand( + this.plugin, + (canvas) => !canvas.readonly && canvas.getSelectionData().nodes.length === 2, + (canvas) => { + const selectedNodes = canvas.getSelectionData().nodes.map((nodeData) => canvas.nodes.get(nodeData.id)).filter((node) => node !== void 0); + if (selectedNodes.length !== 2) return; + const [nodeA, nodeB] = selectedNodes; + const nodeAData = nodeA.getData(); + const nodeBData = nodeB.getData(); + nodeA.setData({ ...nodeAData, x: nodeBData.x, y: nodeBData.y, width: nodeBData.width, height: nodeBData.height }); + nodeB.setData({ ...nodeBData, x: nodeAData.x, y: nodeAData.y, width: nodeAData.width, height: nodeAData.height }); + canvas.pushHistory(canvas.getData()); + } + ) + }); + this.plugin.addCommand({ + id: "copy-wikilink-to-node", + name: "Copy wikilink to node", + checkCallback: CanvasHelper.canvasCommand( + this.plugin, + (canvas) => !canvas.readonly && canvas.selection.size === 1, + (canvas) => { + const file = canvas.view.file; + if (!file) return; + const nodeData = canvas.getSelectionData().nodes[0]; + if (!nodeData) return; + CopyNodeReferenceCanvasExtension.copyWikilinkToNode(file, nodeData); + } + ) + }); + this.plugin.addCommand({ + id: "pull-outgoing-links-to-canvas", + name: "Pull outgoing links to canvas", + checkCallback: CanvasHelper.canvasCommand( + this.plugin, + (canvas) => !canvas.readonly, + (canvas) => { + var _a, _b, _c; + const canvasFile = canvas.view.file; + if (!canvasFile) return; + let selectedNodeIds = canvas.getSelectionData().nodes.map((node) => node.id); + if (selectedNodeIds.length === 0) selectedNodeIds = [...canvas.nodes.keys()]; + const metadata = this.plugin.app.metadataCache.getFileCache(canvasFile); + if (!metadata) return; + const outgoingLinks = /* @__PURE__ */ new Set(); + for (const nodeId of selectedNodeIds) { + let relativeFile = canvasFile; + let nodeOutgoingLinks = (_b = (_a = metadata.nodes) == null ? void 0 : _a[nodeId]) == null ? void 0 : _b.links; + if (!nodeOutgoingLinks) { + const file = (_c = canvas.nodes.get(nodeId)) == null ? void 0 : _c.file; + if (!file) continue; + const fileMetadata = this.plugin.app.metadataCache.getFileCache(file); + nodeOutgoingLinks = fileMetadata == null ? void 0 : fileMetadata.links; + relativeFile = file; + } + if (!nodeOutgoingLinks) continue; + for (const nodeOutgoingLink of nodeOutgoingLinks) { + const resolvedLink = this.plugin.app.metadataCache.getFirstLinkpathDest(nodeOutgoingLink.link, relativeFile.path); + if (!(resolvedLink instanceof import_obsidian16.TFile)) continue; + outgoingLinks.add(resolvedLink); + } + } + const existingFileNodes = /* @__PURE__ */ new Set([canvas.view.file]); + for (const node of canvas.nodes.values()) { + if (node.getData().type !== "file" || !node.file) continue; + existingFileNodes.add(node.file); + } + for (const outgoingLink of outgoingLinks) { + if (existingFileNodes.has(outgoingLink)) continue; + void this.createFileNode(canvas, outgoingLink); + } + } + ) + }); + this.plugin.addCommand({ + id: "pull-backlinks-to-canvas", + name: "Pull backlinks to canvas", + checkCallback: CanvasHelper.canvasCommand( + this.plugin, + (canvas) => !canvas.readonly, + (canvas) => { + const canvasFile = canvas.view.file; + if (!canvasFile) return; + const selectedNodesData = canvas.getSelectionData().nodes.map((node) => node); + const backlinks = /* @__PURE__ */ new Set(); + if (selectedNodesData.length > 0) { + for (const nodeData of selectedNodesData) { + if (nodeData.type !== "file" || !nodeData.file) continue; + const file = this.plugin.app.vault.getFileByPath(nodeData.file); + if (!file) continue; + const nodeBacklinks = this.plugin.app.metadataCache.getBacklinksForFile(file); + if (!nodeBacklinks) continue; + for (const nodeBacklink of nodeBacklinks.data.keys()) { + const resolvedLink = this.plugin.app.metadataCache.getFirstLinkpathDest(nodeBacklink, file.path); + if (!(resolvedLink instanceof import_obsidian16.TFile)) continue; + backlinks.add(resolvedLink); + } + } + } else { + const canvasBacklinks = this.plugin.app.metadataCache.getBacklinksForFile(canvasFile); + if (!canvasBacklinks) return; + for (const canvasBacklink of canvasBacklinks.data.keys()) { + const resolvedLink = this.plugin.app.metadataCache.getFirstLinkpathDest(canvasBacklink, canvasFile.path); + if (!(resolvedLink instanceof import_obsidian16.TFile)) continue; + backlinks.add(resolvedLink); + } + } + const existingFileNodes = /* @__PURE__ */ new Set([canvas.view.file]); + for (const node of canvas.nodes.values()) { + if (node.getData().type !== "file" || !node.file) continue; + existingFileNodes.add(node.file); + } + for (const backlink of backlinks) { + if (existingFileNodes.has(backlink)) continue; + void this.createFileNode(canvas, backlink); + } + } + ) + }); + } + createTextNode(canvas) { + const size = canvas.config.defaultTextNodeDimensions; + const pos = CanvasHelper.getCenterCoordinates(canvas, size); + canvas.createTextNode({ pos, size }); + } + async createFileNode(canvas, file) { + const size = canvas.config.defaultFileNodeDimensions; + const pos = CanvasHelper.getCenterCoordinates(canvas, size); + file != null ? file : file = await new FileSelectModal(this.plugin.app, void 0, true).awaitInput(); + canvas.createFileNode({ pos, size, file }); + } + cloneNode(canvas, cloneDirection) { + const sourceNode = canvas.selection.values().next().value; + if (!sourceNode) return; + const sourceNodeData = sourceNode.getData(); + const nodeMargin = this.plugin.settings.getSetting("cloneNodeMargin"); + const offset = { + x: (sourceNode.width + nodeMargin) * (cloneDirection === "left" ? -1 : cloneDirection === "right" ? 1 : 0), + y: (sourceNode.height + nodeMargin) * (cloneDirection === "up" ? -1 : cloneDirection === "down" ? 1 : 0) + }; + const clonedNode = canvas.createTextNode({ + pos: { + x: sourceNode.x + offset.x, + y: sourceNode.y + offset.y + }, + size: { + width: sourceNode.width, + height: sourceNode.height + } + }); + clonedNode.setData({ + ...clonedNode.getData(), + color: sourceNodeData.color, + styleAttributes: sourceNodeData.styleAttributes + }); + if (this.plugin.settings.getSetting("zoomToClonedNode")) + canvas.zoomToBbox(clonedNode.getBBox()); + } + expandNode(canvas, expandDirection) { + const node = canvas.selection.values().next().value; + if (!node) return; + const expandNodeStepSize = this.plugin.settings.getSetting("expandNodeStepSize"); + const expand = { + x: expandDirection === "left" ? -1 : expandDirection === "right" ? 1 : 0, + y: expandDirection === "up" ? -1 : expandDirection === "down" ? 1 : 0 + }; + node.setData({ + ...node.getData(), + width: node.width + expand.x * expandNodeStepSize, + height: node.height + expand.y * expandNodeStepSize + }); + } + flipSelection(canvas, horizontally) { + const selectionData = canvas.getSelectionData(); + if (selectionData.nodes.length === 0) return; + const nodeIds = /* @__PURE__ */ new Set(); + for (const nodeData of selectionData.nodes) { + nodeIds.add(nodeData.id); + const node = canvas.nodes.get(nodeData.id); + if (!node) continue; + const newX = horizontally ? 2 * selectionData.center.x - nodeData.x - nodeData.width : nodeData.x; + const newY = horizontally ? nodeData.y : 2 * selectionData.center.y - nodeData.y - nodeData.height; + node.setData({ + ...nodeData, + x: newX, + y: newY + }); + } + for (const edge of canvas.edges.values()) { + const edgeData = edge.getData(); + let newFromSide = edgeData.fromSide; + if (nodeIds.has(edgeData.fromNode) && BBoxHelper.isHorizontal(edgeData.fromSide) === horizontally) + newFromSide = BBoxHelper.getOppositeSide(edgeData.fromSide); + let newToSide = edgeData.toSide; + if (nodeIds.has(edgeData.toNode) && BBoxHelper.isHorizontal(edgeData.toSide) === horizontally) + newToSide = BBoxHelper.getOppositeSide(edgeData.toSide); + edge.setData({ + ...edgeData, + fromSide: newFromSide, + toSide: newToSide + }); + } + canvas.pushHistory(canvas.getData()); + } + navigate(canvas, direction) { + const node = this.getNextNode(canvas, direction); + if (!node) return; + canvas.updateSelection(() => { + canvas.selection = /* @__PURE__ */ new Set([node]); + }); + } + getNextNode(canvas, direction) { + var _a; + const selectedNodeData = (_a = canvas.getSelectionData().nodes) == null ? void 0 : _a.first(); + if (!selectedNodeData) return; + const selectedNodeBBox = { + minX: selectedNodeData.x, + minY: selectedNodeData.y, + maxX: selectedNodeData.x + selectedNodeData.width, + maxY: selectedNodeData.y + selectedNodeData.height + }; + const possibleTargetNodes = Array.from(canvas.nodes.values()).filter((node) => { + const nodeData = node.getData(); + return nodeData.id !== selectedNodeData.id && (nodeData.type === "text" || nodeData.type === "file"); + }); + const closestNode = possibleTargetNodes.reduce((closestNode2, node) => { + const nodeBBox = node.getBBox(); + const isInVerticalRange = selectedNodeBBox.minY <= nodeBBox.maxY && selectedNodeBBox.maxY >= nodeBBox.minY; + const isInHorizontalRange = selectedNodeBBox.minX <= nodeBBox.maxX && selectedNodeBBox.maxX >= nodeBBox.minX; + if (["up", "down"].includes(direction) && !isInHorizontalRange) return closestNode2; + if (["left", "right"].includes(direction) && !isInVerticalRange) return closestNode2; + let distance = -1; + switch (direction) { + case "up": + distance = selectedNodeBBox.minY - nodeBBox.maxY; + break; + case "down": + distance = nodeBBox.minY - selectedNodeBBox.maxY; + break; + case "left": + distance = selectedNodeBBox.minX - nodeBBox.maxX; + break; + case "right": + distance = nodeBBox.minX - selectedNodeBBox.maxX; + break; + } + if (distance < 0) return closestNode2; + if (!closestNode2) return { node, distance }; + if (distance < closestNode2.distance) return { node, distance }; + if (distance === closestNode2.distance) { + const selectedNodeCenter = { + x: selectedNodeData.x + selectedNodeData.width / 2, + y: selectedNodeData.y + selectedNodeData.height / 2 + }; + const closestNodeCenter = { + x: closestNode2.node.x + closestNode2.node.width / 2, + y: closestNode2.node.y + closestNode2.node.height / 2 + }; + const nodeCenter = { + x: node.x + node.width / 2, + y: node.y + node.height / 2 + }; + const closestNodeDistance = Math.sqrt( + Math.pow(selectedNodeCenter.x - closestNodeCenter.x, 2) + Math.pow(selectedNodeCenter.y - closestNodeCenter.y, 2) + ); + const nodeDistance = Math.sqrt( + Math.pow(selectedNodeCenter.x - nodeCenter.x, 2) + Math.pow(selectedNodeCenter.y - nodeCenter.y, 2) + ); + if (nodeDistance < closestNodeDistance) return { node, distance }; + } + return closestNode2; + }, null); + return closestNode == null ? void 0 : closestNode.node; + } +}; + +// src/canvas-extensions/auto-resize-node-canvas-extension.ts +var AutoResizeNodeCanvasExtension = class extends CanvasExtension { + isEnabled() { + return "autoResizeNodeFeatureEnabled"; + } + init() { + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:node-created", + (canvas, node) => this.onNodeCreated(canvas, node) + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:popup-menu-created", + (canvas) => this.onPopupMenuCreated(canvas) + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:node-editing-state-changed", + (canvas, node, editing) => void this.onNodeEditingStateChanged(canvas, node, editing) + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:node-text-content-changed", + (canvas, node, viewUpdate) => void this.onNodeTextContentChanged(canvas, node, viewUpdate.view.dom) + )); + } + isValidNodeType(nodeData) { + return nodeData.type === "text" || nodeData.type === "file" && nodeData.file.endsWith(".md"); + } + onNodeCreated(_canvas, node) { + const autoResizeNodeEnabledByDefault = this.plugin.settings.getSetting("autoResizeNodeEnabledByDefault"); + if (!autoResizeNodeEnabledByDefault) return; + const nodeData = node.getData(); + if (nodeData.type !== "text" && nodeData.type !== "file") return; + node.setData({ + ...node.getData(), + dynamicHeight: true + }); + } + onPopupMenuCreated(canvas) { + if (canvas.readonly) return; + const selectedNodes = canvas.getSelectionData().nodes.filter((nodeData) => this.isValidNodeType(nodeData)).map((nodeData) => canvas.nodes.get(nodeData.id)).filter((node) => node !== void 0); + if (selectedNodes.length === 0) return; + const autoResizeHeightEnabled = selectedNodes.some((node) => node.getData().dynamicHeight); + CanvasHelper.addPopupMenuOption( + canvas, + CanvasHelper.createPopupMenuOption({ + id: "auto-resize-height", + label: autoResizeHeightEnabled ? "Disable auto-resize" : "Enable auto-resize", + icon: autoResizeHeightEnabled ? "scan-text" : "lock", + callback: () => this.toggleAutoResizeHeightEnabled(canvas, selectedNodes, autoResizeHeightEnabled) + }) + ); + } + toggleAutoResizeHeightEnabled(canvas, nodes, autoResizeHeight) { + nodes.forEach((node) => node.setData({ + ...node.getData(), + dynamicHeight: !autoResizeHeight + })); + this.onPopupMenuCreated(canvas); + } + canBeResized(node) { + const nodeData = node.getData(); + return nodeData.dynamicHeight; + } + async onNodeEditingStateChanged(_canvas, node, editing) { + if (!this.isValidNodeType(node.getData())) return; + if (!this.canBeResized(node)) return; + await sleep(10); + if (editing) { + void this.onNodeTextContentChanged(_canvas, node, node.child.editMode.cm.dom); + return; + } + const renderedMarkdownContainer = node.nodeEl.querySelector(".markdown-preview-view.markdown-rendered"); + if (!renderedMarkdownContainer) return; + renderedMarkdownContainer.style.height = "min-content"; + const newHeight = renderedMarkdownContainer.clientHeight; + renderedMarkdownContainer.style.removeProperty("height"); + this.setNodeHeight(node, newHeight); + } + async onNodeTextContentChanged(_canvas, node, dom) { + if (!this.isValidNodeType(node.getData())) return; + if (!this.canBeResized(node)) return; + const cmScroller = dom.querySelector(".cm-scroller"); + if (!cmScroller) return; + cmScroller.style.height = "min-content"; + const newHeight = cmScroller.scrollHeight; + cmScroller.style.removeProperty("height"); + this.setNodeHeight(node, newHeight); + } + setNodeHeight(node, height) { + if (height === 0) return; + const maxHeight = this.plugin.settings.getSetting("autoResizeNodeMaxHeight"); + if (maxHeight != -1 && height > maxHeight) height = maxHeight; + const nodeData = node.getData(); + height = Math.max(height, node.canvas.config.minContainerDimension); + if (this.plugin.settings.getSetting("autoResizeNodeSnapToGrid")) + height = Math.ceil(height / CanvasHelper.GRID_SIZE) * CanvasHelper.GRID_SIZE; + node.setData({ + ...nodeData, + height + }); + } +}; + +// src/canvas-extensions/portals-canvas-extension.ts +var import_obsidian17 = require("obsidian"); +var PORTAL_ID_DELIMITER = "||"; +var PORTAL_ID_PREFIX = `acportal${PORTAL_ID_DELIMITER}`; +var PORTAL_PADDING = 50; +var MIN_OPEN_PORTAL_SIZE = { width: 200, height: 200 }; +var PortalsCanvasExtension = class _PortalsCanvasExtension extends CanvasExtension { + isEnabled() { + return "portalsFeatureEnabled"; + } + init() { + this.plugin.registerEvent(this.plugin.app.vault.on("modify", (file) => { + for (const canvas of this.plugin.getCanvases()) + this.onFileModified(canvas, file); + })); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:popup-menu-created", + (canvas) => this.onPopupMenu(canvas) + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:node-removed", + (canvas, node) => this.onNodeRemoved(canvas, node) + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:node-moved", + (canvas, node, _keyboard) => this.onNodeMoved(canvas, node) + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:node-resized", + (canvas, node) => this.onNodeResized(canvas, node) + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:dragging-state-changed", + (canvas, startedDragging) => this.onDraggingStateChanged(canvas, startedDragging) + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:containing-nodes-requested", + (canvas, bbox, nodes) => this.onContainingNodesRequested(canvas, bbox, nodes) + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:edge-connection-try-dragging:before", + (canvas, edge, event, cancelRef) => this.onEdgeConnectionTryDraggingBefore(canvas, edge, event, cancelRef) + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:edge-connection-dragging:after", + (canvas, edge, event, newEdge, side, previousEnds) => this.onEdgeConnectionDraggingAfter(canvas, edge, event, newEdge, side, previousEnds) + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:selection-changed", + (canvas, oldSelection, updateSelection) => this.onSelectionChanged(canvas, oldSelection, updateSelection) + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:data-requested", + (canvas, data) => this.onGetData(canvas, data) + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:data-loaded:before", + (canvas, data, setData) => { + this.onSetData(canvas, data).then((newData) => { + if (newData.nodes.length === data.nodes.length && newData.edges.length === data.edges.length) return; + setData(newData); + }).catch((error) => { + console.error("Error loading portal data:", error); + new import_obsidian17.Notice("An error occurred while loading portal data. Please check console for details."); + }); + } + )); + } + onFileModified(canvas, file) { + const isAffected = Object.values(canvas.nodes).filter( + (nodeData) => nodeData.getData().type === "file" && nodeData.currentPortalFile === file.path + ).length > 0; + if (!isAffected) return; + canvas.setData(canvas.getData()); + canvas.history.current--; + canvas.history.data.pop(); + } + onContainingNodesRequested(_canvas, _bbox, nodes) { + const filteredNodes = nodes.filter((node) => !_PortalsCanvasExtension.isPortalElement(node.id)); + nodes.splice(0, nodes.length, ...filteredNodes); + } + onSelectionChanged(canvas, _oldSelection, updateSelection) { + updateSelection(() => { + const updatedSelection = Array.from(canvas.selection).filter((canvasElement) => !_PortalsCanvasExtension.isPortalElement(canvasElement.id)); + canvas.selection = new Set(updatedSelection); + }); + } + onDraggingStateChanged(canvas, startedDragging) { + if (!startedDragging) return; + if (!canvas.getSelectionData().nodes.some((node) => node.type === "file" && node.portal)) return; + const objectSnappingEnabled = canvas.options.snapToObjects; + if (!objectSnappingEnabled) return; + canvas.toggleObjectSnapping(false); + const dragEndEventRef = this.plugin.app.workspace.on( + "advanced-canvas:dragging-state-changed", + (canvas2, startedDragging2) => { + if (startedDragging2) return; + canvas2.toggleObjectSnapping(objectSnappingEnabled); + this.plugin.app.workspace.offref(dragEndEventRef); + } + ); + this.plugin.registerEvent(dragEndEventRef); + } + onNodeMoved(canvas, portalNode) { + const portalNodeData = portalNode.getData(); + if (portalNodeData.type !== "file" || !portalNodeData.isPortalLoaded) return; + const nestedNodes = this.getContainingNodes(canvas, portalNode); + const containingNodesBBox = CanvasHelper.getBBox(nestedNodes); + const portalOffset = { + x: portalNodeData.x - containingNodesBBox.minX + PORTAL_PADDING, + y: portalNodeData.y - containingNodesBBox.minY + PORTAL_PADDING + }; + for (const nestedNode of nestedNodes) { + const nestedNodeData = nestedNode.getData(); + nestedNode.setData({ + ...nestedNodeData, + x: nestedNodeData.x + portalOffset.x, + y: nestedNodeData.y + portalOffset.y + }); + } + } + onNodeResized(_canvas, portalNode) { + const portalNodeData = portalNode.getData(); + if (portalNodeData.type !== "file" || !portalNodeData.isPortalLoaded) return; + portalNode.setData({ + ...portalNodeData, + x: portalNode.prevX ? portalNode.prevX : portalNodeData.x, + y: portalNode.prevY ? portalNode.prevY : portalNodeData.y, + width: portalNode.prevWidth ? portalNode.prevWidth : portalNodeData.width, + height: portalNode.prevHeight ? portalNode.prevHeight : portalNodeData.height + }); + } + onNodeRemoved(canvas, portalNode) { + const portalNodeData = portalNode.getData(); + if (portalNodeData.type !== "file" || !portalNodeData.portal) return; + for (const node of this.getContainingNodes(canvas, portalNode, false)) + canvas.removeNode(node); + for (const edge of this.getContainingEdges(canvas, portalNode, false)) + canvas.removeEdge(edge); + } + onEdgeConnectionTryDraggingBefore(_canvas, edge, _event, cancelRef) { + if (!_PortalsCanvasExtension.isPortalElement(edge.id)) return; + cancelRef.value = true; + new import_obsidian17.Notice("Updating edges from portals is not supported yet."); + } + onEdgeConnectionDraggingAfter(canvas, edge, _event, _newEdge, _side, _previousEnds) { + if (_PortalsCanvasExtension.isPortalElement(edge.id)) return; + if (!_PortalsCanvasExtension.isPortalElement(edge.from.node.id) || !_PortalsCanvasExtension.isPortalElement(edge.to.node.id)) return; + canvas.removeEdge(edge); + new import_obsidian17.Notice("Creating edges with both ends in portals are not supported yet."); + } + onPopupMenu(canvas) { + if (canvas.readonly) return; + const selectedFileNodes = canvas.getSelectionData().nodes.map((nodeData) => { + var _a; + const node = canvas.nodes.get(nodeData.id); + if (!node) return null; + if (nodeData.type !== "file") return null; + if (((_a = node.file) == null ? void 0 : _a.extension) === "canvas") return node; + if (nodeData.portal) this.setPortalOpen(canvas, node, false); + return null; + }).filter((node) => node !== null); + if (selectedFileNodes.length !== 1) return; + const portalNode = selectedFileNodes.first(); + const portalNodeData = portalNode.getData(); + if (portalNodeData.portal && portalNodeData.file !== portalNode.currentPortalFile) + this.setPortalOpen(canvas, portalNode, true); + CanvasHelper.addPopupMenuOption( + canvas, + CanvasHelper.createPopupMenuOption({ + id: "toggle-portal", + label: portalNodeData.portal ? "Close portal" : "Open portal", + icon: portalNodeData.portal ? "door-open" : "door-closed", + callback: () => { + this.setPortalOpen(canvas, portalNode, !portalNodeData.portal); + this.onPopupMenu(canvas); + } + }) + ); + } + setPortalOpen(canvas, portalNode, open) { + const portalNodeData = portalNode.getData(); + portalNode.setData({ + ...portalNodeData, + portal: open + }); + portalNode.currentPortalFile = open ? portalNodeData.file : void 0; + canvas.setData(canvas.getData()); + } + // Remove all edges and nodes from portals + onGetData(_canvas, data) { + data.nodes = data.nodes.filter((nodeData) => !_PortalsCanvasExtension.isPortalElement(nodeData.id)); + for (const nodeData of data.nodes) delete nodeData.isPortalLoaded; + const portalsIdMap = new Map( + data.nodes.filter((nodeData) => nodeData.portal).map((nodeData) => [nodeData.id, nodeData]) + ); + data.edges = data.edges.filter((edgeData) => { + var _a; + if (_PortalsCanvasExtension.isPortalElement(edgeData.id)) return false; + const isFromNodeFromPortal = _PortalsCanvasExtension.getNestedIds(edgeData.fromNode).length > 1; + const isToNodeFromPortal = _PortalsCanvasExtension.getNestedIds(edgeData.toNode).length > 1; + if (!isFromNodeFromPortal && !isToNodeFromPortal) return true; + if (isFromNodeFromPortal && isToNodeFromPortal) return false; + const targetPortalId = this.getParentPortalId(isFromNodeFromPortal ? edgeData.fromNode : edgeData.toNode); + const targetPortalData = portalsIdMap.get(targetPortalId); + if (!targetPortalData) return false; + (_a = targetPortalData.interdimensionalEdges) != null ? _a : targetPortalData.interdimensionalEdges = []; + targetPortalData.interdimensionalEdges.push(edgeData); + return false; + }); + } + // Add all edges and nodes from portals + async onSetData(canvas, dataRef) { + if (!(dataRef == null ? void 0 : dataRef.nodes)) return dataRef; + const data = JSON.parse(JSON.stringify(dataRef)); + const addedData = await Promise.all(data.nodes.map((nodeData) => this.tryOpenPortal(canvas, nodeData))); + for (const newData of addedData) { + data.nodes.push(...newData.nodes); + data.edges.push(...newData.edges); + } + for (const nodeData of data.nodes) { + if (nodeData.type !== "file" || !nodeData.isPortalLoaded) continue; + const interdimensionalEdges = nodeData.interdimensionalEdges; + if (!interdimensionalEdges) continue; + for (const edge of interdimensionalEdges) data.edges.push(edge); + delete nodeData.interdimensionalEdges; + } + return data; + } + async tryOpenPortal(canvas, portalNodeData, nestedPortalFiles = /* @__PURE__ */ new Set()) { + const addedData = { nodes: [], edges: [] }; + if (portalNodeData.type !== "file" || !portalNodeData.portal) return addedData; + if (portalNodeData.file === canvas.view.file.path) return addedData; + if (nestedPortalFiles.has(portalNodeData.file)) return addedData; + nestedPortalFiles.add(portalNodeData.file); + const portalFile = this.plugin.app.vault.getAbstractFileByPath(portalNodeData.file); + if (!(portalFile instanceof import_obsidian17.TFile) || portalFile.extension !== "canvas") return addedData; + const portalFileDataString = await this.plugin.app.vault.cachedRead(portalFile); + if (portalFileDataString === "") return addedData; + const portalFileData = JSON.parse(portalFileDataString); + if (!portalFileData) return addedData; + portalNodeData.isPortalLoaded = true; + const sourceMinCoordinates = CanvasHelper.getBBox(portalFileData.nodes); + const portalOffset = { + x: portalNodeData.x - sourceMinCoordinates.minX + PORTAL_PADDING, + y: portalNodeData.y - sourceMinCoordinates.minY + PORTAL_PADDING + }; + for (const nodeDataFromPortal of portalFileData.nodes) { + let newNodeId = `${portalNodeData.id}${PORTAL_ID_DELIMITER}${nodeDataFromPortal.id}`; + if (!newNodeId.startsWith(PORTAL_ID_PREFIX)) + newNodeId = PORTAL_ID_PREFIX + newNodeId; + const addedNode = { + ...nodeDataFromPortal, + id: newNodeId, + x: nodeDataFromPortal.x + portalOffset.x, + y: nodeDataFromPortal.y + portalOffset.y + }; + addedData.nodes.push(addedNode); + const nestedNodes = await this.tryOpenPortal(canvas, addedNode, nestedPortalFiles); + addedData.nodes.push(...nestedNodes.nodes); + addedData.edges.push(...nestedNodes.edges); + } + for (const edgeDataFromPortal of portalFileData.edges) { + let newEdgeId = `${portalNodeData.id}${PORTAL_ID_DELIMITER}${edgeDataFromPortal.id}`; + if (!newEdgeId.startsWith(PORTAL_ID_PREFIX)) + newEdgeId = PORTAL_ID_PREFIX + newEdgeId; + let fromNodeId = `${portalNodeData.id}${PORTAL_ID_DELIMITER}${edgeDataFromPortal.fromNode}`; + if (!fromNodeId.startsWith(PORTAL_ID_PREFIX)) + fromNodeId = PORTAL_ID_PREFIX + fromNodeId; + let toNodeId = `${portalNodeData.id}${PORTAL_ID_DELIMITER}${edgeDataFromPortal.toNode}`; + if (!toNodeId.startsWith(PORTAL_ID_PREFIX)) + toNodeId = PORTAL_ID_PREFIX + toNodeId; + addedData.edges.push({ + ...edgeDataFromPortal, + id: newEdgeId, + fromNode: fromNodeId, + toNode: toNodeId + }); + } + const targetSize = this.getPortalSize(CanvasHelper.getBBox(addedData.nodes)); + portalNodeData.width = targetSize.width; + portalNodeData.height = targetSize.height; + return addedData; + } + // Helper functions + getPortalSize(sourceBBox) { + const sourceSize = { + width: sourceBBox.maxX - sourceBBox.minX, + height: sourceBBox.maxY - sourceBBox.minY + }; + const targetSize = { + width: Math.max(sourceSize.width + PORTAL_PADDING * 2, MIN_OPEN_PORTAL_SIZE.width), + height: Math.max(sourceSize.height + PORTAL_PADDING * 2, MIN_OPEN_PORTAL_SIZE.height) + }; + if (!Number.isFinite(targetSize.width)) targetSize.width = MIN_OPEN_PORTAL_SIZE.width; + if (!Number.isFinite(targetSize.height)) targetSize.height = MIN_OPEN_PORTAL_SIZE.height; + return targetSize; + } + getContainingNodes(canvas, portalNode, directChildren = true) { + return Array.from(canvas.nodes.values()).filter((node) => this.isChildOfPortal(portalNode.getData(), node.getData(), directChildren)); + } + getContainingEdges(canvas, portalNode, directChildren = true) { + return Array.from(canvas.edges.values()).filter((edge) => this.isChildOfPortal(portalNode.getData(), edge.getData(), directChildren)); + } + getParentPortalId(elementId) { + const nestedIds = _PortalsCanvasExtension.getNestedIds(elementId); + if (nestedIds.length < 2) return void 0; + return nestedIds.slice(0, -1).join(PORTAL_ID_DELIMITER); + } + static getNestedIds(id) { + if (!this.isPortalElement(id)) return [id]; + const trimmedId = id.substring(PORTAL_ID_PREFIX.length); + return trimmedId.split(PORTAL_ID_DELIMITER); + } + static isPortalElement(id) { + return id.startsWith(PORTAL_ID_PREFIX); + } + isChildOfPortal(portal, canvasElement, directChild = true) { + const nestedIds = _PortalsCanvasExtension.getNestedIds(canvasElement.id); + if (nestedIds.length < 2) return false; + return canvasElement.id !== portal.id && // Not the portal itself + nestedIds.contains(portal.id) && // Is a child of the portal + (!directChild || nestedIds[nestedIds.length - 2] === portal.id); + } +}; + +// src/canvas-extensions/frontmatter-control-button-canvas-extension.ts +var import_obsidian18 = require("obsidian"); +var FrontmatterControlButtonCanvasExtension = class extends CanvasExtension { + isEnabled() { + return "canvasMetadataCompatibilityEnabled"; + } + init() { + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:canvas-changed", + (canvas) => this.addQuickSettings(canvas) + )); + } + addQuickSettings(canvas) { + var _a; + if (!canvas) return; + const settingsContainer = (_a = canvas.quickSettingsButton) == null ? void 0 : _a.parentElement; + if (!settingsContainer) return; + CanvasHelper.addControlMenuButton( + settingsContainer, + CanvasHelper.createControlMenuButton({ + id: "properties-button", + icon: "info", + label: "Properties", + callback: () => { + var _a2; + const propertiesPlugin = this.plugin.app.internalPlugins.plugins["properties"]; + if (!(propertiesPlugin == null ? void 0 : propertiesPlugin._loaded)) { + new import_obsidian18.Notice(`Core plugin "Properties view" was not found or isn't enabled. Enable it and restart Obsidian.`); + return; + } + let propertiesLeaf = (_a2 = this.plugin.app.workspace.getLeavesOfType("file-properties").first()) != null ? _a2 : null; + if (!propertiesLeaf) { + propertiesLeaf = this.plugin.app.workspace.getRightLeaf(false); + void (propertiesLeaf == null ? void 0 : propertiesLeaf.setViewState({ type: "file-properties" })); + } + if (propertiesLeaf) void this.plugin.app.workspace.revealLeaf(propertiesLeaf); + } + }) + ); + } +}; + +// src/canvas-extensions/better-default-settings-canvas-extension.ts +var BetterDefaultSettingsCanvasExtension = class extends CanvasExtension { + isEnabled() { + return true; + } + init() { + this.modifyCanvasSettings(this.plugin.getCurrentCanvas()); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:settings-changed", + () => this.modifyCanvasSettings(this.plugin.getCurrentCanvas()) + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:canvas-changed", + (canvas) => this.modifyCanvasSettings(canvas) + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:double-click", + (canvas, event, preventDefault) => void this.onDoubleClick(canvas, event, preventDefault) + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:node-created", + (canvas, node) => { + this.enforceNodeGridAlignment(canvas, node); + this.applyDefaultNodeStyles(canvas, node); + } + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:edge-created", + (canvas, edge) => void this.applyDefaultEdgeStyles(canvas, edge) + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:node-resized", + (canvas, node) => this.enforceMaxNodeWidth(canvas, node) + )); + } + modifyCanvasSettings(canvas) { + if (!canvas) return; + const defaultTextNodeDimensionsArray = this.plugin.settings.getSetting("defaultTextNodeDimensions"); + canvas.config.defaultTextNodeDimensions = { + width: defaultTextNodeDimensionsArray[0], + height: defaultTextNodeDimensionsArray[1] + }; + const defaultFileNodeDimensionsArray = this.plugin.settings.getSetting("defaultFileNodeDimensions"); + canvas.config.defaultFileNodeDimensions = { + width: defaultFileNodeDimensionsArray[0], + height: defaultFileNodeDimensionsArray[1] + }; + canvas.config.minContainerDimension = this.plugin.settings.getSetting("minNodeSize"); + } + async onDoubleClick(canvas, event, preventDefault) { + if (event.defaultPrevented || event.target !== canvas.wrapperEl || canvas.isDragging || canvas.readonly) return; + preventDefault.value = true; + const pos = canvas.posFromEvt(event); + switch (this.plugin.settings.getSetting("nodeTypeOnDoubleClick")) { + case "file": { + const file = await new FileSelectModal(this.plugin.app, void 0, true).awaitInput(); + canvas.createFileNode({ + pos, + position: "center", + file + }); + break; + } + default: + canvas.createTextNode({ + pos, + position: "center" + }); + break; + } + } + enforceNodeGridAlignment(_canvas, node) { + if (!this.plugin.settings.getSetting("alignNewNodesToGrid")) return; + const nodeData = node.getData(); + node.setData({ + ...nodeData, + x: CanvasHelper.alignToGrid(nodeData.x), + y: CanvasHelper.alignToGrid(nodeData.y) + }); + } + applyDefaultNodeStyles(_canvas, node) { + const nodeData = node.getData(); + if (nodeData.type !== "text") return; + let color = this.plugin.settings.getSetting("defaultTextNodeColor").toString(); + if (color === "0") color = void 0; + node.setData({ + ...nodeData, + color, + styleAttributes: { + ...nodeData.styleAttributes, + ...this.plugin.settings.getSetting("defaultTextNodeStyleAttributes") + } + }); + } + async applyDefaultEdgeStyles(canvas, edge) { + var _a; + const edgeData = edge.getData(); + let color = this.plugin.settings.getSetting("defaultEdgeColor").toString(); + if (this.plugin.settings.getSetting("inheritEdgeColorFromNode")) + color = (_a = edge.from.node.getData().color) != null ? _a : color; + if (color === "0") color = void 0; + edge.setData({ + ...edgeData, + color, + styleAttributes: { + ...edgeData.styleAttributes, + ...this.plugin.settings.getSetting("defaultEdgeStyleAttributes") + } + }); + if (canvas.canvasEl.hasClass("is-connecting")) { + await new Promise((resolve) => { + new MutationObserver(() => { + if (!canvas.canvasEl.hasClass("is-connecting")) resolve(); + }).observe(canvas.canvasEl, { attributes: true, attributeFilter: ["class"] }); + }); + } + const lineDirection = this.plugin.settings.getSetting("defaultEdgeLineDirection"); + edge.setData({ + ...edge.getData(), + fromEnd: lineDirection === "bidirectional" ? "arrow" : "none", + toEnd: lineDirection === "nondirectional" ? "none" : "arrow" + }); + } + enforceMaxNodeWidth(_canvas, node) { + const maxNodeWidth = this.plugin.settings.getSetting("maxNodeWidth"); + if (maxNodeWidth <= 0) return; + const nodeData = node.getData(); + if (nodeData.type !== "text" && nodeData.type !== "file" || nodeData.portal) return; + if (nodeData.width <= maxNodeWidth) return; + node.setData({ + ...nodeData, + x: node.prevX !== void 0 ? node.prevX : nodeData.x, + // Reset the position to the previous value + width: maxNodeWidth + }); + } +}; + +// src/canvas-extensions/color-palette-canvas-extension.ts +var DEFAULT_COLORS_COUNT = 6; +var CUSTOM_COLORS_MOD_STYLES_ID = "mod-custom-colors"; +var ColorPaletteCanvasExtension = class extends CanvasExtension { + constructor() { + super(...arguments); + this.observer = null; + } + isEnabled() { + return true; + } + init() { + this.plugin.registerEvent(this.plugin.app.workspace.on( + "window-open", + (_win, _window) => this.updateCustomColorModStyleClasses() + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "css-change", + () => this.updateCustomColorModStyleClasses() + )); + this.updateCustomColorModStyleClasses(); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:popup-menu-created", + (canvas) => this.patchColorSelection(canvas) + )); + this.plugin.register(() => { + var _a; + return (_a = this.observer) == null ? void 0 : _a.disconnect(); + }); + } + updateCustomColorModStyleClasses() { + var _a; + const customCss = this.getCustomColors().map((colorId) => ` + .mod-canvas-color-${colorId} { + --canvas-color: var(--canvas-color-${colorId}); + } + `).join(""); + for (const win of this.plugin.windowsManager.windows) { + const doc = win.activeDocument; + (_a = doc.getElementById(CUSTOM_COLORS_MOD_STYLES_ID)) == null ? void 0 : _a.remove(); + const customColorModStyle = doc.createElement("style"); + customColorModStyle.id = CUSTOM_COLORS_MOD_STYLES_ID; + doc.head.appendChild(customColorModStyle); + customColorModStyle.textContent = customCss; + } + } + patchColorSelection(canvas) { + if (this.observer) this.observer.disconnect(); + this.observer = new MutationObserver((mutations) => { + const colorMenuOpened = mutations.some( + (mutation) => Object.values(mutation.addedNodes).some( + (node) => node.instanceOf(HTMLElement) && node.classList.contains("canvas-submenu") && Object.values(node.childNodes).some( + (node2) => node2.instanceOf(HTMLElement) && node2.classList.contains("canvas-color-picker-item") + ) + ) + ); + if (!colorMenuOpened) return; + const submenu = canvas.menu.menuEl.querySelector(".canvas-submenu"); + if (!submenu) return; + const currentNodeColor = canvas.getSelectionData().nodes.map((node) => node.color).last(); + for (const colorId of this.getCustomColors()) { + const customColorMenuItem = this.createColorMenuItem(canvas, colorId); + if (currentNodeColor === colorId) customColorMenuItem.classList.add("is-active"); + submenu.insertBefore(customColorMenuItem, submenu.lastChild); + } + }); + this.observer.observe(canvas.menu.menuEl, { childList: true }); + } + createColorMenuItem(canvas, colorId) { + const menuItem = activeDocument.createElement("div"); + menuItem.classList.add("canvas-color-picker-item"); + menuItem.classList.add(`mod-canvas-color-${colorId}`); + menuItem.addEventListener("click", () => { + menuItem.classList.add("is-active"); + for (const item of canvas.selection) { + item.setColor(colorId); + } + canvas.requestSave(); + }); + return menuItem; + } + getCustomColors() { + const colors = []; + const style = getComputedStyle(activeDocument.body); + let colorIndex = DEFAULT_COLORS_COUNT + 1; + while (style.getPropertyValue(`--canvas-color-${colorIndex}`)) { + colors.push(colorIndex.toString()); + colorIndex++; + } + return colors; + } +}; + +// src/canvas-extensions/collapsible-groups-canvas-extension.ts +var import_obsidian19 = require("obsidian"); +var CollapsibleGroupsCanvasExtension = class extends CanvasExtension { + isEnabled() { + return "collapsibleGroupsFeatureEnabled"; + } + init() { + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:node-changed", + (canvas, node) => this.onNodeChanged(canvas, node) + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:node-bbox-requested", + (canvas, node, bbox) => this.onNodeBBoxRequested(canvas, node, bbox) + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:copy", + (canvas, selectionData) => this.onCopy(canvas, selectionData) + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:data-requested", + (_canvas, data) => this.expandNodes(data) + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:data-loaded:before", + (_canvas, data, _setData) => this.collapseNodes(data) + )); + this.plugin.addCommand({ + id: "toggle-collapse-group", + name: "Toggle collapse group", + checkCallback: CanvasHelper.canvasCommand( + this.plugin, + (canvas) => canvas.selection.size === 1 && canvas.selection.values().next().value.getData().type === "group", + (canvas) => this.toggleCollapseGroup(canvas, canvas.selection.values().next().value) + ) + }); + } + toggleCollapseGroup(canvas, node) { + const data = node.getData(); + this.setCollapsed(node.canvas, node, data.collapsed ? void 0 : true); + canvas.markMoved(node); + } + onNodeChanged(canvas, groupNode) { + var _a, _b; + const groupNodeData = groupNode.getData(); + if (groupNodeData.type !== "group") return; + (_a = groupNode.collapseEl) == null ? void 0 : _a.remove(); + const collapseEl = activeDocument.createElement("div"); + collapseEl.className = "collapse-button"; + (0, import_obsidian19.setIcon)(collapseEl, groupNodeData.collapsed ? "plus-circle" : "minus-circle"); + collapseEl.onclick = () => this.toggleCollapseGroup(canvas, groupNode); + groupNode.collapseEl = collapseEl; + (_b = groupNode.labelEl) == null ? void 0 : _b.insertAdjacentElement("afterend", collapseEl); + } + onCopy(_canvas, selectionData) { + for (const collapsedGroupData of selectionData.nodes) { + if (collapsedGroupData.type !== "group" || !collapsedGroupData.collapsed || !collapsedGroupData.collapsedData) continue; + selectionData.nodes.push(...collapsedGroupData.collapsedData.nodes.map((nodeData) => ({ + ...nodeData, + // Restore the relative position of the node to the group + x: nodeData.x + collapsedGroupData.x, + y: nodeData.y + collapsedGroupData.y + }))); + selectionData.edges.push(...collapsedGroupData.collapsedData.edges); + } + } + setCollapsed(canvas, groupNode, collapsed) { + groupNode.setData({ ...groupNode.getData(), collapsed }); + canvas.setData(canvas.getData()); + canvas.history.current--; + canvas.pushHistory(canvas.getData()); + canvas.requestSave(); + } + onNodeBBoxRequested(canvas, node, bbox) { + var _a, _b; + const nodeData = node.getData(); + if (nodeData.type !== "group" || !nodeData.collapsed) return; + const collapseElBBox = (_a = node.collapseEl) == null ? void 0 : _a.getBoundingClientRect(); + if (!collapseElBBox) return; + const labelElBBox = (_b = node.labelEl) == null ? void 0 : _b.getBoundingClientRect(); + if (!labelElBBox) return; + const minPos = canvas.posFromClient({ x: collapseElBBox.left, y: collapseElBBox.top }); + const maxPos = canvas.posFromClient({ x: labelElBBox.right, y: collapseElBBox.bottom }); + bbox.minX = minPos.x; + bbox.minY = minPos.y; + bbox.maxX = maxPos.x; + bbox.maxY = maxPos.y; + } + expandNodes(data) { + var _a; + if (!data) return; + data.nodes = (_a = data.nodes) == null ? void 0 : _a.flatMap((groupNodeData) => { + const collapsedData = groupNodeData.collapsedData; + if (collapsedData === void 0) return [groupNodeData]; + delete groupNodeData.collapsedData; + data.edges.push(...collapsedData.edges); + return [groupNodeData, ...collapsedData.nodes.map((nodeData) => ({ + ...nodeData, + // Restore the relative position of the node to the group + x: nodeData.x + groupNodeData.x, + y: nodeData.y + groupNodeData.y + }))]; + }); + } + collapseNodes(data) { + var _a; + (_a = data == null ? void 0 : data.nodes) == null ? void 0 : _a.forEach((groupNodeData) => { + var _a2, _b, _c, _d; + if (!groupNodeData.collapsed) return; + const groupNodeBBox = CanvasHelper.getBBox([groupNodeData]); + const containedNodesData = data.nodes.filter( + (nodeData) => nodeData.id !== groupNodeData.id && BBoxHelper.insideBBox(CanvasHelper.getBBox([nodeData]), groupNodeBBox, false) + ); + const containedEdgesData = data.edges.filter((edgeData) => { + return containedNodesData.some((nodeData) => nodeData.id === edgeData.fromNode) || containedNodesData.some((nodeData) => nodeData.id === edgeData.toNode); + }); + data.nodes = data.nodes.filter((nodeData) => !containedNodesData.includes(nodeData)); + data.edges = data.edges.filter((edgeData) => !containedEdgesData.includes(edgeData)); + const newContainedNodesData = containedNodesData.filter((nodeData) => { + var _a3, _b2, _c2; + return !((_c2 = (_b2 = (_a3 = groupNodeData.collapsedData) == null ? void 0 : _a3.nodes) == null ? void 0 : _b2.some((e) => e.id === nodeData.id)) != null ? _c2 : false); + }); + const newContainedEdgesData = containedEdgesData.filter((edgeData) => { + var _a3, _b2, _c2; + return !((_c2 = (_b2 = (_a3 = groupNodeData.collapsedData) == null ? void 0 : _a3.edges) == null ? void 0 : _b2.some((n) => n.id === edgeData.id)) != null ? _c2 : false); + }); + groupNodeData.collapsedData = { + nodes: [ + ...(_b = (_a2 = groupNodeData.collapsedData) == null ? void 0 : _a2.nodes) != null ? _b : [], + ...newContainedNodesData.map((nodeData) => ({ + ...nodeData, + // Store the relative position of the node to the group + x: nodeData.x - groupNodeData.x, + y: nodeData.y - groupNodeData.y + })) + ], + edges: [ + ...(_d = (_c = groupNodeData.collapsedData) == null ? void 0 : _c.edges) != null ? _d : [], + ...newContainedEdgesData + ] + }; + }); + } +}; + +// src/canvas-extensions/focus-mode-canvas-extension.ts +var CONTROL_MENU_FOCUS_TOGGLE_ID = "focus-mode-toggle"; +var FocusModeCanvasExtension = class extends CanvasExtension { + isEnabled() { + return "focusModeFeatureEnabled"; + } + init() { + this.plugin.addCommand({ + id: "toggle-focus-mode", + name: "Toggle Focus Mode", + checkCallback: CanvasHelper.canvasCommand( + this.plugin, + (_canvas) => true, + (canvas) => this.toggleFocusMode(canvas) + ) + }); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:canvas-changed", + (canvas) => this.addControlMenuToggle(canvas) + )); + } + addControlMenuToggle(canvas) { + var _a; + const settingsContainer = (_a = canvas.quickSettingsButton) == null ? void 0 : _a.parentElement; + if (!settingsContainer) return; + const controlMenuFocusToggle = CanvasHelper.createControlMenuButton({ + id: CONTROL_MENU_FOCUS_TOGGLE_ID, + label: "Focus Mode", + icon: "focus", + callback: () => this.toggleFocusMode(canvas) + }); + CanvasHelper.addControlMenuButton(settingsContainer, controlMenuFocusToggle); + } + toggleFocusMode(canvas) { + var _a, _b; + const controlMenuFocusToggle = (_b = (_a = canvas.quickSettingsButton) == null ? void 0 : _a.parentElement) == null ? void 0 : _b.querySelector(`#${CONTROL_MENU_FOCUS_TOGGLE_ID}`); + if (!controlMenuFocusToggle) return; + const newValue = controlMenuFocusToggle.dataset.toggled !== "true"; + canvas.wrapperEl.dataset.focusModeEnabled = newValue.toString(); + controlMenuFocusToggle.dataset.toggled = newValue.toString(); + } +}; + +// src/canvas-extensions/auto-file-node-edges-canvas-extension.ts +var AUTO_EDGE_ID_PREFIX = "afe"; +var AutoFileNodeEdgesCanvasExtension = class extends CanvasExtension { + isEnabled() { + return "autoFileNodeEdgesFeatureEnabled"; + } + init() { + this.plugin.registerEvent(this.plugin.app.metadataCache.on("changed", (file) => { + for (const canvas of this.plugin.getCanvases()) + this.onMetadataChanged(canvas, file); + })); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:node-added", + (canvas, node) => this.onNodeChanged(canvas, node) + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:node-changed", + (canvas, node) => this.onNodeChanged(canvas, node) + )); + } + onMetadataChanged(canvas, file) { + var _a; + for (const node of canvas.nodes.values()) { + if (node.getData().type !== "file" || ((_a = node.file) == null ? void 0 : _a.path) !== file.path) continue; + this.updateFileNodeEdges(canvas, node); + } + } + onNodeChanged(canvas, node) { + if (node.getData().type !== "file") return; + for (const node2 of canvas.nodes.values()) { + if (node2.getData().type !== "file") continue; + this.updateFileNodeEdges(canvas, node2); + } + } + updateFileNodeEdges(canvas, node) { + const edges = this.getFileNodeEdges(canvas, node); + const newEdges = Array.from(edges.values()).filter((edge) => !canvas.edges.has(edge.id)); + canvas.importData({ nodes: [], edges: newEdges }, false, false); + for (const edge of canvas.edges.values()) { + if (edge.id.startsWith(`${AUTO_EDGE_ID_PREFIX}${node.id}`) && !edges.has(edge.id)) + canvas.removeEdge(edge); + } + } + getFileNodeEdges(canvas, node) { + var _a, _b; + const canvasFile = canvas.view.file; + if (!canvasFile || !node.file) return /* @__PURE__ */ new Map(); + const fileMetadata = this.plugin.app.metadataCache.getFileCache(node.file); + if (!fileMetadata) return /* @__PURE__ */ new Map(); + const linkedFilesFrontmatterKey = this.plugin.settings.getSetting("autoFileNodeEdgesFrontmatterKey"); + const fileLinksToBeLinkedTo = (_b = (_a = fileMetadata.frontmatterLinks) == null ? void 0 : _a.filter((link) => link.key.split(".")[0] === linkedFilesFrontmatterKey)) != null ? _b : []; + const filepathsToBeLinkedTo = fileLinksToBeLinkedTo.map((link) => this.plugin.app.metadataCache.getFirstLinkpathDest(link.link, canvasFile.path)).map((file) => file == null ? void 0 : file.path).filter((path) => path !== null); + const nodesToBeLinkedTo = Array.from(canvas.nodes.values()).filter((otherNode) => { + var _a2; + return otherNode.id !== node.id && filepathsToBeLinkedTo.includes((_a2 = otherNode.file) == null ? void 0 : _a2.path); + }); + const newEdges = /* @__PURE__ */ new Map(); + for (const otherNode of nodesToBeLinkedTo) { + const edgeId = `${AUTO_EDGE_ID_PREFIX}${node.id}${otherNode.id}`; + const bestFromSide = CanvasHelper.getBestSideForFloatingEdge(BBoxHelper.getCenterOfBBoxSide(otherNode.getBBox(), "right"), node); + const bestToSide = CanvasHelper.getBestSideForFloatingEdge(BBoxHelper.getCenterOfBBoxSide(node.getBBox(), "left"), otherNode); + newEdges.set(edgeId, { + id: edgeId, + fromNode: node.id, + fromSide: bestFromSide, + fromFloating: true, + toNode: otherNode.id, + toSide: bestToSide, + toFloating: true + }); + } + return newEdges; + } +}; + +// src/canvas-extensions/flip-edge-canvas-extension.ts +var FlipEdgeCanvasExtension = class extends CanvasExtension { + isEnabled() { + return "flipEdgeFeatureEnabled"; + } + init() { + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:popup-menu-created", + (canvas) => this.onPopupMenuCreated(canvas) + )); + } + onPopupMenuCreated(canvas) { + var _a, _b; + const popupMenuEl = (_a = canvas == null ? void 0 : canvas.menu) == null ? void 0 : _a.menuEl; + if (!popupMenuEl) return; + const POSSIBLE_ICONS = ["lucide-arrow-right", "lucide-move-horizontal", "line-horizontal"]; + let edgeDirectionButton = null; + for (const icon of POSSIBLE_ICONS) { + edgeDirectionButton = (_b = popupMenuEl.querySelector(`button:not([id]) > .svg-icon.${icon}`)) == null ? void 0 : _b.parentElement; + if (edgeDirectionButton) break; + } + if (!edgeDirectionButton) return; + edgeDirectionButton.addEventListener("click", () => this.onEdgeDirectionDropdownCreated(canvas)); + } + onEdgeDirectionDropdownCreated(canvas) { + const dropdownEl = activeDocument.body.querySelector("div.menu"); + if (!dropdownEl) return; + const separatorEl = CanvasHelper.createDropdownSeparatorElement(); + dropdownEl.appendChild(separatorEl); + const flipEdgeButton = CanvasHelper.createDropdownOptionElement({ + icon: "flip-horizontal-2", + label: "Flip Edge", + callback: () => this.flipEdge(canvas) + }); + dropdownEl.appendChild(flipEdgeButton); + } + flipEdge(canvas) { + const selectedEdges = [...canvas.selection].filter( + (item) => item.path !== void 0 + ); + if (selectedEdges.length === 0) return; + for (const edge of selectedEdges) { + edge.update({ + ...edge.from, + node: edge.to.node, + side: edge.to.side + }, { + ...edge.to, + node: edge.from.node, + side: edge.from.side + }); + } + canvas.pushHistory(canvas.getData()); + } +}; + +// src/canvas-extensions/edge-selection-canvas-extension.ts +var DIRECTION_MENU_MAP = { + connected: { + id: "select-connected-edges", + icon: "arrows-selected", + label: "Select Connected Edges" + }, + outgoing: { + id: "select-outgoing-edges", + icon: "arrow-right-selected", + label: "Select Outgoing Edges" + }, + incoming: { + id: "select-incoming-edges", + icon: "arrow-left-selected", + label: "Select Incoming Edges" + } +}; +var EdgeSelectionCanvasExtension = class extends CanvasExtension { + isEnabled() { + return "edgeSelectionEnabled"; + } + init() { + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:popup-menu-created", + (canvas) => this.onPopupMenuCreated(canvas) + )); + } + onPopupMenuCreated(canvas) { + var _a; + const popupMenuEl = (_a = canvas == null ? void 0 : canvas.menu) == null ? void 0 : _a.menuEl; + if (!popupMenuEl) return; + const selectionNodeData = canvas.getSelectionData().nodes; + if (canvas.readonly || selectionNodeData.length === 0) return; + const selectEdgeByDirection = this.plugin.settings.getSetting("selectEdgeByDirection"); + const menuDirectionSet = /* @__PURE__ */ new Set(["connected"]); + if (selectionNodeData.length === 1) { + const node = canvas.nodes.get(selectionNodeData[0].id); + if (!node) return; + const edges = canvas.getEdgesForNode(node); + if (edges.length === 0) return; + if (selectEdgeByDirection) { + edges.forEach((edge) => { + if (edge.from.node === node) { + menuDirectionSet.add("outgoing"); + } else if (edge.to.node === node) { + menuDirectionSet.add("incoming"); + } + }); + } + } else if (selectEdgeByDirection) { + menuDirectionSet.add("outgoing"); + menuDirectionSet.add("incoming"); + } + menuDirectionSet.forEach((direction) => { + const config = DIRECTION_MENU_MAP[direction]; + CanvasHelper.addPopupMenuOption(canvas, CanvasHelper.createPopupMenuOption({ + ...config, + callback: () => CanvasHelper.selectEdgesForNodes(canvas, direction) + })); + }); + } +}; + +// node_modules/html-to-image/es/util.js +function resolveUrl(url, baseUrl) { + if (url.match(/^[a-z]+:\/\//i)) { + return url; + } + if (url.match(/^\/\//)) { + return window.location.protocol + url; + } + if (url.match(/^[a-z]+:/i)) { + return url; + } + const doc = document.implementation.createHTMLDocument(); + const base = doc.createElement("base"); + const a = doc.createElement("a"); + doc.head.appendChild(base); + doc.body.appendChild(a); + if (baseUrl) { + base.href = baseUrl; + } + a.href = url; + return a.href; +} +var uuid = /* @__PURE__ */ (() => { + let counter = 0; + const random = () => ( + // eslint-disable-next-line no-bitwise + `0000${(Math.random() * 36 ** 4 << 0).toString(36)}`.slice(-4) + ); + return () => { + counter += 1; + return `u${random()}${counter}`; + }; +})(); +function toArray(arrayLike) { + const arr = []; + for (let i = 0, l = arrayLike.length; i < l; i++) { + arr.push(arrayLike[i]); + } + return arr; +} +function px(node, styleProperty) { + const win = node.ownerDocument.defaultView || window; + const val = win.getComputedStyle(node).getPropertyValue(styleProperty); + return val ? parseFloat(val.replace("px", "")) : 0; +} +function getNodeWidth(node) { + const leftBorder = px(node, "border-left-width"); + const rightBorder = px(node, "border-right-width"); + return node.clientWidth + leftBorder + rightBorder; +} +function getNodeHeight(node) { + const topBorder = px(node, "border-top-width"); + const bottomBorder = px(node, "border-bottom-width"); + return node.clientHeight + topBorder + bottomBorder; +} +function getImageSize(targetNode, options = {}) { + const width = options.width || getNodeWidth(targetNode); + const height = options.height || getNodeHeight(targetNode); + return { width, height }; +} +function getPixelRatio() { + let ratio; + let FINAL_PROCESS; + try { + FINAL_PROCESS = process; + } catch (e) { + } + const val = FINAL_PROCESS && FINAL_PROCESS.env ? FINAL_PROCESS.env.devicePixelRatio : null; + if (val) { + ratio = parseInt(val, 10); + if (Number.isNaN(ratio)) { + ratio = 1; + } + } + return ratio || window.devicePixelRatio || 1; +} +var canvasDimensionLimit = 16384; +function checkCanvasDimensions(canvas) { + if (canvas.width > canvasDimensionLimit || canvas.height > canvasDimensionLimit) { + if (canvas.width > canvasDimensionLimit && canvas.height > canvasDimensionLimit) { + if (canvas.width > canvas.height) { + canvas.height *= canvasDimensionLimit / canvas.width; + canvas.width = canvasDimensionLimit; + } else { + canvas.width *= canvasDimensionLimit / canvas.height; + canvas.height = canvasDimensionLimit; + } + } else if (canvas.width > canvasDimensionLimit) { + canvas.height *= canvasDimensionLimit / canvas.width; + canvas.width = canvasDimensionLimit; + } else { + canvas.width *= canvasDimensionLimit / canvas.height; + canvas.height = canvasDimensionLimit; + } + } +} +function createImage(url) { + return new Promise((resolve, reject) => { + const img = new Image(); + img.decode = () => resolve(img); + img.onload = () => resolve(img); + img.onerror = reject; + img.crossOrigin = "anonymous"; + img.decoding = "async"; + img.src = url; + }); +} +async function svgToDataURL(svg) { + return Promise.resolve().then(() => new XMLSerializer().serializeToString(svg)).then(encodeURIComponent).then((html) => `data:image/svg+xml;charset=utf-8,${html}`); +} +async function nodeToDataURL(node, width, height) { + const xmlns = "http://www.w3.org/2000/svg"; + const svg = document.createElementNS(xmlns, "svg"); + const foreignObject = document.createElementNS(xmlns, "foreignObject"); + svg.setAttribute("width", `${width}`); + svg.setAttribute("height", `${height}`); + svg.setAttribute("viewBox", `0 0 ${width} ${height}`); + foreignObject.setAttribute("width", "100%"); + foreignObject.setAttribute("height", "100%"); + foreignObject.setAttribute("x", "0"); + foreignObject.setAttribute("y", "0"); + foreignObject.setAttribute("externalResourcesRequired", "true"); + svg.appendChild(foreignObject); + foreignObject.appendChild(node); + return svgToDataURL(svg); +} +var isInstanceOfElement = (node, instance) => { + if (node instanceof instance) + return true; + const nodePrototype = Object.getPrototypeOf(node); + if (nodePrototype === null) + return false; + return nodePrototype.constructor.name === instance.name || isInstanceOfElement(nodePrototype, instance); +}; + +// node_modules/html-to-image/es/clone-pseudos.js +function formatCSSText(style) { + const content = style.getPropertyValue("content"); + return `${style.cssText} content: '${content.replace(/'|"/g, "")}';`; +} +function formatCSSProperties(style) { + return toArray(style).map((name) => { + const value = style.getPropertyValue(name); + const priority = style.getPropertyPriority(name); + return `${name}: ${value}${priority ? " !important" : ""};`; + }).join(" "); +} +function getPseudoElementStyle(className, pseudo, style) { + const selector = `.${className}:${pseudo}`; + const cssText = style.cssText ? formatCSSText(style) : formatCSSProperties(style); + return document.createTextNode(`${selector}{${cssText}}`); +} +function clonePseudoElement(nativeNode, clonedNode, pseudo) { + const style = window.getComputedStyle(nativeNode, pseudo); + const content = style.getPropertyValue("content"); + if (content === "" || content === "none") { + return; + } + const className = uuid(); + try { + clonedNode.className = `${clonedNode.className} ${className}`; + } catch (err) { + return; + } + const styleElement = document.createElement("style"); + styleElement.appendChild(getPseudoElementStyle(className, pseudo, style)); + clonedNode.appendChild(styleElement); +} +function clonePseudoElements(nativeNode, clonedNode) { + clonePseudoElement(nativeNode, clonedNode, ":before"); + clonePseudoElement(nativeNode, clonedNode, ":after"); +} + +// node_modules/html-to-image/es/mimes.js +var WOFF = "application/font-woff"; +var JPEG = "image/jpeg"; +var mimes = { + woff: WOFF, + woff2: WOFF, + ttf: "application/font-truetype", + eot: "application/vnd.ms-fontobject", + png: "image/png", + jpg: JPEG, + jpeg: JPEG, + gif: "image/gif", + tiff: "image/tiff", + svg: "image/svg+xml", + webp: "image/webp" +}; +function getExtension(url) { + const match = /\.([^./]*?)$/g.exec(url); + return match ? match[1] : ""; +} +function getMimeType(url) { + const extension = getExtension(url).toLowerCase(); + return mimes[extension] || ""; +} + +// node_modules/html-to-image/es/dataurl.js +function getContentFromDataUrl(dataURL) { + return dataURL.split(/,/)[1]; +} +function isDataUrl(url) { + return url.search(/^(data:)/) !== -1; +} +function makeDataUrl(content, mimeType) { + return `data:${mimeType};base64,${content}`; +} +async function fetchAsDataURL(url, init, process2) { + throw Error("fetch call was blocked by AC"); + if (res.status === 404) { + throw new Error(`Resource "${res.url}" not found`); + } + const blob = await res.blob(); + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onerror = reject; + reader.onloadend = () => { + try { + resolve(process2({ res, result: reader.result })); + } catch (error) { + reject(error); + } + }; + reader.readAsDataURL(blob); + }); +} +var cache = {}; +function getCacheKey(url, contentType, includeQueryParams) { + let key = url.replace(/\?.*/, ""); + if (includeQueryParams) { + key = url; + } + if (/ttf|otf|eot|woff2?/i.test(key)) { + key = key.replace(/.*\//, ""); + } + return contentType ? `[${contentType}]${key}` : key; +} +async function resourceToDataURL(resourceUrl, contentType, options) { + const cacheKey = getCacheKey(resourceUrl, contentType, options.includeQueryParams); + if (cache[cacheKey] != null) { + return cache[cacheKey]; + } + if (options.cacheBust) { + resourceUrl += (/\?/.test(resourceUrl) ? "&" : "?") + (/* @__PURE__ */ new Date()).getTime(); + } + let dataURL; + try { + const content = await fetchAsDataURL(resourceUrl, options.fetchRequestInit, ({ res: res2, result }) => { + if (!contentType) { + contentType = res2.headers.get("Content-Type") || ""; + } + return getContentFromDataUrl(result); + }); + dataURL = makeDataUrl(content, contentType); + } catch (error) { + dataURL = options.imagePlaceholder || ""; + let msg = `Failed to fetch resource: ${resourceUrl}`; + if (error) { + msg = typeof error === "string" ? error : error.message; + } + if (msg) { + console.warn(msg); + } + } + cache[cacheKey] = dataURL; + return dataURL; +} + +// node_modules/html-to-image/es/clone-node.js +async function cloneCanvasElement(canvas) { + const dataURL = canvas.toDataURL(); + if (dataURL === "data:,") { + return canvas.cloneNode(false); + } + return createImage(dataURL); +} +async function cloneVideoElement(video, options) { + if (video.currentSrc) { + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + canvas.width = video.clientWidth; + canvas.height = video.clientHeight; + ctx === null || ctx === void 0 ? void 0 : ctx.drawImage(video, 0, 0, canvas.width, canvas.height); + const dataURL2 = canvas.toDataURL(); + return createImage(dataURL2); + } + const poster = video.poster; + const contentType = getMimeType(poster); + const dataURL = await resourceToDataURL(poster, contentType, options); + return createImage(dataURL); +} +async function cloneIFrameElement(iframe) { + var _a; + try { + if ((_a = iframe === null || iframe === void 0 ? void 0 : iframe.contentDocument) === null || _a === void 0 ? void 0 : _a.body) { + return await cloneNode(iframe.contentDocument.body, {}, true); + } + } catch (_b) { + } + return iframe.cloneNode(false); +} +async function cloneSingleNode(node, options) { + if (isInstanceOfElement(node, HTMLCanvasElement)) { + return cloneCanvasElement(node); + } + if (isInstanceOfElement(node, HTMLVideoElement)) { + return cloneVideoElement(node, options); + } + if (isInstanceOfElement(node, HTMLIFrameElement)) { + return cloneIFrameElement(node); + } + return node.cloneNode(false); +} +var isSlotElement = (node) => node.tagName != null && node.tagName.toUpperCase() === "SLOT"; +async function cloneChildren(nativeNode, clonedNode, options) { + var _a, _b; + let children = []; + if (isSlotElement(nativeNode) && nativeNode.assignedNodes) { + children = toArray(nativeNode.assignedNodes()); + } else if (isInstanceOfElement(nativeNode, HTMLIFrameElement) && ((_a = nativeNode.contentDocument) === null || _a === void 0 ? void 0 : _a.body)) { + children = toArray(nativeNode.contentDocument.body.childNodes); + } else { + children = toArray(((_b = nativeNode.shadowRoot) !== null && _b !== void 0 ? _b : nativeNode).childNodes); + } + if (children.length === 0 || isInstanceOfElement(nativeNode, HTMLVideoElement)) { + return clonedNode; + } + await children.reduce((deferred, child) => deferred.then(() => cloneNode(child, options)).then((clonedChild) => { + if (clonedChild) { + clonedNode.appendChild(clonedChild); + } + }), Promise.resolve()); + return clonedNode; +} +function cloneCSSStyle(nativeNode, clonedNode) { + const targetStyle = clonedNode.style; + if (!targetStyle) { + return; + } + const sourceStyle = window.getComputedStyle(nativeNode); + if (sourceStyle.cssText) { + targetStyle.cssText = sourceStyle.cssText; + targetStyle.transformOrigin = sourceStyle.transformOrigin; + } else { + toArray(sourceStyle).forEach((name) => { + let value = sourceStyle.getPropertyValue(name); + if (name === "font-size" && value.endsWith("px")) { + const reducedFont = Math.floor(parseFloat(value.substring(0, value.length - 2))) - 0.1; + value = `${reducedFont}px`; + } + if (isInstanceOfElement(nativeNode, HTMLIFrameElement) && name === "display" && value === "inline") { + value = "block"; + } + if (name === "d" && clonedNode.getAttribute("d")) { + value = `path(${clonedNode.getAttribute("d")})`; + } + targetStyle.setProperty(name, value, sourceStyle.getPropertyPriority(name)); + }); + } +} +function cloneInputValue(nativeNode, clonedNode) { + if (isInstanceOfElement(nativeNode, HTMLTextAreaElement)) { + clonedNode.innerHTML = nativeNode.value; + } + if (isInstanceOfElement(nativeNode, HTMLInputElement)) { + clonedNode.setAttribute("value", nativeNode.value); + } +} +function cloneSelectValue(nativeNode, clonedNode) { + if (isInstanceOfElement(nativeNode, HTMLSelectElement)) { + const clonedSelect = clonedNode; + const selectedOption = Array.from(clonedSelect.children).find((child) => nativeNode.value === child.getAttribute("value")); + if (selectedOption) { + selectedOption.setAttribute("selected", ""); + } + } +} +function decorate(nativeNode, clonedNode) { + if (isInstanceOfElement(clonedNode, Element)) { + cloneCSSStyle(nativeNode, clonedNode); + clonePseudoElements(nativeNode, clonedNode); + cloneInputValue(nativeNode, clonedNode); + cloneSelectValue(nativeNode, clonedNode); + } + return clonedNode; +} +async function ensureSVGSymbols(clone, options) { + const uses = clone.querySelectorAll ? clone.querySelectorAll("use") : []; + if (uses.length === 0) { + return clone; + } + const processedDefs = {}; + for (let i = 0; i < uses.length; i++) { + const use = uses[i]; + const id = use.getAttribute("xlink:href"); + if (id) { + const exist = clone.querySelector(id); + const definition = document.querySelector(id); + if (!exist && definition && !processedDefs[id]) { + processedDefs[id] = await cloneNode(definition, options, true); + } + } + } + const nodes = Object.values(processedDefs); + if (nodes.length) { + const ns = "http://www.w3.org/1999/xhtml"; + const svg = document.createElementNS(ns, "svg"); + svg.setAttribute("xmlns", ns); + svg.style.position = "absolute"; + svg.style.width = "0"; + svg.style.height = "0"; + svg.style.overflow = "hidden"; + svg.style.display = "none"; + const defs = document.createElementNS(ns, "defs"); + svg.appendChild(defs); + for (let i = 0; i < nodes.length; i++) { + defs.appendChild(nodes[i]); + } + clone.appendChild(svg); + } + return clone; +} +async function cloneNode(node, options, isRoot) { + if (!isRoot && options.filter && !options.filter(node)) { + return null; + } + return Promise.resolve(node).then((clonedNode) => cloneSingleNode(clonedNode, options)).then((clonedNode) => cloneChildren(node, clonedNode, options)).then((clonedNode) => decorate(node, clonedNode)).then((clonedNode) => ensureSVGSymbols(clonedNode, options)); +} + +// node_modules/html-to-image/es/embed-resources.js +var URL_REGEX = /url\((['"]?)([^'"]+?)\1\)/g; +var URL_WITH_FORMAT_REGEX = /url\([^)]+\)\s*format\((["']?)([^"']+)\1\)/g; +var FONT_SRC_REGEX = /src:\s*(?:url\([^)]+\)\s*format\([^)]+\)[,;]\s*)+/g; +function toRegex(url) { + const escaped = url.replace(/([.*+?^${}()|\[\]\/\\])/g, "\\$1"); + return new RegExp(`(url\\(['"]?)(${escaped})(['"]?\\))`, "g"); +} +function parseURLs(cssText) { + const urls = []; + cssText.replace(URL_REGEX, (raw, quotation, url) => { + urls.push(url); + return raw; + }); + return urls.filter((url) => !isDataUrl(url)); +} +async function embed(cssText, resourceURL, baseURL, options, getContentFromUrl) { + try { + const resolvedURL = baseURL ? resolveUrl(resourceURL, baseURL) : resourceURL; + const contentType = getMimeType(resourceURL); + let dataURL; + if (getContentFromUrl) { + const content = await getContentFromUrl(resolvedURL); + dataURL = makeDataUrl(content, contentType); + } else { + dataURL = await resourceToDataURL(resolvedURL, contentType, options); + } + return cssText.replace(toRegex(resourceURL), `$1${dataURL}$3`); + } catch (error) { + } + return cssText; +} +function filterPreferredFontFormat(str, { preferredFontFormat }) { + return !preferredFontFormat ? str : str.replace(FONT_SRC_REGEX, (match) => { + while (true) { + const [src, , format] = URL_WITH_FORMAT_REGEX.exec(match) || []; + if (!format) { + return ""; + } + if (format === preferredFontFormat) { + return `src: ${src};`; + } + } + }); +} +function shouldEmbed(url) { + return url.search(URL_REGEX) !== -1; +} +async function embedResources(cssText, baseUrl, options) { + if (!shouldEmbed(cssText)) { + return cssText; + } + const filteredCSSText = filterPreferredFontFormat(cssText, options); + const urls = parseURLs(filteredCSSText); + return urls.reduce((deferred, url) => deferred.then((css) => embed(css, url, baseUrl, options)), Promise.resolve(filteredCSSText)); +} + +// node_modules/html-to-image/es/embed-images.js +async function embedProp(propName, node, options) { + var _a; + const propValue = (_a = node.style) === null || _a === void 0 ? void 0 : _a.getPropertyValue(propName); + if (propValue) { + const cssString = await embedResources(propValue, null, options); + node.style.setProperty(propName, cssString, node.style.getPropertyPriority(propName)); + return true; + } + return false; +} +async function embedBackground(clonedNode, options) { + if (!await embedProp("background", clonedNode, options)) { + await embedProp("background-image", clonedNode, options); + } + if (!await embedProp("mask", clonedNode, options)) { + await embedProp("mask-image", clonedNode, options); + } +} +async function embedImageNode(clonedNode, options) { + const isImageElement = isInstanceOfElement(clonedNode, HTMLImageElement); + if (!(isImageElement && !isDataUrl(clonedNode.src)) && !(isInstanceOfElement(clonedNode, SVGImageElement) && !isDataUrl(clonedNode.href.baseVal))) { + return; + } + const url = isImageElement ? clonedNode.src : clonedNode.href.baseVal; + const dataURL = await resourceToDataURL(url, getMimeType(url), options); + await new Promise((resolve, reject) => { + clonedNode.onload = resolve; + clonedNode.onerror = reject; + const image = clonedNode; + if (image.decode) { + image.decode = resolve; + } + if (image.loading === "lazy") { + image.loading = "eager"; + } + if (isImageElement) { + clonedNode.srcset = ""; + clonedNode.src = dataURL; + } else { + clonedNode.href.baseVal = dataURL; + } + }); +} +async function embedChildren(clonedNode, options) { + const children = toArray(clonedNode.childNodes); + const deferreds = children.map((child) => embedImages(child, options)); + await Promise.all(deferreds).then(() => clonedNode); +} +async function embedImages(clonedNode, options) { + if (isInstanceOfElement(clonedNode, Element)) { + await embedBackground(clonedNode, options); + await embedImageNode(clonedNode, options); + await embedChildren(clonedNode, options); + } +} + +// node_modules/html-to-image/es/apply-style.js +function applyStyle(node, options) { + const { style } = node; + if (options.backgroundColor) { + style.backgroundColor = options.backgroundColor; + } + if (options.width) { + style.width = `${options.width}px`; + } + if (options.height) { + style.height = `${options.height}px`; + } + const manual = options.style; + if (manual != null) { + Object.keys(manual).forEach((key) => { + style[key] = manual[key]; + }); + } + return node; +} + +// node_modules/html-to-image/es/embed-webfonts.js +var cssFetchCache = {}; +async function fetchCSS(url) { + let cache2 = cssFetchCache[url]; + if (cache2 != null) { + return cache2; + } + throw Error("fetch call was blocked by AC"); + const cssText = await res.text(); + cache2 = { url, cssText }; + cssFetchCache[url] = cache2; + return cache2; +} +async function embedFonts(data, options) { + let cssText = data.cssText; + const regexUrl = /url\(["']?([^"')]+)["']?\)/g; + const fontLocs = cssText.match(/url\([^)]+\)/g) || []; + const loadFonts = fontLocs.map(async (loc) => { + let url = loc.replace(regexUrl, "$1"); + if (!url.startsWith("https://")) { + url = new URL(url, data.url).href; + } + return fetchAsDataURL(url, options.fetchRequestInit, ({ result }) => { + cssText = cssText.replace(loc, `url(${result})`); + return [loc, result]; + }); + }); + return Promise.all(loadFonts).then(() => cssText); +} +function parseCSS(source) { + if (source == null) { + return []; + } + const result = []; + const commentsRegex = /(\/\*[\s\S]*?\*\/)/gi; + let cssText = source.replace(commentsRegex, ""); + const keyframesRegex = new RegExp("((@.*?keyframes [\\s\\S]*?){([\\s\\S]*?}\\s*?)})", "gi"); + while (true) { + const matches = keyframesRegex.exec(cssText); + if (matches === null) { + break; + } + result.push(matches[0]); + } + cssText = cssText.replace(keyframesRegex, ""); + const importRegex = /@import[\s\S]*?url\([^)]*\)[\s\S]*?;/gi; + const combinedCSSRegex = "((\\s*?(?:\\/\\*[\\s\\S]*?\\*\\/)?\\s*?@media[\\s\\S]*?){([\\s\\S]*?)}\\s*?})|(([\\s\\S]*?){([\\s\\S]*?)})"; + const unifiedRegex = new RegExp(combinedCSSRegex, "gi"); + while (true) { + let matches = importRegex.exec(cssText); + if (matches === null) { + matches = unifiedRegex.exec(cssText); + if (matches === null) { + break; + } else { + importRegex.lastIndex = unifiedRegex.lastIndex; + } + } else { + unifiedRegex.lastIndex = importRegex.lastIndex; + } + result.push(matches[0]); + } + return result; +} +async function getCSSRules(styleSheets, options) { + const ret = []; + const deferreds = []; + styleSheets.forEach((sheet) => { + if ("cssRules" in sheet) { + try { + toArray(sheet.cssRules || []).forEach((item, index) => { + if (item.type === CSSRule.IMPORT_RULE) { + let importIndex = index + 1; + const url = item.href; + const deferred = fetchCSS(url).then((metadata) => embedFonts(metadata, options)).then((cssText) => parseCSS(cssText).forEach((rule) => { + try { + sheet.insertRule(rule, rule.startsWith("@import") ? importIndex += 1 : sheet.cssRules.length); + } catch (error) { + console.error("Error inserting rule from remote css", { + rule, + error + }); + } + })).catch((e) => { + console.error("Error loading remote css", e.toString()); + }); + deferreds.push(deferred); + } + }); + } catch (e) { + const inline = styleSheets.find((a) => a.href == null) || document.styleSheets[0]; + if (sheet.href != null) { + deferreds.push(fetchCSS(sheet.href).then((metadata) => embedFonts(metadata, options)).then((cssText) => parseCSS(cssText).forEach((rule) => { + inline.insertRule(rule, sheet.cssRules.length); + })).catch((err) => { + console.error("Error loading remote stylesheet", err); + })); + } + console.error("Error inlining remote css file", e); + } + } + }); + return Promise.all(deferreds).then(() => { + styleSheets.forEach((sheet) => { + if ("cssRules" in sheet) { + try { + toArray(sheet.cssRules || []).forEach((item) => { + ret.push(item); + }); + } catch (e) { + console.error(`Error while reading CSS rules from ${sheet.href}`, e); + } + } + }); + return ret; + }); +} +function getWebFontRules(cssRules) { + return cssRules.filter((rule) => rule.type === CSSRule.FONT_FACE_RULE).filter((rule) => shouldEmbed(rule.style.getPropertyValue("src"))); +} +async function parseWebFontRules(node, options) { + if (node.ownerDocument == null) { + throw new Error("Provided element is not within a Document"); + } + const styleSheets = toArray(node.ownerDocument.styleSheets); + const cssRules = await getCSSRules(styleSheets, options); + return getWebFontRules(cssRules); +} +async function getWebFontCSS(node, options) { + const rules = await parseWebFontRules(node, options); + const cssTexts = await Promise.all(rules.map((rule) => { + const baseUrl = rule.parentStyleSheet ? rule.parentStyleSheet.href : null; + return embedResources(rule.cssText, baseUrl, options); + })); + return cssTexts.join("\n"); +} +async function embedWebFonts(clonedNode, options) { + const cssText = options.fontEmbedCSS != null ? options.fontEmbedCSS : options.skipFonts ? null : await getWebFontCSS(clonedNode, options); + if (cssText) { + const styleNode = document.createElement("style"); + const sytleContent = document.createTextNode(cssText); + styleNode.appendChild(sytleContent); + if (clonedNode.firstChild) { + clonedNode.insertBefore(styleNode, clonedNode.firstChild); + } else { + clonedNode.appendChild(styleNode); + } + } +} + +// node_modules/html-to-image/es/index.js +async function toSvg(node, options = {}) { + const { width, height } = getImageSize(node, options); + const clonedNode = await cloneNode(node, options, true); + await embedWebFonts(clonedNode, options); + await embedImages(clonedNode, options); + applyStyle(clonedNode, options); + const datauri = await nodeToDataURL(clonedNode, width, height); + return datauri; +} +async function toCanvas(node, options = {}) { + const { width, height } = getImageSize(node, options); + const svg = await toSvg(node, options); + const img = await createImage(svg); + const canvas = document.createElement("canvas"); + const context = canvas.getContext("2d"); + const ratio = options.pixelRatio || getPixelRatio(); + const canvasWidth = options.canvasWidth || width; + const canvasHeight = options.canvasHeight || height; + canvas.width = canvasWidth * ratio; + canvas.height = canvasHeight * ratio; + if (!options.skipAutoScale) { + checkCanvasDimensions(canvas); + } + canvas.style.width = `${canvasWidth}`; + canvas.style.height = `${canvasHeight}`; + if (options.backgroundColor) { + context.fillStyle = options.backgroundColor; + context.fillRect(0, 0, canvas.width, canvas.height); + } + context.drawImage(img, 0, 0, canvas.width, canvas.height); + return canvas; +} +async function toPng(node, options = {}) { + const canvas = await toCanvas(node, options); + return canvas.toDataURL(); +} + +// src/canvas-extensions/export-canvas-extension.ts +var import_obsidian20 = require("obsidian"); +var MAX_ALLOWED_LOADING_TIME = 1e4; +var ExportCanvasExtension = class extends CanvasExtension { + isEnabled() { + return "betterExportFeatureEnabled"; + } + init() { + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:node-breakpoint-changed", + (canvas, node, breakpointRef) => { + if (canvas.screenshotting) breakpointRef.value = true; + } + )); + this.plugin.addCommand({ + id: "export-all-as-image", + name: "Export canvas as image", + checkCallback: CanvasHelper.canvasCommand( + this.plugin, + (canvas) => canvas.nodes.size > 0, + (canvas) => void this.showExportImageSettingsModal(canvas, null) + ) + }); + this.plugin.addCommand({ + id: "export-selected-as-image", + name: "Export selected nodes as image", + checkCallback: CanvasHelper.canvasCommand( + this.plugin, + (canvas) => canvas.selection.size > 0, + (canvas) => void this.showExportImageSettingsModal( + canvas, + canvas.getSelectionData().nodes.map((nodeData) => canvas.nodes.get(nodeData.id)).filter((node) => node !== void 0) + ) + ) + }); + } + async showExportImageSettingsModal(canvas, nodesToExport) { + const modal = new import_obsidian20.Modal(this.plugin.app); + modal.setTitle("Export image settings"); + let pixelRatioSetting = null; + let noFontExportSetting = null; + let transparentBackgroundSetting = null; + const updateDynamicSettings = () => { + var _a, _b, _c, _d, _e, _f; + if (svg) { + (_a = pixelRatioSetting == null ? void 0 : pixelRatioSetting.settingEl) == null ? void 0 : _a.hide(); + (_b = noFontExportSetting == null ? void 0 : noFontExportSetting.settingEl) == null ? void 0 : _b.show(); + (_c = transparentBackgroundSetting == null ? void 0 : transparentBackgroundSetting.settingEl) == null ? void 0 : _c.hide(); + } else { + (_d = pixelRatioSetting == null ? void 0 : pixelRatioSetting.settingEl) == null ? void 0 : _d.show(); + (_e = noFontExportSetting == null ? void 0 : noFontExportSetting.settingEl) == null ? void 0 : _e.hide(); + (_f = transparentBackgroundSetting == null ? void 0 : transparentBackgroundSetting.settingEl) == null ? void 0 : _f.show(); + } + }; + let svg = false; + new import_obsidian20.Setting(modal.contentEl).setName("Export file format").setDesc("Choose the file format to export the canvas as.").addDropdown( + (dropdown) => dropdown.addOptions({ + png: "PNG", + svg: "SVG" + }).setValue(svg ? "svg" : "png").onChange((value) => { + svg = value === "svg"; + updateDynamicSettings(); + }) + ); + let pixelRatioFactor = 1; + pixelRatioSetting = new import_obsidian20.Setting(modal.contentEl).setName("Pixel ratio").setDesc("Higher pixel ratios result in higher resolution images but also larger file sizes.").addSlider( + (slider) => slider.setDynamicTooltip().setLimits(0.2, 5, 0.1).setValue(pixelRatioFactor).onChange((value) => pixelRatioFactor = value) + ); + let noFontExport = true; + noFontExportSetting = new import_obsidian20.Setting(modal.contentEl).setName("Skip font export").setDesc("This will not include the fonts in the exported SVG. This will make the SVG file smaller.").addToggle( + (toggle) => toggle.setValue(noFontExport).onChange((value) => noFontExport = value) + ); + let theme = activeDocument.body.classList.contains("theme-dark") ? "dark" : "light"; + new import_obsidian20.Setting(modal.contentEl).setName("Theme").setDesc("The theme used for the export.").addDropdown( + (dropdown) => dropdown.addOptions({ + light: "Light", + dark: "Dark" + }).setValue(theme).onChange((value) => theme = value) + ); + let watermark = false; + new import_obsidian20.Setting(modal.contentEl).setName("Show logo").setDesc("This will add an Obsidian + Advanced Canvas logo to the bottom left.").addToggle( + (toggle) => toggle.setValue(watermark).onChange((value) => watermark = value) + ); + let garbledText = false; + new import_obsidian20.Setting(modal.contentEl).setName("Privacy mode").setDesc("This will obscure any text on your canvas.").addToggle( + (toggle) => toggle.setValue(garbledText).onChange((value) => garbledText = value) + ); + let transparentBackground = false; + transparentBackgroundSetting = new import_obsidian20.Setting(modal.contentEl).setName("Transparent background").setDesc("This will make the background of the image transparent.").addToggle( + (toggle) => toggle.setValue(transparentBackground).onChange((value) => transparentBackground = value) + ); + new import_obsidian20.Setting(modal.contentEl).addButton( + (button) => button.setButtonText("Save").setCta().onClick(async () => { + modal.close(); + await this.exportImage( + canvas, + nodesToExport, + svg, + svg ? 1 : pixelRatioFactor, + svg ? noFontExport : false, + theme, + watermark, + garbledText, + svg ? true : transparentBackground + ); + }) + ); + updateDynamicSettings(); + modal.open(); + } + async exportImage(canvas, nodesToExport, svg, pixelRatioFactor, noFontExport, theme, watermark, garbledText, transparentBackground) { + var _a, _b, _c; + const cachedTheme = activeDocument.body.classList.contains("theme-dark") ? "dark" : "light"; + if (theme !== cachedTheme) { + activeDocument.body.classList.toggle("theme-dark", theme === "dark"); + activeDocument.body.classList.toggle("theme-light", theme === "light"); + } + const isWholeCanvas = nodesToExport === null; + if (!nodesToExport) nodesToExport = [...canvas.nodes.values()]; + const nodesToExportIds = nodesToExport.map((node) => node.getData().id); + const edgesToExport = [...canvas.edges.values()].filter((edge) => { + const edgeData = edge.getData(); + return nodesToExportIds.includes(edgeData.fromNode) && nodesToExportIds.includes(edgeData.toNode); + }); + const backgroundColor = transparentBackground ? void 0 : window.getComputedStyle(canvas.canvasEl).getPropertyValue("--canvas-background"); + new import_obsidian20.Notice("Exporting the canvas. Please wait..."); + const interactionBlocker = this.getInteractionBlocker(); + activeDocument.body.appendChild(interactionBlocker); + canvas.screenshotting = true; + canvas.canvasEl.classList.add("is-exporting"); + if (garbledText) canvas.wrapperEl.classList.add("is-text-garbled"); + let watermarkEl = null; + const cachedSelection = new Set(canvas.selection); + canvas.deselectAll(); + const cachedViewport = { x: canvas.x, y: canvas.y, zoom: canvas.zoom }; + try { + const targetBoundingBox = CanvasHelper.getBBox([...nodesToExport, ...edgesToExport]); + let enlargedTargetBoundingBox = BBoxHelper.scaleBBox(targetBoundingBox, 1.1); + const enlargedTargetBoundingBoxSize = { width: enlargedTargetBoundingBox.maxX - enlargedTargetBoundingBox.minX, height: enlargedTargetBoundingBox.maxY - enlargedTargetBoundingBox.minY }; + const canvasElSize = { width: canvas.canvasEl.clientWidth, height: canvas.canvasEl.clientHeight }; + const requiredPixelRatio = Math.max(enlargedTargetBoundingBoxSize.width / canvasElSize.width, enlargedTargetBoundingBoxSize.height / canvasElSize.height); + const pixelRatio = svg ? void 0 : Math.round(requiredPixelRatio * pixelRatioFactor); + watermarkEl = watermark ? this.getWatermark(enlargedTargetBoundingBox) : null; + if (watermarkEl) canvas.canvasEl.appendChild(watermarkEl); + const actualAspectRatio = canvas.canvasRect.width / canvas.canvasRect.height; + const targetAspectRatio = (enlargedTargetBoundingBox.maxX - enlargedTargetBoundingBox.minX) / (enlargedTargetBoundingBox.maxY - enlargedTargetBoundingBox.minY); + let adjustedBoundingBox = { ...enlargedTargetBoundingBox }; + if (actualAspectRatio > targetAspectRatio) { + const targetHeight = enlargedTargetBoundingBox.maxY - enlargedTargetBoundingBox.minY; + const actualWidth = targetHeight * actualAspectRatio; + adjustedBoundingBox.maxX = enlargedTargetBoundingBox.minX + actualWidth; + } else { + const targetWidth = enlargedTargetBoundingBox.maxX - enlargedTargetBoundingBox.minX; + const actualHeight = targetWidth / actualAspectRatio; + adjustedBoundingBox.maxY = enlargedTargetBoundingBox.minY + actualHeight; + } + canvas.zoomToRealBbox(adjustedBoundingBox); + canvas.setViewport(canvas.tx, canvas.ty, canvas.tZoom); + await sleep(10); + let canvasScale = parseFloat(((_a = canvas.canvasEl.style.transform.match(/scale\((\d+(\.\d+)?)\)/)) == null ? void 0 : _a[1]) || "1"); + const edgePathsBBox = BBoxHelper.combineBBoxes(edgesToExport.map((edge) => { + const edgeCenter = edge.getCenter(); + const labelWidth = edge.labelElement ? edge.labelElement.wrapperEl.getBoundingClientRect().width / canvasScale : 0; + return { minX: edgeCenter.x - labelWidth / 2, minY: edgeCenter.y, maxX: edgeCenter.x + labelWidth / 2, maxY: edgeCenter.y }; + })); + const enlargedEdgePathsBBox = BBoxHelper.enlargeBBox(edgePathsBBox, 1.1); + enlargedTargetBoundingBox = BBoxHelper.combineBBoxes([enlargedTargetBoundingBox, enlargedEdgePathsBBox]); + adjustedBoundingBox = BBoxHelper.combineBBoxes([adjustedBoundingBox, enlargedEdgePathsBBox]); + canvas.zoomToRealBbox(adjustedBoundingBox); + canvas.setViewport(canvas.tx, canvas.ty, canvas.tZoom); + await sleep(10); + const canvasViewportBBox = canvas.getViewportBBox(); + canvasScale = parseFloat(((_b = canvas.canvasEl.style.transform.match(/scale\((\d+(\.\d+)?)\)/)) == null ? void 0 : _b[1]) || "1"); + let width = (canvasViewportBBox.maxX - canvasViewportBBox.minX) * canvasScale; + let height = (canvasViewportBBox.maxY - canvasViewportBBox.minY) * canvasScale; + if (actualAspectRatio > targetAspectRatio) + width = height * targetAspectRatio; + else height = width / targetAspectRatio; + let unloadedNodes = nodesToExport.filter((node) => node.initialized === false || node.isContentMounted === false); + const startTimestamp = performance.now(); + while (unloadedNodes.length > 0 && performance.now() - startTimestamp < MAX_ALLOWED_LOADING_TIME) { + await sleep(10); + unloadedNodes = nodesToExport.filter((node) => node.initialized === false || node.isContentMounted === false); + } + if (unloadedNodes.length === 0) { + const nodeElements = nodesToExport.map((node) => node.nodeEl); + const edgePathAndArrowElements = edgesToExport.map((edge) => [edge.lineGroupEl, edge.lineEndGroupEl]).flat(); + const edgeLabelElements = edgesToExport.map((edge) => { + var _a2; + return (_a2 = edge.labelElement) == null ? void 0 : _a2.wrapperEl; + }).filter((labelElement) => labelElement !== void 0); + const filter = (element) => { + var _a2, _b2, _c2, _d; + if (((_a2 = element.classList) == null ? void 0 : _a2.contains("canvas-node")) && !nodeElements.includes(element)) + return false; + if (((_c2 = (_b2 = element.parentElement) == null ? void 0 : _b2.classList) == null ? void 0 : _c2.contains("canvas-edges")) && !edgePathAndArrowElements.includes(element)) + return false; + if (((_d = element.classList) == null ? void 0 : _d.contains("canvas-path-label-wrapper")) && !edgeLabelElements.includes(element)) + return false; + return true; + }; + const options = { + pixelRatio, + backgroundColor, + height, + width, + filter + }; + if (noFontExport) options.fontEmbedCSS = ""; + let imageDataUri = svg ? await toSvg(canvas.canvasEl, options) : await toPng(canvas.canvasEl, options); + if (svg) { + const header = ``; + imageDataUri = imageDataUri.replace( + encodeURIComponent(" canvas.selection = cachedSelection); + canvas.setViewport(cachedViewport.x, cachedViewport.y, cachedViewport.zoom); + interactionBlocker.remove(); + if (theme !== cachedTheme) { + activeDocument.body.classList.toggle("theme-dark", cachedTheme === "dark"); + activeDocument.body.classList.toggle("theme-light", cachedTheme === "light"); + } + } + } + getInteractionBlocker() { + const interactionBlocker = activeDocument.createElement("div"); + interactionBlocker.classList.add("progress-bar-container"); + const progressBar = interactionBlocker.createDiv(); + progressBar.classList.add("progress-bar"); + const progressBarMessage = progressBar.createDiv(); + progressBarMessage.classList.add("progress-bar-message", "u-center-text"); + progressBarMessage.innerText = "Generating image..."; + const progressBarIndicator = progressBar.createDiv(); + progressBarIndicator.classList.add("progress-bar-indicator"); + const progressBarLine = progressBarIndicator.createDiv(); + progressBarLine.classList.add("progress-bar-line"); + const progressBarSublineIncrease = progressBarIndicator.createDiv(); + progressBarSublineIncrease.classList.add("progress-bar-subline", "mod-increase"); + const progressBarSublineDecrease = progressBarIndicator.createDiv(); + progressBarSublineDecrease.classList.add("progress-bar-subline", "mod-decrease"); + return interactionBlocker; + } + getWatermark(bbox) { + const bboxWidth = bbox.maxX - bbox.minX; + const width = Math.max(200, bboxWidth * 0.3); + const WATERMARK_SIZE = { width: 215, height: 25 }; + const height = WATERMARK_SIZE.height / WATERMARK_SIZE.width * width; + const watermarkPadding = { + x: bboxWidth * 0.02, + y: bboxWidth * 0.014 + }; + bbox.maxY += height + watermarkPadding.y; + const watermarkEl = activeDocument.createElementNS("http://www.w3.org/2000/svg", "svg"); + watermarkEl.id = "watermark-ac"; + watermarkEl.style.transform = `translate(${bbox.minX + watermarkPadding.x}px, ${bbox.maxY - height - watermarkPadding.y}px)`; + watermarkEl.setAttrs({ + viewBox: `0 0 ${WATERMARK_SIZE.width} ${WATERMARK_SIZE.height}`, + width: width.toString(), + fill: "currentColor" + }); + watermarkEl.innerHTML = ''; + return watermarkEl; + } +}; + +// src/canvas-extensions/floating-edge-canvas-extension.ts +var FloatingEdgeCanvasExtension = class extends CanvasExtension { + isEnabled() { + return "floatingEdgeFeatureEnabled"; + } + init() { + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:data-loaded:after", + (canvas, data, setData) => this.onLoadData(canvas, data) + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:node-moved", + (canvas, node) => this.onNodeMoved(canvas, node) + )); + if (this.plugin.settings.getSetting("allowFloatingEdgeCreation")) { + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:edge-connection-dragging:before", + (canvas, edge, event, newEdge, side) => this.onEdgeStartedDragging(canvas, edge, event, newEdge, side) + )); + } + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:edge-connection-dragging:after", + (canvas, edge, event, newEdge, side) => this.onEdgeStoppedDragging(canvas, edge, event, newEdge, side) + )); + } + onLoadData(canvas, data) { + for (const edgeData of data.edges) { + const edge = canvas.edges.get(edgeData.id); + if (!edge) return console.warn("Imported edge is not yet loaded :("); + this.updateEdgeConnectionSide(edge); + } + } + onNodeMoved(canvas, node) { + const affectedEdges = canvas.getEdgesForNode(node); + for (const edge of affectedEdges) + this.updateEdgeConnectionSide(edge); + } + updateEdgeConnectionSide(edge) { + const edgeData = edge.getData(); + if (edgeData.fromFloating) { + const fixedNodeConnectionPoint = BBoxHelper.getCenterOfBBoxSide(edge.to.node.getBBox(), edge.to.side); + const bestSide = CanvasHelper.getBestSideForFloatingEdge(fixedNodeConnectionPoint, edge.from.node); + if (bestSide !== edge.from.side) { + edge.setData({ + ...edgeData, + fromSide: bestSide + }); + } + } + if (edgeData.toFloating) { + const fixedNodeConnectionPoint = BBoxHelper.getCenterOfBBoxSide(edge.from.node.getBBox(), edge.from.side); + const bestSide = CanvasHelper.getBestSideForFloatingEdge(fixedNodeConnectionPoint, edge.to.node); + if (bestSide !== edge.to.side) { + edge.setData({ + ...edgeData, + toSide: bestSide + }); + } + } + } + onEdgeStartedDragging(canvas, edge, _event, newEdge, _side) { + if (newEdge && this.plugin.settings.getSetting("newEdgeFromSideFloating")) edge.setData({ + ...edge.getData(), + fromFloating: true + // New edges can only get dragged from the "from" side + }); + let cachedViewportNodes = null; + let hasNaNFloatingEdgeDropZones = false; + this.onPointerMove = (event) => { + if (cachedViewportNodes === null || hasNaNFloatingEdgeDropZones || canvas.viewportChanged) { + hasNaNFloatingEdgeDropZones = false; + cachedViewportNodes = canvas.getViewportNodes().map((node) => { + const nodeFloatingEdgeDropZone = this.getFloatingEdgeDropZoneForNode(node); + if (isNaN(nodeFloatingEdgeDropZone.minX) || isNaN(nodeFloatingEdgeDropZone.minY) || isNaN(nodeFloatingEdgeDropZone.maxX) || isNaN(nodeFloatingEdgeDropZone.maxY)) + hasNaNFloatingEdgeDropZones = true; + return [node, nodeFloatingEdgeDropZone]; + }); + } + for (const [node, nodeFloatingEdgeDropZoneClientRect] of cachedViewportNodes) { + const hovering = BBoxHelper.insideBBox({ x: event.clientX, y: event.clientY }, nodeFloatingEdgeDropZoneClientRect, true); + node.nodeEl.classList.toggle("hovering-floating-edge-zone", hovering); + } + }; + activeDocument.addEventListener("pointermove", this.onPointerMove); + } + onEdgeStoppedDragging(_canvas, edge, event, _newEdge, side) { + activeDocument.removeEventListener("pointermove", this.onPointerMove); + const dropZoneNode = side === "from" ? edge.from.node : edge.to.node; + const floatingEdgeDropZone = this.getFloatingEdgeDropZoneForNode(dropZoneNode); + const wasDroppedInFloatingEdgeDropZone = this.plugin.settings.getSetting("allowFloatingEdgeCreation") ? BBoxHelper.insideBBox({ x: event.clientX, y: event.clientY }, floatingEdgeDropZone, true) : false; + const edgeData = edge.getData(); + if (side === "from" && wasDroppedInFloatingEdgeDropZone == edgeData.fromFloating) return; + if (side === "to" && wasDroppedInFloatingEdgeDropZone == edgeData.toFloating) return; + if (side === "from") edgeData.fromFloating = wasDroppedInFloatingEdgeDropZone; + else edgeData.toFloating = wasDroppedInFloatingEdgeDropZone; + edge.setData(edgeData); + this.updateEdgeConnectionSide(edge); + } + getFloatingEdgeDropZoneForNode(node) { + const nodeElClientBoundingRect = node.nodeEl.getBoundingClientRect(); + const nodeFloatingEdgeDropZoneElStyle = window.getComputedStyle(node.nodeEl, ":after"); + const nodeFloatingEdgeDropZoneSize = { + width: parseFloat(nodeFloatingEdgeDropZoneElStyle.getPropertyValue("width")), + height: parseFloat(nodeFloatingEdgeDropZoneElStyle.getPropertyValue("height")) + }; + return { + minX: nodeElClientBoundingRect.left + (nodeElClientBoundingRect.width - nodeFloatingEdgeDropZoneSize.width) / 2, + minY: nodeElClientBoundingRect.top + (nodeElClientBoundingRect.height - nodeFloatingEdgeDropZoneSize.height) / 2, + maxX: nodeElClientBoundingRect.right - (nodeElClientBoundingRect.width - nodeFloatingEdgeDropZoneSize.width) / 2, + maxY: nodeElClientBoundingRect.bottom - (nodeElClientBoundingRect.height - nodeFloatingEdgeDropZoneSize.height) / 2 + }; + } +}; + +// src/canvas-extensions/edge-highlight-canvas-extension.ts +var EdgeHighlightCanvasExtension = class extends CanvasExtension { + isEnabled() { + return "edgeHighlightEnabled"; + } + init() { + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:selection-changed", + (canvas, oldSelection, updateSelection) => this.onSelectionChanged(canvas, oldSelection) + )); + } + onSelectionChanged(canvas, oldSelection) { + var _a, _b, _c, _d, _e; + const connectedEdgesToBeHighlighted = new Set(canvas.getSelectionData().nodes.flatMap((nodeData) => { + var _a2, _b2; + return [ + ...(_a2 = canvas.edgeFrom.get(canvas.nodes.get(nodeData.id))) != null ? _a2 : [], + ...this.plugin.settings.getSetting("highlightIncomingEdges") ? (_b2 = canvas.edgeTo.get(canvas.nodes.get(nodeData.id))) != null ? _b2 : [] : [] + ]; + })); + for (const edge of canvas.edges.values()) { + const isFocused = canvas.selection.has(edge) || connectedEdgesToBeHighlighted.has(edge); + edge.lineGroupEl.classList.toggle("is-focused", isFocused); + (_b = (_a = edge.lineEndGroupEl) == null ? void 0 : _a.classList) == null ? void 0 : _b.toggle("is-focused", isFocused); + (_e = (_d = (_c = edge.labelElement) == null ? void 0 : _c.textareaEl) == null ? void 0 : _d.classList) == null ? void 0 : _e.toggle("is-focused", isFocused); + } + } +}; + +// src/canvas-extensions/dataset-exposers/canvas-metadata-exposer.ts +var CanvasMetadataExposerExtension = class extends CanvasExtension { + isEnabled() { + return true; + } + init() { + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:canvas-metadata-changed", + (canvas) => this.updateExposedSettings(canvas) + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:canvas-changed", + (canvas) => this.updateExposedSettings(canvas) + )); + } + updateExposedSettings(canvas) { + const startNodeId = canvas.metadata["startNode"]; + for (const [nodeId, node] of canvas.nodes) { + if (nodeId === startNodeId) node.nodeEl.dataset.isStartNode = "true"; + else delete node.nodeEl.dataset.isStartNode; + } + } +}; + +// src/canvas-extensions/dataset-exposers/node-exposer.ts +var CANVAS_NODE_IFRAME_BODY_CLASS = "canvas-node-iframe-body"; +function getExposedNodeData(settings) { + const exposedData = []; + if (settings.getSetting("nodeStylingFeatureEnabled")) exposedData.push("styleAttributes"); + if (settings.getSetting("collapsibleGroupsFeatureEnabled")) exposedData.push("collapsed"); + if (settings.getSetting("portalsFeatureEnabled")) exposedData.push("isPortalLoaded"); + return exposedData; +} +var NodeExposerExtension = class extends CanvasExtension { + isEnabled() { + return true; + } + init() { + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:node-changed", + (_canvas, node) => { + var _a, _b; + const nodeData = node == null ? void 0 : node.getData(); + if (!nodeData) return; + this.setDataAttributes(node.nodeEl, nodeData); + const iframe = (_b = (_a = node.nodeEl.querySelector("iframe")) == null ? void 0 : _a.contentDocument) == null ? void 0 : _b.body; + if (iframe) this.setDataAttributes(iframe, nodeData); + } + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:node-editing-state-changed", + (_canvas, node, editing) => { + var _a, _b; + if (!editing) return; + const nodeData = node.getData(); + if (!nodeData) return; + const iframe = (_b = (_a = node.nodeEl.querySelector("iframe")) == null ? void 0 : _a.contentDocument) == null ? void 0 : _b.body; + if (!iframe) return; + iframe.classList.add(CANVAS_NODE_IFRAME_BODY_CLASS); + new MutationObserver(() => iframe.classList.toggle(CANVAS_NODE_IFRAME_BODY_CLASS, true)).observe(iframe, { attributes: true, attributeFilter: ["class"] }); + this.setDataAttributes(iframe, nodeData); + } + )); + } + setDataAttributes(element, nodeData) { + for (const exposedDataKey of getExposedNodeData(this.plugin.settings)) { + const datasetPairs = nodeData[exposedDataKey] && typeof nodeData[exposedDataKey] === "object" ? Object.entries(nodeData[exposedDataKey]) : [[exposedDataKey, nodeData[exposedDataKey]]]; + for (const [key, value] of datasetPairs) { + if (!value) delete element.dataset[key]; + else element.dataset[key] = value; + } + } + } +}; + +// src/canvas-extensions/dataset-exposers/node-interaction-exposer.ts +var TARGET_NODE_DATASET_PREFIX = "target"; +var NodeInteractionExposerExtension = class extends CanvasExtension { + isEnabled() { + return true; + } + init() { + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:node-interaction", + (canvas, node) => { + const nodeData = node == null ? void 0 : node.getData(); + if (!nodeData) return; + const interactionEl = canvas.nodeInteractionLayer.interactionEl; + if (!interactionEl) return; + for (const exposedDataKey of getExposedNodeData(this.plugin.settings)) { + const datasetPairs = nodeData[exposedDataKey] instanceof Object ? Object.entries(nodeData[exposedDataKey]) : [[exposedDataKey, nodeData[exposedDataKey]]]; + for (const [key, value] of datasetPairs) { + const modifiedKey = TARGET_NODE_DATASET_PREFIX + key.toString().charAt(0).toUpperCase() + key.toString().slice(1); + if (!value) delete interactionEl.dataset[modifiedKey]; + else interactionEl.dataset[modifiedKey] = value; + } + } + if (PortalsCanvasExtension.isPortalElement(node.id)) interactionEl.dataset.isFromPortal = "true"; + else delete interactionEl.dataset.isFromPortal; + } + )); + } +}; + +// src/canvas-extensions/dataset-exposers/edge-exposer.ts +function getExposedEdgeData(settings) { + const exposedData = []; + if (settings.getSetting("edgesStylingFeatureEnabled")) exposedData.push("styleAttributes"); + return exposedData; +} +var EdgeExposerExtension = class extends CanvasExtension { + isEnabled() { + return true; + } + init() { + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:edge-changed", + (_canvas, edge) => { + var _a, _b, _c, _d; + const edgeData = edge == null ? void 0 : edge.getData(); + if (!edgeData) return; + for (const exposedDataKey of getExposedEdgeData(this.plugin.settings)) { + const datasetPairs = edgeData[exposedDataKey] && typeof edgeData[exposedDataKey] === "object" ? Object.entries(edgeData[exposedDataKey]) : [[exposedDataKey, edgeData[exposedDataKey]]]; + for (const [key, value] of datasetPairs) { + if (!value) { + delete edge.path.display.dataset[key]; + if ((_a = edge.fromLineEnd) == null ? void 0 : _a.el) delete edge.fromLineEnd.el.dataset[key]; + if ((_b = edge.toLineEnd) == null ? void 0 : _b.el) delete edge.toLineEnd.el.dataset[key]; + } else { + edge.path.display.dataset[key] = value; + if ((_c = edge.fromLineEnd) == null ? void 0 : _c.el) edge.fromLineEnd.el.dataset[key] = value; + if ((_d = edge.toLineEnd) == null ? void 0 : _d.el) edge.toLineEnd.el.dataset[key] = value; + } + } + } + } + )); + } +}; + +// src/canvas-extensions/dataset-exposers/canvas-wrapper-exposer.ts +var EXPOSED_SETTINGS = [ + "disableFontSizeRelativeToZoom", + "hideBackgroundGridWhenInReadonly", + "collapsibleGroupsFeatureEnabled", + "collapsedGroupPreviewOnDrag", + "allowFloatingEdgeCreation" +]; +var CanvasWrapperExposerExtension = class extends CanvasExtension { + isEnabled() { + return true; + } + init() { + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:settings-changed", + () => this.updateExposedSettings(this.plugin.getCurrentCanvas()) + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:canvas-changed", + (canvas) => this.updateExposedSettings(canvas) + )); + this.plugin.registerEvent(this.plugin.app.workspace.on( + "advanced-canvas:dragging-state-changed", + (canvas, dragging) => { + if (dragging) canvas.wrapperEl.dataset.isDragging = "true"; + else delete canvas.wrapperEl.dataset.isDragging; + } + )); + } + updateExposedSettings(canvas) { + if (!canvas) return; + for (const setting of EXPOSED_SETTINGS) { + canvas.wrapperEl.dataset[setting] = JSON.stringify(this.plugin.settings.getSetting(setting)); + } + } +}; + +// src/patchers/bases-table-view-patcher.ts +var BasesTableViewPatcher = class extends Patcher { + async patch() { + if (!this.plugin.settings.getSetting("canvasMetadataCompatibilityEnabled")) return; + const bases = this.plugin.app.internalPlugins.getEnabledPluginById("bases"); + if (!bases) return; + void this.patchViewFactory(bases); + } + async patchViewFactory(bases) { + const that = this; + await Patcher.patchOnce(this.plugin, bases.registrations.table, (resolve) => ({ + factory: Patcher.OverrideExisting((next) => function(...args) { + const view = next.call(this, ...args); + void that.patchTableView(view); + resolve(view); + return view; + }) + })); + } + async patchTableView(basesView) { + const that = this; + await Patcher.patchOnce(this.plugin, basesView, (resolve) => ({ + updateVirtualDisplay: Patcher.OverrideExisting((next) => function(...args) { + const result = next.call(this, ...args); + if (this.rows.length > 0) { + const row = this.rows.first(); + void that.patchTableRow(row); + resolve(row); + } + return result; + }) + })); + } + async patchTableRow(row) { + const that = this; + await Patcher.patchOnce(this.plugin, row, (resolve) => ({ + render: Patcher.OverrideExisting((next) => function(...args) { + let result = next.call(this, ...args); + if (this.cells.length > 0) { + const cell = this.cells.first(); + void that.patchTableCell(cell); + resolve(cell); + result = next.call(this, ...args); + } + return result; + }) + })); + } + async patchTableCell(cell) { + Patcher.patchPrototype(this.plugin, cell, { + render: Patcher.OverrideExisting((next) => function(ctx) { + var _a; + const isCanvas = ((_a = ctx.file) == null ? void 0 : _a.extension) === "canvas"; + if (isCanvas) ctx.file.extension = "md"; + const result = next.call(this, ctx); + if (isCanvas) ctx.file.extension = "canvas"; + return result; + }) + }); + } +}; + +// src/main.ts +var PATCHERS = [ + // Core canvas patchers + CanvasPatcher, + SearchCommandPatcher, + // Core metadata patchers + MetadataCachePatcher, + FileManagerPatcher, + // Direct metadata dependant patchers + PropertiesPatcher, + !(0, import_obsidian21.requireApiVersion)("1.12.0") && BacklinksPatcher, + OutgoingLinksPatcher, + // Metadata dependant patchers + (0, import_obsidian21.requireApiVersion)("1.9.0") && BasesTableViewPatcher, + LinkSuggestionsPatcher, + EmbedPatcher, + SearchPatcher +]; +var CANVAS_EXTENSIONS = [ + // Advanced JSON Canvas Extensions + MetadataCanvasExtension, + NodeStylesExtension, + EdgeStylesExtension, + NodeRatioCanvasExtension, + FloatingEdgeCanvasExtension, + AutoResizeNodeCanvasExtension, + CollapsibleGroupsCanvasExtension, + ColorPaletteCanvasExtension, + PresentationCanvasExtension, + PortalsCanvasExtension, + // UI Extensions (Non-savable data) + CanvasMetadataExposerExtension, + CanvasWrapperExposerExtension, + NodeExposerExtension, + EdgeExposerExtension, + NodeInteractionExposerExtension, + FrontmatterControlButtonCanvasExtension, + BetterDefaultSettingsCanvasExtension, + CommandsCanvasExtension, + BetterReadonlyCanvasExtension, + GroupCanvasExtension, + NodeTemplatesCanvasExtension, + VariableBreakpointCanvasExtension, + EdgeHighlightCanvasExtension, + AutoFileNodeEdgesCanvasExtension, + FlipEdgeCanvasExtension, + ZOrderingCanvasExtension, + ExportCanvasExtension, + FocusModeCanvasExtension, + EncapsulateCanvasExtension, + EdgeSelectionCanvasExtension, + CopyNodeReferenceCanvasExtension +]; +var AdvancedCanvasPlugin = class extends import_obsidian21.Plugin { + async onload() { + IconsHelper.addIcons(); + this.settings = new SettingsManager(this); + await this.settings.loadSettings(); + this.settings.addSettingsTab(); + this.windowsManager = new WindowsManager(this); + this.patchers = PATCHERS.map((Patcher2) => { + if (!Patcher2) return; + try { + return new Patcher2(this); + } catch (e) { + console.error(`Error initializing patcher ${Patcher2.name}:`, e); + } + }); + this.canvasExtensions = CANVAS_EXTENSIONS.map((Extension) => { + try { + return new Extension(this); + } catch (e) { + console.error(`Error initializing ac-extension ${Extension.name}:`, e); + } + }); + } + onunload() { + } + getCanvases() { + return this.app.workspace.getLeavesOfType("canvas").map((leaf) => { + var _a; + return (_a = leaf.view) == null ? void 0 : _a.canvas; + }).filter((canvas) => canvas); + } + getCurrentCanvasView() { + const canvasView = this.app.workspace.getActiveViewOfType(import_obsidian21.ItemView); + if ((canvasView == null ? void 0 : canvasView.getViewType()) !== "canvas") return null; + return canvasView; + } + getCurrentCanvas() { + var _a; + return ((_a = this.getCurrentCanvasView()) == null ? void 0 : _a.canvas) || null; + } + createFileSnapshot(path, content) { + var _a; + const fileRecoveryPlugin = (_a = this.app.internalPlugins.plugins["file-recovery"]) == null ? void 0 : _a.instance; + if (!fileRecoveryPlugin) return; + fileRecoveryPlugin.forceAdd(path, content); + } + // this.app.plugins.plugins["advanced-canvas"].enableDebugMode() + enableDebugMode() { + if (this.debugHelper) return; + this.debugHelper = new DebugHelper(this); + } +}; + +/* nosourcemap */ \ No newline at end of file diff --git a/.obsidian/plugins/advanced-canvas/manifest.json b/.obsidian/plugins/advanced-canvas/manifest.json new file mode 100644 index 00000000..82d8928a --- /dev/null +++ b/.obsidian/plugins/advanced-canvas/manifest.json @@ -0,0 +1,11 @@ +{ + "id": "advanced-canvas", + "name": "Advanced Canvas", + "version": "6.4.0", + "minAppVersion": "1.7.2", + "description": "Supercharge your canvas experience! Create presentations, flowcharts and more!", + "author": "Developer-Mike", + "authorUrl": "https://github.com/Developer-Mike", + "fundingUrl": "https://ko-fi.com/X8X27IA08", + "isDesktopOnly": false +} diff --git a/.obsidian/plugins/advanced-canvas/styles.css b/.obsidian/plugins/advanced-canvas/styles.css new file mode 100644 index 00000000..2a2ae01b --- /dev/null +++ b/.obsidian/plugins/advanced-canvas/styles.css @@ -0,0 +1,536 @@ +/* src/styles.scss */ +.ac-settings .properties-field > .setting-item-info { + flex: 0; + margin: 0; + padding: var(--size-4-1) var(--size-4-2); + border: var(--input-border-width) solid var(--background-modifier-border); + border-radius: var(--input-radius) 0 0 var(--input-radius); +} +.ac-settings .properties-field > .setting-item-control > input { + width: 100%; + border-radius: 0 var(--input-radius) var(--input-radius) 0; +} +.ac-settings .ac-settings-heading:not(:first-child) { + margin-top: var(--size-4-10) !important; +} +.ac-settings .ac-settings-heading:has(.checkbox-container:not(.is-enabled)) { + border-bottom-color: var(--background-modifier-border-hover); +} +.ac-settings .ac-settings-heading .setting-item-description { + margin-inline-end: 20px; +} +.ac-settings .settings-header-children { + transform-origin: top center; + transform: scaleY(1); + transition: transform 0.2s ease-in-out; +} +.ac-settings .ac-settings-heading:has(.checkbox-container:not(.is-enabled)) + .settings-header-children { + opacity: 0.5; + pointer-events: none; + height: 0; + transform: scaleY(0); +} +.ac-settings .setting-item > .setting-item-control input[type=text] { + width: 75px; +} +.ac-settings details.setting-item { + flex-direction: column; + align-items: initial; +} +.ac-settings details.setting-item:not([open]) { + row-gap: 0; +} +.ac-settings details.setting-item[open] { + padding-bottom: var(--size-4-2); +} +.ac-settings details.setting-item[open] > summary { + margin-bottom: var(--size-4-2); +} +.ac-settings details.setting-item > *:not(summary) { + border: 1px solid var(--background-primary); +} +.ac-settings .kofi-banner { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + padding: var(--size-4-3); + background-color: var(--background-primary-alt); + border-radius: var(--radius-m); +} +.ac-settings .kofi-banner .ac-kofi-banner-title { + margin: 0; + font-size: var(--font-ui-small); + font-weight: 600; +} +.ac-settings .kofi-banner .ac-kofi-button { + width: min-content; + height: min-content; + line-height: 0; +} +.ac-settings .kofi-banner .ac-kofi-button img { + min-width: 75px; + width: 25%; + max-width: 100px; +} +.canvas-wrapper > .document-search-container { + transform: translateZ(0); + margin: 0; +} +.canvas-wrapper:not(.mod-readonly) .show-while-readonly { + display: none; +} +.canvas-control-item[data-toggled=true] { + background-color: var(--color-accent); +} +.canvas-control-item[data-toggled=true] svg { + stroke: var(--text-on-accent); +} +.reactive-node, +.canvas-node[data-shape=database], +.canvas-node[data-shape=document], +.canvas-node[data-shape=predefined-process], +.canvas-node[data-shape=diamond] { + --border-color: rgb(var(--canvas-color)); + --border-width: 3px; + --box-shadow: none; +} +.reactive-node.is-focused, +.is-focused.canvas-node[data-shape=database], +.is-focused.canvas-node[data-shape=document], +.is-focused.canvas-node[data-shape=predefined-process], +.is-focused.canvas-node[data-shape=diamond], +.reactive-node.is-selected, +.is-selected.canvas-node[data-shape=database], +.is-selected.canvas-node[data-shape=document], +.is-selected.canvas-node[data-shape=predefined-process], +.is-selected.canvas-node[data-shape=diamond] { + --border-color: var(--color-accent); + --border-width: 5px; + --box-shadow: var(--shadow-border-accent); +} +.reactive-node.is-themed, +.is-themed.canvas-node[data-shape=database], +.is-themed.canvas-node[data-shape=document], +.is-themed.canvas-node[data-shape=predefined-process], +.is-themed.canvas-node[data-shape=diamond] { + --border-color: rgba(var(--canvas-color), 0.7); +} +.reactive-node.is-themed.is-focused, +.is-themed.is-focused.canvas-node[data-shape=database], +.is-themed.is-focused.canvas-node[data-shape=document], +.is-themed.is-focused.canvas-node[data-shape=predefined-process], +.is-themed.is-focused.canvas-node[data-shape=diamond], +.reactive-node.is-themed.is-selected, +.is-themed.is-selected.canvas-node[data-shape=database], +.is-themed.is-selected.canvas-node[data-shape=document], +.is-themed.is-selected.canvas-node[data-shape=predefined-process], +.is-themed.is-selected.canvas-node[data-shape=diamond] { + --border-color: rgb(var(--canvas-color)); + --box-shadow: var(--shadow-border-themed); +} +.canvas-node[data-text-align=center] .markdown-preview-view { + scrollbar-gutter: auto; +} +.canvas-node[data-text-align=center] .markdown-preview-view .markdown-preview-section { + display: flex; + flex-direction: column; + justify-content: center; + text-align: center; + vertical-align: middle; +} +.canvas-node[data-text-align=right] { + text-align: right; +} +.canvas-node[data-shape] .canvas-node-container .markdown-preview-view.markdown-rendered { + transform: unset; +} +.canvas-node[data-shape=pill] .canvas-node-container { + border-radius: 5000px; +} +.canvas-node[data-shape=diamond] { +} +.canvas-node[data-shape=diamond].is-focused, +.canvas-node[data-shape=diamond].is-selected { + border-radius: var(--radius-m); + outline: 2px solid var(--color-accent); + outline-offset: 5px; +} +.canvas-node[data-shape=diamond] .canvas-node-container { + border: none; + box-shadow: none !important; +} +.canvas-node[data-shape=diamond] .canvas-node-container:not(:has(.embed-iframe)) { + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 141.42135624 141.42135624' preserveAspectRatio='none'%3E%3Cstyle%3E rect %7B transform-origin: center; transform: rotate(45deg) scale(1.05); %7D %3C/style%3E%3Crect rx='8' x='20.71067812' y='20.71067812' width='100' height='100' /%3E%3C/svg%3E"); + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 141.42135624 141.42135624' preserveAspectRatio='none'%3E%3Cstyle%3E rect %7B transform-origin: center; transform: rotate(45deg) scale(1.05); %7D %3C/style%3E%3Crect rx='8' x='20.71067812' y='20.71067812' width='100' height='100' /%3E%3C/svg%3E"); + mask-repeat: no-repeat; + -webkit-mask-repeat: no-repeat; + mask-size: 100%; + -webkit-mask-size: 100%; +} +.canvas-node[data-shape=diamond] .canvas-node-container .canvas-node-placeholder::after { + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 141.42135624 141.42135624' preserveAspectRatio='none'%3E%3Cstyle%3E rect %7B transform-origin: center; transform: rotate(45deg) scale(1.05); %7D %3C/style%3E%3Crect rx='8' x='20.71067812' y='20.71067812' width='100' height='100' /%3E%3C/svg%3E"); + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 141.42135624 141.42135624' preserveAspectRatio='none'%3E%3Cstyle%3E rect %7B transform-origin: center; transform: rotate(45deg) scale(1.05); %7D %3C/style%3E%3Crect rx='8' x='20.71067812' y='20.71067812' width='100' height='100' /%3E%3C/svg%3E"); + mask-repeat: no-repeat; + -webkit-mask-repeat: no-repeat; + mask-size: 100%; + -webkit-mask-size: 100%; +} +.canvas-node[data-shape=diamond]::before { + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 141.42135624 141.42135624' preserveAspectRatio='none'%3E%3Cstyle%3E rect %7B transform-origin: center; transform: rotate(45deg) scale(1.05); %7D %3C/style%3E%3Crect rx='8' x='20.71067812' y='20.71067812' width='100' height='100' /%3E%3C/svg%3E"); + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 141.42135624 141.42135624' preserveAspectRatio='none'%3E%3Cstyle%3E rect %7B transform-origin: center; transform: rotate(45deg) scale(1.05); %7D %3C/style%3E%3Crect rx='8' x='20.71067812' y='20.71067812' width='100' height='100' /%3E%3C/svg%3E"); + mask-repeat: no-repeat; + -webkit-mask-repeat: no-repeat; + mask-size: 100%; + -webkit-mask-size: 100%; + content: ""; + position: absolute; + top: calc(var(--border-width) * -1); + left: calc(var(--border-width) * -1); + width: calc(100% + var(--border-width) * 2); + height: calc(100% + var(--border-width) * 2); + background-color: var(--border-color); +} +.canvas-node[data-shape=parallelogram] .canvas-node-container { + transform: skewX(-20deg); + backface-visibility: hidden; +} +.canvas-node[data-shape=parallelogram] .canvas-node-container .canvas-node-content > .markdown-embed-content { + transform: skewX(20deg); +} +.canvas-node[data-shape=circle] .canvas-node-container { + border-radius: 50%; +} +.canvas-node[data-shape=circle] .canvas-node-container .markdown-preview-view { + overflow-y: initial; +} +.canvas-node[data-shape=predefined-process] .canvas-node-container .canvas-node-content { + padding: 0 10px; +} +.canvas-node[data-shape=predefined-process] .canvas-node-container::before, +.canvas-node[data-shape=predefined-process] .canvas-node-container::after { + content: ""; + z-index: 1; + position: absolute; + top: 0; + width: 0; + height: 100%; + border-left: var(--border-width) solid var(--border-color); +} +.canvas-node[data-shape=predefined-process] .canvas-node-container::before { + left: calc(10px - var(--border-width)); +} +.canvas-node[data-shape=predefined-process] .canvas-node-container::after { + right: calc(10px - var(--border-width)); +} +.canvas-node[data-shape=document] { + --border-width: 2.5px; + filter: drop-shadow(0 var(--border-width) 0 var(--border-color)) drop-shadow(0 calc(var(--border-width) * -1) 0 var(--border-color)); +} +.canvas-node[data-shape=document] .canvas-node-container { + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 75 45' preserveAspectRatio='none'%3E%3Cpath d='M75 0 75 39.375Q56.25 29.25 37.5 39.375 18.75 49.5 0 39.375L0 0Z' /%3E%3C/svg%3E"); + -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 75 45' preserveAspectRatio='none'%3E%3Cpath d='M75 0 75 39.375Q56.25 29.25 37.5 39.375 18.75 49.5 0 39.375L0 0Z' /%3E%3C/svg%3E"); + mask-repeat: no-repeat; + -webkit-mask-repeat: no-repeat; + mask-size: 100%; + -webkit-mask-size: 100%; + border: var(--border-width) solid var(--border-color); + border-top: none; + border-bottom: none; +} +.canvas-node[data-shape=document].is-focused, +.canvas-node[data-shape=document].is-selected { + --border-width: 4px; +} +.canvas-node[data-shape=database] { +} +.canvas-node[data-shape=database] .canvas-node-container { + border: var(--border-width) solid var(--border-color); + border-bottom: 0; + border-top: 0; + border-radius: 0; + box-shadow: none !important; +} +.canvas-node[data-shape=database] .canvas-node-container .canvas-node-placeholder { + transform: translateY(25px); +} +.canvas-node[data-shape=database]::before, +.canvas-node[data-shape=database]::after { + content: ""; + position: absolute; + left: 0; + box-sizing: border-box; + width: 100%; + height: 50px; + border-radius: 50%; + border: var(--border-width) solid var(--border-color); + background-color: var(--background-primary); +} +.canvas-node[data-shape=database]::after { + top: -25px; +} +.canvas-node[data-shape=database]::before { + bottom: -25px; +} +.canvas-node[data-shape=database].is-themed .canvas-node-content { + background-color: transparent; +} +.canvas-node[data-shape=database].is-themed:not(:has(.embed-iframe)) .canvas-node-container, +.canvas-node[data-shape=database].is-themed:not(:has(.embed-iframe))::after, +.canvas-node[data-shape=database].is-themed:not(:has(.embed-iframe))::before { + box-shadow: inset 0 0 0 1000px rgba(var(--canvas-color), 0.07) !important; +} +.canvas-node[data-shape=database] .canvas-node-content:not(:has(.embed-iframe)) { + transform: translateY(20px); +} +.canvas-node[data-shape=database]:has(.embed-iframe)::after { + z-index: -1; +} +.canvas-node[data-border=dashed] .canvas-node-container { + box-shadow: none; + border-style: dashed; +} +.canvas-node[data-border=dotted] .canvas-node-container { + box-shadow: none; + border-style: dotted; +} +.canvas-node[data-border=invisible] { + box-shadow: none; +} +.canvas-node[data-border=invisible]:not(.is-focused):not(.is-selected) .canvas-node-container { + border-color: transparent !important; +} +.canvas-node[data-border=invisible] .canvas-node-label { + display: none; +} +.canvas-node[data-border=invisible] .canvas-node-container { + background-color: transparent; + box-shadow: none; +} +.canvas-node[data-border][data-shape=predefined-process] { + --border-width: 2px; +} +.canvas-node[data-border][data-shape=predefined-process] .is-focused, +.canvas-node[data-border][data-shape=predefined-process] .is-selected { + --border-width: 2px; +} +.canvas-node[data-border=dashed][data-shape=predefined-process] .canvas-node-container::before, +.canvas-node[data-border=dashed][data-shape=predefined-process] .canvas-node-container::after { + border-left: var(--border-width) dashed var(--border-color); +} +.canvas-node[data-border=dotted][data-shape=predefined-process] .canvas-node-container::before, +.canvas-node[data-border=dotted][data-shape=predefined-process] .canvas-node-container::after { + border-left: var(--border-width) dotted var(--border-color); +} +.canvas-node[data-border][data-shape=document] .canvas-node-container { + border-top: none; + border-bottom: none; +} +.canvas-edges path[data-path=dotted] { + stroke-dasharray: calc(3px * var(--zoom-multiplier)); +} +.canvas-edges path[data-path=short-dashed] { + stroke-dasharray: 9px; +} +.canvas-edges path[data-path=long-dashed] { + stroke-dasharray: 18px; +} +.canvas-edges [data-arrow=triangle-outline] polygon, +.canvas-edges [data-arrow=diamond-outline] polygon, +.canvas-edges [data-arrow=circle-outline] polygon { + fill: var(--canvas-background); + stroke: rgb(var(--canvas-color)); + stroke-width: calc(3px * var(--zoom-multiplier)); +} +.canvas-edges [data-arrow=thin-triangle] polygon { + fill: transparent; + stroke: rgb(var(--canvas-color)); + stroke-width: calc(4px * var(--zoom-multiplier)); +} +.icon-modal-suggestion { + display: flex; + align-items: center; + gap: var(--size-4-4); +} +.icon-modal-suggestion .icon-modal-suggestion-icon { + line-height: 0; +} +.icon-modal-suggestion .icon-modal-suggestion-id { + font-size: var(--font-ui-smaller); + color: var(--text-muted); +} +.canvas.is-exporting { + --zoom-multiplier: 1; +} +.canvas.is-exporting * { + pointer-events: none !important; + transition: none !important; +} +.canvas.is-exporting .collapse-button { + display: none; +} +.canvas.is-exporting #watermark-ac { + z-index: 9999999; + position: absolute; +} +.canvas-wrapper[data-collapsible-groups-feature-enabled=true] .canvas.is-exporting .canvas-node .canvas-group-label { + left: 0; +} +.progress-bar-modal-ac { + margin-top: 0.75em; +} +.progress-bar-modal-ac.error .setting-progress-bar { + color: var(--color-error); +} +.canvas-wrapper[data-disable-font-size-relative-to-zoom=true] { + --zoom-multiplier: 1 !important; +} +.canvas-wrapper.mod-readonly .canvas-controls .canvas-control-group:nth-child(3), +.canvas-wrapper.mod-readonly .canvas-card-menu { + display: none; +} +.canvas-wrapper.mod-readonly[data-hide-background-grid-when-in-readonly=true] .canvas-background { + visibility: hidden; +} +.collapse-button { + position: absolute; + left: 0; + top: calc(-1 * var(--size-4-1) * var(--zoom-multiplier)); + padding: var(--size-4-1) var(--size-4-2); + transform-origin: bottom left; + transform: translate(0, -100%) scale(var(--zoom-multiplier)); + border-radius: var(--radius-s); + color: var(--text-muted); + background-color: rgba(var(--canvas-color), 0.1); + font-size: 1.5em; + line-height: 1; + pointer-events: initial; + cursor: pointer; + transition: transform 500ms cubic-bezier(0.16, 1, 0.3, 1); +} +.canvas-wrapper[data-collapsible-groups-feature-enabled=true] .canvas-node .canvas-group-label { + left: calc(40px * var(--zoom-multiplier)); +} +.canvas-node[data-collapsed] .canvas-node-container { + display: none; +} +.canvas-node[data-collapsed] .canvas-group-label { + max-width: initial; +} +.canvas-wrapper[data-collapsed-group-preview-on-drag=true][data-is-dragging] .canvas-node[data-collapsed] .canvas-node-container { + display: block; + opacity: 0.5; + border-style: dashed; +} +.canvas-wrapper[data-collapsed-group-preview-on-drag=true][data-is-dragging] .canvas-node[data-collapsed] .canvas-node-container .canvas-node-content { + background-color: transparent; +} +.canvas-node-interaction-layer[data-target-collapsed] .canvas-node-resizer { + pointer-events: none; + cursor: inherit; +} +.canvas-node-interaction-layer[data-target-collapsed] .canvas-node-resizer .canvas-node-connection-point { + display: none; + pointer-events: none; +} +.canvas-wrapper[data-allow-floating-edge-creation=true] .canvas.is-connecting .canvas-node:not(.canvas-node-group)::after { + all: unset; + content: ""; + z-index: 100; + position: absolute; + top: 50%; + left: 50%; + width: max(10px, 100% - 50px * var(--zoom-multiplier) * 2); + height: max(10px, 100% - 50px * var(--zoom-multiplier) * 2); + transform: translate(-50%, -50%); + border-radius: var(--radius-m); + outline: calc(4px * var(--zoom-multiplier)) dashed hsla(var(--color-accent-hsl), 0.5); +} +.canvas-wrapper[data-allow-floating-edge-creation=true] .canvas.is-connecting .canvas-node:not(.canvas-node-group).hovering-floating-edge-zone::after { + outline-color: var(--color-accent); + outline-style: solid; + background-color: hsla(var(--color-accent-hsl), 0.1); +} +.canvas-wrapper[data-focus-mode-enabled=true] .canvas:has(.canvas-node.is-focused) .canvas-node:not(.is-focused) { + filter: blur(5px); +} +.canvas-wrapper[data-focus-mode-enabled=true] .canvas:has(.canvas-node.is-focused) .canvas-edges > g:not(.is-focused) { + filter: blur(5px); +} +.canvas-wrapper[data-focus-mode-enabled=true] .canvas:has(.canvas-node.is-focused) .canvas-path-label-wrapper > .canvas-path-label:not(.is-focused) { + filter: blur(5px); +} +.canvas-wrapper.presentation-mode .canvas-controls { + visibility: hidden; +} +.canvas-wrapper.presentation-mode .canvas-card-menu { + visibility: hidden; +} +.canvas-wrapper:not(.presentation-mode) .canvas-node[data-is-start-node=true]::before { + content: "Start"; + position: absolute; + top: calc(-1 * var(--size-4-1) * var(--zoom-multiplier)); + right: 0; + transform: translate(0, -100%) scale(var(--zoom-multiplier)); + transform-origin: bottom right; + max-width: calc(100% / var(--zoom-multiplier)); + padding: var(--size-4-1) var(--size-4-2); + font-size: 1em; + border-radius: var(--radius-s); + color: var(--color-green); + background-color: rgba(var(--color-green-rgb), 0.1); +} +.canvas-node[data-is-portal-loaded=true] { + pointer-events: all; +} +.canvas-node[data-is-portal-loaded=true]:not(.is-focused) { + pointer-events: none; +} +.canvas-node[data-is-portal-loaded=true]:not(.is-focused) .canvas-node-label { + pointer-events: all; +} +.canvas-node[data-is-portal-loaded=true] .canvas-node-container { + background-color: transparent; + border-style: dashed; +} +.canvas-node[data-is-portal-loaded=true] .canvas-node-container .canvas-node-content { + display: none; +} +.canvas-node-interaction-layer[data-is-from-portal=true] .canvas-node-resizer { + pointer-events: none; + cursor: inherit; +} +.canvas-node-interaction-layer[data-is-from-portal=true] .canvas-node-resizer .canvas-node-connection-point { + pointer-events: all; +} +.ac-card-embed { + display: flex; + flex-direction: column; + overflow: hidden; + background-color: var(--background-primary); + border: 1px solid rgb(var(--canvas-color)); + border-radius: var(--radius-m); + box-shadow: var(--shadow-stationary); +} +.ac-card-embed.is-themed { + background-color: rgba(var(--canvas-color), 0.07); + border-color: rgba(var(--canvas-color), 0.7); + box-shadow: inset 0 0 0 1px rgba(var(--canvas-color), 0.7), var(--shadow-stationary); +} +.ac-card-embed > .markdown-content { + position: relative; + overflow-y: auto; + overflow-wrap: break-word; + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + padding: 0 var(--size-4-4); + font-size: var(--font-text-size); + font-family: var(--font-text); + line-height: var(--line-height-normal); + color: var(--text-normal); + user-select: text; + -webkit-user-select: text; + scrollbar-gutter: stable; +} diff --git a/.obsidian/plugins/better-embedded-canvas/main.js b/.obsidian/plugins/better-embedded-canvas/main.js new file mode 100644 index 00000000..21c1c4f3 --- /dev/null +++ b/.obsidian/plugins/better-embedded-canvas/main.js @@ -0,0 +1,9 @@ +/* +THIS IS A GENERATED/BUNDLED FILE BY ESBUILD +if you want to view the source, please visit the github repository of this plugin: +https://github.com/kotaindah55/better-embedded-canvas +*/ + +var P=Object.defineProperty;var _=Object.getOwnPropertyDescriptor;var q=Object.getOwnPropertyNames;var G=Object.prototype.hasOwnProperty;var j=(n,e)=>{for(var t in e)P(n,t,{get:e[t],enumerable:!0})},w=(n,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of q(e))!G.call(n,s)&&s!==t&&P(n,s,{get:()=>e[s],enumerable:!(i=_(e,s))||i.enumerable});return n},k=(n,e,t)=>(w(n,e,"default"),t&&w(t,e,"default"));var Q=n=>w(P({},"__esModule",{value:!0}),n);var te={};j(te,{BetterEmbeddedCanvasPlugin:()=>C,default:()=>ee});module.exports=Q(te);var a={};k(a,require("obsidian"));function U(n){return{app:n,containerEl:createDiv(),history:{backHistory:[],forwardHistory:[]}}}function D(n){let e=n.internalPlugins.getPluginById("canvas").views.canvas;u=e(U(n)).canvas.constructor}var u;function O(n){let e=new u(n);return e.quickSettingsButton.parentElement?.detach(),e.undoBtnEl.parentElement?.detach(),a.Platform.isMobile||e.canvasControlsEl.lastElementChild?.detach(),e.cardMenuEl.detach(),e.setReadonly(!0),e.wrapperEl.addEventListener("drop",t=>t.preventDefault(),!0),e}function I(n,e,t){let i=n.embedRegistry,s=i.embedByExtension[e]??null;return i.unregisterExtension(e),i.registerExtension(e,t),s}function g(n,e){return n.internalPlugins.getPluginById(e)}function x(n){return n.matches("body > div.print *")}var S={A3:{width:1123,height:1587},A4:{width:794,height:1123},A5:{width:559,height:794},Legal:{width:816,height:1344},Letter:{width:816,height:1054},Tabloid:{width:1056,height:1632}},A=37;var L={tooltipOpenCanvas:"Open canvas",tooltipEnableInteraction:"Enable interaction",tooltipDisableInteraction:"Disable interaction",reloadNotesPrompt:{title:"Reload notes",desc:"Select \u201CReload\u201D to reload all open notes and revert embedded canvases to their default (your data is preserved).",buttonReload:"Reload"},settings:{showCanvasName:{name:"Show canvas name",desc:"Show canvas name as embed title."}}};var M={tooltipOpenCanvas:"Buka kanvas",tooltipEnableInteraction:"Nyalakan interaksi",tooltipDisableInteraction:"Matikan interaksi",reloadNotesPrompt:{title:"Muat ulang catatan",desc:"Pilih \u201CMuat ulang\u201D untuk memuat ulang catatan yang terbuka dan mengembalikan kanvas yang disematkan pada tampilan bawaan (data Anda tetap terjaga).",buttonReload:"Muat ulang"},settings:{showCanvasName:{name:"Tampilkan nama kanvas",desc:"Tampilkan nama kanvas sebagai judul sematan."}}};var T=i18next.createInstance({lng:(0,a.getLanguage)(),fallbackLng:"en",resources:{en:{translation:L},id:{translation:M}}});T.init();var l=T.t.bind(T);var c=new Map,p=new Map;function X(n){let{wrapperEl:e}=n,t=e.getBoundingClientRect(),i=t.left+e.clientLeft,s=t.top+e.clientTop,r=e.clientWidth,o=e.clientHeight;n.canvasRect={left:i,top:s,width:r,height:o,cx:i+r/2,cy:s+o/2,minX:-r/2,minY:-o/2,maxX:r/2,maxY:r/2}}function B(){p.has(this)||p.set(this,(0,a.debounce)(()=>{c.get(this)?.forEach(n=>X(n.canvas)),p.delete(this)},20)())}function R(n){c.forEach(e=>e.forEach(n))}function N(n){let e=n.contentEl.win,t=c.get(e);t?t.add(n):(c.set(e,new Set([n])),e.addEventListener("scroll",B,{capture:!0,passive:!0}))}function z(n){let e=n.contentEl.win,t=c.get(e);t?.delete(n),t&&!t.size&&(p.get(e)?.cancel(),p.delete(e),c.delete(e),e.removeEventListener("scroll",B,!0))}function H(){p.forEach(n=>n.cancel()),p.clear(),c.forEach((n,e)=>{e.removeEventListener("scroll",B,!0),n.forEach(t=>t.unload())}),c.clear()}var K=300;function $(n){return n.matches(".canvas-node *")}var h=class n extends a.Component{app;canvas;contentEl;plugin;file;ctx;subpath;becPlugin;containerEl;headerEl;mainControlsEl;zoomControlsEl;openCanvasBtnEl;toggleInteractionBtnEl;resizeObserver;mutationObserver;constructor(e,t,i,s){super(),this.app=t.app,this.plugin=g(this.app,"canvas").instance,this.becPlugin=e,this.ctx=t,this.file=i,this.subpath=s,this.containerEl=t.containerEl,this.containerEl.addClass("canvas-embed","better-canvas-embed"),this.containerEl.toggleClass("inline-embed",t.showInline??!1),this.headerEl=this.containerEl.createDiv("embed-title",r=>{r.createSpan("file-embed-icon",o=>(0,a.setIcon)(o,"lucide-layout-dashboard")),r.appendText(" "+i.basename),r.addEventListener("click",o=>this.openOnClick(o)),r.toggle(e.settings.get("showCanvasName"))}),this.contentEl=this.containerEl.createDiv("canvas-content"),this.resizeObserver=new ResizeObserver(()=>this.canvas.onResize()),this.mutationObserver=new MutationObserver(()=>this.updateHeight()),this.canvas=O(this),this.zoomControlsEl=this.canvas.canvasControlsEl.firstElementChild,this.mainControlsEl=this.canvas.canvasControlsEl.createDiv({cls:["canvas-control-group","mod-raised"],prepend:!0}),this.openCanvasBtnEl=this.mainControlsEl.createDiv("canvas-control-item",r=>{(0,a.setIcon)(r,"lucide-maximize-2"),(0,a.setTooltip)(r,l("tooltipOpenCanvas"),{placement:"left"}),r.addEventListener("click",o=>this.openOnClick(o))}),this.toggleInteractionBtnEl=this.mainControlsEl.createDiv("canvas-control-item",r=>{(0,a.setIcon)(r,"pointer"),(0,a.setTooltip)(r,l("tooltipDisableInteraction"),{placement:"left"}),r.addEventListener("click",()=>this.handleInteractionBtnClick())}),this.containerEl.hasClass("internal-embed")||this.headerEl.detach()}onload(){if(this.canvas.load(),this.registerEvent(this.app.vault.on("modify",this.handleModify,this)),this.registerEvent(this.becPlugin.settings.on("settings-changed",this.handleSettingsChange,this)),N(this),this.canvas.noInteraction=!!(this.app.loadLocalStorage(`${this.becPlugin.manifest.id}:no-interaction`)??!0),this.toggleInteraction(!this.canvas.noInteraction),x(this.containerEl)&&!$(this.containerEl)){let e=this.app.vault.getConfig("pdfExportSettings");if(!e)return;let t=this.containerEl.doc.body,i=t.find(":scope > .print > .markdown-preview-view"),{pageSize:s,margin:r,landscape:o}=e,d=o?S[s].height:S[s].width,m=r=="0"?A:0,y=parseInt(i.getCssPropertyValue("padding-inline").replace("px","")),W=d-m*2-y*2;this.containerEl.setCssStyles({width:W+"px"})}}onunload(){this.app.workspace.trigger("advanced-canvas:canvas-view-unloaded:before",this),this.resizeObserver.disconnect(),this.mutationObserver.disconnect(),this.canvas.unload(),z(this)}toggleInteraction(e){this.canvas.deselectAll(),this.canvas.noInteraction=!e,this.zoomControlsEl.toggle(e),(0,a.setIcon)(this.toggleInteractionBtnEl,e?"pointer":"pointer-off"),(0,a.setTooltip)(this.toggleInteractionBtnEl,l(e?"tooltipDisableInteraction":"tooltipEnableInteraction"),{placement:"left"})}requestSave(){}saveLocalData(){}async loadFile(){let e=await this.app.vault.cachedRead(this.file);await this.setData(e,!0)}async setData(e,t){try{let i=JSON.parse(e);this.canvas.setData(i)}catch(i){console.error(i)}t?x(this.containerEl)?this.initRender():this.containerEl.onNodeInserted(()=>this.initRender(),!0):this.canvas.requestFrame(),this.app.workspace.trigger("advanced-canvas:canvas-changed",this.canvas)}initRender(){this.updateHeight(),this.canvas.zoomToFitQueued=!0,this.canvas.onResize(),this.resizeObserver.observe(this.containerEl),this.mutationObserver.observe(this.containerEl,{attributes:!0,attributeFilter:["width"]})}async openOnClick(e){await this.app.workspace.getLeaf(a.Keymap.isModEvent(e)).openFile(this.file)}updateHeight(){let e=Number(this.containerEl.getAttr("width"));this.contentEl.setCssStyles({height:e&&e>K?e+"px":K+"px"})}async handleModify(e){if(e!=this.file)return;let t=await this.app.vault.cachedRead(this.file);await this.setData(t,!1)}handleInteractionBtnClick(){let e=this.canvas.noInteraction??!1;R(t=>t.toggleInteraction(e)),this.app.saveLocalStorage(`${this.becPlugin.manifest.id}:no-interaction`,!e)}handleSettingsChange(e){if(e.has("showCanvasName")){let t=this.becPlugin.settings.get("showCanvasName");this.headerEl.toggle(t)}}static create(e,t,i,s){return new n(e,t,i,s)}};function V(n,e){let t=Object.keys(e).map(i=>J(n,i,e[i]));return t.length===1?t[0]:function(){t.forEach(i=>i())}}function J(n,e,t){let i=n[e],s=n.hasOwnProperty(e),r=s?i:function(){return Object.getPrototypeOf(n)[e].apply(this,arguments)},o=t(r);return i&&Object.setPrototypeOf(o,i),Object.setPrototypeOf(d,o),n[e]=d,m;function d(...y){return o===r&&n[e]===d&&m(),o.apply(this,y)}function m(){n[e]===d&&(s?n[e]=r:delete n[e]),o!==r&&(o=r,Object.setPrototypeOf(d,i||Function))}}function v(n,e,t){return i[n]=n,i;function i(...s){return(e[n]===n?e:t).apply(this,s)}}function F(n){n.register(V(u.prototype,{onWheel:e=>v(n.manifest.id,e,function(t){this.noInteraction||(e.call(this,t),this.view instanceof h&&t.stopPropagation())}),onPointerdown:e=>v(n.manifest.id,e,function(t){this.noInteraction||e.call(this,t)}),onPriorityPointerdown:e=>v(n.manifest.id,e,function(t){this.noInteraction||e.call(this,t)}),onPointermove:e=>v(n.manifest.id,e,function(t){this.noInteraction||e.call(this,t)}),updateSelection:e=>v(n.manifest.id,e,function(t){this.noInteraction||e.call(this,t)})}))}var E=class n extends a.Modal{constructor(e){super(e),this.setTitle(l("reloadNotesPrompt.title")),this.contentEl.createEl("p",{text:l("reloadNotesPrompt.desc")}),this.contentEl.createDiv("modal-button-container",t=>{new a.ButtonComponent(t).setCta().setButtonText(l("reloadNotesPrompt.buttonReload")).onClick(()=>{this.reloadNotes(),this.close()}),new a.ButtonComponent(t).setButtonText(i18next.t("dialogue.button-cancel")).onClick(()=>this.close())})}reloadNotes(){this.app.workspace.getLeavesOfType("markdown").forEach(e=>{e.view instanceof a.MarkdownView&&e.rebuildView()})}static open(e){new n(e).open()}};var b=class extends a.PluginSettingTab{plugin;constructor(e){super(e.app,e),this.plugin=e}display(){this.plugin.settings.defer(!0),new a.Setting(this.containerEl).setName(l("settings.showCanvasName.name")).setDesc(l("settings.showCanvasName.desc")).addToggle(e=>e.setValue(this.plugin.settings.get("showCanvasName")).onChange(t=>this.plugin.settings.commit("showCanvasName",()=>t)))}hide(){super.hide(),this.containerEl.empty(),this.plugin.settings.defer(!1)}},f=class extends a.Component{plugin;requestSave;dispatcher;settings;isDeferred;changed;constructor(e){super(),this.plugin=e,this.settings=Z(),this.requestSave=(0,a.debounce)(()=>e.saveData(this.settings),100),this.dispatcher=new a.Events,this.isDeferred=!1,this.changed=new Set}async onload(){Object.assign(this.settings,await this.plugin.loadData())}onunload(){this.changed.clear(),this.isDeferred=!1}on(e,t,i){return this.dispatcher.on(e,t,i)}defer(e){e!=this.isDeferred&&(this.isDeferred=e,this.isDeferred||this.trigger())}commit(e,t,i){let s=this.settings[e],r=t(s);i=(o,d)=>o===d,i(s,r)||(this.settings[e]=r,this.requestSave(),this.changed.add(e)),this.isDeferred||this.trigger()}get(e){return this.settings[e]}trigger(){this.changed.size&&(this.dispatcher.trigger("settings-changed",new Set(this.changed)),this.changed.clear())}};function Z(){return{showCanvasName:!0}}var C=class extends a.Plugin{settings;settingTab;builtinCanvasEmbedCreator;constructor(e,t){super(e,t),this.builtinCanvasEmbedCreator=null,this.settings=this.addChild(new f(this)),this.settingTab=new b(this),D(e),F(this)}async onload(){await super.onload(),this.addSettingTab(this.settingTab),this.registerEvent(this.app.internalPlugins.on("change",this.handleInternalPluginChange,this)),this.handleInternalPluginChange(g(this.app,"canvas"))}onunload(){super.onunload(),H(),this.builtinCanvasEmbedCreator&&I(this.app,"canvas",this.builtinCanvasEmbedCreator),E.open(this.app)}handleInternalPluginChange(e){e.instance.id=="canvas"&&(e.enabled?this.builtinCanvasEmbedCreator=I(this.app,"canvas",(t,i,s)=>this.app.plugins.isEnabled("advanced-canvas")&&s?this.builtinCanvasEmbedCreator(t,i,s):t.depth!==void 0&&t.depth>2?this.builtinCanvasEmbedCreator(t,i,s):h.create(this,t,i,s)):this.builtinCanvasEmbedCreator=null)}},ee=C; + +/* nosourcemap */ \ No newline at end of file diff --git a/.obsidian/plugins/better-embedded-canvas/manifest.json b/.obsidian/plugins/better-embedded-canvas/manifest.json new file mode 100644 index 00000000..e9e215b8 --- /dev/null +++ b/.obsidian/plugins/better-embedded-canvas/manifest.json @@ -0,0 +1,10 @@ +{ + "id": "better-embedded-canvas", + "name": "Better Embedded Canvas", + "version": "0.2.4", + "minAppVersion": "1.10.0", + "description": "Give your embedded canvas better display and interactivity.", + "author": "Kotaindah55", + "authorUrl": "https://github.com/kotaindah55", + "isDesktopOnly": false +} \ No newline at end of file diff --git a/.obsidian/plugins/better-embedded-canvas/styles.css b/.obsidian/plugins/better-embedded-canvas/styles.css new file mode 100644 index 00000000..9af19eae --- /dev/null +++ b/.obsidian/plugins/better-embedded-canvas/styles.css @@ -0,0 +1 @@ +body{--embedded-canvas-min-height: 300px;--embedded-canvas-border-width: 1px;--embedded-canvas-border-color: var(--background-modifier-border)}.canvas-embed.better-canvas-embed.internal-embed>.canvas-content>.canvas-wrapper{position:relative}.canvas-embed.better-canvas-embed>.embed-title{width:fit-content;cursor:pointer}.canvas-embed.better-canvas-embed>.canvas-content{height:var(--embedded-canvas-min-height);border:var(--embedded-canvas-border-width) solid var(--embedded-canvas-border-color);border-radius:var(--radius-m);overflow:hidden}.canvas-embed.better-canvas-embed>.canvas-content>.canvas-wrapper>.canvas-controls{visibility:hidden}.canvas-embed.better-canvas-embed>.canvas-content:hover>.canvas-wrapper>.canvas-controls{visibility:visible}.canvas-embed.better-canvas-embed .canvas-wrapper.mod-readonly[data-hide-background-grid-when-in-readonly=true] .canvas-background{visibility:visible}.canvas-embed.better-canvas-embed .canvas-node-content.markdown-embed>.markdown-embed-content>.markdown-preview-view>.markdown-preview-sizer{margin-left:unset;margin-right:unset}.canvas-embed.better-canvas-embed .inline-title{display:block} \ No newline at end of file diff --git a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/00 Start hier - David.md b/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/00 Start hier - David.md index ca8fea47..3d302641 100644 --- a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/00 Start hier - David.md +++ b/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/00 Start hier - David.md @@ -29,7 +29,7 @@ Nach Davids Feedback wurde die Lead-Zuweisung ergaenzt: - `Felder zuweisen` fuehrt nach S02L. - S02L ordnet Formularfragen per Dropdown Lead-Feldern zu. - E05 zeigt fehlende oder unmoegliche Zuweisungen. -- Advanced-Canvas und D07-D09 bleiben Referenz, aber nicht Hauptfluss. +- Die alten Advanced-Screens und S01B sind aus dem Obsidian-Handoff entfernt; S01C/S01D/S01E bleiben nur als Referenz-Beispiele. ## Review-Reihenfolge @@ -51,15 +51,9 @@ Nach Davids Feedback wurde die Lead-Zuweisung ergaenzt: Danach nur bei Bedarf: -- [[Screens/80 S01B Gefuehrter Fragen Editor]] - [[Screens/81 S01C Website Formular Editor]] - [[Screens/82 S01D Umfrage Editor]] - [[Screens/83 S01E Internes Formular Editor]] -- [[Screens/90 ADV Form-Builder Canvas]] -- [[Screens/90b ADV Form-Builder Canvas 2]] -- [[Screens/91 D07 Logic Regel Detail]] -- [[Screens/92 D08 Action Detail mit Mapping]] -- [[Screens/93 D09 Publish Blocker Summary]] ## Bitte pruefen diff --git a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/D-FORM David Uebergabe 2026-07-09.md b/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/D-FORM David Uebergabe 2026-07-09.md index f537213f..ac3fc241 100644 --- a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/D-FORM David Uebergabe 2026-07-09.md +++ b/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/D-FORM David Uebergabe 2026-07-09.md @@ -48,7 +48,7 @@ Die erste Builder-Richtung war fachlich vollstaendig, aber fuer normale Handwerk | Technische Feldtypen | Verstaendliche Fragetypen | | Lead-Mapping wirkt wie Admin-Konfiguration | Lead-Felder werden einfach per Dropdown zugewiesen | -Empfehlung: S02 ist der Haupteditor. ADV-S01 bleibt nur Referenz fuer spaetere Advanced-/Power-User-Funktionen. +Empfehlung: S02 ist der Haupteditor. S01B und die alten Advanced-Screens sind aus dem Obsidian-Handoff entfernt, damit der Review nicht mehr doppelt wirkt. ## 3. Aktueller Hauptfluss @@ -85,21 +85,15 @@ S00 Formulare verwalten | 12 | E03 | S05 Keine Antworten | Empty State fuer Antworten-Tab | https://app.subframe.com/f023064a5ca0/design/6f56c14f-d66e-4734-8a29-a900ac74a8a7/edit | | 13 | E04 | S05 Export Fehler | Fehlerzustand fuer Excel/CSV-Export | https://app.subframe.com/f023064a5ca0/design/00553ea4-9839-4d22-b540-9b9ef0bf014f/edit | -## 5. Spaetere Varianten und Referenzseiten +## 5. Spaetere Varianten -Diese Seiten sind gebaut oder dokumentiert, aber nicht Teil des aktuellen Hauptflusses: +Diese Seiten liegen noch als optionale Beispiele im Subframe-Flow, sind aber nicht Teil des aktuellen Hauptflusses: | Nr. | ID | Screen | Status | Subframe | |---|---|---|---|---| -| 80 | S01B | Gefuehrter Fragen Editor | Spaeter / Referenz | https://app.subframe.com/f023064a5ca0/design/d0fb9c3d-4b03-4d59-bd98-9f668ab018eb/edit | | 81 | S01C | Website Formular Editor | Spaeter / Referenz | https://app.subframe.com/f023064a5ca0/design/d7d33e94-5c12-48e8-8ec6-1e94590ee89c/edit | | 82 | S01D | Umfrage Editor | Spaeter / Referenz | https://app.subframe.com/f023064a5ca0/design/5abd7799-7058-40b3-9772-884cbda97b9c/edit | | 83 | S01E | Internes Formular Editor | Spaeter / Referenz | https://app.subframe.com/f023064a5ca0/design/ea08fd1c-1a76-4e2c-b27c-1091baa0586a/edit | -| 90 | ADV-S01 | Form-Builder Canvas | Advanced / Referenz | https://app.subframe.com/f023064a5ca0/design/e5502f88-9858-460e-abae-671f28464270/edit | -| 90b | ADV-S01-2 | Form-Builder Canvas 2 | Advanced / Referenz | https://app.subframe.com/f023064a5ca0/design/9529ac6f-eb71-4e5c-8774-7cebe9c8162c/edit | -| 91 | D07 | Logic Regel Detail | Advanced / Referenz | https://app.subframe.com/f023064a5ca0/design/07b60457-8151-46b9-ba34-34eceffc08bb/edit | -| 92 | D08 | Action Detail mit Mapping | Advanced / Referenz | https://app.subframe.com/f023064a5ca0/design/0759d779-91f5-4e06-b1d6-7cd0095461ea/edit | -| 93 | D09 | Publish Blocker Summary | Advanced / Referenz | https://app.subframe.com/f023064a5ca0/design/211e8ca7-fc0b-4c52-a4a4-a1e660209e58/edit | ## 6. Formulararten @@ -171,12 +165,7 @@ Fuer den einfachen Haupteditor: - Harte Blocker zeigen einen Banner/Popover mit Sprungzielen. - Unpublish braucht einen Confirm-Dialog, weil der Public-Link danach nicht mehr erreichbar ist. -Fuer Advanced/Referenz: - -- D07 zeigt Logic-Regel-Detail. -- D08 zeigt Action-Detail mit Mapping. -- D09 zeigt Publish-Blocker-Summary. -- D01/D02/D03/D06/D10 und M01-M04 sind in Markdown dokumentiert, aber nicht zwingend als Subframe-Hauptscreen noetig. +Als reine Markdown-Notizen bleiben D01/D02/D03/D06/D10 und M01-M04 dokumentiert. Sie sind keine Subframe-Hauptscreens und nicht Teil der Review-Reihenfolge. ## 10. Public Formular @@ -224,7 +213,7 @@ Bitte David pruefen lassen: - Passt E05 fuer fehlende/unmoegliche Lead-Zuweisungen? - Sind S03 Design und S05 Antworten tief genug fuer D-FORM, ohne in andere Tickets abzurutschen? - Sind Public States S04/V01/V02/V03 vollstaendig genug? -- Duerfen S01B/S01C/S01D/S01E, ADV-S01 Canvas 1/2 und D07/D08/D09 als Referenz liegen bleiben? +- Sollen S01C/S01D/S01E als spaetere Beispielvarianten im Subframe-Flow liegen bleiben? - Muessen M01-M04 als echte Dialog-Screens gebaut werden, oder reicht Dokumentation? - In Subframe bitte bei S02L/E05 nochmal visuell pruefen, ob das richtige Default Page Layout gesetzt ist. @@ -248,15 +237,9 @@ Bitte David pruefen lassen: Danach nur bei Bedarf: -16. S01B Gefuehrter Fragen Editor -17. S01C Website Formular Editor -18. S01D Umfrage Editor -19. S01E Internes Formular Editor -20. ADV-S01 Form-Builder Canvas -21. ADV-S01-2 Form-Builder Canvas 2 -22. D07 Logic Regel Detail -23. D08 Action Detail mit Mapping -24. D09 Publish Blocker Summary +16. S01C Website Formular Editor +17. S01D Umfrage Editor +18. S01E Internes Formular Editor ## 14. Lokale Detaildateien @@ -279,4 +262,4 @@ Wichtige Detailnotizen im Paket: ## 15. Kurztext zum Mitschicken -Hey David, hier ist die aufgeraeumte D-FORM-Uebergabe. Ich habe den Builder nach deinem Feedback nochmal vereinfacht und die Lead-Zuweisung ergaenzt: S02 bleibt der einfache Google-Forms-nahe Haupteditor, S02L zeigt die Lead-Felder-Zuweisung per Dropdown, E05 deckt fehlende/unmoegliche Zuweisungen ab. Advanced-Canvas und D07-D09 bleiben als Referenz, aber nicht als Hauptfluss. Kannst du bitte einmal pruefen, ob die Richtung und die offenen Punkte fuer dich passen? +Hey David, hier ist die aufgeraeumte D-FORM-Uebergabe. Ich habe den Builder nach deinem Feedback nochmal vereinfacht und die Lead-Zuweisung ergaenzt: S02 bleibt der einfache Google-Forms-nahe Haupteditor, S02L zeigt die Lead-Felder-Zuweisung per Dropdown, E05 deckt fehlende/unmoegliche Zuweisungen ab. Die alten Advanced-Screens und S01B sind aus dem Obsidian-Handoff raus, damit der Review nur noch den aktuellen Hauptfluss zeigt. Kannst du bitte einmal pruefen, ob die Richtung und die offenen Punkte fuer dich passen? diff --git a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/David Review Checkliste.md b/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/David Review Checkliste.md index a2617276..d5f049ae 100644 --- a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/David Review Checkliste.md +++ b/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/David Review Checkliste.md @@ -17,11 +17,11 @@ Startpunkt fuer die weitergebbare Version: [[00 Start hier - David]] und [[D-FOR David hat den groben fachlichen Scope als passend bewertet. Die Empfehlungen aus dieser Checkliste werden damit als Arbeitsentscheidung uebernommen. -Wichtige Nacharbeit aus dem Review: Beim alten Advanced-S01 fehlte die Interaktionslogik auf der Ebene "was passiert nach Klick?". Diese Nacharbeit ist in [[Screens/90 ADV Form-Builder Canvas|ADV-S01 Form-Builder Canvas]] ergaenzt: Feldbearbeitung, Feldaktionen, Logic, Actions, Sharing, Preview, Settings, Publish und Unpublish sind jetzt nach Inline/Inspector, Menue, Panel, Popover oder Confirm-Dialog sortiert. +Wichtige Nacharbeit aus dem Review: Die Interaktionslogik wurde in den einfachen S02-Hauptfluss uebersetzt. Logic bleibt als `Bedingung` an der Frage, Actions als `Nach Absenden`/Lead-Card und Publish-Blocker als Banner/Popover im selben Editor. -Subframe ist als Draft gebaut und verlinkt. Der aktuelle Hauptfluss ist S00, E01/E02, S01, S02, S03, S04/V01/V02/V03, S05 und E03/E04. S01B/S01C/S01D/S01E, ADV-S01 Canvas 1/2 und D07/D08/D09 bleiben spaetere Varianten bzw. Referenz. +Subframe ist als Draft gebaut und verlinkt. Der aktuelle Hauptfluss ist S00, E01/E02, S01, S02, S02L/E05, S03, S04/V01/V02/V03, S05 und E03/E04. S01C/S01D/S01E bleiben spaetere Varianten bzw. Referenz. S01B und die alten Advanced-Screens wurden aus dem Obsidian-Handoff entfernt. -Aktuelle Arbeitsentscheidung: Fuer normale Handwerksbetriebe wird S02 als einfache Google-Forms-nahe Richtung empfohlen. Alle Formulararten starten in S02 mit anderer Vorlage/Startfragen. Der alte Profi-Canvas bleibt als Advanced-/Referenzscreen, nicht als erste Nutzerfuehrung. Als Kunden-/Handwerker-Wording wird `Bereich` statt `Gewerk` verwendet. +Aktuelle Arbeitsentscheidung: Fuer normale Handwerksbetriebe wird S02 als einfache Google-Forms-nahe Richtung empfohlen. Alle Formulararten starten in S02 mit anderer Vorlage/Startfragen. Als Kunden-/Handwerker-Wording wird `Bereich` statt `Gewerk` verwendet. ## 0b. David-Feedback Lead-Zuweisung @@ -44,7 +44,7 @@ Ergebnis: Die Abgrenzung `D-FORM = Form-Builder-Canvas + oeffentliches Formular` | Quelle | Aussage | Konsequenz fuer D-FORM | |---|---|---| -| `ticket-docs/D-FORM.md` | D-FORM deckt nur Form-Builder-Canvas und oeffentliches Formular ab. Booking/Kalender gehen an `D-BOOK`; Forms-Liste, Submissions und Signatur sind Compose/andere Tickets. | D-FORM bleibt bei Builder und Public: S02 einfacher Editor plus S04/Public-Varianten; ADV-S01 nur als Referenz. | +| `ticket-docs/D-FORM.md` | D-FORM deckt Form-Builder und oeffentliches Formular ab. Booking/Kalender gehen an `D-BOOK`; Forms-Liste, Submissions und Signatur sind Compose/andere Tickets. | D-FORM bleibt bei Builder und Public: S02 einfacher Editor plus S04/Public-Varianten. | | `designer-briefs/P16-kalender-booking-forms-signatur.md` | P16 ist gebuendelt, aber soll in Gruppen getrennt werden: Kalender-Sync, Booking, Form-Builder, Signatur. | Nicht alle P16-Screens gehoeren in D-FORM. | | `SCREEN-MAP.md` Paragraph 3 | Listet Form-Builder-Canvas, oeffentliches Formular und Submissions im Umfeld. Submissions ist A2/A3-Compose, Builder/Public sind Bespoke. | Submissions nicht als D-FORM-Bespoke bauen. | | `D-BOOK.md` | D-BOOK deckt Booking-Page und Kalender-View ab; verweist Form-Builder/Public Form zurueck auf D-FORM. | Booking und Kalender bleiben raus. | @@ -55,7 +55,7 @@ Ergebnis: Die Abgrenzung `D-FORM = Form-Builder-Canvas + oeffentliches Formular` | Punkt | Empfehlung | David-Entscheidung | |---|---|---| -| D-FORM umfasst den Builder; aktuell S02 als Haupteditor und ADV-S01 als Advanced-Referenz. | Ja. | angenommen | +| D-FORM umfasst den Builder; aktuell S02 als Haupteditor. | Ja. | angenommen | | D-FORM umfasst S04 Oeffentliches Formular. | Ja. | angenommen | | V01 Public Erfolg/Redirect wird als eigene Variante gebaut. | Ja, weil copy- und vertrauenskritisch. | angenommen | | V02 Public Formular nicht verfuegbar wird als eigene Variante gebaut. | Ja, weil externer Nutzer blockiert ist. | angenommen | @@ -144,12 +144,9 @@ Ergebnis: Die Abgrenzung `D-FORM = Form-Builder-Canvas + oeffentliches Formular` | Punkt | Empfehlung | David-Entscheidung | |---|---|---| -| 5 Hauptframes/Varianten bauen: S01, S02, V01, V02, V03. | Ja. | angenommen | -| Zusaetzlich D07 Logic Regel Detail bauen. | Ja, damit Logic visuell pruefbar ist. | angenommen als Empfehlung | -| Zusaetzlich D08 Action Detail mit Mapping bauen. | Ja, weil Mapping komplex und fehleranfaellig ist. | angenommen als Empfehlung | -| Zusaetzlich D09 Publish Blocker Summary bauen. | Ja, weil Publish ein kritischer Moment ist. | angenommen als Empfehlung | +| Hauptfluss bauen: S00, S01, S02, S02L/E05, S03, S04/V01/V02/V03, S05 und E01-E04. | Ja. | angenommen | | M01 Feld loeschen Confirm und M02 Unpublish Confirm vorerst nur dokumentieren. | Ja, fachlich klar; spaeter baubar, falls David es sehen will. | angenommen als Empfehlung | -| Sharing, Logic, Actions, Preview und Settings nicht als eigene Subframe-Screens, sondern als Bereiche/Zustaende von S01. | Ja. | angenommen; Interaktionslogik in S01 nachgezogen | +| Sharing, Logic, Actions, Preview und Settings nicht als eigene Subframe-Screens, sondern einfach im S02-Kontext halten. | Ja. | angenommen | | Nach Subframe-Bau Links in Paket, Screen-Dateien, Tracker und Handoff eintragen. | Ja. | erledigt | ## 11. David-Feedback: S01 Nacharbeit @@ -169,14 +166,14 @@ Arbeitsentscheidung fuer S01: | Publish | Publish-Check inline im Builder; bei Blockern Summary mit Sprungzielen, bei Erfolg Toast/Statuswechsel. | | Unpublish | Confirm-Dialog, weil der Public-Link danach nicht mehr erreichbar ist. | -Damit reicht S01 weiter als Hauptscreen. D07, D08 und D09 sind als wichtige S01-Zustandsframes gebaut. Die kleineren Menues und Confirm-Dialoge bleiben dokumentiert und muessen nicht automatisch eigene Subframe-Screens werden. +Damit reicht S02 als Haupteditor. Die kleineren Menues und Confirm-Dialoge bleiben dokumentiert und muessen nicht automatisch eigene Subframe-Screens werden. -Vor-Subframe-Nacharbeit erledigt: Die wichtigsten ADV-S01-Zustaende sind als eigene Markdown-Dateien ausgearbeitet: [[Screens/D01 Sharing Panel URL QR iFrame]], [[Screens/D02 Post-Submit Actions]], [[Screens/D03 Conditional Logic Editor]], [[Screens/D06 Feldkarten Menue]], [[Screens/M01 Feld loeschen Confirm]], [[Screens/91 D07 Logic Regel Detail|D07 Logic Regel Detail]], [[Screens/92 D08 Action Detail mit Mapping|D08 Action Detail mit Mapping]], [[Screens/93 D09 Publish Blocker Summary|D09 Publish Blocker Summary]], [[Screens/M02 Unpublish Confirm]], [[Screens/M03 Archive Confirm]], [[Screens/M04 Unsaved Changes Confirm]]. +Vor-Subframe-Nacharbeit erledigt: Die wichtigsten Detailzustaende sind als Markdown-Dateien ausgearbeitet: [[Screens/D01 Sharing Panel URL QR iFrame]], [[Screens/D02 Post-Submit Actions]], [[Screens/D03 Conditional Logic Editor]], [[Screens/D06 Feldkarten Menue]], [[Screens/M01 Feld loeschen Confirm]], [[Screens/M02 Unpublish Confirm]], [[Screens/M03 Archive Confirm]], [[Screens/M04 Unsaved Changes Confirm]]. Weitere Nacharbeit nach neuer Rueckmeldung: [[Screens/D10 Builder Interaktionsfaelle]] dokumentiert jetzt die "Was passiert wenn...?"-Faelle: Auswahlfolgen, Checkboxen, vergessene Zwischenschritte, Datei/Foto, Dropdowns, halb gesetztes Mapping, Publish-Blocker und konkrete Modal-/Popover-Inhalte. -Aktueller Subframe-Stand: Der Hauptfluss S00/E01/E02/S01/S02/S03/S04/V01/V02/V03/S05/E03/E04 ist gebaut und verlinkt. S01B/S01C/S01D/S01E, ADV-S01 Canvas 1/2 und D07/D08/D09 sind gebaut, aber aktuell nur spaetere Varianten bzw. Referenz. M01/M02/M03/M04 und D10 bleiben dokumentiert. +Aktueller Subframe-Stand: Der Hauptfluss S00/E01/E02/S01/S02/S02L/E05/S03/S04/V01/V02/V03/S05/E03/E04 ist gebaut und verlinkt. S01C/S01D/S01E sind gebaut, aber aktuell nur spaetere Varianten bzw. Referenz. M01/M02/M03/M04 und D10 bleiben dokumentiert. ## 12. Kurztext fuer David -`D-FORM ist fachlich passend abgegrenzt, die Advanced-Builder-Klicklogik ist nachgezogen und der Subframe-Draft ist gebaut. Der aktuelle Hauptfluss ist S00, S01, S02, S03, S04/Public States und S05 plus E01-E04. S02 ist die Google-Forms-nahe Hauptvariante; Logic/Actions/Publish bleiben dort bewusst einfach und inline. S01B/S01C/S01D/S01E, ADV-S01 Canvas 1/2 und D07/D08/D09 sind nur spaetere Varianten bzw. Referenz. S05 bleibt ohne Kennzahl-Kacheln und ohne extra Link-teilen-Button.` +`D-FORM ist fachlich passend abgegrenzt und der Subframe-Draft ist gebaut. Der aktuelle Hauptfluss ist S00, S01, S02, S02L/E05, S03, S04/Public States und S05 plus E01-E04. S02 ist die Google-Forms-nahe Hauptvariante; Logic/Actions/Publish bleiben dort bewusst einfach und inline. S01C/S01D/S01E sind nur spaetere Varianten bzw. Referenz. S05 bleibt ohne Kennzahl-Kacheln und ohne extra Link-teilen-Button.` diff --git a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/01 S00 Formulare verwalten.md b/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/01 S00 Formulare verwalten.md index e9d4145b..ac71d914 100644 --- a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/01 S00 Formulare verwalten.md +++ b/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/01 S00 Formulare verwalten.md @@ -41,7 +41,7 @@ Der Screen ist nicht der Builder selbst, sondern die Formular-Zentrale vor dem B - Klick auf `Neues Formular` fuehrt zu [[04 S01 Formularart auswaehlen|S01 Formularart auswaehlen]]. - Klick auf `Bearbeiten` oeffnet aktuell [[05 S02 Einfacher Fragen Editor|S02]]. -- Die Formularart bestimmt in S02 nur Vorlage, Startfragen und Hinweise. S01B/S01C/S01D/S01E bleiben spaetere Varianten bzw. Referenzseiten. +- Die Formularart bestimmt in S02 nur Vorlage, Startfragen und Hinweise. S01C/S01D/S01E bleiben spaetere Varianten bzw. Referenzseiten. - Filterchips koennen kombiniert werden, z. B. `Erstellt` + `Unveroeffentlicht` oder `Website / Extern` + `Lead`. - Als Beispiel sind mehrere Status-Chips aktiv, damit klar ist: Es ist kein einzelner Tab, sondern eine Mehrfachauswahl. Art-Chips stehen daneben als zusaetzliche auswaehlbare Filter bereit. - Aktionen je Zeile: bearbeiten, Vorschau, Link kopieren, Antworten ansehen, duplizieren, loeschen. diff --git a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/04 S01 Formularart auswaehlen.md b/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/04 S01 Formularart auswaehlen.md index ee9bbbb1..713f981a 100644 --- a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/04 S01 Formularart auswaehlen.md +++ b/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/04 S01 Formularart auswaehlen.md @@ -50,7 +50,7 @@ Entscheidung 2026-07-08: Weniger ist hier besser. Keine separate Vorlagenzeile u | Fragebogen / Umfrage | Feedback sammeln und Antworten spaeter auswerten. | Oeffnet [[05 S02 Einfacher Fragen Editor|S02]] mit Umfrage-Startfragen. | | Internes Formular | Meldungen und Anfragen fuer Mitarbeitende. | Oeffnet [[05 S02 Einfacher Fragen Editor|S02]] mit interner Formularvorlage. | -Arbeitsentscheidung 2026-07-08: S02 ist der Haupt-Editor fuer alle vier Formulararten. S01B/S01C/S01D/S01E bleiben nur spaetere Varianten bzw. Referenzseiten. +Arbeitsentscheidung 2026-07-08: S02 ist der Haupt-Editor fuer alle vier Formulararten. S01C/S01D/S01E bleiben nur spaetere Varianten bzw. Referenzseiten. ## Lead-Formular Verhalten @@ -81,5 +81,5 @@ Qualitaetsregel: - Nutzt das vorhandene `Default Page Layout`. - Keine eigene Sidebar oder Header-Navigation. -- Liegt im selben Flow neben `ADV-S01-2 Form-Builder Canvas 2`. +- Liegt im D-FORM-Flow neben S02/S02L/E05, S03, S04 und S05. - Ersetzt nicht S04 Public Form, sondern liegt vor dem Builder. diff --git a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/05 S02 Einfacher Fragen Editor.md b/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/05 S02 Einfacher Fragen Editor.md index 56f83bc3..49e6f909 100644 --- a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/05 S02 Einfacher Fragen Editor.md +++ b/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/05 S02 Einfacher Fragen Editor.md @@ -159,7 +159,7 @@ S02 ist der Haupt-Editor fuer normale Nutzer. Deshalb werden Logic, Actions und | Actions | `Nach Absenden` bleibt im Screen. Bei Lead-Formularen ist `Lead anlegen` aktiv und zeigt `Felder zuweisen`. | Kein Automation-Editor, aber die wichtigste Lead-Zuordnung ist sichtbar. | | Publish-Blocker | Einfacher Banner/Popover im selben Screen mit Sprung zur betroffenen Frage. | Verstaendlich genug, kein eigener Screen noetig. | -D07, D08 und D09 bleiben als Advanced-/Referenzseiten fuer spaeter oder fuer David, falls er die Detailtiefe sehen will. Fuer den aktuellen S02-Hauptfluss sind sie nicht notwendig. Die Lead-Zuweisung ist aber nicht mehr nur Hintergrundlogik, sondern ein eigener S02-Zustand. +Die alten Advanced-Detailframes sind aus dem Obsidian-Handoff entfernt. Fuer den aktuellen S02-Hauptfluss sind sie nicht notwendig. Die Lead-Zuweisung ist aber nicht mehr nur Hintergrundlogik, sondern ein eigener S02-Zustand. ## Publish @@ -180,6 +180,6 @@ Qualitaetsregel: - Nutzt das vorhandene `Default Page Layout`. - Keine eigene Sidebar oder Header-Navigation. -- Liegt im selben Flow neben `ADV-S01-2 Form-Builder Canvas 2`. +- Liegt im D-FORM-Flow neben S02L/E05, S03, S04 und S05. - Nah an Google Forms, aber mit HiveGrid-Komponenten. - Lead-Verknuepfung bleibt einfach sichtbar. `Felder zuweisen` wird als einfacher S02-Zustand gezeigt, nicht als Advanced-Mapping. diff --git a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/80 S01B Gefuehrter Fragen Editor.md b/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/80 S01B Gefuehrter Fragen Editor.md deleted file mode 100644 index ad701ed0..00000000 --- a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/80 S01B Gefuehrter Fragen Editor.md +++ /dev/null @@ -1,110 +0,0 @@ ---- -kind: screen -project: HiveGrid Screen Project -ticket: D-FORM -package: WF-D-FORM Form Builder -id: S01B -screen: Gefuehrter Fragen Editor -typ: Simple Builder Alternative -umsetzung: Alternative bauen -status: Subframe Draft / Review offen -route: /forms/:id/edit -subframe: "https://app.subframe.com/f023064a5ca0/design/d0fb9c3d-4b03-4d59-bd98-9f668ab018eb/edit" ---- - -# S01B - Gefuehrter Fragen Editor - -## Ziel - -Noch weniger Komplexitaet als S02. Der Nutzer wird durch wenige Schritte gefuehrt: Fragen, Einstellungen, Vorschau, Veroeffentlichen. - -## Leitidee - -S01B ist fuer Nutzer, die bei zu vielen Buttons sofort aussteigen. - -Der Screen sagt: - -`Erst Fragen. Dann pruefen. Dann teilen.` - -## Aufbau - -Interne Seite mit vorhandenem `Default rage Layout`. Keine eigene Sidebar nachbauen. - -Screen-Zonen: - -- Kopfzeile mit Formularname, Speichern, Vorschau. -- Step-Leiste oben: 1 Fragen, 2 Einstellungen, 3 Vorschau, 4 Veroeffentlichen. -- Linke schmale Fragenliste: Frage 1, Frage 2, Frage 3. -- Hauptbereich: nur die aktuell gewaehlte Frage als grosse Karte. -- Rechte Seite: Hilfe/rreview-Zusammenfassung, nicht voller Inspector. - -## Hauptaktionen - -| Aktion | UI | -|---|---| -| Frage hinzufuegen | grosser Button `+ Frage hinzufuegen` unter Fragenliste | -| Fragetyp wechseln | grosses Dropdown in der Fragekarte | -| Option hinzufuegen | Inline in der Fragekarte | -| rflichtfrage | Toggle mit Text `Muss beantwortet werden` | -| Bedingung | Button `Nur anzeigen, wenn ...` | -| Frage loeschen | Drei-runkte-Menue, Confirm nur bei Risiko | -| Vorschau | eigener Schritt oder Button oben | - -## Vereinfachung gegenueber S02 - -| S02 | S01B | -|---|---| -| rechte Mini-Werkzeugleiste | keine Werkzeugleiste | -| mehrere Fragekarten sichtbar | Fokus auf eine Frage | -| tabs wie Google Forms | Schrittfolge | -| mehr direkte Kontrolle | mehr Fuehrung | - -## Lead-Formular Verhalten - -Bei Lead-Formular: - -- Schritt `Einstellungen` zeigt automatisch: - - Lead-Erstellung aktiv - - Ziel-ripeline - - verantwortliches Team -- Nutzer kann diese Werte aendern, muss aber nichts mappen. -- Fragenliste startet mit vorgeschlagenen Lead-Fragen. - -## rublic rreview - -Der Schritt `Vorschau` zeigt, wie der Kunde das Formular sieht. - -Wichtig: - -- Keine rDF-Sprache. -- Keine interne Builder-Sprache. -- rreview entspricht S02 rublic Form. - -## rublish - -Im Schritt `Veroeffentlichen` sieht Nutzer: - -- rublic Link -- QR-Code -- Einbetten -- Status der rruefung - -Fehlt etwas, steht es als einfache Checkliste: - -- `Frage 2 braucht mindestens eine Antwortoption.` -- `Oeffentliche URL fehlt.` -- `Datenschutz-Hinweis pruefen.` - -## Subframe-Hinweis - -Gebaut in Subframe: - -https://app.subframe.com/f023064a5ca0/design/d0fb9c3d-4b03-4d59-bd98-9f668ab018eb/edit - -Qualitaetsregel: - -- Nutzt das vorhandene `Default rage Layout`. -- Keine eigene Sidebar oder Header-Navigation. -- Liegt im selben Flow neben `ADV-S01-2 Form-Builder Canvas 2`. -- Weniger Google-Forms-nah als S02, aber fuer sehr unsichere Nutzer wahrscheinlich leichter. -- Lead-Verknuepfung bleibt als einfache aktive Einstellung sichtbar, ohne Mapping-ranel. diff --git a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/81 S01C Website Formular Editor.md b/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/81 S01C Website Formular Editor.md index 638e3328..5fde6e8c 100644 --- a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/81 S01C Website Formular Editor.md +++ b/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/81 S01C Website Formular Editor.md @@ -53,5 +53,5 @@ Qualitaetsregel: - Nutzt das vorhandene `Default Page Layout`. - Keine eigene Sidebar oder Header-Navigation. -- Liegt im selben Flow neben `ADV-S01-2 Form-Builder Canvas 2`. +- Liegt im D-FORM-Flow als optionale Referenzvariante. - Wiederhergestellte Layout-Version vom 2026-07-07; alte kaputte Subframe-ID wurde ersetzt. diff --git a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/82 S01D Umfrage Editor.md b/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/82 S01D Umfrage Editor.md index 05a5a89f..cfeda73b 100644 --- a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/82 S01D Umfrage Editor.md +++ b/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/82 S01D Umfrage Editor.md @@ -51,5 +51,5 @@ Qualitaetsregel: - Nutzt das vorhandene `Default Page Layout`. - Keine eigene Sidebar oder Header-Navigation. -- Liegt im selben Flow neben `ADV-S01-2 Form-Builder Canvas 2`. +- Liegt im D-FORM-Flow als optionale Referenzvariante. - Korrigierte Version; die falsch generierte Variante ohne Layout wurde geloescht. diff --git a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/83 S01E Internes Formular Editor.md b/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/83 S01E Internes Formular Editor.md index 50866772..8f7375f0 100644 --- a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/83 S01E Internes Formular Editor.md +++ b/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/83 S01E Internes Formular Editor.md @@ -52,5 +52,5 @@ Qualitaetsregel: - Nutzt das vorhandene `Default Page Layout`. - Keine eigene Sidebar oder Header-Navigation. -- Liegt im selben Flow neben `ADV-S01-2 Form-Builder Canvas 2`. +- Liegt im D-FORM-Flow als optionale Referenzvariante. - Wiederhergestellte Layout-Version vom 2026-07-07; alte kaputte Subframe-ID wurde ersetzt. diff --git a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/90 ADV Form-Builder Canvas.md b/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/90 ADV Form-Builder Canvas.md deleted file mode 100644 index 5bbf6859..00000000 --- a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/90 ADV Form-Builder Canvas.md +++ /dev/null @@ -1,661 +0,0 @@ ---- -kind: screen -project: HiveGrid Screen Project -ticket: D-FORM -package: WF-D-FORM Form Builder -id: ADV-S01 -screen: Form-Builder Canvas -typ: Bespoke Builder -umsetzung: Bauen -status: Subframe Draft / Review offen -route: /forms/:id/edit -subframe: "https://app.subframe.com/f023064a5ca0/design/e5502f88-9858-460e-abae-671f28464270/edit" ---- - -# ADV-S01 - Form-Builder Canvas - -## Ziel - -Interne Nutzer bauen, konfigurieren, pruefen und veroeffentlichen ein Formular an einem Arbeitsort. - -Der Screen muss Feldaufbau, Validierung, Conditional Logic, Preview, Sharing und Post-Submit-Actions sichtbar machen, ohne in mehrere neue Routen zu zerfallen. - -## Pattern / Archetyp - -BESPOKE Builder. - -Nutzt A4 Form/Input-Kit fuer die Feldtypen und A7 Zustands-Kit fuer Empty/Loading/Error/Permission. - -## Arbeitsbereiche - -Dies ist keine finale Layout-Vorgabe, sondern die Arbeitslogik fuer David. - -| Bereich | Zweck | Sichtbarkeit | -|---|---|---| -| Header / Statusbar | Form-Name, Status, Save, Preview, Publish, Share. | dauerhaft sichtbar | -| Feld-Palette | 12 Feldtypen hinzufuegen. | primaer | -| Canvas | Feldliste in Reihenfolge, Drag-and-Drop, Empty State, Feldfehler. | primaer | -| Feld-Inspector | ausgewaehltes Feld konfigurieren. | primaer | -| Logic | Wenn-Dann-Regeln fuer sichtbare Felder. | Sekundaer-Tab/Panel | -| Actions | Post-Submit-Actions und Field-Mapping. | Sekundaer-Tab/Panel | -| Sharing | Public URL, QR, iFrame. | Sekundaer-Tab/Panel | -| Preview | Public-nahe Vorschau. | Umschaltmodus oder Side Preview | -| Settings | Slug, Success, Redirect, Branding, technische Public-Settings. | Sekundaer-Tab/Panel | - -Empfehlung: Der Screen soll sich wie ein Builder anfuehlen, nicht wie eine lange Settings-Seite. Palette, Canvas und Inspector sind die Arbeitsmitte. Alles andere ist nah dran, aber nicht dauernd gleich laut. - -## Detail-Brainstorming S01 - -Status: Arbeitsentwurf fuer David. Nach Review vom 2026-07-06 wurde die Interaktionslogik nachgezogen; finale Subframe-Freigabe bleibt offen. - -Ziel dieses Blocks: Den riesigen Builder-Umfang so aufteilen, dass David daraus einen echten Subframe-Frame bauen kann, ohne dass wichtige Produktentscheidungen im Kopf bleiben. - -### Layout-Optionen - -| Option | Aufbau | Vorteil | Risiko | Empfehlung | -|---|---|---|---|---| -| D | Feste Builder-Shell: links Palette, Mitte Canvas, rechts Inspector; oben Header; sekundaere Modi als Tabs/Drawer. | Fuehlt sich wie ein echter Builder an, alle Kernaktionen sind nah beieinander. | Dichte hoch; braucht gute visuelle Hierarchie. | Empfohlen fuer den Advanced Builder. | -| B | Getrennte Seiten/Tabs: Felder, Logic, Actions, Sharing, Settings, Preview. | Ruhiger je Bereich. | Zerlegt den Canvas, mehr Navigationslast, weniger Bespoke-Gefuehl. | Nicht fuer v1-Hauptscreen. | -| C | Canvas + alles andere in Modals. | Hauptscreen wirkt leerer. | Viele Modal-Wechsel, Mapping und Preview werden unhandlich. | Nur fuer Confirm-/Blocker-Dialoge. | - -Arbeitsentscheidung: S01 soll als **Builder-Shell** gedacht werden, nicht als Settings-Seite. Der primaere Arbeitszustand zeigt Palette, Canvas und Inspector gleichzeitig. Logic, Actions, Sharing, Preview und Settings sind Modi am selben Datensatz. - -### Empfohlene Bildschirmaufteilung - -| Zone | Inhalt | Verhalten | -|---|---|---| -| Topbar | Breadcrumb, Form-Name, Status, Save, Preview, Publish, Share, More. | Bleibt dauerhaft sichtbar. | -| Linke Palette | Feldtypen, Such-/Filtermöglichkeit, Templates/Abschnitte falls erlaubt. | Schmal, scanbar, Drag oder Klick fuegt Feld hinzu. | -| Canvas-Mitte | Feldliste, Dropzonen, Feldkarten, Empty State, Fehleranker. | Hauptarbeitsflaeche; Auswahl steuert rechten Inspector. | -| Rechter Inspector | Eigenschaften des ausgewaehlten Felds oder Form-/Modus-Einstellungen. | Kontextsensitiv, kein leerer rechter Bereich. | -| Modus-Leiste | Builder, Logic, Actions, Sharing, Preview, Settings. | Als Tabs/Segment im Arbeitsbereich oder rechts oben; keine neuen Routen. | -| Bottom/Status-Hinweis | Unsaved Changes, letzte Speicherung, Publish-Blocker kurz. | Dezent, damit es nicht mit Canvas konkurriert. | - -### Modus-Matrix - -| Modus | Hauptfrage | Was David zeigen sollte | Eigener Screen? | -|---|---|---|---| -| Builder | Welche Felder hat das Formular und wie sind sie konfiguriert? | Palette, Canvas, Inspector. | nein, Kern von S01 | -| Logic | Wann werden Felder angezeigt oder verborgen? | Regel-Liste, Regel-Editor, kaputte Referenzen, Test-Hinweise. | nein | -| Actions | Was passiert nach Submit? | Action-Liste, Mapping, Test/Preview, Warnungen. | nein | -| Sharing | Wie wird das Formular verteilt? | Public URL, QR, iFrame, Copy-Aktionen, Publish-Hinweis. | nein | -| Preview | Wie sieht das Public Form aus? | Public-nahe Vorschau, Desktop/Mobile-Umschalter, Logic live. | nein, aber im S01 klar sichtbar | -| Settings | Welche Form-Stammdaten steuern die Public-Version? | Slug, Beschreibung, Success, Redirect, Close/Limit, Branding. | nein | - -Empfehlung: Builder bleibt Default. Preview darf prominent sein, aber nicht die Feldbearbeitung verdraengen. - -## Detail: Interaktionslogik / Was passiert nach Klick? - -Diese Nacharbeit beantwortet Davids Review-Hinweis vom 2026-07-06. S01 muss nicht jede kleine UI-Ebene als eigenen Screen bauen, aber die wichtigsten Klicks brauchen eine klare Entscheidung: inline, Inspector, Menue, Panel/Drawer, Popover oder Confirm-Dialog. - -### Grundprinzipien - -| UI-Form | Wann verwenden? | Regel fuer S01 | -|---|---|---| -| Inline / Canvas | Schnelle direkte Manipulation am Feld. | Auswahl, Drag, Pflicht-Quicktoggle, Fehleranker bleiben im Canvas. | -| Rechter Inspector | Alles, was ein Feld oder ein gewaehltes Objekt dauerhaft konfiguriert. | Feld bearbeiten passiert hier, nicht im Modal. | -| 3-Punkte-Menue | Sekundaere Aktionen auf Feld-/Action-/Regel-Karten. | Duplizieren, Loeschen, Deaktivieren, Drchivieren-Einstiege. | -| Panel / Drawer im S01 | Komplexere Konfiguration mit mehreren Feldern. | Logic-Regel, Action-Detail, Sharing, Settings bleiben im selben Screen. | -| Popover | Kurze Auswahl oder kurzer Statuscheck. | Action-Typ waehlen, Publish-Kurzcheck, Copy/Share-Hilfe. | -| Confirm-Dialog | Destruktive oder public-wirksame Aktionen. | Loeschen mit Abhaengigkeiten, Unpublish, Archive, ungespeichert verlassen. | -| Toast | Ergebnisfeedback ohne Entscheidung. | Gespeichert, kopiert, veroeffentlicht, dupliziert. | - -Arbeitsentscheidung: Routinearbeit bleibt im Builder. Modals sind nur fuer echte Risiko-Momente erlaubt. - -### Klick-Matrix - -| Aktion | Einstieg | Was passiert nach Klick? | UI-Form | Eigener Subframe-Frame? | -|---|---|---|---|---| -| Feld per Palette anklicken | Linke Palette | Feld wird am Ende eingefuegt, markiert und im Inspector geoeffnet. | Inline + Inspector | nein | -| Feld per Drag hinzufuegen | Linke Palette -> Canvas | Dropzonen erscheinen, Feld landet an Position, danach selected. | Canvas-Zustand | nein | -| Feld auswaehlen | Feldkarte | Fokusrahmen auf Karte, rechter Inspector zeigt Basisdaten. | Inline + Inspector | nein | -| Feld bearbeiten | Inspector | Label, Key, Pflichtfeld, Optionen, Validation usw. werden rechts bearbeitet. | Inspector | nein | -| Pflichtfeld schnell setzen | Feldkarte oder Inspector | Toggle aendert Pflichtstatus, Dirty State wird gesetzt. | Inline | nein | -| Feld duplizieren | Feldkarte-Aktion oder 3-Punkte-Menue | Kopie entsteht unter Original, neuer `field_key` wird vorgeschlagen, Kopie ist selected. | Menue + Toast | nein | -| Feld loeschen ohne Abhaengigkeit | 3-Punkte-Menue oder Gefahrenzone | Kurzer Confirm ist optional; danach Feld entfernen und Toast zeigen. | Menue/Confirm klein | nur dokumentieren | -| Feld loeschen mit Logic/Action-Bezug | 3-Punkte-Menue oder Gefahrenzone | Confirm zeigt betroffene Regeln/Actions; nach Loeschen werden Referenzen markiert oder geloescht. | Confirm-Dialog | nur dokumentieren | -| Feld verschieben | Drag Handle | Reihenfolge aendert sich, Dropziel sichtbar, Dirty State. | Canvas-Zustand | nein | -| Feld einklappen | Feldkarte | Karte wird kompakt, bleibt in Reihenfolge sichtbar. | Inline | nein | -| Fehler anklicken | Summary oder Feldkarte | Scroll/Focus zum betroffenen Feld und passenden Inspector-Abschnitt. | Inline Navigation | nein | -| Logic-Modus oeffnen | Modus-Leiste | Canvas bleibt Kontext, Logic-Liste/Editor wird als Arbeitsbereich sichtbar. | Panel im S01 | nein | -| Logic-Regel anlegen | Button im Logic-Modus | Neue Regel startet als Detailbereich im Panel; kein Modal. | Panel/Drawer | nur dokumentieren | -| Logic-Regel bearbeiten | Regelkarte | Detailbereich oeffnet/rechts aktualisiert sich. | Panel/Inspector | nein | -| Logic-Regel deaktivieren | Toggle auf Regelkarte | Regel bleibt sichtbar, wirkt aber nicht; kein Confirm. | Inline | nein | -| Logic-Regel loeschen | 3-Punkte-Menue | Bei aktiver/published Form Confirm, sonst kleine Menue-Aktion mit Undo/Toast. | Menue + Confirm nach Risiko | nur dokumentieren | -| Actions-Modus oeffnen | Modus-Leiste | Action-Liste und Detailbereich erscheinen im selben Builder. | Panel im S01 | nein | -| Post-Submit-Action hinzufuegen | Button im Actions-Modus | Typ-Auswahl oeffnet als kleines Auswahlpanel/Popover, danach Detailkonfiguration. | Popover + Panel/Drawer | nur dokumentieren | -| Action bearbeiten | Actionkarte | Detailpanel zeigt Ziel, Mapping, Test und Warnungen. | Panel/Drawer | nein | -| Field-Mapping setzen | Action-Detail | Mapping-Zeilen werden inline bearbeitet. | Inline im Panel | nein | -| Action testen | Action-Detail | Ergebnis erscheint inline im Action-Detail, zusaetzlich Toast bei Erfolg. | Inline + Toast | nein | -| Action deaktivieren | Toggle auf Actionkarte | Action bleibt sichtbar, wird beim Submit nicht ausgefuehrt. | Inline | nein | -| Action loeschen | 3-Punkte-Menue | Confirm, wenn Action aktiv/published oder Mapping besitzt. | Confirm-Dialog | nur dokumentieren | -| Sharing oeffnen | Header-Share oder Modus-Leiste | Sharing-Panel mit URL, QR, iFrame wird sichtbar. | Panel im S01 | nein | -| Public URL kopieren | Sharing-Panel | Copy-Aktion, danach Toast. | Button + Toast | nein | -| QR/iFrame kopieren | Sharing-Panel | Copy/Download, danach Toast; kein Modal. | Button + Toast | nein | -| Sharing im Draft | Sharing-Panel | URL/QR/iFrame ausgegraut, Publish-Hinweis und Sprung zu Publish/Settings. | Inline Hinweis | nein | -| Preview oeffnen | Header oder Modus-Leiste | Builder wechselt in Public-nahe Vorschau mit Desktop/Mobile-Umschalter. | Modus im S01 | nein | -| Settings oeffnen | Modus-Leiste | Slug, Success, Redirect, Branding und Public-Settings erscheinen im Panel/Inspector. | Panel im S01 | nein | -| Publish klicken | Header | Publish-Check laeuft inline; bei Blockern Summary mit Sprungzielen. | Inline Check / Popover | nur dokumentieren | -| Publish erfolgreich | Header | Status wechselt zu Published, Public URL aktiv, Toast. | Statuswechsel + Toast | nein | -| Publish mit Warnungen | Header | Nicht-blockierende Warnungen erscheinen vor Bestaetigung. | Popover/Inline Summary | nur dokumentieren | -| Unpublish klicken | Header/More | Confirm erklaert, dass Public-Link nicht mehr erreichbar ist. | Confirm-Dialog | nur dokumentieren | -| Archive klicken | More-Menue | Confirm, weil Bearbeitung/Public-Kontext endet. | Confirm-Dialog | nur dokumentieren | -| Weg navigieren mit Dirty State | Zurueck/Breadcrumb/Routewechsel | Warnung vor ungespeicherten Aenderungen. | Confirm-Dialog | nur dokumentieren | - -### Zusaetzliche dokumentierte Zustaende nach Review - -Diese Zustaende muessen bewusst beschrieben werden. D07, D08 und D09 sind nach aktueller Empfehlung zusaetzliche Subframe-Zustaende; die restlichen bleiben dokumentiert. - -| ID | Zustand / Ebene | UI-Form | Empfehlung | -|---|---|---|---| -| D01 | [[D01 Sharing Panel URL QR iFrame]] | Panel im S01 | In S01 integrieren, kein eigener Screen. | -| D02 | [[D02 Post-Submit Actions]] | Panel im S01 | In S01 integrieren, kein eigener Screen. | -| D03 | [[D03 Conditional Logic Editor]] | Panel im S01 | In S01 integrieren, kein eigener Screen. | -| D06 | [[D06 Feldkarten Menue]] | 3-Punkte-Menue | In S01 dokumentieren, kein eigener Screen. | -| M01 | [[M01 Feld loeschen Confirm]] | Kleiner Confirm-Dialog | Nur als Zustand dokumentieren; extra Frame nur wenn David ihn sehen will. | -| D07 | [[91 D07 Logic Regel Detail|D07 Logic Regel Detail]] | Panel/Drawer im S01 | Als zusaetzlichen Subframe-Zustand bauen, damit Logic visuell pruefbar ist. | -| D08 | [[92 D08 Action Detail mit Mapping|D08 Action Detail mit Mapping]] | Panel/Drawer im S01 | Als zusaetzlichen Subframe-Zustand bauen, weil Mapping komplex ist. | -| D09 | [[93 D09 Publish Blocker Summary|D09 Publish Blocker Summary]] | Inline Banner/Popover | Als zusaetzlichen Subframe-Zustand bauen, weil Publish kritisch ist. | -| M02 | [[M02 Unpublish Confirm]] | Confirm-Dialog | Nur dokumentieren, weil public-wirksam. | -| M03 | [[M03 Archive Confirm]] | Confirm-Dialog | Nur dokumentieren, nicht Hauptscreen. | -| M04 | [[M04 Unsaved Changes Confirm]] | Confirm-Dialog | Standardzustand, nur dokumentieren. | -| D10 | [[D10 Builder Interaktionsfaelle]] | Case-Matrix | Auswahlfolgen, vergessene Pflichtteile, Dropdowns, Popovers und Folgefehler dokumentieren. | - -### Antworten auf Davids Beispiel-Fragen - -| Frage | Antwort fuer D-FORM | -|---|---| -| Feld bearbeiten: inline im rechten Inspector oder eigenes Modal? | Rechter Inspector. Kein Modal fuer normale Feldbearbeitung. | -| Feld loeschen/duplizieren: Feldkarte, 3-Punkte-Menue oder Confirm? | Duplizieren ueber Feldkarte/3-Punkte-Menue ohne Confirm. Loeschen ueber 3-Punkte-Menue oder Gefahrenzone; Confirm bei Abhaengigkeiten oder published Risiko. | -| Logic-Regel anlegen: gleiches Panel, Drawer oder Modal? | Gleiches Logic-Panel bzw. rechter Detailbereich im S01. Kein Modal. | -| Post-Submit-Action hinzufuegen: Inline, Drawer oder Modal? | Typauswahl als kleines Auswahlpanel/Popover; Konfiguration im Action-Detail-Panel/Drawer. | -| Sharing oeffnen: Tab/Panel, Popover oder Modal? | Panel/Modus im S01. Copy-Aktionen nur mit Toast. | -| Publish/Unpublish: Inline-Check, Popover oder Confirm? | Publish als Inline-Check mit Summary/Popover; Unpublish als Confirm-Dialog. | - -### Was-passiert-wenn-Faelle - -Die Detailfaelle fuer Auswahlfolgen, vergessene Zwischenschritte, Checkboxen, Dropdowns, Bild/Datei-Folgen, Modaltexte und Publish-Blocker liegen in [[D10 Builder Interaktionsfaelle]]. - -Merksatz fuer den Advanced Builder: Jede Auswahl, die Folgefelder veraendert, muss sichtbar machen, was danach wichtig wird. Wenn Nutzer einen mittleren Pflichtschritt vergessen, bleibt der Fehler lokal sichtbar und wird beim Publish/Test mit konkretem Sprungziel aufgegriffen. - -## Detail: Header / Statusbar - -| Element | Zustand / Inhalt | Aktion / Ergebnis | -|---|---|---| -| Breadcrumb | Formulare > Formularname | Zur Forms-Liste zurueck, aber Forms-Liste ist nicht Teil von D-FORM. | -| Form-Name inline | Name, Dirty Marker, ggf. fehlender Name | Klick/Enter bearbeitet Name; leerer Name blockiert Publish. | -| Status-Badge | Draft, Published, Archived, Unpublished changes, Error | Erklaert, ob Public-Link aktiv ist. | -| Autosave/Save | Ungespeichert, Speichert, Gespeichert, Fehler | Speichert Form-Stammdaten, Felder, Logic, Actions. | -| Preview | Desktop/Mobile/Public-nahe Vorschau | Oeffnet Preview-Modus im selben Builder. | -| Publish | Draft -> Published | Startet Publish-Check; bei Blockern kein Publish. | -| Unpublish | Published -> Draft/private | Zeigt Warnung, weil Public-Link danach nicht mehr nutzbar ist. | -| Share | URL/QR/iFrame | Oeffnet Sharing-Modus; bei Draft mit Hinweis. | -| More-Menue | Duplizieren, Drchivieren, ggf. Vorlage erstellen | Keine destruktive Aktion ohne Confirm. | - -### Header-Zustaende - -| Zustand | UI-Hinweis | Naechste Aktion | -|---|---|---| -| Entwurf ohne Fehler | Draft-Badge, Save aktiv wenn dirty | Weiter bearbeiten oder publishen. | -| Entwurf mit Blockern | Validation-Summary sichtbar | Fehler anklicken und beheben. | -| Published ohne Aenderungen | Published-Badge, Public-Link aktiv | Teilen oder unpublishen. | -| Published mit ungespeicherten Aenderungen | Hinweis "Aenderungen noch nicht veroeffentlicht" | Speichern/Publish-Update pruefen. | -| Archived | Bearbeitung eingeschraenkt oder read-only | Duplizieren oder zur Liste zurueck. | -| Save fehlgeschlagen | Banner/Inline in Headernaehe | Erneut speichern. | - -## Detail: Feld-Palette - -Die Palette soll nicht wie eine lange Komponentenliste wirken, sondern wie ein schneller Baukasten. - -| Gruppe | Feldtypen | Notiz | -|---|---|---| -| Text & Kontakt | Text, Textarea, Email, Telefon | Die haeufigsten Felder oben. | -| Zahlen & Datum | Zahl, Waehrung, Datum, Datum-Range | Mit kurzem Format-Hinweis. | -| Auswahl | Select, Multi-Select, Boolean | Radio/Checkbox-Liste sind Darstellungsvarianten, keine extra Feldtypen. | -| Upload | Datei | Foto ist mobile Upload-Variante. | - -### Palette-Aktionen - -| Aktion | Ergebnis | -|---|---| -| Feld anklicken | Feld wird am Ende des Canvas eingefuegt und direkt ausgewaehlt. | -| Feld ziehen | Dropzone im Canvas erscheint; Feld wird an Position eingefuegt. | -| Feld suchen | Palette filtert nach Feldtyp/Use Case. | -| Feld disabled | Tooltip erklaert, warum Feldtyp nicht verfuegbar ist. | - -### Palette-Abgrenzung - -| Element | In Palette? | Begruendung | -|---|---|---| -| mCaptcha/Honeypot | nein | Systemschutz, kein normales Formularfeld. | -| Signature | nein fuer v1 | Signatur-Kontext bleibt separat/offen. | -| Address | nein fuer v1 | Haengt an Maps/Autocomplete, v2 oder Spezialbaustein. | -| Section Header / Description | offen | Kann nuetzlich sein, ist aber kein A4-12-Feldtyp. Als "Content Block" spaeter pruefen. | -| Hidden Field | nein fuer v1 | Personalisierte Links/URL-Parameter sind v2. | - -## Detail: Canvas - -Der Canvas ist keine freie Zeichenflaeche, sondern eine sortierbare Formularstruktur. - -| Canvas-Element | Zweck | -|---|---| -| Formular-Intro | Titel/Beschreibung aus Stammdaten als Kontext, nicht voll editieren im Canvas. | -| Feldkarte | Label, Typ, Pflichtmarker, kurzer Helptext, Fehlerstatus, Aktionen. | -| Dropzone zwischen Feldern | Zeigt, wo ein neues/gezogenes Feld landet. | -| Ausgewaehltes Feld | Sichtbarer Fokusrahmen; Inspector zeigt Details. | -| Inline-Fehler | Direkt an Feldkarte, wenn Label/Optionen/Validation fehlen. | -| Canvas-Summary | Oben im Canvas: Anzahl Felder, sichtbare Fehler, Logic-Warnungen. | - -### Canvas-Aktionen - -| Aktion | Erwartetes Verhalten | -|---|---| -| Feld auswahlen | Rechts oeffnet Field Inspector. | -| Feld verschieben | Reihenfolge aendert sich; Dirty State wird gesetzt. | -| Feld duplizieren | Kopie unter Original; neuer field_key wird vorgeschlagen. | -| Feld loeschen | Confirm, wenn Feld in Logic oder Actions verwendet wird. | -| Feld einklappen/ausklappen | Lange Formulare bleiben scanbar. | -| Feld als Pflicht markieren | Schnellaktion oder Inspector-Setting. | -| Fehler anklicken | Springt zum betroffenen Feld/Inspector-Abschnitt. | - -### Canvas-Zustaende - -| Zustand | Darstellung | Umsetzung | -|---|---|---| -| Empty | Grosse Dropzone mit CTA "Feld hinzufuegen"; Beispiel-Felder optional. | Bauen in S01 | -| Dragging | Dropzones sichtbar, Zielposition klar. | In S01 beschreiben | -| Feld selected | Fokusrahmen + Inspector offen. | Bauen | -| Feld invalid | Feldkarte mit Warnung und Summary oben. | Bauen/dokumentieren | -| Lange Liste | Sticky Header, Feldkarten kompakt, ggf. Mini-Navigation. | Beschreiben | -| Referenzbruch | Feldkarte zeigt "Wird in Logic/Action verwendet". | Wichtig fuer Delete/Publish | - -## Detail: Feld-Inspector - -Der Inspector ist der eigentliche Produktivbereich fuer Felddetails. Er braucht klare Sektionen, sonst wird der Builder unlesbar. - -| Inspector-Sektion | Inhalt | Sichtbarkeit | -|---|---|---| -| Basis | Label, field_key, Beschreibung, Placeholder, Pflichtfeld. | immer | -| Darstellung | Breite/Variante, Helptext-Position, Auswahl-Darstellung. | je Feldtyp | -| Optionen | Select/Multi-Select-Optionen, Reihenfolge, Default. | Auswahlfelder | -| Validation | Min/max, Regex, Datei-Typ/Groesse, Custom Error. | je Feldtyp | -| Default | Vorbefuellung, sofern erlaubt. | optional | -| Logic-Hinweis | Welche Regeln dieses Feld nutzen. | wenn betroffen | -| Mapping-Hinweis | Welche Actions dieses Feld verwenden. | wenn betroffen | -| Gefahrenzone | Feld loeschen. | unten, klar getrennt | - -### Field-Key-Regel - -`field_key` soll sichtbar, aber nicht lauter als Label sein. Empfehlung: - -- Automatisch aus Label erzeugen. -- Editierbar in Advanced/Basisdetails. -- Warnung, wenn doppelt oder ungueltig. -- Beim Umbenennen zeigen, ob Actions/Logic betroffen sind. - -## Detail: Feldtypen und Inspector-Properties - -| Feldtyp | Basis-Properties | Spezielle Properties | Validation | -|---|---|---|---| -| Text | Label, Key, Helptext, Placeholder, Pflicht | Einzeilig, Default | min/max Laenge, Regex, Custom Error | -| Textarea | Label, Key, Helptext, Placeholder, Pflicht | Zeilenhoehe, Markdown-Anmutung optional | min/max Laenge, Custom Error | -| Zahl | Label, Key, Helptext, Placeholder, Pflicht | Ganzzahl/Dezimal, Schrittweite | min/max Wert, Custom Error | -| Waehrung | Label, Key, Helptext, Pflicht | Waehrung, Dezimalstellen, Format | min/max Betrag | -| Datum | Label, Key, Helptext, Pflicht | Datum, optional Zeit als v2/Variante | min/max Datum | -| Datum-Range | Label, Key, Helptext, Pflicht | Start-/End-Label, Zeitraum | Ende nach Start, min/max Datum | -| Select | Label, Key, Helptext, Pflicht | Optionen, Default, Darstellung Dropdown/Radio | mind. 1 Option, keine leeren Optionen | -| Multi-Select | Label, Key, Helptext, Pflicht | Optionen, max. Auswahl, Chips/Checkboxen | min/max Auswahl | -| Boolean | Label, Key, Helptext | Checkbox/Toggle, Zustimmungstext | Muss true sein optional | -| Email | Label, Key, Helptext, Placeholder, Pflicht | Formatvalidierung, ggf. Submitter-Email markieren | Email-Format | -| Telefon | Label, Key, Helptext, Placeholder, Pflicht | Land/Format-Hinweis | Telefonformat | -| Datei | Label, Key, Helptext, Pflicht | Erlaubte Dateitypen, max. Groesse, mehrere Dateien, Foto-Hinweis | Datei-Typ, Groesse | - -### Feldtypen-Entscheidungen offen fuer David - -| Frage | Empfehlung | -|---|---| -| URL als eigener Feldtyp? | Nicht in A4-12, fuer v1 als Text mit URL-Validation oder spaeterer Spezialtyp. | -| Foto als eigener Feldtyp? | Nein, als Datei-Upload-Variante mit Mobile-Kamera-Hinweis. | -| Signature als Feldtyp? | Nicht in D-FORM v1, weil Signatur-Wizard/Abnahme eigener Kontext ist. | -| Section Header/Description Block? | Als optionaler Content Block pruefen, aber nicht als Pflichtbestandteil der 12 Feldtypen. | -| Hidden Field? | Raus fuer v1, weil personalisierte Links/URL-Parameter v2 sind. | - -### Feld-Interaktionsfaelle - -Siehe [[D10 Builder Interaktionsfaelle]] fuer: - -- Select-Feld ohne Optionen. -- Datei/Fotofeld mit Upload-, Kamera-, Groessen- und Typ-Hinweisen. -- Field Key wird geaendert und betrifft Logic/Actions. -- Option wird geloescht, obwohl sie in Logic genutzt wird. -- Feldtyp wird nachtraeglich geaendert. - -## Detail: Validation - -Validation muss zwei Ebenen haben: Feldvalidierung beim Bearbeiten und Publish-Validierung fuer das gesamte Formular. - -| Ebene | Beispiele | UI-Form | -|---|---|---| -| Feld-Validation | Label fehlt, Option leer, min > max, Datei ohne erlaubte Typen. | Inline am Feld + Inspector. | -| Form-Validation | Kein sichtbares Feld, Slug fehlt, Slug-Konflikt, kaputte Logic. | Summary-Banner + klickbare Liste. | -| Action-Validation | Pflichtmapping fehlt, Webhook-URL ungueltig, Empfaenger fehlt. | Im Actions-Modus + Publish-Summary. | -| Sharing-Validation | Draft nicht public, iFrame ohne Publish, Allowed-Origin-Warnung. | Im Sharing-Modus. | - -### Publish-Blocker - -| Blocker | Warum blockiert? | Wo zeigen? | -|---|---|---| -| Formularname fehlt | Public/Intern nicht identifizierbar. | Header + Summary | -| Slug fehlt oder Konflikt | Public URL kann nicht erzeugt werden. | Settings + Summary | -| Kein sichtbares Feld | Public Form waere leer. | Canvas Empty + Summary | -| Feld ohne Label | Public Form waere unklar. | Feldkarte + Inspector | -| Select ohne Optionen | Submitter kann nicht antworten. | Feldkarte + Inspector | -| Datum-Range ungueltig | Regel ist widerspruechlich. | Inspector | -| Kaputte Logic-Referenz | Public-Anzeige nicht verlaesslich. | Logic-Modus + Summary | -| Pflichtmapping fehlt | Action wuerde fehlschlagen. | Actions-Modus + Summary | - -### Nicht blockierend, aber warnen - -| Warnung | Grund | -|---|---| -| Required Email ohne Submitter-Email-Markierung | Kann fuer Auto-Match/Antwortmail relevant sein. | -| Datei-Upload ohne Typbeschraenkung | Risiko fuer Public Form. | -| Sehr langes Formular | Public-Conversion/Mobile leidet. | -| Published Form wird stark geaendert | Bestehender Public-Link bleibt aktiv, Aenderung ist relevant. | - -## Detail: Conditional Logic - -V1 soll einfach bleiben: Anzeige/Ausblenden, keine komplexe Verzweigungsmaschine. - -| Element | Inhalt | -|---|---| -| Regelname optional | z. B. "Zeige Upload bei Foto vorhanden". | -| Wenn-Feld | Nur vorhandene Felder, die als Bedingung sinnvoll sind. | -| Operator | ist, ist nicht, enthaelt, enthaelt nicht, ist leer, ist nicht leer. | -| Wert | Abhaengig vom Feldtyp, z. B. Option bei Select. | -| Dann-Aktion | Feld anzeigen oder Feld verstecken. | -| Ziel-Feld | Das Feld, dessen Sichtbarkeit gesteuert wird. | -| Aktiv/Inaktiv | Regel testen oder parken. | - -### Logic-Regeln - -| Fall | UI-Verhalten | -|---|---| -| Bedingungsfeld geloescht | Regel wird rot markiert und blockiert Publish. | -| Ziel-Feld geloescht | Regel wird rot markiert und blockiert Publish. | -| Ziel-Feld required aber versteckt | Hinweis: Pflicht gilt nur wenn sichtbar. | -| Mehrere Regeln fuer ein Feld | Hinweis auf Kombinationslogik; v1 braucht einfache Erklaerung. | -| Regel testen | Preview zeigt, welche Felder live erscheinen/verschwinden. | - -Nicht v1: Multi-Step-Flow, Repeater, Cross-Field-Rechnung, If/Else-Baum, Scoring, A/B-Testing. - -## Detail: Publish / Unpublish - -| Aktion | Ablauf | -|---|---| -| Publish | Nutzer klickt Publish, System prueft Blocker, bei Erfolg Status Published und Public URL aktiv. | -| Publish mit Blockern | Kein Publish; Summary zeigt Fehler mit Sprungziel. | -| Update published Form | Hinweis, dass Aenderungen die public Version betreffen; Save/Publish-Update klar trennen falls noetig. | -| Unpublish | Confirm mit Hinweis, dass Public URL nicht mehr erreichbar ist. | -| Archive | More-Menue, Confirm; nicht als Hauptaktion. | - -Empfehlung: Publish ist kein eigener Screen. Ein Confirm/Popover reicht, solange Blocker klar im Builder sichtbar sind. - -## Detail: Sharing - -Sharing ist v1, aber nur fuer einfache Verteilung. - -| Bereich | Inhalt | Aktion | -|---|---|---| -| Public URL | `app.hivegrid.de/f/{tenant}/{slug}` | Kopieren, oeffnen. | -| QR-Code | QR fuer Public URL | Kopieren/downloaden. | -| iFrame | Embed-Code mit Breite/Hoehe/Auto-Resize-Hinweis | Code kopieren. | -| Publish-Hinweis | Draft/Unpublished zeigt Warnung | Zum Publish springen. | -| v2-Abgrenzung | Personalisierte Links, JS-Snippet | Nicht als aktiver UI-Teil. | - -### Sharing-Zustaende - -| Zustand | Darstellung | -|---|---| -| Draft | URL-Vorschau ausgegraut, Hinweis "erst nach Publish erreichbar". | -| Published | URL, QR, iFrame aktiv. | -| Slug fehlt | Sharing blockiert und verweist auf Settings. | -| Public geschlossen/Limit erreicht | Hinweis, dass Link zwar existiert, Public Form aber blockieren kann. | - -## Detail: Post-Submit-Actions - -Actions sind sichtbar, aber S01 darf nicht wie ein voller Automation-Editor werden. - -| Action-Typ | Zweck in S01 | Konfiguration | -|---|---|---| -| Email an Submitter | Bestaetigung senden. | Empfaengerfeld, Betreff, Nachricht/Template. | -| Email an Team | Intern informieren. | Empfaenger, Betreff, Felder-Auswahl. | -| Lead erstellen | Submission in Lead-Inbox geben. | Mapping: Name, Email, Telefon, Nachricht, Quelle. | -| Kontakt erstellen | CRM-Kontakt anlegen. | Mapping, Duplikat-/Auto-Match-Hinweis. | -| Projekt erstellen | Projekt/Auftrag anlegen. | Pipeline/Status, Pflichtmapping, Verantwortlicher. | -| Task erstellen | Aufgabe erzeugen. | Titel, Beschreibung, Faelligkeit, Assignee. | -| Auto-Match | Bestehende Entitaet per Email/Telefon verknuepfen. | Match-Felder, Fallback. | -| Webhook | Externes System informieren. | URL, Methode falls noetig, Test. | -| Automation-Trigger | M17 Automation starten. | Automation-Auswahl/Trigger. | -| KWP Push | Laut M05 vorhanden, aber KWP v1 raus. | Nicht aktiv in D-FORM v1, hoechstens v2/raus markieren. | - -### Action-UI - -| Element | Zweck | -|---|---| -| Action-Liste | Reihenfolge, Typ, Status, Warnungen. | -| Add Action | Typ-Auswahl mit kurzer Erklaerung. | -| Action-Detail | Ziel, Bedingungen, Mapping, Test. | -| Field-Mapping-Zeilen | Form-Feld -> Ziel-Feld; Pflichtfelder sichtbar. | -| Test/Preview | Zeigt, was bei einer Beispiel-Submission erzeugt wuerde. | -| Action aktiv/inaktiv | Action parken ohne Loeschen. | - -### Action-Blocker - -| Blocker | UI | -|---|---| -| Pflichtmapping fehlt | Warnung an Action + Publish-Summary. | -| Ziel-Pipeline fehlt | Inline im Projekt-Action-Detail. | -| Email-Empfaenger fehlt | Inline im Email-Action-Detail. | -| Webhook-URL ungueltig | Inline + Test disabled. | -| Automation nicht aktiv/verfuegbar | Warnung mit Erklaerung. | - -Nicht in S01 bauen: ActionLog, Re-Run, Fehlerhistorie je Submission. Das gehoert zur Submissions-Ansicht. - -## Detail: Permissions / Read-only - -| Permission / Rolle | Darf sehen | Darf tun | -|---|---|---| -| `forms.basic` | ggf. Submissions, aber nicht Builder-Fokus. | Kein Builder-Hauptflow. | -| `forms.builder` | Builder sehen und bearbeiten. | Felder, Logic, Settings speichern. | -| `forms.publisher` | Builder + Sharing. | Publish/Unpublish, Export/Submissions ausserhalb D-FORM. | -| `forms.admin` | Alles im Builder. | Actions, Templates, administrative Optionen. | - -### Permission-Zustaende in S01 - -| Zustand | UI | -|---|---| -| Keine Builder-Rechte | Permission-State statt Builder; CTA "Admin kontaktieren" oder zurueck. | -| Read-only wegen fehlender Publish-Rechte | Publish disabled mit Tooltip, Bearbeitung ggf. erlaubt. | -| Actions nur Admin | Actions-Modus disabled oder read-only mit Hinweis. | -| Archived Form | Bearbeitung eingeschraenkt; Duplizieren als naechste Aktion. | - -## Daten - -- Form-Stammdaten: Name, Beschreibung, Slug, Status, Subtype. -- Form-Felder: Reihenfolge, Feldtyp, Label, Beschreibung/Hilfe, Placeholder, Pflichtfeld. -- Feldtypen v1: Text, Textarea, Zahl, Waehrung, Datum, Datum-Range, Select, Multi-Select, Boolean, Email, Telefon, Datei. -- Feld-Konfiguration: Optionen, Default, Validation, Datei-/Foto-Regeln je Feldtyp. -- Conditional Logic: einfache Wenn-Dann-Regeln. -- Live-Vorschau: gerenderte Form in Public-nahem Zustand. -- Sharing: Public URL, QR-Code, iFrame-Code. -- Post-Submit-Actions: Lead/Kontakt/Projekt/Task, Email, Webhook, Automation-Trigger, Auto-Match. -- Field-Mapping je Action. -- Publish-Status und moegliche Publish-Blocker. - -## Header / Statusbar - -| Element | Zweck | -|---|---| -| Form-Name inline | schnell umbenennen. | -| Status-Badge | Draft, Published, Archived, Fehler. | -| Save | Entwurf sichern. | -| Preview | Public-nahe Vorschau oeffnen/umschalten. | -| Publish/Unpublish | Public-Verfuegbarkeit steuern. | -| Share | URL, QR und iFrame erreichbar machen. | -| Validation-Summary | zeigt, ob Publish blockiert ist. | - -## Aktionen - -- Feld aus Palette hinzufuegen. -- Feld per Drag-and-Drop verschieben. -- Feld auswaehlen. -- Feld bearbeiten. -- Feld duplizieren. -- Feld loeschen. -- Feld als Pflichtfeld markieren. -- Validierung konfigurieren. -- Optionen fuer Select/Multi-Select/Radio pflegen. -- Conditional-Logic-Regel anlegen, bearbeiten, deaktivieren, loeschen. -- Vorschau ansehen. -- Form speichern. -- Form publishen/unpublishen. -- Public URL kopieren. -- QR-Code kopieren/downloaden. -- iFrame-Code kopieren. -- Post-Submit-Action hinzufuegen. -- Field-Mapping setzen. -- Webhook testen oder validieren. - -## Feldtypen v1 - -| Feldtyp | Was David sichtbar machen sollte | -|---|---| -| Text | Label, Placeholder, Pflicht, min/max Laenge. | -| Textarea | Label, Helptext, Pflicht, min/max Laenge. | -| Zahl | Label, min/max, Dezimal/Ganzzahl-Hinweis. | -| Waehrung | Betrag, Waehrung/Format, min/max. | -| Datum | Date-Picker, Pflicht, min/max Datum optional. | -| Datum-Range | Start/Ende, Validierung Ende nach Start. | -| Select | Optionen, ein Wert. | -| Multi-Select | Optionen, mehrere Werte. | -| Boolean | Checkbox/Toggle, Pflicht-/Zustimmungsfall. | -| Email | Email-Validation automatisch. | -| Telefon | Telefonformat-Hinweis. | -| Datei | Upload, erlaubte Typen, max. Groesse; Foto als mobile Upload-Variante. | - -Anti-Spam-Felder wie Honeypot/mCaptcha sind Systemschutz, nicht Teil der sichtbaren 12-Feld-Palette. - -## Logic-Arbeitsbereich - -| Element | Zweck | -|---|---| -| Regel-Liste | Alle aktiven Regeln scannen. | -| Neue Regel | Wenn Feld Y Operator Wert Z, dann Feld X anzeigen/verstecken. | -| Feld-Auswahl | Nur vorhandene Felder; kaputte Referenzen sichtbar markieren. | -| Operatoren | v1 simpel: ist, ist nicht, enthaelt, ist leer/nicht leer. | -| Ziel-Feld | Welches Feld wird angezeigt oder verborgen. | -| Regel aktiv/inaktiv | Regel testen oder parken. | - -Nicht bauen: komplexe If/Else-Baeume, Multi-Step-Flow, Cross-Field-Rechnung. - -## Post-Submit-Actions Schnitt - -| Bereich | In S01 zeigen? | Notiz | -|---|---|---| -| Action-Typ waehlen | ja | Lead/Kontakt/Projekt/Task, Email, Webhook, Automation-Trigger, Auto-Match. | -| Reihenfolge | ja | Actions laufen in definierter Reihenfolge. | -| Aktiv/Inaktiv | ja | Einzelne Actions deaktivierbar. | -| Field-Mapping | ja | Form-Feld -> Ziel-Feld, Pflichtwarnungen sichtbar. | -| Bedingung pro Action | optional | Nur einfache Filter, kein voller Logic-Builder. | -| Test/Preview | ja | Was wuerde erzeugt/gesendet? | -| Voller ActionLog/Re-Run | nein | Gehoert zur Submissions-Ansicht, nicht zum Builder-Canvas. | - -## Sharing-Arbeitsbereich - -| Element | Zweck | -|---|---| -| Public URL | Link kopieren, ggf. oeffnen. | -| QR-Code | Fuer Print/Monteure/Kundenkontakt. | -| iFrame-Code | Einbettung auf Website. | -| Publish-Hinweis | Sharing ist erst sinnvoll, wenn published. | -| v2-Hinweis intern | Personalisierte Links und JS-Snippet nicht in v1. | - -## Settings-Arbeitsbereich - -| Element | Zweck | -|---|---| -| Slug | Public URL steuern. | -| Success Message | Text nach Submit. | -| Redirect URL | optionales Weiterleitungsziel. | -| Tenant Branding / Theme | Logo/Farbe/Fallback, soweit v1 vorgesehen. | -| Allowed Origins | falls iFrame/API eingeschraenkt wird. | -| Submission Limit / Close Date | optional, falls Dev fuer v1 bestaetigt. | - -## Banner / Hinweise - -- Publish-Blocker-Banner: zeigt konkrete fehlende Punkte, z. B. kein sichtbares Feld, fehlender Slug, ungueltige Logic-Regel. -- Unsaved-Changes-Hinweis: wenn Nutzer weg navigiert oder publizieren will. -- Reuse-Hinweis optional: Public Renderer wird auch fuer HR/Booking-Intake wiederverwendet, aber nicht als sichtbarer Feature-Text im UI. - -## Toasts / Action-Feedback - -- Gespeichert. -- Veroeffentlicht. -- Entwurf deaktiviert/unpublished. -- Public URL kopiert. -- QR-Code kopiert/downloadet. -- iFrame-Code kopiert. -- Action gespeichert. -- Test-Webhook erfolgreich/fehlgeschlagen. - -## Meldungsentscheidung - -| Ereignis | UI-Form | Blockiert? | Umsetzung | Copy/Notiz | -|---|---|---|---|---| -| Keine Felder im Canvas | Empty State im Canvas | nein | Bauen | Feld aus Palette hinzufuegen. | -| Feld ohne Label | Inline am Feld + Summary | ja fuer Publish | Nur dokumentieren | Feld konkret benennen. | -| Required Email ohne Email-Validation | Inline-Warnung | nein/Publish-Warnung | Nur dokumentieren | Vorschlag: Email-Validation hinzufuegen. | -| Logic-Regel referenziert geloeschtes Feld | Inline im Logic-Panel + Summary | ja fuer Publish | Nur dokumentieren | Regel reparieren oder deaktivieren. | -| Ungueltige Webhook-URL | Inline im Action-Panel | ja fuer Action | Nur dokumentieren | URL muss http/https und erreichbar sein. | -| Publish erfolgreich | Toast + Statuswechsel | nein | Nur dokumentieren | Veroeffentlicht. | -| Kopieren erfolgreich | Toast | nein | Nur dokumentieren | In Zwischenablage kopiert. | - -## Zustaende - -| Zustand | Beschreibung | Umsetzung | -|---|---|---| -| Leer | Neues Formular ohne Felder; Canvas zeigt Drop-Zone und CTA. | Bauen | -| Laden | Form-Definition und Actions werden geladen. | Wiederverwenden | -| Fehler | Form kann nicht geladen werden. | Nur dokumentieren | -| Keine Berechtigung | Nutzer hat keine `forms.builder`/`forms.publish`-Rechte. | Nur dokumentieren | -| Dirty State | Ungespeicherte Aenderungen. | Nur dokumentieren | -| Publish blocked | Form hat Fehler, die Public-Version verhindern. | Nur dokumentieren | - -## Blockierende Error States - -| Zustand | Was sieht der Nutzer? | Primaeraktion | Sekundare Aktion | -|---|---|---|---| -| Publish blockiert | Banner mit Liste der fehlenden Punkte. | Fehler beheben | Als Entwurf speichern | -| Keine Berechtigung | Permission-State mit Klartext. | Zurueck zu Formularen | Admin kontaktieren | -| Form nicht gefunden | Fehler-State. | Zurueck zu Formularen | - | - -## Permissions / Gates - -- `forms.builder`: Felder und Konfiguration bearbeiten. -- `forms.publisher`: Publish/Unpublish und Sharing aktivieren. -- `forms.admin`: Actions, Mapping und administrative Optionen. - -## Subframe - -- [Subframe](https://app.subframe.com/f023064a5ca0/design/e5502f88-9858-460e-abae-671f28464270/edit) - -Layout-Regel: Der Advanced Builder nutzt das vorhandene Subframe `Default Page Layout`. Keine eigene Sidebar nachbauen. - -## Notizen - -Der Builder soll nicht den Automation-Flow-Editor nachbauen. Post-Submit-Actions brauchen nur Reihenfolge, Typ, Ziel und Field-Mapping. - -Conditional Logic ist v1, aber eng: Wenn Feld Y = Wert Z, dann Feld X anzeigen. Kein Repeater, kein Multi-Step, keine Cross-Field-Validation. - -Wichtig fuer David: Der Screen ist eine Arbeitsflaeche, kein Marketing-Screen. Dichte ist erlaubt, solange die Hauptarbeit gut scannbar bleibt. diff --git a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/90b ADV Form-Builder Canvas 2.md b/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/90b ADV Form-Builder Canvas 2.md deleted file mode 100644 index 91e05c4d..00000000 --- a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/90b ADV Form-Builder Canvas 2.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -kind: screen -project: HiveGrid Screen Project -ticket: D-FORM -package: WF-D-FORM Form Builder -id: ADV-S01-2 -screen: Form-Builder Canvas 2 -typ: Advanced / Referenz -umsetzung: Referenz behalten -status: Subframe Draft / aktuell nicht Hauptfluss -route: /forms/:id/edit -subframe: "https://app.subframe.com/f023064a5ca0/design/9529ac6f-eb71-4e5c-8774-7cebe9c8162c/edit" ---- - -# ADV-S01-2 - Form-Builder Canvas 2 - -## Status - -Diese Seite ist aktuell **nicht Teil des Hauptflusses**. - -Sie bleibt als Advanced-/Referenzscreen erhalten, falls David oder spaetere Umsetzung die umfangreichere Builder-Logik pruefen will. - -## Warum nicht Hauptfluss - -- Zu viel sichtbare Komplexitaet fuer normale Handwerksbetriebe. -- S02 ist aktuell die empfohlene Google-Forms-nahe Hauptvariante. -- Logic, Actions und Publish-Blocker werden in S02 einfacher geloest. - -## Subframe - -Gebaut in Subframe: - -https://app.subframe.com/f023064a5ca0/design/9529ac6f-eb71-4e5c-8774-7cebe9c8162c/edit - diff --git a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/91 D07 Logic Regel Detail.md b/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/91 D07 Logic Regel Detail.md deleted file mode 100644 index bf532cba..00000000 --- a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/91 D07 Logic Regel Detail.md +++ /dev/null @@ -1,98 +0,0 @@ ---- -kind: screen -project: HiveGrid Screen Project -ticket: D-FORM -package: WF-D-FORM Form Builder -id: D07 -screen: Logic Regel Detail -typ: Panel-Zustand in S01 -umsetzung: Bauen -status: Subframe Draft / Review offen -route: in S01 Logic-Modus, kein eigener Screen -subframe: "https://app.subframe.com/f023064a5ca0/design/07b60457-8151-46b9-ba34-34eceffc08bb/edit" ---- - -# D07 - Logic Regel Detail - -## Ziel - -Interne Nutzer koennen eine einzelne Sichtbarkeitsregel sauber anlegen oder bearbeiten. - -## Pattern / Archetyp - -Detailpanel / Drawer innerhalb des Logic-Modus von S01. - -## Daten - -- Regelname optional -- Bedingungsfeld -- Operator -- Wert -- Dann-Aktion: anzeigen oder verstecken -- Zielfeld -- Status aktiv/inaktiv -- Preview-/Testhinweis - -## Aktionen - -- Bedingungsfeld waehlen -- Operator waehlen -- Wert setzen -- Zielfeld waehlen -- Aktion anzeigen/verstecken waehlen -- Regel speichern -- Regel testen -- Regel deaktivieren -- Regel loeschen - -## Verhalten - -- `Neue Regel` oeffnet dieses Detailpanel im selben S01-Screen. -- Keine Modal-Kaskade fuer Logic. -- Feldauswahlen zeigen nur vorhandene passende Felder. -- Wenn ein referenziertes Feld fehlt, bleibt die Regel sichtbar und markiert kaputt. -- Test/Preview kann direkt den Preview-Modus beeinflussen oder eine kurze Vorschau im Panel zeigen. -- Detailfaelle fuer unvollstaendige Auswahlfolgen stehen in [[D10 Builder Interaktionsfaelle]]. - -## Zustaende - -| Zustand | Beschreibung | Umsetzung | -|---|---|---| -| Neue Regel | leeres Detailpanel mit Pflichtfeldern. | Nur dokumentieren | -| Gueltige Regel | alle Referenzen vorhanden. | Nur dokumentieren | -| Unvollstaendig | Pflichtteil fehlt; Regel kann nicht aktiv werden. | Nur dokumentieren | -| Referenz kaputt | Bedingungs- oder Zielfeld fehlt. | Nur dokumentieren | -| Testmodus | Preview zeigt Wirkung der Regel. | In S01 dokumentieren | - -## Auswahlfolge im Panel - -| Schritt | Verhalten | -|---|---| -| 1. Wenn-Feld | Nach Auswahl wird das Operator-Dropdown passend zum Feldtyp aktiv. | -| 2. Operator | Bei `ist leer`/`ist nicht leer` entfaellt das Wertfeld. Bei Select/Boolean wird das Wertfeld zur passenden Auswahl. | -| 3. Wert | Pflicht nur bei Operatoren, die einen Wert brauchen. | -| 4. Dann-Aktion | `anzeigen` oder `verstecken`; keine komplexe If/Else-Struktur. | -| 5. Zielfeld | Darf nicht identisch mit dem Bedingungsfeld sein. | -| 6. Aktiv | Erst aktivierbar, wenn alle Pflichtteile gueltig sind. | - -## Fehlerlogik - -| Fehler | Anzeige | Folge | -|---|---|---| -| Operator fehlt | Inline unter Operator. | Test disabled, aktive Regel nicht moeglich. | -| Wert fehlt | Inline unter Wertfeld. | Test disabled, Publish blockiert nur bei aktiver Regel. | -| Zielfeld fehlt | Inline unter Zielfeld. | Regel unvollstaendig. | -| Referenz kaputt | Rote Markierung an Regel und Detailpanel. | Publish blockiert, Sprung aus Summary. | -| Feldtyp geaendert | Dlter Wert kann ungueltig werden. | Wert neu waehlen oder Regel deaktivieren. | - -## Subframe - -- [Subframe](https://app.subframe.com/f023064a5ca0/design/07b60457-8151-46b9-ba34-34eceffc08bb/edit) - -Gebaut als zusaetzlicher Builder-Zustand. Das Detail bleibt fachlich ein Zustand im Logic-Modus von S01, ist aber als eigener Frame sichtbar, damit David die Regelbearbeitung pruefen kann. - -Layout-Regel: D07 nutzt das vorhandene Subframe `Default Page Layout`. Keine eigene Sidebar oder Header-Navigation nachbauen. - -## Notizen - -Die Regel-Logik bleibt bewusst eng: Wenn Feld Y Operator Wert Z, dann Feld X anzeigen/verstecken. diff --git a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/92 D08 Action Detail mit Mapping.md b/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/92 D08 Action Detail mit Mapping.md deleted file mode 100644 index 671bb921..00000000 --- a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/92 D08 Action Detail mit Mapping.md +++ /dev/null @@ -1,112 +0,0 @@ ---- -kind: screen -project: HiveGrid Screen Project -ticket: D-FORM -package: WF-D-FORM Form Builder -id: D08 -screen: Action Detail mit Mapping -typ: Panel-Zustand in S01 -umsetzung: Bauen -status: Subframe Draft / Review offen -route: in S01 Actions-Modus, kein eigener Screen -subframe: "https://app.subframe.com/f023064a5ca0/design/0759d779-91f5-4e06-b1d6-7cd0095461ea/edit" ---- - -# D08 - Action Detail mit Mapping - -## Ziel - -Interne Nutzer konfigurieren eine einzelne Post-Submit-Action und sehen sofort, ob Pflichtmapping oder Zielwerte fehlen. - -## Pattern / Archetyp - -Detailpanel / Drawer innerhalb des Actions-Modus von S01. - -## Daten - -- Action-Typ -- Action-Name -- Status aktiv/inaktiv -- Ziel, z. B. Empfaenger, Pipeline, Projektstatus, Webhook-URL -- Field-Mapping: Formularfeld -> Ziel-Feld -- Pflichtfelder -- optionale Bedingungen -- Teststatus - -## Aktionen - -- Action benennen -- Ziel konfigurieren -- Mapping-Zeilen setzen -- optionale Bedingung setzen -- Test ausfuehren -- Action speichern -- Action deaktivieren -- Action loeschen - -## Verhalten - -- Nach Auswahl eines Action-Typs oeffnet sich dieses Detailpanel. -- Mapping wird inline als Zeilenliste bearbeitet. -- Pflichtmapping wird klar markiert. -- Test zeigt ein Ergebnis im Panel, kein eigener Screen. -- Loeschen einer aktiven oder published-relevanten Action nutzt Confirm. -- Detailfaelle fuer halb gesetztes Mapping, Typwechsel und fehlende Pflichtziele stehen in [[D10 Builder Interaktionsfaelle]]. - -## Action-Typen - -| Typ | Wichtigste Konfiguration | -|---|---| -| Email Submitter | Submitter-Email-Feld, Betreff, Nachricht. | -| Email Team | Empfaenger, Betreff, Feldauswahl. | -| Lead | Mapping fuer Name, Email, Telefon, Nachricht, Quelle. | -| Kontakt | Kontakt-Mapping und Duplikat-/Auto-Match-Hinweis. | -| Projekt | Pipeline, Status, Verantwortlicher, Pflichtmapping. | -| Task | Titel, Beschreibung, Faelligkeit, Assignee. | -| Auto-Match | Match-Felder, Fallback. | -| Webhook | URL, Test, Payload-Vorschau. | -| Automation-Trigger | Automation-Auswahl und Trigger-Hinweis. | - -## Zustaende - -| Zustand | Beschreibung | Umsetzung | -|---|---|---| -| Neu | Action-Typ gewaehlt, Detail noch leer. | Nur dokumentieren | -| Vollstaendig | Pflichtfelder und Mapping gesetzt. | Nur dokumentieren | -| Pflichtmapping fehlt | Publish oder Action-Test blockiert. | Nur dokumentieren | -| Test erfolgreich | Inline-Ergebnis plus Toast. | Wiederverwenden | -| Test fehlgeschlagen | Inline-Fehler im Panel. | Nur dokumentieren | - -## Auswahlfolge im Panel - -| Schritt | Verhalten | -|---|---| -| 1. Action-Typ | Bestimmt, welche Pflichtfelder und Mapping-Zeilen erscheinen. | -| 2. Ziel | Empfaenger, Pipeline, Webhook-URL, Automation oder Entity-Ziel wird gesetzt. | -| 3. Mapping | Formularfelder werden Ziel-Feldern zugeordnet. | -| 4. Optional Bedingung | Nur einfache Bedingung; kein zweiter Logic-Builder. | -| 5. Test | Erst aktiv, wenn Pflichtziel und Pflichtmapping vollstaendig sind. | -| 6. Aktiv | Aktive unvollstaendige Actions blockieren Publish; inaktive unvollstaendige Actions nicht. | - -## Was-passiert-wenn-Faelle - -| Fall | UI-Reaktion | Folge | -|---|---|---| -| Action-Typ gewaehlt, Pflichtziel vergessen | Inline-Fehler im Zielbereich. | Test disabled; aktive Action blockiert Publish. | -| Mapping-Ziel gesetzt, Quelle vergessen | Mapping-Zeile markiert fehlende Quelle. | Test disabled; aktive Action blockiert Publish. | -| Email Submitter ohne Email-Feld | Hinweis mit CTD `Email-Feld hinzufuegen`. | Action bleibt unvollstaendig. | -| Projekt erstellen ohne Pipeline | Pipeline-Auswahl markiert. | Publish blockiert aktive Action. | -| Webhook-URL ungueltig | URL-Feld zeigt Fehler. | Test disabled. | -| Action-Typ nach Mapping wechseln | Confirm/Popover erklaert, dass Mapping ungueltig werden kann. | Nutzer bestaetigt oder bricht ab. | - -## Subframe - -- [Subframe](https://app.subframe.com/f023064a5ca0/design/0759d779-91f5-4e06-b1d6-7cd0095461ea/edit) - -Gebaut als zusaetzlicher Builder-Zustand. Das Detail bleibt fachlich ein Zustand im Actions-Modus von S01, ist aber als eigener Frame sichtbar, weil Mapping und Pflichtfelder fuer den Builder sehr fehleranfaellig sind. - -Layout-Regel: D08 nutzt das vorhandene Subframe `Default Page Layout`. Keine eigene Sidebar oder Header-Navigation nachbauen. - -## Notizen - -ActionLog, Re-Run und Fehlerhistorie pro Submission bleiben ausserhalb von D-FORM. diff --git a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/93 D09 Publish Blocker Summary.md b/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/93 D09 Publish Blocker Summary.md deleted file mode 100644 index e41fc2b8..00000000 --- a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/93 D09 Publish Blocker Summary.md +++ /dev/null @@ -1,114 +0,0 @@ ---- -kind: screen -project: HiveGrid Screen Project -ticket: D-FORM -package: WF-D-FORM Form Builder -id: D09 -screen: Publish Blocker Summary -typ: Inline-Zustand in S01 -umsetzung: Bauen -status: Subframe Draft / Review offen -route: in S01 nach Klick auf Publish -subframe: "https://app.subframe.com/f023064a5ca0/design/211e8ca7-fc0b-4c52-a4a4-a1e660209e58/edit" ---- - -# D09 - Publish Blocker Summary - -## Ziel - -Interne Nutzer verstehen nach Klick auf Publish sofort, warum ein Formular nicht veroeffentlicht werden kann und wo sie es korrigieren. - -## Pattern / Archetyp - -Inline Summary-Banner / Popover mit Sprungzielen im S01-Builder. - -## Daten - -- Anzahl Blocker -- Blocker-Gruppe: Form, Feld, Logic, Actions, Sharing/Settings -- betroffener Feldname oder Action-Name -- Ziel im Builder, zu dem gesprungen werden kann -- nicht-blockierende Warnungen - -## Aktionen - -- Blocker anklicken -- zum Feld springen -- zum Inspector-Abschnitt springen -- Logic-Modus oeffnen -- Actions-Modus oeffnen -- Settings oeffnen -- Publish nach Korrektur erneut versuchen - -## Verhalten - -- Klick auf `Publish` startet einen Check im selben Screen. -- Bei Blockern wird nicht published. -- Die Summary erscheint oben im Builder oder als klarer Inline-Bereich nahe der Topbar. -- Jeder Blocker hat ein Sprungziel. -- Nicht-blockierende Warnungen koennen als eigener Abschnitt erscheinen und erlauben Publish nach Bestaetigung. -- Bei Erfolg gibt es Statuswechsel und Toast, keinen eigenen Erfolgsscreen. -- Ausfuehrliche Auswahl-/Vergessensfaelle stehen in [[D10 Builder Interaktionsfaelle]]. - -## Blocker - -| Blocker | Ziel | -|---|---| -| Formularname fehlt | Header/Form-Name | -| Slug fehlt oder Konflikt | Settings | -| Kein sichtbares Feld | Canvas Empty State | -| Feld ohne Label | Feldkarte + Inspector | -| Select ohne Optionen | Feldkarte + Inspector Optionen | -| Kaputte Logic | Logic-Modus | -| Pflichtmapping fehlt | Actions-Modus | -| Webhook-URL ungueltig | Action-Detail | - -## Was-passiert-wenn-Faelle - -| Fall | Summary-Verhalten | Sprungziel | -|---|---|---| -| Nutzer hat Select-Feld angelegt, aber Optionen vergessen | Blocker `Auswahlfeld hat keine Optionen`. | Feldkarte + Inspector `Optionen` | -| Nutzer hat Logic-Regel halb gebaut | Blocker `Logic-Regel unvollstaendig`, wenn aktiv. | Logic-Regel-Detail | -| Nutzer hat Action-Typ gewaehlt, aber Mapping vergessen | Blocker `Pflichtmapping fehlt`, wenn Action aktiv. | Action-Detail mit Mapping | -| Nutzer hat Datei-Feld ohne Typbeschraenkung | Warnung, nicht zwingend Blocker. | Datei-Feld Inspector | -| Nutzer hat Field Key ungueltig geaendert | Blocker `Field Key ungueltig oder doppelt`. | Inspector Basis/Advanced | -| Nutzer hat nur inaktive unvollstaendige Action | Optional Warnung, kein harter Blocker. | Actions-Liste | -| Nutzer hat unsaved changes | Erst speichern oder Save-Check zeigen. | Header/Save | - -## Zustaende - -| Zustand | Beschreibung | Umsetzung | -|---|---|---| -| Keine Blocker | Publish erfolgreich. | Toast/Statuswechsel | -| Blocker vorhanden | Summary mit Sprungzielen. | Nur dokumentieren | -| Nur Warnungen | Nutzer kann trotzdem publishen. | Nur dokumentieren | -| Save vorher noetig | Hinweis, dass Aenderungen erst gespeichert werden. | Nur dokumentieren | - -## Copy-Richtung - -| Situation | Copy-Beispiel | -|---|---| -| Feld ohne Label | `Dieses Feld braucht ein Label, bevor das Formular veroeffentlicht werden kann.` | -| Select ohne Optionen | `Fuege mindestens eine Option hinzu, damit Nutzer antworten koennen.` | -| Logic unvollstaendig | `Diese Regel ist noch nicht vollstaendig. Ergaenze Operator, Wert oder Zielfeld.` | -| Mapping fehlt | `Diese Action braucht noch eine Zuordnung fuer Pflichtfelder.` | -| Nur Warnung | `Du kannst veroeffentlichen, aber diese Einstellung sollte geprueft werden.` | - -## Subframe - -Gebaut in Subframe: - -https://app.subframe.com/f023064a5ca0/design/211e8ca7-fc0b-4c52-a4a4-a1e660209e58/edit - -Qualitaetsregel: - -- Interner Builder-Zustand nutzt das vorhandene `Default Page Layout`. -- Keine eigene Sidebar oder Header-Navigation nachbauen. -- D09 bleibt Inline Summary/Banner im Builder, kein Modal/Dialog. -- Publish bleibt deaktiviert, bis harte Blocker geloest sind. -- Warnungen sind sichtbar, blockieren aber nicht zwingend. -- Jeder Blocker hat ein klares Sprungziel in Feld, Logic, Actions oder Settings. - -## Notizen - -Publish ist kein Wizard. Der Nutzer bleibt im Builder und korrigiert direkt dort. diff --git a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/D01 Sharing Panel URL QR iFrame.md b/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/D01 Sharing Panel URL QR iFrame.md index 18cce80a..d4b7add5 100644 --- a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/D01 Sharing Panel URL QR iFrame.md +++ b/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/D01 Sharing Panel URL QR iFrame.md @@ -8,7 +8,7 @@ screen: Sharing Panel URL QR iFrame typ: Panel-Zustand in S01 umsetzung: Nur dokumentieren status: Vor Subframe / dokumentiert -route: in ADV-S01 Form-Builder Canvas, kein eigener Screen +route: Detailnotiz, kein eigener Screen subframe: "-" --- diff --git a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/D02 Post-Submit Actions.md b/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/D02 Post-Submit Actions.md index 7cc7b803..91db9fda 100644 --- a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/D02 Post-Submit Actions.md +++ b/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/D02 Post-Submit Actions.md @@ -5,10 +5,10 @@ ticket: D-FORM package: WF-D-FORM Form Builder id: D02 screen: Post-Submit Actions -typ: Panel-Zustand in S01 +typ: Detailnotiz im S02-Kontext umsetzung: Nur dokumentieren status: Vor Subframe / dokumentiert -route: in ADV-S01 Form-Builder Canvas, kein eigener Screen +route: Detailnotiz, kein eigener Screen subframe: "-" --- @@ -20,7 +20,7 @@ Interne Nutzer legen fest, was nach dem Absenden eines Public Forms passieren so ## Pattern / Archetyp -Action-Liste plus Detailpanel innerhalb von S01. +Action-Liste plus Detailbereich innerhalb des einfachen Builders. ## Daten @@ -46,9 +46,9 @@ Action-Liste plus Detailpanel innerhalb von S01. ## Verhalten -- Klick auf `Actions` wechselt in den Actions-Modus im selben S01-Screen. +- Klick auf `Nach Absenden` zeigt die Action-/Lead-Optionen im selben S02-Kontext. - `Add Action` oeffnet eine kleine Typ-Auswahl als Popover oder Auswahlpanel. -- Nach Auswahl eines Action-Typs oeffnet sich das Action-Detail, siehe [[92 D08 Action Detail mit Mapping|D08 Action Detail mit Mapping]]. +- Nach Auswahl eines Action-Typs oeffnet sich die Detailkonfiguration im selben Bereich. - Die Liste zeigt Warnungen direkt an der betroffenen Action. - ActionLog, Re-Run und Submission-Historie gehoeren nicht hierher, sondern zu Submissions. - Auswahl- und Mapping-Folgefaelle stehen in [[D10 Builder Interaktionsfaelle]]. @@ -61,7 +61,7 @@ Action-Liste plus Detailpanel innerhalb von S01. | Kurzbeschreibung | Pro Typ ein Satz, was nach Submit passiert. | | Disabled-Zustaende | Typ disabled, wenn Rechte, Integration oder Voraussetzung fehlen. | | Suche optional | Nur wenn Liste zu lang wirkt. | -| Kein Modal | Das Popover waehlt nur den Typ; Detailarbeit passiert in [[92 D08 Action Detail mit Mapping|D08 Action Detail mit Mapping]]. | +| Kein Modal | Das Popover waehlt nur den Typ; Detailarbeit passiert im selben Builder-Kontext. | ## Zustaende @@ -79,12 +79,12 @@ Action-Liste plus Detailpanel innerhalb von S01. |---|---| | Nutzer waehlt Typ, bricht Detail ab | Action wird nicht angelegt oder bleibt als unvollstaendiger Draft, je nach Save-Logik. | | Nutzer erstellt Action, laesst sie inaktiv | Keine Publish-Blockade, aber Warnung in Action-Liste. | -| Nutzer erstellt aktive Action ohne Pflichtmapping | Publish blockiert und springt zu [[92 D08 Action Detail mit Mapping|D08 Action Detail mit Mapping]]. | +| Nutzer erstellt aktive Action ohne Pflichtmapping | Publish blockiert und springt zur betroffenen Zuweisung. | | Nutzer loescht Action | Confirm nur bei aktiver/published-relevanter Action. | ## Subframe -Kein eigener Hauptscreen. Actions bleiben Arbeitsbereich in S01. Ein extra Frame ist nur noetig, wenn David die geoeffnete Action-Liste separat sehen will. +Kein eigener Hauptscreen. Actions bleiben Arbeitsbereich im einfachen Builder. Ein extra Frame ist nur noetig, wenn David die geoeffnete Action-Liste separat sehen will. ## Notizen diff --git a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/D03 Conditional Logic Editor.md b/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/D03 Conditional Logic Editor.md index 3a999e70..81f7c8bf 100644 --- a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/D03 Conditional Logic Editor.md +++ b/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/D03 Conditional Logic Editor.md @@ -5,10 +5,10 @@ ticket: D-FORM package: WF-D-FORM Form Builder id: D03 screen: Conditional Logic Editor -typ: Panel-Zustand in S01 +typ: Detailnotiz im S02-Kontext umsetzung: Nur dokumentieren status: Vor Subframe / dokumentiert -route: in ADV-S01 Form-Builder Canvas, kein eigener Screen +route: Detailnotiz, kein eigener Screen subframe: "-" --- @@ -20,7 +20,7 @@ Interne Nutzer koennen einfache Anzeige-/Ausblende-Regeln fuer Formularfelder er ## Pattern / Archetyp -Regel-Liste plus Detailpanel innerhalb von S01. +Einfache Bedingung direkt an der Frage; komplexere Regel-Liste bleibt nur dokumentiert. ## Daten @@ -44,8 +44,8 @@ Regel-Liste plus Detailpanel innerhalb von S01. ## Verhalten -- Klick auf `Logic` wechselt in den Logic-Modus im selben S01-Screen. -- `Neue Regel` oeffnet kein Modal, sondern ein Detailpanel, siehe [[91 D07 Logic Regel Detail|D07 Logic Regel Detail]]. +- Klick auf `Bedingung` oeffnet die einfache Logik direkt an der Frage. +- `Neue Regel` oeffnet kein Modal, sondern bleibt inline im Builder-Kontext. - Kaputte Referenzen werden in der Regel-Liste sichtbar markiert. - Kaputte aktive Regeln blockieren Publish. - Required-Felder, die durch Logic versteckt werden, sind im Public Form nur required, wenn sie sichtbar sind. diff --git a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/D06 Feldkarten Menue.md b/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/D06 Feldkarten Menue.md index 24117880..d14cb08e 100644 --- a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/D06 Feldkarten Menue.md +++ b/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/D06 Feldkarten Menue.md @@ -8,7 +8,7 @@ screen: Feldkarten Menue typ: Inline-Zustand in S01 umsetzung: Nur dokumentieren status: Vor Subframe / dokumentiert -route: in ADV-S01 Form-Builder Canvas, kein eigener Screen +route: Detailnotiz, kein eigener Screen subframe: "-" --- diff --git a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/D10 Builder Interaktionsfaelle.md b/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/D10 Builder Interaktionsfaelle.md index e658dd6a..73647b8b 100644 --- a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/D10 Builder Interaktionsfaelle.md +++ b/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/D10 Builder Interaktionsfaelle.md @@ -5,10 +5,10 @@ ticket: D-FORM package: WF-D-FORM Form Builder id: D10 screen: Builder Interaktionsfaelle -typ: Case-Matrix in S01 +typ: Case-Matrix im S02-Kontext umsetzung: Nur dokumentieren status: Vor Subframe / dokumentiert -route: in ADV-S01 Form-Builder Canvas, kein eigener Screen +route: Detailnotiz, kein eigener Screen subframe: "-" --- @@ -130,8 +130,5 @@ Nicht jedes Detail muss ein eigener Frame werden. Dber vor Subframe muss klar se Kein eigener Subframe-Screen. D10 ist eine Vor-Subframe-Case-Matrix. Wenn David einzelne Faelle visuell sehen will, sollten zuerst diese Kandidaten gebaut werden: -- [[93 D09 Publish Blocker Summary|D09 Publish Blocker Summary]] -- [[92 D08 Action Detail mit Mapping|D08 Action Detail mit Mapping]] -- [[91 D07 Logic Regel Detail|D07 Logic Regel Detail]] - [[M01 Feld loeschen Confirm]] - [[M02 Unpublish Confirm]] diff --git a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Simple Builder Neuausrichtung.md b/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Simple Builder Neuausrichtung.md index d8b44a57..5f98d50c 100644 --- a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Simple Builder Neuausrichtung.md +++ b/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Simple Builder Neuausrichtung.md @@ -45,7 +45,7 @@ Neue Richtung: einfacher Formular-Editor nach Google-Forms-Gefuehl, aber mit Hiv | E03 | [[Screens/12 E03 S05 Keine Antworten|E03 S05 Keine Antworten]] | Empty State, wenn ein Formular noch keine Einsendungen hat. | | E04 | [[Screens/13 E04 S05 Export Fehler|E04 S05 Export Fehler]] | Error State, wenn Excel/CSV-Export scheitert. | -S01B/S01C/S01D/S01E sind gebaut, aber nicht mehr Hauptfluss. Sie liegen als spaetere Varianten bzw. Referenz bei 80-83. +S01C/S01D/S01E sind gebaut, aber nicht Hauptfluss. Sie bleiben als spaetere Varianten bzw. Referenz bei 81-83. S01B wurde aus dem Obsidian-Handoff entfernt. ## Einfacher Ablauf @@ -100,11 +100,9 @@ Bewusst nicht im aktuellen S02-Hauptdropdown: Lineare Skala, Bewertung, Multiple S02 wurde in Subframe gebaut, weil es am naechsten an Google Forms liegt und schnell vergleichbar ist. -S01B wurde als zweite Alternative gebaut, ist aktuell aber nicht Hauptfluss. - S01C/S01D/S01E wurden als Beispiel-Editoren gebaut, sind aktuell aber nur Referenz. Im Hauptfluss oeffnen alle Formulararten S02 mit anderer Vorlage/Startfragen. -Der alte ADV-S01 und die Detailseiten D07/D08/D09 bleiben als fachliche Vollstaendigkeits-Referenz erhalten, sollten aber nicht mehr die erste Empfehlung fuer Handwerker sein. +Die alten Advanced-Screens wurden aus dem Obsidian-Handoff entfernt, damit S02 als klare Empfehlung stehen bleibt. ## Ergaenzung: Lead Felder zuweisen diff --git a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Subframe Bau Log.md b/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Subframe Bau Log.md index 5bf39770..fc9d2c6d 100644 --- a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Subframe Bau Log.md +++ b/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Subframe Bau Log.md @@ -35,22 +35,12 @@ Flow-Link: https://app.subframe.com/f023064a5ca0/flow/afb1c257-46f0-4b23-aaed-d7 | 11 | S05 | Antworten Tabelle | Hauptfluss / Auswertung | https://app.subframe.com/f023064a5ca0/design/9e8eeab7-44ea-4b77-85e6-0818d97e1ac3/edit | | 12 | E03 | S05 Keine Antworten | Hauptfluss / Empty | https://app.subframe.com/f023064a5ca0/design/6f56c14f-d66e-4734-8a29-a900ac74a8a7/edit | | 13 | E04 | S05 Export Fehler | Hauptfluss / Error | https://app.subframe.com/f023064a5ca0/design/00553ea4-9839-4d22-b540-9b9ef0bf014f/edit | -| 80 | S01B | Gefuehrter Fragen Editor | Spaeter / Referenz | https://app.subframe.com/f023064a5ca0/design/d0fb9c3d-4b03-4d59-bd98-9f668ab018eb/edit | | 81 | S01C | Website Formular Editor | Spaeter / Referenz | https://app.subframe.com/f023064a5ca0/design/d7d33e94-5c12-48e8-8ec6-1e94590ee89c/edit | | 82 | S01D | Umfrage Editor | Spaeter / Referenz | https://app.subframe.com/f023064a5ca0/design/5abd7799-7058-40b3-9772-884cbda97b9c/edit | | 83 | S01E | Internes Formular Editor | Spaeter / Referenz | https://app.subframe.com/f023064a5ca0/design/ea08fd1c-1a76-4e2c-b27c-1091baa0586a/edit | -| 90 | ADV-S01 | Form-Builder Canvas | Advanced / Referenz | https://app.subframe.com/f023064a5ca0/design/e5502f88-9858-460e-abae-671f28464270/edit | -| 90b | ADV-S01-2 | Form-Builder Canvas 2 | Advanced / Referenz | https://app.subframe.com/f023064a5ca0/design/9529ac6f-eb71-4e5c-8774-7cebe9c8162c/edit | -| 91 | D07 | Logic Regel Detail | Advanced / Referenz | https://app.subframe.com/f023064a5ca0/design/07b60457-8151-46b9-ba34-34eceffc08bb/edit | -| 92 | D08 | Action Detail mit Mapping | Advanced / Referenz | https://app.subframe.com/f023064a5ca0/design/0759d779-91f5-4e06-b1d6-7cd0095461ea/edit | -| 93 | D09 | Publish Blocker Summary | Advanced / Referenz | https://app.subframe.com/f023064a5ca0/design/211e8ca7-fc0b-4c52-a4a4-a1e660209e58/edit | ## Qualitaetscheck Builder -- ADV-S01 ist interne Advanced-Mitarbeitersicht, kein Public Formular. -- ADV-S01 nutzt das vorhandene Subframe `Default Page Layout`. Keine eigene Sidebar nachbauen. -- Die extra generierte Seite `ADV-S01 Form-Builder Canvas Default Layout` wurde geloescht, weil sie die Sidebar selbst nachgebaut hatte. -- ADV-S01-2 Form-Builder Canvas 2 liegt als neue Builder-Basis im gleichen Flow. - S01 ist als vereinfachter Einstieg gebaut: Nutzer waehlt Formularart vor dem Builder. - S01 nutzt das vorhandene `Default Page Layout`, nicht eine selbst nachgebaute Sidebar. - S01 enthaelt Website-/Externes Formular, Lead-Formular, Fragebogen/Umfrage und Internes Formular. @@ -59,16 +49,13 @@ Flow-Link: https://app.subframe.com/f023064a5ca0/flow/afb1c257-46f0-4b23-aaed-d7 - S02 nutzt das vorhandene `Default Page Layout`, nicht eine selbst nachgebaute Sidebar. - S02 ist die Google-Forms-nahe Alternative: Titelkarte, Fragekarten, Google-Forms-nahe Fragetyp-Auswahl, mehrere editierbare Optionen, Pflichtfrage-Toggle und erweiterte Werkzeugleiste. - S02 zeigt Lead-Verknuepfung als ruhige blaue Card. Die Card nutzt denselben Layout-Aufbau wie die gruene `Lead anlegen`-Card: Icon links, Titel/Badge/Beschreibung in der Mitte, Aktionen rechts. -- Arbeitsentscheidung: S02 ist aktuell der Haupt-Editor fuer alle Formulararten. S01B/S01C/S01D/S01E bleiben spaetere Varianten bzw. Referenz. +- Arbeitsentscheidung: S02 ist aktuell der Haupt-Editor fuer alle Formulararten. S01C/S01D/S01E bleiben spaetere Varianten bzw. Referenz; S01B ist aus dem Obsidian-Handoff entfernt. - S02-Fragetyp-Dropdown zeigt den aktuell gewaehlten Typ, z. B. `Multiple-Choice-Frage`; `Fragetyp` ist kein sichtbarer Wert. - Im aktuellen S02-Hauptdropdown bleiben nur: Kurzantwort, Absatz, Multiple-Choice-Frage, Mehrfachauswahl, Drop-down, Datei oder Bild hochladen, Datum und Zeit. Lineare Skala, Bewertung und Raster sind gestrichen. - In S02 bleiben Logic, Actions und Publish-Blocker einfach: `Bedingung` an der Frage, `Nach Absenden`/Lead anlegen als Card mit `Felder zuweisen`, Publish-Blocker als Banner/Popover im selben Screen. - S02L ist gebaut: Lead-Felder werden per Dropdown-Vorlage Formularfragen zugewiesen. Die blaue Lead-Card nutzt dort denselben Aufbau wie in S02. - E05 ist gebaut: unmoegliche oder fehlende Lead-Zuweisungen werden sichtbar markiert. - Layout-Hinweis fuer S02L/E05: Inhaltlich sind beide Zustaende gebaut. In Subframe bei der finalen Sichtpruefung einmal kontrollieren, ob das vorhandene `Default Page Layout` aktiv ist. -- S01B nutzt das vorhandene `Default Page Layout`, nicht eine selbst nachgebaute Sidebar. -- S01B ist die gefuehrte Alternative: Schrittleiste, linke Fragenliste, eine aktive Fragekarte und rechte Hilfe-/Preview-Zusammenfassung. -- S01B zeigt Lead-Verknuepfung als einfache aktive Einstellung mit Ziel-Pipeline/Team, ohne Mapping-Panel. - S01C nutzt das vorhandene `Default Page Layout`, nicht eine selbst nachgebaute Sidebar. - S01C ist das Beispiel fuer Website-/Externes Formular: oeffentliche Anfrage, Public-Link-Hinweis, mCaptcha/Datenschutz, kein Lead-Mapping. - S01D nutzt das vorhandene `Default Page Layout`, nicht eine selbst nachgebaute Sidebar. @@ -87,17 +74,7 @@ Flow-Link: https://app.subframe.com/f023064a5ca0/flow/afb1c257-46f0-4b23-aaed-d7 - E01/E02 decken die wichtigen leeren Zustaende der Formular-Zentrale ab: noch keine Formulare und Suche/Filter ohne Treffer. - E03/E04 decken die wichtigen Zustaende im Antworten-Tab ab: noch keine Antworten und Export-Fehler. - Layout-Check fuer E01-E04: In Subframe die Page Layout Auswahl pruefen und bei Bedarf `Default Page Layout` setzen. Per MCP kann ich Inhalte bauen/editieren, aber den Page-Layout-Typ nicht verlaesslich setzen. -- ADV-S01 zeigt Topbar mit Save, Preview, Share und Publish. -- ADV-S01 zeigt links die Feld-Palette, mittig den Canvas und rechts den Feld-Inspector. -- ADV-S01 macht die Arbeitsmodi Builder, Logik, Aktionen, Freigabe, Vorschau und Einstellungen sichtbar. -- ADV-S01 zeigt einen konkreten Publish-Blocker fuer ein Select-Feld ohne Optionen. -- D07 nutzt das vorhandene `Default Page Layout`, nicht eine selbst nachgebaute Sidebar. -- D07 zeigt den Logik-Modus mit Regel-Liste, gewaehltem Regel-Detail, Test/Preview, Inline-Validation und Publish-Blocker-Hinweis. -- D08 nutzt das vorhandene `Default Page Layout`, nicht eine selbst nachgebaute Sidebar. -- D08 zeigt den Aktionen-Modus mit Action-Liste, Add-Action-Popover, Mapping-Tabelle, Ziel-/Pipeline-Fehlern, Test/Preview und Publish-Blocker-Hinweis. -- D09 nutzt das vorhandene `Default Page Layout`, nicht eine selbst nachgebaute Sidebar. -- D09 zeigt Publish als blockierten internen Zustand: deaktivierter Publish-Button, 5 harte Blocker, nicht-blockierende Warnungen, Sprungziele und erneute Pruefung. -- D09 bleibt Inline Summary/Banner im Builder und ist kein Modal/Dialog. +- Die alten Advanced-Screens und D07-D09 wurden aus dem Obsidian-Handoff entfernt, weil S02 der aktuelle Review-Hauptfluss ist. ## Qualitaetscheck Public @@ -128,7 +105,7 @@ Fuer den alten D-FORM-Scope sind keine empfohlenen Frames mehr offen. Durch Davids neues Lead-Feedback wurden zwei Zustaende ergaenzt und in Subframe gebaut: S02L `Lead Felder zuweisen` und E05 `S02 Lead Zuweisung pruefen`. S00, S01, S02, S02L/E05, S03, S04/Public States, S05 und E01-E04 bilden weiter den aktuellen Hauptfluss. -S01B/S01C/S01D/S01E, ADV-S01 Canvas 1/2 und D07/D08/D09 sind gebaut, aber aktuell nicht relevant fuer den Hauptfluss. Sie bleiben als spaetere Varianten bzw. Referenzseiten erhalten. +S01C/S01D/S01E sind gebaut, aber aktuell nicht relevant fuer den Hauptfluss. Sie bleiben als spaetere Varianten bzw. Referenzseiten erhalten. S01B und die alten Advanced-Screens wurden aus dem Obsidian-Handoff entfernt. Subframe-Seitenliste: Per MCP kann ich bestehende Pages aktuell nicht umbenennen oder per Drag-and-drop sortieren. Fachlich gilt die Review-Reihenfolge unten. Falls die sichtbare Subframe-Liste sauber sortiert werden soll, die Pages in Subframe manuell umbenennen/ziehen. @@ -149,15 +126,9 @@ Empfohlene sichtbare Sortierung: 13. `11 S05 Antworten Tabelle` 14. `12 E03 S05 Keine Antworten` 15. `13 E04 S05 Export Fehler` -16. `80 S01B Gefuehrter Fragen Editor` -17. `81 S01C Website Formular Editor` -18. `82 S01D Umfrage Editor` -19. `83 S01E Internes Formular Editor` -20. `90 ADV Form-Builder Canvas` -21. `90b ADV Form-Builder Canvas 2` -22. `91 D07 Logic Regel Detail` -23. `92 D08 Action Detail mit Mapping` -24. `93 D09 Publish Blocker Summary` +16. `81 S01C Website Formular Editor` +17. `82 S01D Umfrage Editor` +18. `83 S01E Internes Formular Editor` M01-M04 bleiben dokumentierte Confirm-Zustaende und werden nur gebaut, wenn David sie explizit anfordert. @@ -177,7 +148,7 @@ Falls spaeter D-FORM-M-Zustaende gebaut werden, muessen sie als `Dialog`-Kompone - M03 Archive Confirm - M04 Unsaved Changes Confirm -D07 und D08 bleiben Panel/Drawer-Zustaende im Builder. D09 bleibt Inline Summary/Banner. Diese drei sind keine Dialoge. +Die alten Advanced-Detailframes D07/D08/D09 sind nicht mehr Teil dieses Obsidian-Handoffs. ## Naechster Subframe-Schritt @@ -199,4 +170,4 @@ Review in Subframe in dieser Reihenfolge: 14. `E03 S05 Keine Antworten` 15. `E04 S05 Export Fehler` -Danach nur bei Bedarf die spaeteren Referenzseiten pruefen: S01B/S01C/S01D/S01E, ADV-S01 Canvas 1/2 und D07/D08/D09. +Danach nur bei Bedarf die spaeteren Referenzseiten pruefen: S01C/S01D/S01E. diff --git a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Subframe Bauplan.md b/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Subframe Bauplan.md index f95f0ad4..cd3c0e90 100644 --- a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Subframe Bauplan.md +++ b/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Subframe Bauplan.md @@ -53,19 +53,13 @@ Empfehlung fuer Review und Weiterarbeit: zuerst den aktuellen Hauptfluss pruefen | 13 | S05 Antworten Tabelle | Antworten-Tab als Excel-/Tabellenansicht. | | 14 | E03 S05 Keine Antworten | Wichtiger Empty State im Antworten-Tab. | | 15 | E04 S05 Export Fehler | Wichtiger Fehlerzustand beim Export. | -| 80 | S01B Gefuehrter Fragen Editor | Spaetere Alternative fuer Nutzer, die noch mehr Fuehrung brauchen. | | 81 | S01C Website Formular Editor | Spaetere Beispiel-/Referenzseite fuer Website-/externes Formular. | | 82 | S01D Umfrage Editor | Spaetere Beispiel-/Referenzseite fuer Fragebogen/Umfrage. | | 83 | S01E Internes Formular Editor | Spaetere Beispiel-/Referenzseite fuer internes Mitarbeiterformular. | -| 90 | ADV-S01 Form-Builder Canvas | Aelterer Profi-Canvas als Referenz. | -| 90b | ADV-S01-2 Form-Builder Canvas 2 | Advanced-/Referenz-Builder; nicht mehr erste Nutzerrichtung. | -| 91 | D07 Logic Regel Detail | Advanced-Zustand fuer Sichtbarkeitsregeln. | -| 92 | D08 Action Detail mit Mapping | Advanced-Zustand fuer Nach-Submit-Aktionen. | -| 93 | D09 Publish Blocker Summary | Advanced-Zustand fuer Publish-Blocker. | M01/M02/M03/M04 und D10 bleiben dokumentiert und muessen nicht als erste Subframe-Frames gebaut werden. -Stand 2026-07-09: Die einfachen Builder-Screens sind gebaut und werden fuer Review vor den alten Canvas-Entwuerfen einsortiert. S02L und E05 wurden nach Davids Lead-Feedback ergaenzt. Die Links stehen im [[Subframe Bau Log]]. Fuer die weitergebbare Version siehe [[00 Start hier - David]] und [[D-FORM David Uebergabe 2026-07-09]]. +Stand 2026-07-09: Die einfachen Builder-Screens sind gebaut. S02L und E05 wurden nach Davids Lead-Feedback ergaenzt. S01B und die alten Advanced-Screens wurden aus dem Obsidian-Handoff entfernt. Die Links stehen im [[Subframe Bau Log]]. Fuer die weitergebbare Version siehe [[00 Start hier - David]] und [[D-FORM David Uebergabe 2026-07-09]]. ### Nutzerfluss fuer die Simple-Builder-Richtung @@ -128,15 +122,9 @@ Frames / Designs: | E02 | `E02 S00 Keine Treffer` | Hauptfluss / Empty State | | E03 | `E03 S05 Keine Antworten` | Hauptfluss / Empty State | | E04 | `E04 S05 Export Fehler` | Hauptfluss / Error State | -| S01B | `S01B Gefuehrter Fragen Editor` | Spaeter / Referenz | | S01C | `S01C Website Formular Editor` | Spaeter / Referenz | | S01D | `S01D Umfrage Editor` | Spaeter / Referenz | | S01E | `S01E Internes Formular Editor` | Spaeter / Referenz | -| ADV-S01-2 | `ADV-S01-2 Form-Builder Canvas 2` | Advanced Builder / Referenz | -| ADV-S01 | `ADV-S01 Form-Builder Canvas` | Aelterer Bespoke Builder / Referenz | -| D07 | `D07 Logic Regel Detail` | Advanced / Referenz | -| D08 | `D08 Action Detail mit Mapping` | Advanced / Referenz | -| D09 | `D09 Publish Blocker Summary` | Advanced / Referenz | Hinweis: In der Subframe-Seitenliste tragen S01C, S01D, S01E, S03, S04 und S05 aktuell noch das Suffix `Layout`. Zielnamen fuer Review/Handoff sind die oben stehenden Namen ohne `Layout`. @@ -158,10 +146,10 @@ Diese vorhandenen Komponenten/Patterns sollen genutzt werden. | Bereich | Komponente | Einsatz in D-FORM | |---|---|---| -| Layout | Default Page Layout | S00/S01/S02/S03/S05/E01-E04 als interne Hauptseiten im vorhandenen Subframe Page Layout. S01B-E, ADV-S01 Canvas und D07-D09 bleiben spaetere interne Referenzseiten. S04/V01/V02/V03 bleiben public ohne App-Shell. | +| Layout | Default Page Layout | S00/S01/S02/S03/S05/E01-E04 als interne Hauptseiten im vorhandenen Subframe Page Layout. S01C-E bleiben spaetere interne Referenzseiten. S04/V01/V02/V03 bleiben public ohne App-Shell. | | Navigation | Breadcrumb | S02 oben: `Formulare / SHK Anfrageformular`. | | Navigation | Page Header with Back | S02 Header mit Zurueck, Formname, Status, Save, Preview, Share, Publish. | -| Navigation | Tabs | S02 Tabs: Fragen, Design, Antworten. ADV-S01 behaelt Builder, Logic, Actions, Sharing, Preview, Settings als Referenz. | +| Navigation | Tabs | S02 Tabs: Fragen, Design, Antworten. | | Actions | Button | Save, Publish, Add Field, Add Action, Submit. | | Actions | Icon Button | Feldkarten-Menue, More-Menue, Close/Archive-Trigger. | | Form Inputs | Text Input | Name, Slug, Field Key, Email, Telefon. | @@ -211,10 +199,8 @@ Wenn einer dieser M-Zustaende spaeter in Subframe gebaut wird, muss die vorhande Abgrenzung: -- `D07 Logic Regel Detail` = Panel/Drawer im Builder, kein Modal. -- `D08 Action Detail mit Mapping` = Panel/Drawer im Builder, kein Modal. -- `D09 Publish Blocker Summary` = Inline Banner/Summary im Builder, kein Modal. - Public V01/V02 duerfen zwar card-artig zentriert aussehen, sind aber trotzdem volle Public-Zustaende und keine Dialoge. +- D-/M-Detailnotizen sind Markdown-Anforderungen und nicht Teil der aktuellen Subframe-Review-Reihenfolge. ## 6. S04: fertiges Kundenformular @@ -310,115 +296,19 @@ Default Page Layout ### Layout-Regel fuer interne Builder-Screens -S00, S01, S02, S03, S05 und E01-E04 muessen in Subframe das vorhandene `Default Page Layout` verwenden. S01B-E, ADV-S01 Canvas und D07-D09 sind spaetere interne Referenzseiten und verwenden ebenfalls keine eigene Sidebar. Wichtig: Das Layout selbst bringt App-Shell/Navigation mit. In den Screens keine eigene Sidebar oder Header-Navigation nachbauen. +S00, S01, S02, S03, S05 und E01-E04 muessen in Subframe das vorhandene `Default Page Layout` verwenden. S01C-E sind spaetere interne Referenzseiten und verwenden ebenfalls keine eigene Sidebar. Wichtig: Das Layout selbst bringt App-Shell/Navigation mit. In den Screens keine eigene Sidebar oder Header-Navigation nachbauen. Nicht verwenden fuer den Builder: `Blank`, public/zentrierte Form-Seiten oder `Default Page Layout with inner Sidebar`. Die innere Sidebar ist eher fuer Settings geeignet; der Builder hat bereits eigene Innenbereiche wie Feld-Palette, Canvas, Logic-Liste, Action-Liste und Inspector. Public-Screens S04, V01, V02 und V03 bleiben bewusst ohne App-Shell und ohne Sidebar. -### Modus-Tabs +### Tabs -| Modus | Was zeigen? | -|---|---| -| Builder | Palette + Canvas + Inspector. | -| Logic | Regel-Liste und D07 Detail. | -| Actions | Action-Liste und D08 Mapping. | -| Sharing | URL, QR, iFrame; Draft-Hinweis. | -| Preview | Vorschau von S04, kein PDF. | -| Settings | Slug, Success, Redirect, Branding, Limits. | +S02 nutzt nur die Review-relevanten Tabs: Fragen, Design und Antworten. Logic, Actions und Publish bleiben in S02 bewusst einfach und inline. -## 8. D07 Logic Regel Detail +## 8. Entfernte Advanced-Detailframes -Ziel: Eine einzelne Logic-Regel bauen. - -Subframe-Zustand: - -```text -Logic Mode - Left/Top: Regel-Liste - Detailpanel: D07 Logic Regel Detail -``` - -Inhalt: - -- Regelname -- Wenn-Feld -- Operator -- Wert -- Dann-Aktion: anzeigen / verstecken -- Zielfeld -- Aktiv Switch -- Test / Save -- Inline `message` bei Fehler - -Wichtig: - -- `Neue Regel` oeffnet kein Modal. -- Operator steuert, ob Wert gebraucht wird. -- Wenn Operator `ist leer`, verschwindet das Wertfeld. -- Unvollstaendige aktive Regel blockiert Publish. - -## 9. D08 Action Detail mit Mapping - -Ziel: Nach Submit festlegen, was passieren soll. - -Subframe-Zustand: - -```text -Actions Mode - Action-Liste - Add Action Popover - Detailpanel: D08 Action Detail mit Mapping -``` - -Inhalt: - -- Action-Typ -- Action-Name -- Status aktiv/inaktiv -- Ziel: Empfaenger, Pipeline, Webhook URL usw. -- Field-Mapping Tabelle: Ziel-Feld -> Form-Feld -- Pflichtmapping -- Teststatus -- Save / Test / Delete - -Wichtig: - -- Add Action oeffnet Popover/Auswahlpanel, kein grosses Modal. -- Mapping ist der Kern. -- Aktive Action ohne Pflichtmapping blockiert Publish. -- Inaktive unvollstaendige Action darf als Draft bleiben. - -## 10. D09 Publish Blocker Summary - -Ziel: Mitarbeiter versteht, warum Publish nicht geht. - -Subframe-Zustand: - -```text -ADV-S01 Header - Publish Button -ADV-S01 Body - D09 Summary Banner - Blocker-Liste mit Sprungzielen -``` - -Blocker: - -- Formularname fehlt -- Slug fehlt oder Konflikt -- Kein sichtbares Feld -- Feld ohne Label -- Select ohne Optionen -- Kaputte Logic -- Pflichtmapping fehlt -- Webhook URL ungueltig - -Wichtig: - -- Publish bleibt im Builder, kein Wizard. -- Jeder Blocker hat ein Sprungziel. -- Warnungen duerfen getrennt von Blockern angezeigt werden. +S01B, ADV-S01, ADV-S01-2 und die Detailframes D07/D08/D09 wurden aus dem Obsidian-Handoff entfernt. Die fachlichen Detailnotizen zu Sharing, Actions, Logic, Feldmenue, Interaktionsfaellen und Confirm-Dialogen bleiben als Markdown-Anforderungen liegen, sind aber keine Review-Screens. ## 11. Was NICHT passieren soll @@ -427,7 +317,7 @@ Wichtig: - Preview nicht als PDF verstehen. - Logic nicht als PDF-/Dokumentgenerator verstehen. - Actions nicht als kompletter Automation-Editor bauen. -- Confirm-Dialoge nicht vor D07/D08/D09 priorisieren. +- Confirm-Dialoge nicht vor dem S02-Hauptfluss priorisieren. - Keine neuen globalen Subframe Standards aus D-FORM ableiten. ## 12. Umsetzungsstand in Subframe @@ -453,15 +343,9 @@ Gebaut und verlinkt: 13. S05 Antworten Tabelle 14. E03 S05 Keine Antworten 15. E04 S05 Export Fehler -80. S01B Gefuehrter Fragen Editor 81. S01C Website Formular Editor 82. S01D Umfrage Editor 83. S01E Internes Formular Editor -90. ADV-S01 Form-Builder Canvas -90b. ADV-S01-2 Form-Builder Canvas 2 -91. D07 Logic Regel Detail -92. D08 Action Detail mit Mapping -93. D09 Publish Blocker Summary Gebaut nach Davids Lead-Feedback: diff --git a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Varianten/V01 Public Erfolg oder Redirect.md b/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Varianten/V01 Public Erfolg oder Redirect.md deleted file mode 100644 index 2ba6f162..00000000 --- a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Varianten/V01 Public Erfolg oder Redirect.md +++ /dev/null @@ -1,106 +0,0 @@ ---- -kind: screen -project: HiveGrid Screen Project -ticket: D-FORM -package: WF-D-FORM Form Builder -id: V01 -screen: Public Erfolg oder Redirect -typ: Public Feedback -umsetzung: Bauen -status: Subframe Draft / Review offen -route: /f/{tenant}/{slug} -subframe: "https://app.subframe.com/f023064a5ca0/design/c06ecfc3-1f86-4dfe-9cfd-cec8b8629253/edit" ---- - -# V01 - Public Erfolg oder Redirect - -## Ziel - -Der Submitter versteht sicher, dass die Einreichung angekommen ist, oder wird sauber an ein konfiguriertes Redirect-Ziel weitergeleitet. - -## Pattern / Archetyp - -BESPOKE-Public Feedback. - -## Daten - -- Tenant-Logo/Fallback. -- Erfolgstitel. -- Erfolgstext aus Form-Konfiguration. -- Optionales Redirect-Ziel. -- Optionaler Link zur Website/zurueck. - -## Detail-Brainstorming - -Der wichtigste Punkt: Nach erfolgreichem Submit darf nichts so wirken, als sei die Anfrage unsicher oder verloren. Duch wenn Actions im Hintergrund fehlschlagen, bleibt die Public-Bestaetigung auf die gespeicherte Einreichung bezogen. - -### Varianten - -| Variante | Wann | UI | -|---|---|---| -| Success ohne Redirect | Kein Redirect konfiguriert. | Danke-Ansicht mit Erfolgstitel, Text und optionalem Zurueck-Link. | -| Success mit Auto-Redirect | Redirect URL konfiguriert. | Kurze Bestaetigung + Countdown/Weiterleitungshinweis. | -| Success mit manuellem Redirect | Auto-Redirect wird nicht sofort ausgefuehrt oder soll bestaetigt werden. | Button "Weiter" zur Zielseite. | -| Redirect fehlerhaft | Weiterleitung klappt nicht, Submission ist aber gespeichert. | Erfolg bleibt sichtbar; Link erneut oeffnen oder Anbieter kontaktieren. | - -### Copy-Vorschlag - -| Zustand | Copy-Richtung | -|---|---| -| Erfolg | "Danke, Ihre Dngaben wurden uebermittelt." | -| Naechster Schritt | "Wir melden uns, falls weitere Informationen benoetigt werden." | -| Redirect | "Sie werden gleich weitergeleitet." | -| Redirect-Fehler | "Ihre Dngaben sind angekommen. Die Weiterleitung konnte nicht geoeffnet werden." | - -### Nicht zeigen - -- Keine Action-Ausfuehrung. -- Kein Lead-/Projekt-/Task-Hinweis. -- Kein Webhook-/Automation-Status. -- Keine interne Submission-ID, ausser spaeter bewusst als Referenznummer geplant. - -## Aktionen - -- Bei Success ohne Redirect: Seite schliessen oder zur Website zurueck. -- Bei Redirect: automatisch oder per Button weiter. - -## Banner / Hinweise - -- Kein technischer ActionLog im Public UI. -- Keine internen Begriffe wie Lead, Projekt, Webhook oder Automation. - -## Toasts / Action-Feedback - -- Keine Toasts; Success-State ist die Rueckmeldung. - -## Meldungsentscheidung - -| Ereignis | UI-Form | Blockiert? | Umsetzung | Copy/Notiz | -|---|---|---|---|---| -| Submit erfolgreich | Success-State | nein | Bauen | Anfrage ist angekommen. | -| Redirect konfiguriert | Success + Weiterleitung | nein | Bauen | Falls Redirect dauert, klare Zwischenmeldung. | - -## Zustaende - -| Zustand | Beschreibung | Umsetzung | -|---|---|---| -| Erfolgstext | Danke-/Bestaetigungsansicht. | Bauen | -| Redirect | Nutzer wird weitergeleitet. | Bauen | - -## Blockierende Error States - -| Zustand | Was sieht der Nutzer? | Primaeraktion | Sekundare Aktion | -|---|---|---|---| -| Redirect-Ziel nicht erreichbar | Kurzer Fehler nach Submit, Einreichung bleibt erfolgreich. | Link erneut oeffnen | Anbieter kontaktieren | - -## Permissions / Gates - -- Keine Rolle. - -## Subframe - -https://app.subframe.com/f023064a5ca0/design/c06ecfc3-1f86-4dfe-9cfd-cec8b8629253/edit - -## Notizen - -Wichtig: Wenn Submit gespeichert wurde, darf ein kaputter Redirect nicht so wirken, als sei die Einreichung verloren. diff --git a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Varianten/V02 Public Formular nicht verfuegbar.md b/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Varianten/V02 Public Formular nicht verfuegbar.md deleted file mode 100644 index 8cda11d2..00000000 --- a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Varianten/V02 Public Formular nicht verfuegbar.md +++ /dev/null @@ -1,115 +0,0 @@ ---- -kind: screen -project: HiveGrid Screen Project -ticket: D-FORM -package: WF-D-FORM Form Builder -id: V02 -screen: Public Formular nicht verfuegbar -typ: Public Blocker -umsetzung: Bauen -status: Subframe Draft / Review offen -route: /f/{tenant}/{slug} -subframe: "https://app.subframe.com/f023064a5ca0/design/f96ec12a-5e6a-471d-9a9c-6babcb57c74c/edit" ---- - -# V02 - Public Formular nicht verfuegbar - -## Ziel - -Externe Nutzer landen nicht auf einem rohen 404/500, sondern bekommen eine ruhige, verstaendliche Meldung mit naechster Handlung. - -## Pattern / Archetyp - -BESPOKE-Public Blocker / A7 Public Error. - -## Daten - -- Tenant-Logo/Fallback, falls Tenant noch aufloesbar ist. -- Kurzer Titel. -- Klartext-Grund, soweit sicher: - - Formular nicht veroeffentlicht - - Formular archiviert - - Formular geschlossen - - Link ungueltig -- Optionaler Kontakt-/Website-Link. - -## Detail-Brainstorming - -Dieser Zustand ersetzt rohe Fehlerseiten. Der Nutzer ist blockiert, deshalb braucht er eine ruhige Erklaerung und eine naechste Handlung. - -### Blocker-Typen - -| Zustand | Interner Grund | Public-Copy-Richtung | Primaeraktion | -|---|---|---|---| -| Nicht veroeffentlicht | `status=draft` oder `is_public=false` | "Dieses Formular ist aktuell nicht verfuegbar." | Anbieter kontaktieren oder zurueck | -| Drchiviert | `status=archived` | "Dieses Formular ist nicht mehr aktiv." | Anbieter kontaktieren | -| Geschlossen | `submission_close_at` vorbei | "Der Dnnahmezeitraum ist beendet." | Anbieter kontaktieren | -| Limit erreicht | `submission_limit` erreicht | "Dieses Formular nimmt aktuell keine weiteren Einreichungen an." | Anbieter kontaktieren | -| Link ungueltig | Tenant/Slug nicht gefunden | "Der Link ist ungueltig oder nicht mehr aktuell." | Link pruefen / Anbieter kontaktieren | -| Tenant nicht aufloesbar | Tenant fehlt/gesperrt | Neutraler Fehler ohne interne Details. | Zurueck | - -### Was David zeigen sollte - -| Element | Notiz | -|---|---| -| Logo/Fallback | Nur wenn sicher, dass Tenant aufloesbar ist. | -| Titel | Kurz, nicht alarmistisch. | -| Klartext | Kein 404, kein Stacktrace, keine internen IDs. | -| Kontakt/Zurueck | Nur wenn bekannt/konfiguriert. | -| Erneut laden | Nur bei potentiell temporaeren Fehlern. | - -### Nicht zeigen - -- Kein interner Form-Name, wenn Link/Tenant unsicher ist. -- Keine technischen Statuscodes. -- Keine Aussage, ob ein bestimmter Kunde/Tenant existiert, falls das Datenschutz-/Security-riskant ist. - -## Aktionen - -- Zur Anbieter-Website zurueck. -- Anbieter kontaktieren. -- Erneut laden. - -## Banner / Hinweise - -- Keine technischen Details. -- Keine Aussage, die interne Form-Namen oder IDs leakt. - -## Toasts / Action-Feedback - -- Keine Toasts. - -## Meldungsentscheidung - -| Ereignis | UI-Form | Blockiert? | Umsetzung | Copy/Notiz | -|---|---|---|---|---| -| Formular nicht veroeffentlicht | Screen | ja | Bauen | Dieses Formular ist aktuell nicht verfuegbar. | -| Formular geschlossen | Screen | ja | Bauen | Dnnahmezeitraum ist beendet. | -| Formular nicht gefunden | Screen | ja | Bauen | Link pruefen oder Anbieter kontaktieren. | -| Tenant nicht aufloesbar | Screen | ja | Bauen | Neutral, ohne interne Details. | - -## Zustaende - -| Zustand | Beschreibung | Umsetzung | -|---|---|---| -| Nicht veroeffentlicht | Entwurf/Unpublished. | Bauen | -| Drchiviert/geschlossen | Keine Einreichung mehr moeglich. | Bauen | -| Nicht gefunden | Slug/Tenant passt nicht. | Bauen | - -## Blockierende Error States - -| Zustand | Was sieht der Nutzer? | Primaeraktion | Sekundare Aktion | -|---|---|---|---| -| Nicht verfuegbar | Ruhiger Public-Blocker. | Anbieter kontaktieren | Zurueck | - -## Permissions / Gates - -- Public Gate: `status=published` und `is_public=true`. - -## Subframe - -https://app.subframe.com/f023064a5ca0/design/f96ec12a-5e6a-471d-9a9c-6babcb57c74c/edit - -## Notizen - -Dieser Zustand ist wichtig fuer Vertrauen. Ein externes Formular darf nicht wie eine kaputte App wirken. diff --git a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Varianten/V03 Public Captcha oder Submit Fehler.md b/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Varianten/V03 Public Captcha oder Submit Fehler.md deleted file mode 100644 index 89bb1b3c..00000000 --- a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Varianten/V03 Public Captcha oder Submit Fehler.md +++ /dev/null @@ -1,117 +0,0 @@ ---- -kind: screen -project: HiveGrid Screen Project -ticket: D-FORM -package: WF-D-FORM Form Builder -id: V03 -screen: Public Captcha oder Submit Fehler -typ: Public Error -umsetzung: Bauen -status: Subframe Draft / Review offen -route: /f/{tenant}/{slug} -subframe: "https://app.subframe.com/f023064a5ca0/design/95c96bc9-35d4-47ce-b510-7d8ea1f41936/edit" ---- - -# V03 - Public Captcha oder Submit Fehler - -## Ziel - -Der Submitter kann einen Captcha- oder Submit-Fehler verstehen und korrigieren, ohne Vertrauen in das Formular zu verlieren. - -## Pattern / Archetyp - -BESPOKE-Public Error / A7 Error-State. - -## Daten - -- Aktuelle Feldwerte sollen erhalten bleiben, soweit technisch moeglich. -- Captcha-Status. -- Dllgemeiner Submit-Status. - -## Detail-Brainstorming - -V03 ist kein eigener neuer Flow, sondern ein Public-Fehlerzustand von S02. Er wird gebaut, weil die Copy und das Vertrauen wichtig sind. - -### Fehlerarten - -| Fehler | Ort | UI | Werte behalten? | -|---|---|---|---| -| Captcha fehlt | Captcha-Bereich | Inline-Hinweis | ja | -| Captcha fehlgeschlagen | Captcha-Bereich oder Banner nah am Submit | Erneut pruefen | ja | -| Netzwerkfehler | Submit-Bereich | Banner + Retry | ja | -| Serverfehler vor Speicherung | Submit-Bereich | Banner + Retry | ja | -| Serverfehler nach Speicherung | Success bevorzugen, falls Speicherung sicher war | Nicht als Verlust darstellen | ja | -| Datei-Upload fehlgeschlagen | Datei-Feld | Inline + Datei erneut waehlen | andere Werte ja | - -### Copy-Richtung - -| Zustand | Copy | -|---|---| -| Captcha fehlt | "Bitte bestaetigen Sie die Sicherheitspruefung." | -| Captcha fehlgeschlagen | "Die Sicherheitspruefung konnte nicht bestaetigt werden. Bitte versuchen Sie es erneut." | -| Submit Fehler | "Das Formular konnte gerade nicht gesendet werden. Bitte versuchen Sie es erneut." | -| Wiederholter Fehler | "Wenn der Fehler bestehen bleibt, kontaktieren Sie bitte den Anbieter." | -| Datei-Fehler | "Diese Datei konnte nicht hochgeladen werden. Bitte pruefen Sie Typ und Groesse." | - -### Retry-Regeln - -| Regel | Grund | -|---|---| -| Eingaben erhalten | Lange Public-Formulare duerfen nicht neu ausgefuellt werden muessen. | -| Submit waehrend Retry sperren | Doppel-Submit verhindern. | -| Ersten Fehler sichtbar machen | Nutzer soll nicht suchen muessen. | -| Technische Details verstecken | Vertrauen bewahren. | -| Kontakt nur als Fallback | Nicht sofort vom Formular wegschicken. | - -## Aktionen - -- Captcha erneut bestaetigen. -- Formular erneut senden. -- Seite neu laden. -- Anbieter kontaktieren, falls Fehler bleibt. - -## Banner / Hinweise - -- Captcha-Fehler in der Naehe des Captcha-Bereichs. -- Submit-Fehler als Banner oberhalb des Submit-Bereichs. -- Keine internen Begriffe wie ActionLog, Webhook, DB oder Queue. - -## Toasts / Action-Feedback - -- Keine Toasts im Public UI. - -## Meldungsentscheidung - -| Ereignis | UI-Form | Blockiert? | Umsetzung | Copy/Notiz | -|---|---|---|---|---| -| Captcha fehlt | Inline beim Captcha | ja | Bauen | Bitte bestaetigen, dass Sie kein automatisierter Zugriff sind. | -| Captcha fehlgeschlagen | Inline/Banner | ja | Bauen | Bitte erneut versuchen. | -| Netzwerkfehler beim Submit | Banner | ja | Bauen | Einreichung konnte nicht gesendet werden. | -| Serverfehler nach Submit | Banner | ja | Bauen | Keine technischen Details, erneut versuchen. | - -## Zustaende - -| Zustand | Beschreibung | Umsetzung | -|---|---|---| -| Captcha Error | Bot-Schutz blockiert. | Bauen | -| Submit Error | Speicherung/Einreichung klappt nicht. | Bauen | -| Retry Loading | Nutzer sendet erneut. | Wiederverwenden | - -## Blockierende Error States - -| Zustand | Was sieht der Nutzer? | Primaeraktion | Sekundare Aktion | -|---|---|---|---| -| Captcha fehlgeschlagen | Fehler am Captcha. | Captcha erneut pruefen | - | -| Submit mehrfach fehlgeschlagen | Banner mit Hilfehinweis. | Erneut versuchen | Anbieter kontaktieren | - -## Permissions / Gates - -- Public. Kein Login. - -## Subframe - -https://app.subframe.com/f023064a5ca0/design/95c96bc9-35d4-47ce-b510-7d8ea1f41936/edit - -## Notizen - -Wenn moeglich, sollen eingegebene Antworten erhalten bleiben. Das ist besonders wichtig bei langen SHK-Dufnahmeformularen mit Fotos oder Text. diff --git a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/WF-D-FORM Form Builder.canvas b/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/WF-D-FORM Form Builder.canvas index fad40c8b..0ac2eee9 100644 --- a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/WF-D-FORM Form Builder.canvas +++ b/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/WF-D-FORM Form Builder.canvas @@ -1,87 +1,54 @@ { "nodes":[ - {"id":"group-states","type":"group","x":-120,"y":220,"width":4340,"height":680,"label":"Dokumentierte ADV-S01-Zustaende / Was-passiert-wenn-Cases"}, - {"id":"group-internal","type":"group","x":-120,"y":-520,"width":3100,"height":600,"label":"S01 Interner Builder Hauptflow"}, - {"id":"group-public","type":"group","x":-120,"y":1060,"width":2460,"height":620,"label":"Public Form Flow"}, - {"id":"group-simple","type":"group","x":-120,"y":1840,"width":4380,"height":1120,"label":"Neue Simple Builder Alternative"}, - {"id":"group-notes","type":"group","x":3220,"y":-520,"width":1000,"height":600,"label":"Scope / Subframe-Fokus"}, - {"id":"package","type":"file","file":"02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/WF-D-FORM Form Builder.md","x":0,"y":-350,"width":420,"height":210}, - {"id":"s01","type":"file","file":"02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/90 ADV Form-Builder Canvas.md","x":620,"y":-350,"width":420,"height":210}, - {"id":"d10","type":"file","file":"02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/D10 Builder Interaktionsfaelle.md","x":1860,"y":-350,"width":460,"height":210}, - {"id":"note-subframe-focus","type":"text","text":"# Subframe-Fokus\n\nAktueller Hauptfluss:\n- 01 S00 Verwaltung\n- 02/03 Empty States\n- 04 S01 Formularart\n- 05 S02 Haupt-Editor\n- 06 S03 Design\n- 07-10 Public Form/States\n- 11-13 Antworten/States\n\n80-93 bleiben spaeter/Referenz.\n\nM01/M02/M03/M04 und D10 bleiben dokumentiert.","x":3360,"y":-360,"width":720,"height":330}, - {"id":"note-out-of-scope","type":"text","text":"# Nicht in D-FORM\n\n- Vollstaendige Submissions-Verwaltung\n- Submission-Detail\n- Booking\n- Kalender\n- Signatur\n- HR-Screens\n- Personalisierte Links\n- JS-Snippet\n\nS03 ist nur die schlanke Formular-Zentrale, nicht das ganze Forms-/Submissions-Modul.","x":3360,"y":-20,"width":720,"height":300}, - {"id":"d01","type":"file","file":"02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/D01 Sharing Panel URL QR iFrame.md","x":0,"y":360,"width":420,"height":190}, - {"id":"note-states","type":"text","text":"# ADV-S01-Zustaende\n\nD07, D08 und D09 sind als zusaetzliche Subframe-Zustaende gebaut.\n\nDie restlichen D-/M-Zustaende bleiben dokumentiert.\n\nSie klaeren:\n- welche UI-Ebene oeffnet\n- was im Dropdown/Popover/Modal steht\n- welche Folgefehler entstehen\n- was Publish/Test blockiert","x":3120,"y":330,"width":540,"height":260}, - {"id":"note-builder-core","type":"text","text":"# Builder-Kern\n\nDauerhaft sichtbar:\n- Header / Status\n- Feld-Palette\n- Canvas\n- Feld-Inspector\n\nArbeitsmodi:\n- Logic\n- Actions\n- Sharing\n- Preview\n- Settings","x":1240,"y":-365,"width":420,"height":260}, - {"id":"d02","type":"file","file":"02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/D02 Post-Submit Actions.md","x":520,"y":360,"width":420,"height":190}, - {"id":"d03","type":"file","file":"02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/D03 Conditional Logic Editor.md","x":1040,"y":360,"width":420,"height":190}, - {"id":"d06","type":"file","file":"02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/D06 Feldkarten Menue.md","x":1560,"y":360,"width":420,"height":190}, - {"id":"m01","type":"file","file":"02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/M01 Feld loeschen Confirm.md","x":2080,"y":360,"width":420,"height":190}, - {"id":"d07","type":"file","file":"02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/91 D07 Logic Regel Detail.md","x":1040,"y":620,"width":420,"height":190}, - {"id":"d08","type":"file","file":"02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/92 D08 Action Detail mit Mapping.md","x":520,"y":620,"width":420,"height":190}, - {"id":"d09","type":"file","file":"02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/93 D09 Publish Blocker Summary.md","x":2600,"y":360,"width":420,"height":190}, - {"id":"m02","type":"file","file":"02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/M02 Unpublish Confirm.md","x":2600,"y":620,"width":420,"height":190}, - {"id":"m03","type":"file","file":"02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/M03 Archive Confirm.md","x":3120,"y":620,"width":420,"height":190}, - {"id":"m04","type":"file","file":"02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/M04 Unsaved Changes Confirm.md","x":3640,"y":620,"width":420,"height":190}, - {"id":"s02","type":"file","file":"02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/07 S04 Oeffentliches Formular.md","x":0,"y":1250,"width":420,"height":210}, - {"id":"v01","type":"file","file":"02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/08 V01 Public Erfolg oder Redirect.md","x":620,"y":1120,"width":420,"height":210}, - {"id":"v02","type":"file","file":"02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/09 V02 Public Formular nicht verfuegbar.md","x":620,"y":1410,"width":420,"height":210}, - {"id":"v03","type":"file","file":"02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/10 V03 Public Captcha oder Submit Fehler.md","x":1240,"y":1410,"width":420,"height":210}, - {"id":"note-public","type":"text","text":"# Public-Regel\n\nKeine App-Shell.\n\nRuhiges Kundenformular:\n- Branding-Kopf\n- Intro\n- Felder\n- mCaptcha\n- Submit\n- Success/Error\n\nMobile zuerst mitdenken.","x":1240,"y":1120,"width":420,"height":250}, - {"id":"note-public-reuse","type":"text","text":"# Public Renderer\n\nD-FORM baut das Public Form.\n\nDndere Bereiche wie HR oder Booking koennen den Renderer spaeter wiederverwenden, sind aber keine D-FORM-Screens.","x":1800,"y":1230,"width":420,"height":240}, - {"id":"simple-direction","type":"file","file":"02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Simple Builder Neuausrichtung.md","x":0,"y":1990,"width":420,"height":190}, - {"id":"s00","type":"file","file":"02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/04 S01 Formularart auswaehlen.md","x":560,"y":1990,"width":420,"height":190}, - {"id":"s01a","type":"file","file":"02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/05 S02 Einfacher Fragen Editor.md","x":1120,"y":1900,"width":420,"height":190}, - {"id":"s01b","type":"file","file":"02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/80 S01B Gefuehrter Fragen Editor.md","x":1120,"y":2120,"width":420,"height":190}, - {"id":"s01c","type":"file","file":"02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/81 S01C Website Formular Editor.md","x":1680,"y":1840,"width":420,"height":190}, - {"id":"s01d","type":"file","file":"02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/82 S01D Umfrage Editor.md","x":1680,"y":2060,"width":420,"height":190}, - {"id":"s01e","type":"file","file":"02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/83 S01E Internes Formular Editor.md","x":1680,"y":2280,"width":420,"height":190}, - {"id":"note-simple-builder","type":"text","text":"# Neue Richtung gebaut\n\nNicht mehr sofort Profi-Canvas.\n\nS00 ist die Formular-Zentrale.\nS01 fuehrt immer in S02.\n\nDie Formularart setzt nur Vorlage/Startfragen:\n- Website/Extern\n- Lead\n- Umfrage\n- Intern\n\nTabs im Formular:\n- Fragen\n- Design: S03\n- Antworten: S05\n\nS01B/C/D/E sind Referenz.\n\nLogic bleibt als einfache Bedingung an der Frage.\nActions bleiben als Lead-Hinweis/Automatik.","x":2320,"y":1990,"width":620,"height":420}, - {"id":"s03","type":"file","file":"02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/01 S00 Formulare verwalten.md","x":560,"y":2470,"width":420,"height":190}, - {"id":"s04","type":"file","file":"02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/06 S03 Formular Design.md","x":2320,"y":2440,"width":420,"height":190}, - {"id":"s05","type":"file","file":"02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/11 S05 Antworten Tabelle.md","x":2840,"y":2440,"width":420,"height":190} + {"id":"group-simple","type":"group","x":-80,"y":160,"width":3900,"height":1060,"label":"D-FORM Hauptflow - Interner Builder"}, + {"id":"group-public","type":"group","x":1160,"y":1340,"width":2320,"height":640,"label":"Public Formular und States"}, + {"id":"group-notes","type":"group","x":-80,"y":-280,"width":3040,"height":360,"label":"Review-Fokus / Scope"}, + {"id":"package","type":"file","file":"02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/WF-D-FORM Form Builder.md","x":0,"y":-150,"width":360,"height":160}, + {"id":"s03","type":"file","file":"02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/01 S00 Formulare verwalten.md","x":0,"y":380,"width":420,"height":190}, + {"id":"note-subframe-focus","type":"text","text":"# Review-Fokus\n\nAktueller Hauptfluss:\n- 01 S00 Verwaltung\n- 02/03 Empty States\n- 04 S01 Formularart\n- 05 S02 Haupteditor\n- 05b/05c Lead-Zuweisung\n- 06 S03 Design\n- 07-10 Public Form/States\n- 11-13 Antworten/States\n\n81-83 sind nur spaetere Referenz-Beispiele.","x":520,"y":-190,"width":760,"height":240}, + {"id":"note-out-of-scope","type":"text","text":"# Nicht in D-FORM\n\n- Vollstaendige Submissions-Verwaltung\n- Submission-Detail\n- Booking\n- Kalender\n- Signatur\n- HR-Screens\n- Personalisierte Links\n- JS-Snippet\n\nS00 ist nur die schlanke Formular-Zentrale. S05 ist nur die Antwortenansicht im Formular.","x":1480,"y":-190,"width":760,"height":240}, + {"id":"s00","type":"file","file":"02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/04 S01 Formularart auswaehlen.md","x":670,"y":385,"width":420,"height":190}, + {"id":"s01a","type":"file","file":"02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/05 S02 Einfacher Fragen Editor.md","x":1240,"y":860,"width":420,"height":190}, + {"id":"s01c","type":"file","file":"02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/81 S01C Website Formular Editor.md","x":2600,"y":200,"width":420,"height":170}, + {"id":"s01d","type":"file","file":"02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/82 S01D Umfrage Editor.md","x":2600,"y":400,"width":420,"height":170}, + {"id":"s01e","type":"file","file":"02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/83 S01E Internes Formular Editor.md","x":2600,"y":600,"width":420,"height":170}, + {"id":"s04","type":"file","file":"02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/06 S03 Formular Design.md","x":2120,"y":765,"width":420,"height":190}, + {"id":"s05","type":"file","file":"02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/11 S05 Antworten Tabelle.md","x":2120,"y":1005,"width":420,"height":190}, + {"id":"note-simple-builder","type":"text","text":"# Aktueller Builder-Flow\n\nS00 ist die Formular-Zentrale.\nS01 waehlt die Formularart.\nS02 ist der Haupteditor.\n\nDie Formularart setzt nur Vorlage/Startfragen:\n- Website/Extern\n- Lead\n- Umfrage\n- Intern\n\nTabs im Formular:\n- Fragen\n- Design: S03\n- Antworten: S05\n\nS01C/D/E bleiben Referenz-Beispiele, nicht Review-Pfad.","x":3260,"y":285,"width":500,"height":360}, + {"id":"simple-direction","type":"file","file":"02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Simple Builder Neuausrichtung.md","x":3300,"y":765,"width":420,"height":160}, + {"id":"v01","type":"file","file":"02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/08 V01 Public Erfolg oder Redirect.md","x":1870,"y":1420,"width":420,"height":190}, + {"id":"note-public","type":"text","text":"# Public-Regel\n\nKeine App-Shell.\n\nRuhiges Kundenformular:\n- Branding-Kopf\n- Intro\n- Felder\n- mCaptcha\n- Submit\n- Success/Error\n\nMobile zuerst mitdenken.","x":2350,"y":1420,"width":420,"height":250}, + {"id":"s02","type":"file","file":"02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/07 S04 Oeffentliches Formular.md","x":1240,"y":1540,"width":420,"height":190}, + {"id":"v02","type":"file","file":"02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/09 V02 Public Formular nicht verfuegbar.md","x":1870,"y":1660,"width":420,"height":190}, + {"id":"v03","type":"file","file":"02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/Screens/10 V03 Public Captcha oder Submit Fehler.md","x":2350,"y":1700,"width":420,"height":190}, + {"id":"note-public-reuse","type":"text","text":"# Public Renderer\n\nD-FORM baut das Public Form.\n\nAndere Bereiche wie HR oder Booking koennen den Renderer spaeter wiederverwenden, sind aber keine D-FORM-Screens.","x":3000,"y":1550,"width":420,"height":220} ], "edges":[ - {"id":"edge-package-s01","fromNode":"package","fromSide":"right","toNode":"s01","toSide":"left","label":"S01"}, - {"id":"edge-s01-core","fromNode":"s01","fromSide":"right","toNode":"note-builder-core","toSide":"left","label":"Arbeitsbereiche"}, - {"id":"edge-core-d10","fromNode":"note-builder-core","fromSide":"right","toNode":"d10","toSide":"left","label":"Was passiert wenn"}, - {"id":"edge-s01-d01","fromNode":"s01","fromSide":"bottom","toNode":"d01","toSide":"top","label":"Share"}, - {"id":"edge-s01-d02","fromNode":"s01","fromSide":"bottom","toNode":"d02","toSide":"top","label":"Actions"}, - {"id":"edge-s01-d03","fromNode":"s01","fromSide":"bottom","toNode":"d03","toSide":"top","label":"Logic"}, - {"id":"edge-s01-d06","fromNode":"s01","fromSide":"bottom","toNode":"d06","toSide":"top","label":"Feldmenue"}, - {"id":"edge-d06-m01","fromNode":"d06","fromSide":"right","toNode":"m01","toSide":"left","label":"loeschen"}, - {"id":"edge-d03-d07","fromNode":"d03","fromSide":"bottom","toNode":"d07","toSide":"top","label":"Regel Detail"}, - {"id":"edge-d02-d08","fromNode":"d02","fromSide":"bottom","toNode":"d08","toSide":"top","label":"Action Detail"}, - {"id":"edge-s01-d09","fromNode":"s01","fromSide":"bottom","toNode":"d09","toSide":"top","label":"Publish"}, - {"id":"edge-d09-m02","fromNode":"d09","fromSide":"bottom","toNode":"m02","toSide":"top","label":"Unpublish"}, - {"id":"edge-s01-m03","fromNode":"s01","fromSide":"bottom","toNode":"m03","toSide":"top","label":"Archive"}, - {"id":"edge-s01-m04","fromNode":"s01","fromSide":"bottom","toNode":"m04","toSide":"top","label":"Dirty State"}, - {"id":"edge-d10-d07","fromNode":"d10","fromSide":"bottom","toNode":"d07","toSide":"top","label":"Logic Cases"}, - {"id":"edge-d10-d08","fromNode":"d10","fromSide":"bottom","toNode":"d08","toSide":"top","label":"Mapping Cases"}, - {"id":"edge-d10-d09","fromNode":"d10","fromSide":"bottom","toNode":"d09","toSide":"top","label":"Blocker Cases"}, - {"id":"edge-s01-s02","fromNode":"s01","fromSide":"bottom","toNode":"s02","toSide":"top","label":"publish + share"}, - {"id":"edge-s02-v01","fromNode":"s02","fromSide":"right","toNode":"v01","toSide":"left","label":"Submit ok"}, + {"id":"edge-s02-v01","fromNode":"s02","fromSide":"right","toNode":"v01","toSide":"left","label":"Submit erfolgreich"}, {"id":"edge-s02-v02","fromNode":"s02","fromSide":"right","toNode":"v02","toSide":"left","label":"nicht verfuegbar"}, - {"id":"edge-s02-v03","fromNode":"s02","fromSide":"right","toNode":"v03","toSide":"left","label":"Fehler"}, + {"id":"edge-s02-v03","fromNode":"s02","fromSide":"right","toNode":"v03","toSide":"left","label":"Fehler beim Submit"}, {"id":"edge-s02-public","fromNode":"s02","fromSide":"right","toNode":"note-public","toSide":"left","label":"Public-UX"}, - {"id":"edge-public-reuse","fromNode":"note-public","fromSide":"right","toNode":"note-public-reuse","toSide":"left","label":"Reuse"}, - {"id":"edge-focus-out","fromNode":"note-subframe-focus","fromSide":"bottom","toNode":"note-out-of-scope","toSide":"top","label":"Grenzen"}, - {"id":"edge-package-simple","fromNode":"package","fromSide":"bottom","toNode":"simple-direction","toSide":"top","label":"neue Richtung"}, - {"id":"edge-simple-s00","fromNode":"simple-direction","fromSide":"right","toNode":"s00","toSide":"left","label":"Einstieg"}, - {"id":"edge-s00-s01a","fromNode":"s00","fromSide":"right","toNode":"s01a","toSide":"left","label":"Variante D"}, - {"id":"edge-s00-s01b","fromNode":"s00","fromSide":"right","toNode":"s01b","toSide":"left","label":"Variante B"}, - {"id":"edge-s00-s01c","fromNode":"s00","fromSide":"right","toNode":"s01c","toSide":"left","label":"Website"}, - {"id":"edge-s00-s01d","fromNode":"s00","fromSide":"right","toNode":"s01d","toSide":"left","label":"Umfrage"}, - {"id":"edge-s00-s01e","fromNode":"s00","fromSide":"right","toNode":"s01e","toSide":"left","label":"Intern"}, - {"id":"edge-simple-s03","fromNode":"simple-direction","fromSide":"right","toNode":"s03","toSide":"left","label":"Verwaltung"}, - {"id":"edge-s03-s00","fromNode":"s03","fromSide":"top","toNode":"s00","toSide":"bottom","label":"Neues Formular"}, - {"id":"edge-s03-s01a","fromNode":"s03","fromSide":"right","toNode":"s01a","toSide":"bottom","label":"Bearbeiten"}, - {"id":"edge-s01a-s04","fromNode":"s01a","fromSide":"right","toNode":"s04","toSide":"left","label":"Design"}, - {"id":"edge-s01a-s05","fromNode":"s01a","fromSide":"right","toNode":"s05","toSide":"left","label":"Antworten"}, + {"id":"edge-public-reuse","fromNode":"note-public","fromSide":"right","toNode":"note-public-reuse","toSide":"left","label":"Wiederverwendung"}, + {"id":"edge-focus-out","fromFloating":false,"toFloating":false,"fromNode":"note-subframe-focus","fromSide":"right","toNode":"note-out-of-scope","toSide":"left","label":"Grenzen"}, + {"id":"edge-package-focus","fromNode":"package","fromSide":"right","toNode":"note-subframe-focus","toSide":"left","label":"Review"}, + {"id":"edge-package-simple","fromNode":"package","fromSide":"bottom","toNode":"s03","toSide":"top","label":"Hauptfluss"}, + {"id":"edge-s00-s01a","fromNode":"s00","fromSide":"right","toNode":"s01a","toSide":"left","label":"Weiter zu S02"}, + {"id":"edge-s00-s01c","fromNode":"s00","fromSide":"right","toNode":"s01c","toSide":"left","label":"Referenz: Website"}, + {"id":"edge-s00-s01d","fromNode":"s00","fromSide":"right","toNode":"s01d","toSide":"left","label":"Referenz: Umfrage"}, + {"id":"edge-s00-s01e","fromNode":"s00","fromSide":"right","toNode":"s01e","toSide":"left","label":"Referenz: Intern"}, + {"id":"edge-s03-s00","fromNode":"s03","fromSide":"right","toNode":"s00","toSide":"left","label":"Neues Formular"}, + {"id":"edge-s03-s01a","toFloating":false,"fromNode":"s03","fromSide":"right","toNode":"s01a","toSide":"left","label":"Bearbeiten"}, + {"id":"edge-s01a-s04","fromNode":"s01a","fromSide":"right","toNode":"s04","toSide":"left","label":"Tab Design"}, + {"id":"edge-s01a-s05","fromNode":"s01a","fromSide":"right","toNode":"s05","toSide":"left","label":"Tab Antworten"}, {"id":"edge-s01a-note","fromNode":"s01a","fromSide":"right","toNode":"note-simple-builder","toSide":"left","label":"Prinzip"}, - {"id":"edge-s01b-note","fromNode":"s01b","fromSide":"right","toNode":"note-simple-builder","toSide":"left","label":"Prinzip"}, {"id":"edge-s01c-note","fromNode":"s01c","fromSide":"right","toNode":"note-simple-builder","toSide":"left","label":"Beispiel"}, {"id":"edge-s01d-note","fromNode":"s01d","fromSide":"right","toNode":"note-simple-builder","toSide":"left","label":"Beispiel"}, - {"id":"edge-s01e-note","fromNode":"s01e","fromSide":"right","toNode":"note-simple-builder","toSide":"left","label":"Beispiel"} - ] -} + {"id":"edge-s01e-note","fromNode":"s01e","fromSide":"right","toNode":"note-simple-builder","toSide":"left","label":"Beispiel"}, + {"id":"edge-s01a-s02-public","fromNode":"s01a","fromSide":"bottom","toNode":"s02","toSide":"top","label":"Public Vorschau"}, + {"id":"edge-note-simple-direction","fromNode":"note-simple-builder","fromSide":"bottom","toNode":"simple-direction","toSide":"top","label":"Entscheidung"} + ], + "metadata":{ + "version":"1.0-1.0", + "frontmatter":{} + } +} \ No newline at end of file diff --git a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/WF-D-FORM Form Builder.md b/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/WF-D-FORM Form Builder.md index 8d932600..6ce26b4f 100644 --- a/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/WF-D-FORM Form Builder.md +++ b/02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/WF-D-FORM Form Builder.md @@ -26,22 +26,22 @@ Start fuer Review: [[00 Start hier - David]] ## 1. Kurzfassung -Dieses Paket bereitet `D-FORM` fuer David vor: den internen Form-Builder-Canvas und das oeffentliche Formular. +Dieses Paket bereitet `D-FORM` fuer David vor: den einfachen internen Formular-Editor und das oeffentliche Formular. Der Fokus liegt bewusst auf den zwei Bespoke-Erlebnissen: -- Form-Builder-Canvas fuer interne Nutzer +- S02 Fragen-Editor fuer interne Nutzer - Oeffentliches Formular fuer externe Submitter -Fuer den eigentlichen Subframe-Bau gilt [[Subframe Bauplan]] als konkrete Bau-Reihenfolge und Komponenten-Zuordnung. Interne Builder-Screens wie S01/D07/D08/D09 nutzen das vorhandene Default Page Layout; keine eigene Sidebar oder Header-Navigation nachbauen. Public-Screens bleiben ohne App-Shell. +Fuer den eigentlichen Subframe-Bau gilt [[Subframe Bauplan]] als konkrete Bau-Reihenfolge und Komponenten-Zuordnung. Interne Builder-Screens wie S00/S01/S02/S03/S05 nutzen das vorhandene Default Page Layout; keine eigene Sidebar oder Header-Navigation nachbauen. Public-Screens bleiben ohne App-Shell. Die vollstaendige Forms-/Submissions-Verwaltung, Booking, Kalender und Signatur werden nicht in diesem Paket gebaut. Als schlanker Einstieg wurde aber S00 `Formulare verwalten` ergaenzt, damit Nutzer vorhandene Formulare finden und in den Builder springen koennen. -**Wichtig:** Dieses Paket ist jetzt ein tragfaehiger Detail-Brainstorming-Entwurf fuer David. Davids Review vom 2026-07-06 hat den fachlichen Scope bestaetigt, aber fuer den Builder mehr Interaktionslogik verlangt. Diese "was passiert nach Klick?"-Ebene ist im Advanced-Builder nachgezogen. D-FORM ist in Subframe als Draft gebaut: S00 bis S05, die Public-Screens S04/V01/V02/V03 sowie D07 Logic Regel Detail, D08 Action Detail mit Mapping und D09 Publish Blocker Summary sind gebaut. +**Wichtig:** Dieses Paket ist jetzt auf den einfachen S02-Hauptfluss ausgerichtet. D-FORM ist in Subframe als Draft gebaut: S00 bis S05, S02L/E05 fuer Lead-Zuweisung und die Public-Screens S04/V01/V02/V03. Vor einer echten Fertig-Markierung muss David die Subframe-Frames final reviewen. Danach koennen nur noch gezielte Korrekturen oder explizit nachgeforderte Zustands-/Modalframes folgen. -**Nachtrag 2026-07-07:** Fuer Handwerksbetriebe wirkt der bisherige S01 fachlich stark, aber zu komplex. Deshalb wurden einfachere Alternativen gebaut: zuerst [[Screens/01 S00 Formulare verwalten|S00 Formulare verwalten]] als Startseite, danach [[Screens/04 S01 Formularart auswaehlen|S01 Formularart auswaehlen]] und Google-Forms-artige Beispiel-Editoren je Formularart. Gebaute Varianten: [[Screens/05 S02 Einfacher Fragen Editor|S02 Einfacher Fragen Editor]], [[Screens/80 S01B Gefuehrter Fragen Editor|S01B Gefuehrter Fragen Editor]], [[Screens/81 S01C Website Formular Editor|S01C Website Formular Editor]], [[Screens/82 S01D Umfrage Editor|S01D Umfrage Editor]] und [[Screens/83 S01E Internes Formular Editor|S01E Internes Formular Editor]]. Ergaenzt wurden ausserdem [[Screens/06 S03 Formular Design|S03 Formular Design]] und [[Screens/11 S05 Antworten Tabelle|S05 Antworten Tabelle]]. Die bisherige Logic bleibt im Hintergrund erhalten, aber der Einstieg soll nicht mehr wie ein Profi-Builder wirken. +**Nachtrag 2026-07-07:** Fuer Handwerksbetriebe wirkt der bisherige Builder fachlich stark, aber zu komplex. Deshalb wurden einfachere Alternativen gebaut: zuerst [[Screens/01 S00 Formulare verwalten|S00 Formulare verwalten]] als Startseite, danach [[Screens/04 S01 Formularart auswaehlen|S01 Formularart auswaehlen]] und Google-Forms-artige Beispiel-Editoren. Hauptvariante ist [[Screens/05 S02 Einfacher Fragen Editor|S02 Einfacher Fragen Editor]]. [[Screens/81 S01C Website Formular Editor|S01C Website Formular Editor]], [[Screens/82 S01D Umfrage Editor|S01D Umfrage Editor]] und [[Screens/83 S01E Internes Formular Editor|S01E Internes Formular Editor]] bleiben spaetere Beispiele. Ergaenzt wurden ausserdem [[Screens/06 S03 Formular Design|S03 Formular Design]] und [[Screens/11 S05 Antworten Tabelle|S05 Antworten Tabelle]]. **Nachtrag 2026-07-08:** Die Simple-Builder-Richtung wurde nachgeschaerft: S00 nutzt Mehrfachfilter nach Status und Art, S02 hat mehrere editierbare Optionen und eine erweiterte Google-Forms-nahe Werkzeugleiste, S04 enthaelt eine Datenschutz-Checkbox, S03/S05 haben keinen eigenen Teilen-Tab mehr und S05 dient nur Antworten/Export. @@ -49,6 +49,8 @@ Vor einer echten Fertig-Markierung muss David die Subframe-Frames final reviewen **Nachtrag 2026-07-09:** Fuer Davids Review wurde ein kompletter Obsidian-faehiger Paketordner vorbereitet. Die aktuelle Uebergabe liegt in [[D-FORM David Uebergabe 2026-07-09]]. Die S02/S02L-Lead-Hinweise sind optisch an die gruene `Lead anlegen`-Card angeglichen: Icon links, Titel/Badge/Beschreibung in der Mitte, Aktion rechts. +**Cleanup 2026-07-10:** S01B, die alten Advanced-Screens 90/90b/91/92/93 und die doppelten Dateien unter `Varianten/` wurden aus Obsidian entfernt. Der Handoff zeigt nur noch den aktuellen Hauptfluss plus S01C/S01D/S01E als optionale Beispiele. + --- ## 2. Intent Check @@ -133,19 +135,11 @@ Merksatz: D-FORM hat wenige eigene Routen, aber einen riesigen Detailumfang inne | Bereich | Stand | Naechster Schritt | |---|---|---| -| ADV-S01 Builder-Layout | Arbeitsentwurf in ADV-S01 ergaenzt und als Subframe-Frame gebaut. | David prueft Drei-Zonen-Builder in Subframe. | -| ADV-S01 Feld-Palette | Arbeitsentwurf in ADV-S01 ergaenzt. | Entscheiden, ob Content Blocks wie Section Header in v1 sichtbar werden. | -| ADV-S01 Canvas / Drag-and-Drop | Arbeitsentwurf in ADV-S01 ergaenzt. | Drag-Zustaende ggf. in Subframe besonders sichtbar machen. | -| ADV-S01 Feld-Inspector | Arbeitsentwurf in ADV-S01 ergaenzt. | Properties je Feldtyp reviewen. | -| ADV-S01 Interaktionslogik | Nach David-Feedback ergaenzt und als Zustandsdateien ausgearbeitet. D07/D08/D09 sind als zusaetzliche Subframe-Zustaende gebaut. | David prueft die Subframe-Frames und entscheidet nur noch ueber Feinschliff oder Zusatzframes. | -| ADV-S01 Validation / Publish | Arbeitsentwurf in ADV-S01 ergaenzt und als D09 gebaut. | David prueft Blocker-Copy, Warnungen und Sprungziele. | -| ADV-S01 Conditional Logic | Arbeitsentwurf in ADV-S01 ergaenzt. | V1-Grenzen final bestaetigen. | -| ADV-S01 Sharing | Arbeitsentwurf in ADV-S01 ergaenzt. | iFrame/QR/URL-Darstellung finalisieren. | -| ADV-S01 Post-Submit-Actions | Arbeitsentwurf in ADV-S01 ergaenzt. | Action-Typen und Mapping-Tiefe final reviewen. | -| ADV-S01 Permissions | Arbeitsentwurf in ADV-S01 ergaenzt. | Read-only/No-Permission-Zustand final entscheiden. | +| S02 Haupteditor | Simple Builder mit Fragekarten, Fragetypen, Bedingung und Lead-Hinweis gebaut. | David prueft die einfache Richtung. | +| S02L/E05 Lead-Zuweisung | Lead-Felder werden per Dropdown zugewiesen; Fehlerzustand ist gebaut. | David prueft, ob die Zuordnung reicht. | | S04 Public Form | Arbeitsentwurf in S04 ergaenzt. | David validiert Public Layout, Mobile, Submit und Branding. | -| V01/V02/V03 Public-Varianten | Arbeitsentwurf in Varianten ergaenzt. | David validiert Copy und Public-Fehlerlogik. | -| Canvas/Tracker/Handoff | Nach P2-Auth-Referenz synchronisiert: Hauptflow plus ADV-S01-Zustaende/Case-Matrix im Canvas sichtbar. | Vor Subframe nur noch David-Freigabe bzw. Auswahl moeglicher Zusatzframes. | +| V01/V02/V03 Public-States | Public Success, Nicht-verfuegbar und Captcha/Submit-Fehler gebaut. | David validiert Copy und Public-Fehlerlogik. | +| Canvas/Tracker/Handoff | Auf aktuellen Hauptfluss bereinigt; alte Advanced-Frames aus Obsidian entfernt. | Vor Subframe nur noch David-Freigabe bzw. gezielte Korrekturen. | --- ## 3d. David-Feedback 2026-07-06 @@ -161,13 +155,13 @@ Nacharbeit: In S01 fehlte die Interaktionslogik fuer konkrete Aktionen. Die wich | Feld loeschen | 3-Punkte-Menue/Gefahrenzone; Confirm bei Abhaengigkeiten oder public-wirksamen Folgen. | | Logic-Regel anlegen | Logic-Panel/Detailbereich im selben S01, kein Modal. | | Post-Submit-Action hinzufuegen | Typauswahl als Popover/kleines Auswahlpanel, Konfiguration im Action-Panel/Drawer. | -| Sharing oeffnen | Sharing-Panel/Modus im ADV-S01, kein Modal. | +| Sharing oeffnen | Public-Link-/Teilen-Kontext im einfachen Builder, kein eigener Review-Screen. | | Publish | Inline-Check mit Summary/Popover; Blocker springen zu Feld, Logic, Action oder Settings. | | Unpublish | Confirm-Dialog, weil Public-Link danach nicht mehr erreichbar ist. | -Bewertung: Die 5 Hauptframes bleiben bestehen. Nach Empfehlung wurden zusaetzlich D07 Logic Regel Detail, D08 Action Detail mit Mapping und D09 Publish Blocker Summary als Subframe-Zustaende gebaut. M01/M02 und die restlichen D-/M-Zustaende bleiben vorerst dokumentiert. +Bewertung: Der aktuelle Review konzentriert sich auf den S02-Hauptfluss. M01/M02 und die restlichen D-/M-Zustaende bleiben vorerst dokumentiert. -Nachgezogene Vor-Subframe-Dateien: [[Screens/D01 Sharing Panel URL QR iFrame]], [[Screens/D02 Post-Submit Actions]], [[Screens/D03 Conditional Logic Editor]], [[Screens/D06 Feldkarten Menue]], [[Screens/M01 Feld loeschen Confirm]], [[Screens/91 D07 Logic Regel Detail|D07 Logic Regel Detail]], [[Screens/92 D08 Action Detail mit Mapping|D08 Action Detail mit Mapping]], [[Screens/93 D09 Publish Blocker Summary|D09 Publish Blocker Summary]], [[Screens/D10 Builder Interaktionsfaelle]], [[Screens/M02 Unpublish Confirm]], [[Screens/M03 Archive Confirm]], [[Screens/M04 Unsaved Changes Confirm]]. +Dokumentierte Detaildateien: [[Screens/D01 Sharing Panel URL QR iFrame]], [[Screens/D02 Post-Submit Actions]], [[Screens/D03 Conditional Logic Editor]], [[Screens/D06 Feldkarten Menue]], [[Screens/M01 Feld loeschen Confirm]], [[Screens/D10 Builder Interaktionsfaelle]], [[Screens/M02 Unpublish Confirm]], [[Screens/M03 Archive Confirm]], [[Screens/M04 Unsaved Changes Confirm]]. Nach weiterer Rueckmeldung wurde D10 als Case-Matrix ergaenzt: Was passiert, wenn Nutzer etwas auswaehlen, eine Checkbox setzen, einen mittleren Pflichtschritt vergessen, Bild/Datei hinzufuegen, Optionen loeschen, Mapping halb setzen oder eine public-wirksame Aktion ausloesen. @@ -181,10 +175,10 @@ Nachgezogen fuer D-FORM: | P2-Muster | D-FORM-Umsetzung | |---|---| -| Screen-Dateien mit Ziel, Daten, Aktionen, Verhalten, Zustaenden und Subframe-Hinweis | ADV-S01/S02/S04/V01-V03 sowie D01-D10 und M01-M04 entsprechend ausgearbeitet. | -| D-/M-Zustaende als eigene Dateien, auch wenn sie nicht gebaut werden | ADV-S01-Zustaende liegen als eigene Vor-Subframe-Dateien im `Screens`-Ordner; D07/D08/D09 sind als zusaetzliche Subframe-Zustaende gebaut. | +| Screen-Dateien mit Ziel, Daten, Aktionen, Verhalten, Zustaenden und Subframe-Hinweis | S02/S02L/E05/S04/V01-V03 sowie D-/M-Notizen entsprechend ausgearbeitet. | +| D-/M-Zustaende als eigene Dateien, auch wenn sie nicht gebaut werden | Detailnotizen liegen im `Screens`-Ordner, sind aber nicht Teil der aktuellen Subframe-Review-Reihenfolge. | | Canvas zeigt Hauptflow plus dokumentierte Zustaende/Notizen | `WF-D-FORM Form Builder.canvas` wurde erweitert: Hauptflow, D-/M-Zustaende, D10 Case-Matrix, Public Flow und Scope-Notizen. | -| Tracker fuehrt dokumentierte Zustaende als eigene Zeilen | Tracker enthaelt D01-D10 und M01-M04; D07/D08/D09 stehen auf `Bauen`, M01/M02/M03/M04 bleiben dokumentiert. | +| Tracker fuehrt dokumentierte Zustaende als eigene Zeilen | Tracker enthaelt die D-/M-Notizen als Dokumentation ohne Canvas-Link. | Bewertung: D-FORM ist auf dem gleichen Vor-Subframe-Dokumentationsniveau wie die besser ausgearbeiteten P2 Auth Pakete. Subframe ist als Draft gebaut und verlinkt; Review und Feinschliff folgen. @@ -330,9 +324,9 @@ Empfehlung: David sollte **Palette + Canvas + Inspector** als Kernarbeit versteh | Public Erfolg oder Redirect | Submit-Ende ist copy- und vertrauenskritisch. | Bauen | | Public Formular nicht verfuegbar | Externer Blocker braucht klare Loesung. | Bauen | | Public Captcha oder Submit Fehler | Hauefiger Public-Fehler, muss sauber wirken. | Bauen oder dokumentieren | -| Sharing-Panel | URL/QR/iFrame gehoeren in Builder. | In ADV-S01 integrieren | -| Post-Submit-Actions | Field-Mapping muss sichtbar sein. | In ADV-S01 integrieren | -| Conditional-Logic-Editor | v1-Pflicht, aber eng schneiden. | In ADV-S01 integrieren | +| Sharing-Panel | URL/QR/iFrame gehoeren in den Public-Link-Kontext. | Dokumentieren | +| Post-Submit-Actions | Lead-Feldzuweisung muss sichtbar sein. | In S02L/E05 abgebildet | +| Conditional-Logic-Editor | v1 eng schneiden. | Als einfache Bedingung an der Frage | | Forms-Liste | Compose aus D2, nicht D-FORM. | Raus | | Submissions | Compose aus A2/A3, nicht D-FORM. | Raus | | Booking-Intake | Eigenes Ticket `D-BOOK`. | Raus | @@ -344,7 +338,6 @@ Empfehlung: David sollte **Palette + Canvas + Inspector** als Kernarbeit versteh | ID | Screen/Zustand | Ist eigener Arbeitsmoment? | Neue Struktur/Entscheidung/Risiko? | Entscheidung | Begruendung | |---|---|---|---|---|---| -| ADV-S01 | [[Screens/90 ADV Form-Builder Canvas|ADV-S01 Form-Builder Canvas]] | ja | ja, schwerste P16-UI | Referenz behalten | Nicht mehr Hauptfluss; S02 ist die einfache Richtung. | | S04 | [[Screens/07 S04 Oeffentliches Formular|S04 Oeffentliches Formular]] | ja | ja, Public Renderer und Branding | Bauen | Extern sichtbar, hoher Polish. | | V01 | [[Screens/08 V01 Public Erfolg oder Redirect|V01 Public Erfolg oder Redirect]] | teilweise | ja, Submit-Ende und Copy | Bauen | Public Dbschluss soll nicht Dev-Default sein. | | V02 | [[Screens/09 V02 Public Formular nicht verfuegbar|V02 Public Formular nicht verfuegbar]] | ja | ja, blockierender Public-State | Bauen | Nutzer kann nicht fortfahren. | @@ -354,9 +347,6 @@ Empfehlung: David sollte **Palette + Canvas + Inspector** als Kernarbeit versteh | D03 | [[Screens/D03 Conditional Logic Editor]] | nein | ja, Regel-Editor | Nur dokumentieren | In S01 als Panel/Tab dokumentieren. | | D06 | [[Screens/D06 Feldkarten Menue]] | nein | ja, Klick-Logik nach David-Feedback | Nur dokumentieren | 3-Punkte-Menue fuer Duplizieren, Loeschen, ggf. Deaktivieren. | | M01 | [[Screens/M01 Feld loeschen Confirm]] | nein | ja, Abhaengigkeiten in Logic/Actions | Nur dokumentieren | Confirm nur bei Risiko, kein normaler Bearbeitungsdialog. | -| D07 | [[Screens/91 D07 Logic Regel Detail|D07 Logic Regel Detail]] | teilweise | ja, Regelbearbeitung | Referenz behalten | Fuer S02 nicht Hauptfluss; bei Advanced-Bedarf pruefen. | -| D08 | [[Screens/92 D08 Action Detail mit Mapping|D08 Action Detail mit Mapping]] | teilweise | ja, Field-Mapping | Referenz behalten | Fuer S02 nicht Hauptfluss; Mapping bleibt verborgen. | -| D09 | [[Screens/93 D09 Publish Blocker Summary|D09 Publish Blocker Summary]] | ja | ja, Publish-Entscheidung | Referenz behalten | In S02 reicht Banner/Popover im selben Screen. | | D10 | [[Screens/D10 Builder Interaktionsfaelle]] | nein | ja, Auswahlfolgen und Edge Cases | Nur dokumentieren | Case-Matrix fuer Dropdowns, Checkboxen, vergessene Zwischenschritte, Datei/Bild und Modal-Inhalte. | | M02 | [[Screens/M02 Unpublish Confirm]] | nein | ja, public-wirksam | Nur dokumentieren | Confirm-Dialog wegen Public-Link. | | M03 | [[Screens/M03 Archive Confirm]] | nein | ja, destruktiv/Statuswechsel | Nur dokumentieren | Confirm aus More-Menue. | @@ -370,21 +360,16 @@ Entscheidung: `Bauen`, `Wiederverwenden`, `Nur dokumentieren`, `Spaeter`, `Raus` Der urspruengliche Advanced-Subframe-Fokus lag auf diesen **8 Frames/Zustaenden**: -- ADV-S01 Form-Builder Canvas - S04 Oeffentliches Formular - V01 Public Erfolg oder Redirect - V02 Public Formular nicht verfuegbar - V03 Public Captcha oder Submit Fehler -- D07 Logic Regel Detail -- D08 Action Detail mit Mapping -- D09 Publish Blocker Summary Nach der Simple-Builder-Neuausrichtung sind zusaetzlich gebaut und fuer Review nach vorne gezogen: - S00 Formulare verwalten - S01 Formularart auswaehlen - S02 Einfacher Fragen Editor -- S01B Gefuehrter Fragen Editor - S01C Website Formular Editor - S01D Umfrage Editor - S01E Internes Formular Editor @@ -395,15 +380,11 @@ Nicht als eigene Frames bauen: - Kein Sharing/Teilen als eigener Hauptreiter im Simple Builder; Public-Link/QR/Einbettung gehoeren in Veroeffentlichen/Public-Link-Kontext. - Actions, Logic, Preview und Settings als Arbeitsbereiche von S01 bzw. einfache Zusatzoptionen im Simple Builder. -- Feldkarten-Menue, allgemeine Panels, D10 Case-Matrix und Confirm-Dialoge als dokumentierte ADV-S01-Zustaende. +- Feldkarten-Menue, allgemeine Panels, D10 Case-Matrix und Confirm-Dialoge als dokumentierte Detailnotizen. - Vollstaendige Forms-Liste und Submissions, weil diese Compose aus A2/A3 sind. S00/S05 sind nur schlanke D-FORM-Einstiegs-/Antwortansichten. - Booking/Kalender/Signatur, weil diese eigene Tickets haben. -Als zusaetzliche S01-Zustandsframes bauen: - -- D07 Logic Regel Detail -- D08 Action Detail mit Mapping -- D09 Publish Blocker Summary +Keine zusaetzlichen Advanced-Zustandsframes im aktuellen Handoff bauen. ### Copy-kritische Zustaende @@ -444,16 +425,12 @@ Als zusaetzliche S01-Zustandsframes bauen: | S05 | [[Screens/11 S05 Antworten Tabelle|S05 Antworten Tabelle]] | Builder Tab | Bauen | `/forms/:id/edit` | Subframe Draft / Review offen | | E03 | [[Screens/12 E03 S05 Keine Antworten|E03 S05 Keine Antworten]] | Empty State | Bauen | `/forms/:id/edit` | Subframe Draft / Review offen | | E04 | [[Screens/13 E04 S05 Export Fehler|E04 S05 Export Fehler]] | Error State | Bauen | `/forms/:id/edit` | Subframe Draft / Review offen | -| S01B | [[Screens/80 S01B Gefuehrter Fragen Editor|S01B Gefuehrter Fragen Editor]] | Spaeter / Referenz | Bauen | `/forms/:id/edit` | Subframe Draft / Referenz | | S01C | [[Screens/81 S01C Website Formular Editor|S01C Website Formular Editor]] | Spaeter / Referenz | Bauen | `/forms/:id/edit` | Subframe Draft / Referenz | | S01D | [[Screens/82 S01D Umfrage Editor|S01D Umfrage Editor]] | Spaeter / Referenz | Bauen | `/forms/:id/edit` | Subframe Draft / Referenz | | S01E | [[Screens/83 S01E Internes Formular Editor|S01E Internes Formular Editor]] | Spaeter / Referenz | Bauen | `/forms/:id/edit` | Subframe Draft / Referenz | -| ADV-S01 | [[Screens/90 ADV Form-Builder Canvas|ADV-S01 Form-Builder Canvas]] | Advanced Builder / Referenz | Bauen | `/forms/:id/edit` | Subframe Draft / Referenz | -| S01 | [[Screens/90b ADV Form-Builder Canvas 2|ADV-S01-2 Form-Builder Canvas 2]] | Advanced Builder / Referenz | Bauen | `/forms/:id/edit` | Subframe Draft / Referenz | +### Detailnotizen ohne eigene Review-Screens -### ADV-S01-Zustaende und Zusatzframes vor Subframe - -Diese Eintraege sind ausgearbeitet. D07/D08/D09 wurden nach Empfehlung als zusaetzliche Subframe-Zustaende gebaut; die restlichen bleiben dokumentiert. +Diese Eintraege sind ausgearbeitet, bleiben aber Dokumentation und sind nicht Teil der aktuellen Subframe-Review-Reihenfolge. | ID | Zustand | Typ | Umsetzung | |---|---|---|---| @@ -462,9 +439,6 @@ Diese Eintraege sind ausgearbeitet. D07/D08/D09 wurden nach Empfehlung als zusae | D03 | [[Screens/D03 Conditional Logic Editor]] | Panel-Zustand | Nur dokumentieren | | D06 | [[Screens/D06 Feldkarten Menue]] | Inline-Zustand | Nur dokumentieren | | M01 | [[Screens/M01 Feld loeschen Confirm]] | Confirm Modal | Nur dokumentieren | -| D07 | [[Screens/91 D07 Logic Regel Detail|D07 Logic Regel Detail]] | Panel-Zustand | Bauen | -| D08 | [[Screens/92 D08 Action Detail mit Mapping|D08 Action Detail mit Mapping]] | Panel-Zustand | Bauen | -| D09 | [[Screens/93 D09 Publish Blocker Summary|D09 Publish Blocker Summary]] | Inline-Zustand | Bauen | | D10 | [[Screens/D10 Builder Interaktionsfaelle]] | Case-Matrix | Nur dokumentieren | | M02 | [[Screens/M02 Unpublish Confirm]] | Confirm Modal | Nur dokumentieren | | M03 | [[Screens/M03 Archive Confirm]] | Confirm Modal | Nur dokumentieren | @@ -477,25 +451,25 @@ Diese Eintraege sind ausgearbeitet. D07/D08/D09 wurden nach Empfehlung als zusae Interner Hauptarbeitsort: ```text -ADV-S01 Form-Builder Canvas - - Header / Status / Save / Publish - - Feld-Palette - - Canvas - - Feld-Inspector - - Arbeitsmodi: Logic, Actions, Sharing, Preview, Settings +S02 Einfacher Fragen Editor + - Fragen + - Lead-Hinweis / Felder zuweisen + - Bedingung direkt an der Frage + - Design-Tab S03 + - Antworten-Tab S05 ``` -Dokumentierte ADV-S01-Zustaende: +Dokumentierte Detailnotizen: ```text -S01 -> D01 Sharing Panel -S01 -> D02 Post-Submit Actions -> D08 Action Detail mit Mapping -S01 -> D03 Conditional Logic Editor -> D07 Logic Regel Detail -S01 -> D06 Feldkarten Menue -> M01 Feld loeschen Confirm -S01 -> D09 Publish Blocker Summary -> M02 Unpublish Confirm -S01 -> M03 Archive Confirm -S01 -> M04 Unsaved Changes Confirm -S01 -> D10 Builder Interaktionsfaelle +S02 -> D01 Sharing Panel +S02 -> D02 Post-Submit Actions +S02 -> D03 Conditional Logic Editor +S02 -> D06 Feldkarten Menue -> M01 Feld loeschen Confirm +S02 Publish-Kontext -> M02 Unpublish Confirm +S02 -> M03 Archive Confirm +S02 -> M04 Unsaved Changes Confirm +S02 -> D10 Builder Interaktionsfaelle ``` Public Flow: @@ -528,20 +502,14 @@ S04 Oeffentliches Formular - [[Screens/11 S05 Antworten Tabelle|S05 Antworten Tabelle]] - [[Screens/12 E03 S05 Keine Antworten|E03 S05 Keine Antworten]] - [[Screens/13 E04 S05 Export Fehler|E04 S05 Export Fehler]] -- [[Screens/80 S01B Gefuehrter Fragen Editor|S01B Gefuehrter Fragen Editor]] - [[Screens/81 S01C Website Formular Editor|S01C Website Formular Editor]] - [[Screens/82 S01D Umfrage Editor|S01D Umfrage Editor]] - [[Screens/83 S01E Internes Formular Editor|S01E Internes Formular Editor]] -- [[Screens/90 ADV Form-Builder Canvas|ADV-S01 Form-Builder Canvas]] -- [[Screens/90b ADV Form-Builder Canvas 2|ADV-S01-2 Form-Builder Canvas 2]] - [[Screens/D01 Sharing Panel URL QR iFrame]] - [[Screens/D02 Post-Submit Actions]] - [[Screens/D03 Conditional Logic Editor]] - [[Screens/D06 Feldkarten Menue]] - [[Screens/M01 Feld loeschen Confirm]] -- [[Screens/91 D07 Logic Regel Detail|D07 Logic Regel Detail]] -- [[Screens/92 D08 Action Detail mit Mapping|D08 Action Detail mit Mapping]] -- [[Screens/93 D09 Publish Blocker Summary|D09 Publish Blocker Summary]] - [[Screens/D10 Builder Interaktionsfaelle]] - [[Screens/M02 Unpublish Confirm]] - [[Screens/M03 Archive Confirm]] @@ -554,9 +522,9 @@ S04 Oeffentliches Formular - [x] Jeder Screen ist mit Umsetzung markiert. - [x] Jedes sichtbare Feld hat eine Datenquelle. Als Detail-Brainstorming dokumentiert. - [x] Jeder Button hat eine Aktion. Nach David-Feedback um Interaktionslogik ergaenzt. -- [x] Die wichtigsten ADV-S01-Klickzustaende sind als eigene Vor-Subframe-Dateien dokumentiert. +- [x] Die wichtigsten Klickzustaende sind als Markdown-Detailnotizen dokumentiert. - [x] Die wichtigsten Was-passiert-wenn-Faelle sind in D10 als Case-Matrix dokumentiert. -- [x] Canvas ist nach P2-Auth-Referenz erweitert: Hauptflow, ADV-S01-Zustaende, D10, Public Flow und Scope-Notizen. +- [x] Canvas ist auf Hauptflow, Public Flow und Scope-Notizen bereinigt. - [x] Banner/Inline-Meldungen sind dokumentiert. - [x] Toasts/Action-Feedback sind dokumentiert. - [x] Meldungs-/Error-State-Matrix ist ausgefuellt. @@ -564,8 +532,8 @@ S04 Oeffentliches Formular - [x] Relevante Zustaende sind beschrieben. - [x] Offene Fragen sind sichtbar. - [x] Ticket-Handoff-Entwurf ist in `Tickets/Fertige Tickets/` angelegt. -- [x] Detail-Brainstorming fuer ADV-S01/S02/S04/V01/V02/V03 ist abgeschlossen. +- [x] Detail-Brainstorming fuer S02/S04/V01/V02/V03 ist abgeschlossen. - [x] David-Review-Rueckmeldung ist erfolgt. - [x] S01-Interaktionslogik nach David-Feedback ist eingearbeitet. - [ ] David-Freigabe nach S01-Nacharbeit ist erfolgt. -- [x] Subframe-Link fuer Flow, Hauptfluss 01-13 und Referenzseiten 80-93 ist eingetragen. +- [x] Subframe-Link fuer Flow und Hauptfluss 01-13 ist eingetragen. diff --git a/02 Wireframes/P12 Automationen/WF-P12 Flow-Editor/Screens/M01 Probelauf.md b/02 Wireframes/P12 Automationen/WF-P12 Flow-Editor/Screens/M01 Probelauf.md index acafa8b8..464db50d 100644 --- a/02 Wireframes/P12 Automationen/WF-P12 Flow-Editor/Screens/M01 Probelauf.md +++ b/02 Wireframes/P12 Automationen/WF-P12 Flow-Editor/Screens/M01 Probelauf.md @@ -9,7 +9,7 @@ typ: Modal umsetzung: Bauen status: Validiert route: "aus S01 (Probelauf-Knopf / vor Aktivierung)" -subframe: "" +subframe: "https://app.subframe.com/f023064a5ca0/design/ea231a78-eddb-4c04-97a8-7574d45d8576/edit" --- # M01 — Probelauf (Dry-Run) @@ -38,4 +38,9 @@ A5 Modal + Simulations-Liste. ## Subframe -- (erst nach explizitem OK) +- [M01 Probelauf Dialog](https://app.subframe.com/f023064a5ca0/design/ea231a78-eddb-4c04-97a8-7574d45d8576/edit) + +### Abgleich Subframe vs. Spec (2026-07-10) + +- Spec abgedeckt: »Trifft 143 Datensätze«, erste 5 Beispiele, Klartext-Simulation, Schwellen-Bestätigung, aktivieren/zurück +- Abweichung: Bestätigung als **Checkbox statt Bestätigungs-Phrase**; »Aktivieren«-Button ist nicht disabled, solange die Checkbox leer ist diff --git a/02 Wireframes/P12 Automationen/WF-P12 Flow-Editor/Screens/M02 Schleifen-Warnung.md b/02 Wireframes/P12 Automationen/WF-P12 Flow-Editor/Screens/M02 Schleifen-Warnung.md index 9356fa6b..9d68d8fc 100644 --- a/02 Wireframes/P12 Automationen/WF-P12 Flow-Editor/Screens/M02 Schleifen-Warnung.md +++ b/02 Wireframes/P12 Automationen/WF-P12 Flow-Editor/Screens/M02 Schleifen-Warnung.md @@ -9,7 +9,7 @@ typ: Modal umsetzung: Bauen status: Validiert route: "aus S01 bei Schleifen-Verdacht" -subframe: "" +subframe: "https://app.subframe.com/f023064a5ca0/design/8e8943a5-36ea-4792-a3e0-590cb88fd9df/edit" --- # M02 — Schleifen-/Raten-Warnung @@ -31,4 +31,11 @@ A5 Modal + Mini-Graph. ## Subframe -- (erst nach explizitem OK) +- [M02 Schleifen-Warnung Dialog](https://app.subframe.com/f023064a5ca0/design/8e8943a5-36ea-4792-a3e0-590cb88fd9df/edit) + +### Abgleich Subframe vs. Spec (2026-07-10) + +- Spec abgedeckt: Mini-Graph mit markiertem Kreis, Klartext-Erklärung, »Beheben«-Knopf, Anomalie-Copy (2.000-mal/5 Min, typisch 5–10/Tag) +- Abweichung: Schleife und Raten-Anomalie **in einem Dialog kombiniert** (Spec: Anomalie als eigene Variante) +- Abweichung: **»Trotzdem aktivieren«-Button** widerspricht der Fehlermatrix (Schleife blockiert) — klären +- Kreis als Text-Pill markiert statt als sichtbare Rück-Kante im Graph diff --git a/02 Wireframes/P12 Automationen/WF-P12 Flow-Editor/Screens/M03 Start-Auswahl.md b/02 Wireframes/P12 Automationen/WF-P12 Flow-Editor/Screens/M03 Start-Auswahl.md index 99049e0b..2c04f73c 100644 --- a/02 Wireframes/P12 Automationen/WF-P12 Flow-Editor/Screens/M03 Start-Auswahl.md +++ b/02 Wireframes/P12 Automationen/WF-P12 Flow-Editor/Screens/M03 Start-Auswahl.md @@ -9,7 +9,7 @@ typ: Modal umsetzung: Bauen status: Validiert route: "»+ Neue Automation« (aus der Automation-Liste)" -subframe: "" +subframe: "https://app.subframe.com/f023064a5ca0/design/8b398273-682e-4cf2-851b-3b1a89da4d90/edit" --- # M03 — Start-Auswahl @@ -29,4 +29,10 @@ A5 Dialog (echtes Modal). ## Subframe -- (erst nach explizitem OK) +- [M03 Start-Auswahl Dialog](https://app.subframe.com/f023064a5ca0/design/8b398273-682e-4cf2-851b-3b1a89da4d90/edit) + +### Abgleich Subframe vs. Spec (2026-07-10) + +- Spec vollständig abgedeckt: leer ODER Vorlage, SHK + eigene, Kategorien, Use-Case-Beschreibungen, eingebetteter Vorlagen-Katalog +- Zusätzlich: Suchfeld, Flow-Vorschau-Chips, Nutzungszahlen (»38-mal verwendet«) +- Abweichung: **doppelter CTA** (»Verwenden« pro Karte UND »Mit Auswahl weiter« im Footer) — Interaktionsmodell klären diff --git a/02 Wireframes/P12 Automationen/WF-P12 Flow-Editor/Screens/S01 Flow-Editor.md b/02 Wireframes/P12 Automationen/WF-P12 Flow-Editor/Screens/S01 Flow-Editor.md index f169c3a4..6cd1cffd 100644 --- a/02 Wireframes/P12 Automationen/WF-P12 Flow-Editor/Screens/S01 Flow-Editor.md +++ b/02 Wireframes/P12 Automationen/WF-P12 Flow-Editor/Screens/S01 Flow-Editor.md @@ -9,7 +9,7 @@ typ: BESPOKE (schwerste UI) umsetzung: Bauen status: Validiert route: "/automations/:id/edit" -subframe: "" +subframe: "https://app.subframe.com/f023064a5ca0/design/a42ecf81-5d6e-4fa1-9076-b22010cd5ecf/edit" --- # S01 — Flow-Editor ⭐ @@ -41,4 +41,15 @@ Automationen zusammenklicken wie ein Diagramm — verständlich für Büro-Mensc ## Subframe -- (erst nach explizitem OK) +Zwei Varianten gebaut (Flow »(MCP) HiveGrid Workflow Engine / Automationen P12«) — **Richtungs-Entscheidung offen**: + +- [Variante A — Dreispalten kompakt](https://app.subframe.com/f023064a5ca0/design/a42ecf81-5d6e-4fa1-9076-b22010cd5ecf/edit): klassisches Konfig-Formular rechts (näher an der validierten Spec »Klick auf Baustein → Panel«) +- [Variante B — Geführter Inspector](https://app.subframe.com/f023064a5ca0/design/ce6a73d8-ae0c-4d1c-b8f8-f426dfe19d32/edit): rechts durchgängiger Schritt-Wizard mit Fortschritt, Prüfliste und »So liest es sich«-Klartext — weicht vom validierten Klick-Muster ab, passt aber zu Klartext-First + +### Abgleich Subframe vs. Spec (2026-07-10) + +- Beide Varianten halten die Grundanordnung Katalog · Canvas · Konfig ein (Katalog einklappbar, Undo/Redo, Auto-Anordnen, Zoom, Mini-Map vorhanden) +- Inline-Validierung zeigt nur »Pflichtfeld fehlt« und »Nicht verbunden« — Fall **»Schleifen-Verdacht« fehlt** am Baustein +- Kein **»Deaktivieren«** im Kopf (Spec: Aktivieren/Deaktivieren) — nur disabled »Aktivieren« +- D01 (Aktivieren-Blocker-Liste) und D02 (Anomalie-Banner) sind voll ausgebaut, obwohl laut Screen-Liste »Nur dokumentieren« — inhaltlich Spec-konform +- Kanten nur gerade vertikale Linien (linearer Flow, keine Verzweigung dargestellt) diff --git a/02 Wireframes/P12 Automationen/WF-P12 Flow-Editor/WF-P12 Flow-Editor.canvas b/02 Wireframes/P12 Automationen/WF-P12 Flow-Editor/WF-P12 Flow-Editor.canvas index 4ead7b7e..b1a87a3c 100644 --- a/02 Wireframes/P12 Automationen/WF-P12 Flow-Editor/WF-P12 Flow-Editor.canvas +++ b/02 Wireframes/P12 Automationen/WF-P12 Flow-Editor/WF-P12 Flow-Editor.canvas @@ -11,9 +11,30 @@ {"id":"note-embed","type":"text","text":"# Einbettung\n\nBaustein-Konfig-Panels (rechtes Panel) kommen aus WF-P12 Verwaltung (S05).\n\nAnordnung validiert: Katalog links · Canvas Mitte · Konfig rechts.","x":0,"y":560,"width":460,"height":240} ], "edges":[ - {"id":"e-entry","fromNode":"note-entry","fromSide":"right","toNode":"m03","toSide":"left","toEnd":"arrow","styleAttributes":{"path":"dotted"}}, + { + "id":"e-entry", + "fromNode":"note-entry", + "fromSide":"right", + "toNode":"m03", + "toSide":"left", + "toEnd":"arrow", + "styleAttributes":{"path":"dotted"} + }, {"id":"e-start","fromNode":"m03","fromSide":"right","toNode":"s01","toSide":"left","toEnd":"arrow","label":"leer oder Vorlage"}, {"id":"e-dryrun","fromNode":"s01","fromSide":"bottom","toNode":"m01","toSide":"top","toEnd":"arrow","label":"Probelauf / vor Aktivierung"}, - {"id":"e-loop","fromNode":"s01","fromSide":"bottom","toNode":"m02","toSide":"top","toEnd":"arrow","label":"bei Schleifen-Verdacht","styleAttributes":{"path":"long-dashed"}} - ] -} + { + "id":"e-loop", + "fromNode":"s01", + "fromSide":"bottom", + "toNode":"m02", + "toSide":"top", + "toEnd":"arrow", + "label":"bei Schleifen-Verdacht", + "styleAttributes":{"path":"long-dashed"} + } + ], + "metadata":{ + "version":"1.0-1.0", + "frontmatter":{} + } +} \ No newline at end of file diff --git a/02 Wireframes/P12 Automationen/WF-P12 Flow-Editor/WF-P12 Flow-Editor.md b/02 Wireframes/P12 Automationen/WF-P12 Flow-Editor/WF-P12 Flow-Editor.md index fda94352..761fb43f 100644 --- a/02 Wireframes/P12 Automationen/WF-P12 Flow-Editor/WF-P12 Flow-Editor.md +++ b/02 Wireframes/P12 Automationen/WF-P12 Flow-Editor/WF-P12 Flow-Editor.md @@ -5,16 +5,16 @@ ticket: D-FLOW package: WF-P12 Flow-Editor typ: Flow status: Validiert -subframe: "" +subframe: "https://app.subframe.com/f023064a5ca0/design/a42ecf81-5d6e-4fa1-9076-b22010cd5ecf/edit" canvas: "WF-P12 Flow-Editor.canvas" --- # WF-P12 Flow-Editor -Status: Validiert (Screen-Liste) -Subframe-Link: - +Status: Validiert (Screen-Liste), Subframe-Erstentwürfe vorhanden +Subframe-Link: Flow »(MCP) HiveGrid Workflow Engine / Automationen P12« — [S01 Variante A](https://app.subframe.com/f023064a5ca0/design/a42ecf81-5d6e-4fa1-9076-b22010cd5ecf/edit) · [S01 Variante B](https://app.subframe.com/f023064a5ca0/design/ce6a73d8-ae0c-4d1c-b8f8-f426dfe19d32/edit) · [M01](https://app.subframe.com/f023064a5ca0/design/ea231a78-eddb-4c04-97a8-7574d45d8576/edit) · [M02](https://app.subframe.com/f023064a5ca0/design/8e8943a5-36ea-4792-a3e0-590cb88fd9df/edit) · [M03](https://app.subframe.com/f023064a5ca0/design/8b398273-682e-4cf2-851b-3b1a89da4d90/edit) Canvas: [Canvas]() -Stand: 2026-07-09 +Stand: 2026-07-10 (Subframe-Abgleich, Links eingetragen) --- @@ -113,9 +113,12 @@ S01 bettet ein: Baustein-Konfig-Panels (WF-P12 Verwaltung S05) - Kanten-Interaktion (Klick-Verbinden vs. nur Ziehen) + Auto-Layout-Verhalten → Subframe-Iterationen - Dry-Run-Schwelle: Default-Wert → Dev +- **S01-Variante entscheiden** (Abgleich 2026-07-10): A »Dreispalten kompakt« (klassisches Konfig-Panel, näher an validierter Spec) vs. B »Geführter Inspector« (Schritt-Wizard rechts) — David +- M02: »Trotzdem aktivieren«-Button widerspricht Blockier-Regel der Fehlermatrix — David +- M03: doppelter CTA (»Verwenden« je Karte + »Mit Auswahl weiter« im Footer) — Interaktionsmodell klären ## Handoff-Check - [x] Jeder Screen ist mit Umsetzung markiert. - [x] Meldungs-/Error-State-Matrix ist ausgefüllt. -- [ ] Subframe-Link ist eingetragen. *(Subframe erst nach explizitem OK)* +- [x] Subframe-Link ist eingetragen. *(Erstentwürfe, Abgleich s. Screen-Dateien; Varianten-Entscheidung offen)* diff --git a/02 Wireframes/P12 Automationen/WF-P12 Verwaltung/Screens/S01 Automation-Liste.md b/02 Wireframes/P12 Automationen/WF-P12 Verwaltung/Screens/S01 Automation-Liste.md index e6c57bef..e1d88f64 100644 --- a/02 Wireframes/P12 Automationen/WF-P12 Verwaltung/Screens/S01 Automation-Liste.md +++ b/02 Wireframes/P12 Automationen/WF-P12 Verwaltung/Screens/S01 Automation-Liste.md @@ -9,7 +9,7 @@ typ: Liste umsetzung: Bauen status: Validiert route: "/automations" -subframe: "" +subframe: "https://app.subframe.com/f023064a5ca0/design/336ccdfb-80f9-45bf-a158-e74cabb70a20/edit" --- # S01 — Automation-Liste @@ -38,4 +38,11 @@ A2 Liste. ## Subframe -- (erst nach explizitem OK) +- [S01 Automation-Liste](https://app.subframe.com/f023064a5ca0/design/336ccdfb-80f9-45bf-a158-e74cabb70a20/edit) + +### Abgleich Subframe vs. Spec (2026-07-10) + +- Spec abgedeckt: Zeilen-Anatomie, »nur meine«-Filter, Anomalie-Banner, »+ Neue Automation«, Läufe-Zugang, »Als Vorlage speichern« +- Zusätzlich: KPI-Kacheln, Suchfeld, Status-/Auslöser-Filter, Spalten »Verantwortlich« + »Aktionen«, »Vorlagen«-Button +- **Copy-Bug**: Spalte »Läufe (30 Tage)« zeigt »143 Datensätze« statt Lauf-Anzahl +- Status-Pill hat zusätzlich Wert »Fehler« (Spec: nur aktiv/pausiert/Entwurf) diff --git a/02 Wireframes/P12 Automationen/WF-P12 Verwaltung/Screens/S02 Lauf-Liste.md b/02 Wireframes/P12 Automationen/WF-P12 Verwaltung/Screens/S02 Lauf-Liste.md index 510b03fb..a86f60dd 100644 --- a/02 Wireframes/P12 Automationen/WF-P12 Verwaltung/Screens/S02 Lauf-Liste.md +++ b/02 Wireframes/P12 Automationen/WF-P12 Verwaltung/Screens/S02 Lauf-Liste.md @@ -9,7 +9,7 @@ typ: Liste umsetzung: Bauen status: Validiert route: "/automations/:id/executions" -subframe: "" +subframe: "https://app.subframe.com/f023064a5ca0/design/0fab5d8c-1e6a-4220-863d-7e46aba0f06c/edit" --- # S02 — Lauf-Liste @@ -37,4 +37,10 @@ A2 Liste. ## Subframe -- (erst nach explizitem OK) +- [S02 Lauf-Liste](https://app.subframe.com/f023064a5ca0/design/0fab5d8c-1e6a-4220-863d-7e46aba0f06c/edit) + +### Abgleich Subframe vs. Spec (2026-07-10) + +- Spec abgedeckt: Zeit/Status/Klartext-Summary/Probelauf-Marker/Dauer, alle drei Filter, Zeile → Detail +- Zusätzlich: StatsCard-Reihe, Suchfeld, »Nur Probeläufe«-Switch, Spalte »Betroffene Datensätze«, Status »Läuft«, Pagination +- **Ohne DefaultPageLayout** (App-Sidebar fehlt — anders als S01) diff --git a/02 Wireframes/P12 Automationen/WF-P12 Verwaltung/Screens/S03 Lauf-Detail.md b/02 Wireframes/P12 Automationen/WF-P12 Verwaltung/Screens/S03 Lauf-Detail.md index b2bc911e..ebddec60 100644 --- a/02 Wireframes/P12 Automationen/WF-P12 Verwaltung/Screens/S03 Lauf-Detail.md +++ b/02 Wireframes/P12 Automationen/WF-P12 Verwaltung/Screens/S03 Lauf-Detail.md @@ -9,7 +9,7 @@ typ: Detail + Timeline umsetzung: Bauen status: Validiert route: "/automations/:id/executions/:exec_id" -subframe: "" +subframe: "https://app.subframe.com/f023064a5ca0/design/9997a3c4-d18b-449f-bbbd-4fe6dd24f3df/edit" --- # S03 — Lauf-Detail @@ -39,4 +39,12 @@ A3 + Timeline. ## Subframe -- (erst nach explizitem OK) +- [S03 Lauf-Detail](https://app.subframe.com/f023064a5ca0/design/9997a3c4-d18b-449f-bbbd-4fe6dd24f3df/edit) + +### Abgleich Subframe vs. Spec (2026-07-10) + +- Spec vollständig abgedeckt: lesbare Auslöser-Daten, Probelauf-Marker, Klartext-Timeline mit Input/Output, 24-h-Countdown, Retry + »ab Schritt X fortsetzen« +- Zusätzlich: rechte Sidebar (Laufdaten/Ergebnis/»Aktion erforderlich«), Quick-Fix »Person zuweisen« +- Redundanz: Countdown doppelt (Banner + Sidebar) plus dritter »Rückgängig«-Header-Button — straffen +- **Demo-Widerspruch**: Lauf als »Probelauf« markiert, bietet aber Rückgängig an (Probeläufe schreiben laut S02-Copy nichts) +- Ohne DefaultPageLayout diff --git a/02 Wireframes/P12 Automationen/WF-P12 Verwaltung/Screens/S04 Vorlagen-Katalog.md b/02 Wireframes/P12 Automationen/WF-P12 Verwaltung/Screens/S04 Vorlagen-Katalog.md index c00ac5d8..fe37ad89 100644 --- a/02 Wireframes/P12 Automationen/WF-P12 Verwaltung/Screens/S04 Vorlagen-Katalog.md +++ b/02 Wireframes/P12 Automationen/WF-P12 Verwaltung/Screens/S04 Vorlagen-Katalog.md @@ -9,7 +9,7 @@ typ: Liste/Karten umsetzung: Bauen status: Validiert route: "/automations/templates" -subframe: "" +subframe: "https://app.subframe.com/f023064a5ca0/design/46e04956-4903-4733-9345-81b18f34e858/edit" --- # S04 — Vorlagen-Katalog @@ -36,4 +36,11 @@ A2 Karten-Liste. ## Subframe -- (erst nach explizitem OK) +- [S04 Vorlagen-Katalog](https://app.subframe.com/f023064a5ca0/design/46e04956-4903-4733-9345-81b18f34e858/edit) + +### Abgleich Subframe vs. Spec (2026-07-10) + +- Spec abgedeckt: Karten nach Kategorie mit Name/Beschreibung/Nutzungs-Anzahl/»Verwenden«, SHK + eigene unterscheidbar +- Copy-Abweichung: Button heißt »Eigene Vorlage erstellen« statt »aus Automation erstellen« +- Zusätzlich: Suchfeld, Modul-/Kategorie-Filter, »Beliebt«-Tab, Flow-Schritt-Chips, Hinweise »Variable/Verbindung erforderlich« +- Einbettung in Start-Auswahl (M03) nur dort dargestellt, hier eigenständige Seite · Ohne DefaultPageLayout diff --git a/02 Wireframes/P12 Automationen/WF-P12 Verwaltung/Screens/S05 Baustein-Konfig-Panels.md b/02 Wireframes/P12 Automationen/WF-P12 Verwaltung/Screens/S05 Baustein-Konfig-Panels.md index 0cf02e52..ff475f2f 100644 --- a/02 Wireframes/P12 Automationen/WF-P12 Verwaltung/Screens/S05 Baustein-Konfig-Panels.md +++ b/02 Wireframes/P12 Automationen/WF-P12 Verwaltung/Screens/S05 Baustein-Konfig-Panels.md @@ -9,7 +9,7 @@ typ: Compose-Panel (eingebettet) umsetzung: Bauen status: Validiert route: "eingebettet im Flow-Editor (rechtes Panel)" -subframe: "" +subframe: "https://app.subframe.com/f023064a5ca0/design/83599a34-97b4-4881-beb4-8b7e35d11aa0/edit" --- # S05 — Baustein-Konfig-Panels @@ -37,8 +37,16 @@ Compose — Formular-Bausteine (A4) im Editor-Panel. ## Subframe -- (erst nach explizitem OK) +- [S05 Baustein-Konfig-Panels](https://app.subframe.com/f023064a5ca0/design/83599a34-97b4-4881-beb4-8b7e35d11aa0/edit) + +### Abgleich Subframe vs. Spec (2026-07-10) + +- Spec abgedeckt: genau die beiden Muster-Panels (Auslöser »Neue Anfrage« + Aktion »Aufgabe anlegen«), Klartext-Labels, Inline-Validierung mit Badge am Baustein +- Strukturabweichung: beide Panels untereinander auf einer Seite statt je Baustein; ToggleGroup Auslöser/Aktion ohne aktiven Wert (Code-Fehler) +- Zusätzlich: Beispielkontakt-Vorschau, »So liest es sich«-Zusammenfassung, »Kontakt verknüpfen«-Switch +- Tippfehler in Copy: »Bauhow-Analyse« ## Notizen - KWP-Push: nur Registry-Eintrag, kein Konfig-UI (v1-Grenze). +- Vorplanung aller 31 Panels (Archetypen + Feld-Katalog): [[S05a Baustein-Konfig Vorplanung]] (2026-07-11). diff --git a/02 Wireframes/P12 Automationen/WF-P12 Verwaltung/Screens/S05a Baustein-Konfig Vorplanung.md b/02 Wireframes/P12 Automationen/WF-P12 Verwaltung/Screens/S05a Baustein-Konfig Vorplanung.md new file mode 100644 index 00000000..13ac7ea5 --- /dev/null +++ b/02 Wireframes/P12 Automationen/WF-P12 Verwaltung/Screens/S05a Baustein-Konfig Vorplanung.md @@ -0,0 +1,140 @@ +--- +kind: screen +project: HiveGrid Screen Project +ticket: D-P12 +package: WF-P12 Verwaltung +id: S05a +screen: Baustein-Konfig Vorplanung (Katalog aller Panels) +typ: Vorplanung / Katalog +umsetzung: Planen +status: Subframe Draft / Review offen +route: "Modal über dem Flow-Editor (Dialog-Komponente, Klick auf Baustein)" +--- + +# S05a — Baustein-Konfig Vorplanung + +Vorplanung für **alle 31 Bausteine** aus der Baustein-Bibliothek (Stand Planung 2026-07-11, Screenshot-Abgleich). +Ziel: je Baustein ein eigenes Konfig-Panel in Subframe ablegen, das später einzeln angepasst wird. +Statt 31 Panels von null zu bauen: **9 Muster-Archetypen** definieren, je Archetyp ein Muster-Panel bauen, die restlichen Panels als Kopien mit angepassten Feldern ableiten. + +Verwandt: [[S05 Baustein-Konfig-Panels]] (die zwei existierenden Muster), [[S01 Flow-Editor]]. + +--- + +## Farbschema der Kategorien (aus der Planung) + +| Kategorie | Farbe | Anzahl | +|---|---|---| +| Auslöser | Grün | 16 | +| Aktionen: Daten & CRM | Rot | 2 | +| Aktionen: Kommunikation | Blau | 1 | +| Aktionen: Aufgaben | Gelb | 2 | +| Logik | Violett | 8 | +| Integration | Grau | 2 | + +> ⚠️ Abweichung: Die Subframe-Entwürfe von Baustein-Picker/Konfig (2026-07-11) nutzen Orange für Auslöser und Blau für alle Aktionen. Vor dem Weiterbauen auf dieses Schema (Grün/Rot/Blau/Gelb/Violett/Grau) vereinheitlichen. + +--- + +## Archetypen (Muster-Panels) + +| # | Archetyp | Grundaufbau des Panels | +|---|---|---| +| T1 | Datensatz-Auslöser | Filter-Abschnitt: welche Datensätze zählen (Quelle, Pipeline, Feld). Kein Pflichtfeld nötig — läuft auch ungefiltert. | +| T2 | Zeit-/Termin-Auslöser | Zeitbezug: Vorlauf (»3 Tage vorher«), Terminart, bzw. Wiederholungsregel beim Zeitplan. | +| T3 | Eingangs-Auslöser (E-Mail/Formular/Webhook) | Quelle wählen (Postfach, Formular, Webhook-URL anzeigen) + Vorschau der ankommenden Daten (Payload-Felder als Chips für spätere Schritte). | +| A1 | Datensatz-Aktion | Ziel-Datensatz wählen + Feld-Zuordnung (welches Feld bekommt welchen Wert, mit Platzhalter-Chips). | +| A2 | Kommunikations-Aktion | Kanal (E-Mail/Push/intern), Empfänger, Betreff/Text mit Platzhalter-Chips. | +| A3 | Aufgaben-Aktion | Muster existiert (S05): Titel, Verantwortlich (Pflicht), Fälligkeit, Priorität, Notiz. | +| L1 | Bedingungs-Logik | Bedingungszeilen: Feld · Operator · Wert (+ Zeile hinzufügen). Klartext-Zusammenfassung. | +| L2 | Ablauf-Logik | Ein Hauptwert: Dauer (Warten), Liste (Schleife), Variable + Wert (Wert setzen). | +| I1 | Integration | Technischer: URL, Methode, Kopfzeilen/Payload — bewusst als »Profi-Baustein« gekennzeichnet. | + +Jedes Panel hat einheitlich: Kopf (Kategorie-farbiges Icon, Name, Klartext-Untertitel, Schließen), Suchfeld entfällt, Pflichtfeld-Validierung inline + Badge am Baustein, »So liest es sich«-Satz unten, Fuß mit Übernehmen/Abbrechen. + +--- + +## Katalog: alle 31 Panels + +### Auslöser (Grün) + +| Baustein | Kurzbeschreibung (Karte) | Ausgänge | Archetyp | Konfig-Felder (Panel) | +|---|---|---|---|---| +| Neuer Kontakt | Wenn ein Kontakt angelegt wird | 1 | T1 | Quelle (alle/manuell/Import/Formular), optional Tag-Filter | +| Kontakt geändert | Wenn sich Kontaktdaten ändern | 1 | T1 | Welche Felder beobachten (alle oder Auswahl) | +| Neue Anfrage | Wenn eine Anfrage eingeht | 1 | T1 | Quelle (Website-Formular/E-Mail/manuell), optional Bereich | +| Lead qualifiziert | Wenn ein Lead qualifiziert wird | 1 | T1 | Pipeline, Qualifizierungs-Stufe | +| Projekt gewonnen | Wenn ein Projekt gewonnen wird | 1 | T1 | Pipeline/Board, optional Mindestwert | +| Status gewechselt | Wenn eine Karte die Spalte wechselt | 1 | T1 | Board, von Spalte (optional), nach Spalte | +| Projekt angelegt | Wenn ein Projekt erstellt wird | 1 | T1 | Projektart/Board, optional Quelle | +| Deadline naht | Wenn eine Frist bald abläuft | 1 | T2 | Fristtyp (Aufgabe/Projekt/Zahlungsziel), Vorlauf (z. B. 3 Tage vorher) | +| E-Mail eingegangen | Wenn eine E-Mail ankommt | 1 | T3 | Postfach, optional Betreff-/Absender-Filter | +| E-Mail unzustellbar | Wenn eine Kampagnen-Mail bounct | 1 | T3 | Kampagne (alle oder Auswahl), Bounce-Art (hart/weich) | +| Formular abgeschickt | Wenn ein Formular eingeht | 1 | T3 | Formular wählen (aus D-FORM), Felder-Vorschau als Chips | +| Termin gebucht | Wenn ein Termin gebucht wird | 1 | T2 | Terminart/Kalender, optional Mitarbeiter | +| Termin storniert | Wenn ein Termin abgesagt wird | 1 | T2 | Terminart/Kalender, Storno durch (Kunde/intern/beide) | +| Ereignis erstellt | Wenn ein Kalender-Eintrag entsteht | 1 | T2 | Kalender, Ereignisart | +| Zeitplan | Zu festen Zeiten, z. B. montags 8 Uhr | 1 | T2 | Wiederholung (täglich/wöchentlich/monatlich), Wochentag(e), Uhrzeit | +| Webhook empfangen | Wenn ein externes System ruft | 1 | T3 | Webhook-URL (generiert, kopierbar), Secret, Beispiel-Payload-Vorschau | + +### Aktionen: Daten & CRM (Rot) + +| Baustein | Kurzbeschreibung | Ausgänge | Archetyp | Konfig-Felder | +|---|---|---|---|---| +| Kontakt ändern | Datensatz aktualisieren | 1 | A1 | Welcher Kontakt (aus Auslöser/suchen), Feld-Zuordnungen (Feld → neuer Wert, Platzhalter) | +| Notiz anlegen | Notiz am Datensatz speichern | 1 | A1 | Ziel-Datensatz, Notiztext (Platzhalter), optional sichtbar für | + +### Aktionen: Kommunikation (Blau) + +| Baustein | Kurzbeschreibung | Ausgänge | Archetyp | Konfig-Felder | +|---|---|---|---|---| +| Benachrichtigung senden | E-Mail, Push oder intern | 1 | A2 | Kanal (Pflicht), Empfänger (Person/Team/Kontakt aus Auslöser), Betreff, Text mit Platzhalter-Chips | + +### Aktionen: Aufgaben (Gelb) + +| Baustein | Kurzbeschreibung | Ausgänge | Archetyp | Konfig-Felder | +|---|---|---|---|---| +| Aufgabe anlegen | Neue Aufgabe erstellen | 1 | A3 | Titel (Platzhalter), Verantwortlich (Pflicht), Fälligkeit, Priorität, Notiz — Muster existiert | +| Aufgabe zuweisen | Person oder Team zuordnen | 1 | A3 | Welche Aufgabe (aus vorherigem Schritt), neue Person/Team (Pflicht), optional Benachrichtigung an | + +### Logik (Violett) + +| Baustein | Kurzbeschreibung | Ausgänge | Archetyp | Konfig-Felder | +|---|---|---|---|---| +| Wenn-Dann | Verzweigt nach Bedingung | Ja / Nein | L1 | Bedingungszeilen (Feld · Operator · Wert) | +| Oder | Wenn mind. eine Bedingung zutrifft | Ja / Nein | L1 | Bedingungszeilen, Hinweis »eine reicht« | +| Und | Wenn alle Bedingungen zutreffen | Ja / Nein | L1 | Bedingungszeilen, Hinweis »alle nötig« | +| Switch | Mehrere Pfade verzweigen | 1 / 2 / 3 … | L1 | Feld wählen + je Pfad ein Wert (+ Pfad hinzufügen, Sonst-Pfad) | +| Filter | Datensätze filtern | 1 | L1 | Bedingungszeilen; Klartext »nur weiter, wenn …« | +| Warten | Pausiert, z. B. 3 Tage | 1 | L2 | Dauer (Zahl + Einheit) oder bis Datum/Uhrzeit aus Feld | +| Schleife | Für jedes Element wiederholen | Weiter / Fertig | L2 | Liste wählen (z. B. alle Aufgaben des Projekts), max. Durchläufe | +| Wert setzen | Variable setzen oder lesen | 1 | L2 | Variablenname, Wert (Platzhalter erlaubt) | + +### Integration (Grau) + +| Baustein | Kurzbeschreibung | Ausgänge | Archetyp | Konfig-Felder | +|---|---|---|---|---| +| HTTP-Anfrage | Externen Dienst aufrufen | 1 | I1 | URL (Pflicht), Methode, Kopfzeilen, Payload (Platzhalter), Antwort-Vorschau | +| An KWP übergeben | Projekt an KWP senden | 1 | — | **ab v2** — nur Registry-Eintrag, kein Konfig-UI (v1-Grenze, s. S05-Notiz) | + +--- + +## Vorgehen für Subframe + +1. Farbschema im Picker/Konfig-Entwurf auf das Planungs-Schema angleichen (Grün/Rot/Blau/Gelb/Violett/Grau). +2. Je Archetyp **ein** Muster-Panel als eigene Subframe-Seite bauen (9 Seiten; A3 existiert als Basis aus der Konfig-Runde 2026-07-11). +3. Danach die restlichen 21 v1-Panels als Kopien der Muster-Seiten ablegen und Felder gemäß Katalog oben anpassen (30 Seiten gesamt, KWP entfällt in v1). +4. Benennung in Subframe: »K-T1 Neuer Kontakt«, »K-A2 Benachrichtigung senden« … (K = Konfig-Panel), eigener Flow oder Gruppe, damit der Editor-Flow übersichtlich bleibt. + +## Umsetzungsstand Subframe (2026-07-12) + +- **Entscheidung (David, 2026-07-12): Konfig als MODAL** (Dialog-Komponente), nicht rechtes Panel/Drawer — n8n-Muster: Klick auf Baustein öffnet Dialog. +- **Alle 30 v1-Panels gebaut** (Nacht-Build 02:37–03:00 Uhr, KWP entfällt v1-gemäß): Benennung »K- « im Flow [(MCP) HiveGrid Workflow Engine / Automationen P12](https://app.subframe.com/f023064a5ca0/flow/22d511a9-1d8f-4554-89ad-86729b8d7738/edit). +- Muster/Blaupause: **K-T1 Neuer Kontakt** (Dialog.Content ohne Overrides = 560 px/16 px/24 px laut Layout-Regeln, Labels + Helptexte über Komponenten-Props, jede Feld-Erklärung als helpText, Erklärungs-Box mit Fragezeichen in Kategoriefarbe, Fuß rechts: Abbrechen/Übernehmen). Größere Panels (Kontakt ändern, Benachrichtigung, Aufgabe anlegen, Wenn-Dann, Switch, HTTP) mit `size="medium"` = 720 px. +- Readback-Check aller 30 Seiten bestanden (Dialog.Content, keine p-0/gap-0-Overrides, helpTexts, Erklärungs-Box). **Händisches Review durch David steht aus.** + +## Offene Fragen + +- Ausgänge-Darstellung im Panel (zeigt das Panel die Ausgänge Ja/Nein bzw. 1/2/3 als Info an?) — David +- Switch: wie viele Pfade maximal in v1? +- Webhook/HTTP: wie viel Technik zeigen wir Handwerkern (Profi-Kennzeichnung reicht?) diff --git a/02 Wireframes/P12 Automationen/WF-P12 Verwaltung/WF-P12 Verwaltung.canvas b/02 Wireframes/P12 Automationen/WF-P12 Verwaltung/WF-P12 Verwaltung.canvas index 46dd36a4..3450033a 100644 --- a/02 Wireframes/P12 Automationen/WF-P12 Verwaltung/WF-P12 Verwaltung.canvas +++ b/02 Wireframes/P12 Automationen/WF-P12 Verwaltung/WF-P12 Verwaltung.canvas @@ -1,8 +1,8 @@ { "nodes":[ - {"id":"md-paket","type":"file","file":"02 Wireframes/P12 Automationen/WF-P12 Verwaltung/WF-P12 Verwaltung.md","x":-80,"y":-440,"width":460,"height":280}, {"id":"group-verw","type":"group","x":-80,"y":-100,"width":2500,"height":380,"label":"Übersicht und Verläufe"}, {"id":"group-extra","type":"group","x":-80,"y":460,"width":1820,"height":380,"label":"Vorlagen + Konfig-Panels"}, + {"id":"md-paket","type":"file","file":"02 Wireframes/P12 Automationen/WF-P12 Verwaltung/WF-P12 Verwaltung.md","x":-80,"y":-440,"width":460,"height":280}, {"id":"note-entry","type":"text","text":"# Einstieg\n\nSidebar → Automation.","x":0,"y":0,"width":300,"height":200}, {"id":"s01","type":"file","file":"02 Wireframes/P12 Automationen/WF-P12 Verwaltung/Screens/S01 Automation-Liste.md","x":500,"y":0,"width":420,"height":200}, {"id":"s02","type":"file","file":"02 Wireframes/P12 Automationen/WF-P12 Verwaltung/Screens/S02 Lauf-Liste.md","x":1140,"y":0,"width":420,"height":200}, @@ -14,24 +14,22 @@ "edges":[ { "id":"e-entry", + "styleAttributes":{"path":"dotted"}, "fromNode":"note-entry", "fromSide":"right", "toNode":"s01", - "toSide":"left", - "toEnd":"arrow", - "styleAttributes":{"path":"dotted"} + "toSide":"left" }, - {"id":"e-runs","fromNode":"s01","fromSide":"right","toNode":"s02","toSide":"left","toEnd":"arrow","label":"Läufe"}, - {"id":"e-detail","fromNode":"s02","fromSide":"right","toNode":"s03","toSide":"left","toEnd":"arrow","label":"Lauf öffnen"}, + {"id":"e-runs","fromNode":"s01","fromSide":"right","toNode":"s02","toSide":"left","label":"Läufe"}, + {"id":"e-detail","fromNode":"s02","fromSide":"right","toNode":"s03","toSide":"left","label":"Lauf öffnen"}, { "id":"e-template", + "styleAttributes":{"path":"long-dashed"}, "fromNode":"s01", "fromSide":"bottom", "toNode":"s04", "toSide":"top", - "toEnd":"arrow", - "label":"als Vorlage speichern", - "styleAttributes":{"path":"long-dashed"} + "label":"als Vorlage speichern" } ], "metadata":{ diff --git a/02 Wireframes/P12 Automationen/WF-P12 Verwaltung/WF-P12 Verwaltung.md b/02 Wireframes/P12 Automationen/WF-P12 Verwaltung/WF-P12 Verwaltung.md index c9c212e8..5b595fa1 100644 --- a/02 Wireframes/P12 Automationen/WF-P12 Verwaltung/WF-P12 Verwaltung.md +++ b/02 Wireframes/P12 Automationen/WF-P12 Verwaltung/WF-P12 Verwaltung.md @@ -5,16 +5,16 @@ ticket: D-P12 package: WF-P12 Verwaltung typ: Flow status: Validiert -subframe: "" +subframe: "https://app.subframe.com/f023064a5ca0/design/336ccdfb-80f9-45bf-a158-e74cabb70a20/edit" canvas: "WF-P12 Verwaltung.canvas" --- # WF-P12 Verwaltung -Status: Validiert (Screen-Liste) -Subframe-Link: - +Status: Validiert (Screen-Liste), Subframe-Erstentwürfe vorhanden +Subframe-Link: Flow »(MCP) HiveGrid Workflow Engine / Automationen P12« — [S01](https://app.subframe.com/f023064a5ca0/design/336ccdfb-80f9-45bf-a158-e74cabb70a20/edit) · [S02](https://app.subframe.com/f023064a5ca0/design/0fab5d8c-1e6a-4220-863d-7e46aba0f06c/edit) · [S03](https://app.subframe.com/f023064a5ca0/design/9997a3c4-d18b-449f-bbbd-4fe6dd24f3df/edit) · [S04](https://app.subframe.com/f023064a5ca0/design/46e04956-4903-4733-9345-81b18f34e858/edit) · [S05](https://app.subframe.com/f023064a5ca0/design/83599a34-97b4-4881-beb4-8b7e35d11aa0/edit) Canvas: [Canvas]() -Stand: 2026-07-09 +Stand: 2026-07-10 (Subframe-Abgleich, Links eingetragen) --- @@ -101,10 +101,13 @@ S05 (Konfig-Panels) -> eingebettet in Flow-Editor S01 ## 10. Offene Fragen -- Welche zwei Muster-Panels für S05 (Vorschlag: Auslöser »Neue Anfrage« + Aktion »Aufgabe anlegen«) → beim Subframe-Bau +- ~~Welche zwei Muster-Panels für S05~~ → umgesetzt wie vorgeschlagen: Auslöser »Neue Anfrage« + Aktion »Aufgabe anlegen« (Abgleich 2026-07-10) +- S02–S05 stehen in Subframe ohne DefaultPageLayout (App-Shell) — vereinheitlichen? +- S01: Copy-Bug Spalte »Läufe (30 Tage)« zeigt »143 Datensätze« → fixen +- S03: Demo-Widerspruch Probelauf-Marker + Rückgängig-Angebot → Demo-Daten korrigieren ## Handoff-Check - [x] Jeder Screen ist mit Umsetzung markiert. - [x] Meldungs-/Error-State-Matrix ist ausgefüllt. -- [ ] Subframe-Link ist eingetragen. *(Subframe erst nach explizitem OK)* +- [x] Subframe-Link ist eingetragen. *(Erstentwürfe, Abgleich s. Screen-Dateien)* diff --git a/02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/Screens/M01 Neuer Channel DM.md b/02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/Screens/M01 Neuer Channel DM.md new file mode 100644 index 00000000..f2e37b90 --- /dev/null +++ b/02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/Screens/M01 Neuer Channel DM.md @@ -0,0 +1,37 @@ +--- +kind: screen +project: HiveGrid Screen Project +ticket: D-CHAT + AK-P15b +package: WF-P15 Team-Chat +id: M01 +screen: Neuer Channel / DM +typ: Modal +umsetzung: Bauen +status: Entwurf +route: "aus S01/S02" +subframe: "" +--- + +# M01 — Neuer Channel / DM + +## Ziel + +Ein neuer Gruppen-Channel oder eine Direktnachricht ist schnell erstellt, mit klarer Privatsphäre. + +## Was das Tool können muss + +- Umschalter: Gruppe / Direktnachricht +- Gruppe: Name, Beschreibung, öffentlich/privat, Mitglieder wählen +- DM: eine oder mehrere Personen suchen; bestehende DM erkennen und öffnen statt Duplikat +- Optional: Projekt verknüpfen nur, wenn aus Projekt-Kontext geöffnet +- Vorschau, wer Zugriff bekommt +- Validierung: Name fehlt, private Gruppe ohne Mitglieder, DM mit sich selbst, fehlende Berechtigung +- Nach Anlegen direkt zu S02 + +## Pattern / Archetyp + +A5 Modal. + +## Subframe + +- (erst nach explizitem OK) diff --git a/02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/Screens/M02 DM-Audit-Begruendung.md b/02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/Screens/M02 DM-Audit-Begruendung.md new file mode 100644 index 00000000..fcff5bc5 --- /dev/null +++ b/02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/Screens/M02 DM-Audit-Begruendung.md @@ -0,0 +1,37 @@ +--- +kind: screen +project: HiveGrid Screen Project +ticket: D-CHAT + AK-P15b +package: WF-P15 Team-Chat +id: M02 +screen: DM-Audit-Begründung +typ: Modal +umsetzung: Bauen +status: Entwurf +route: "aus S08" +subframe: "" +--- + +# M02 — DM-Audit-Begründung + +## Ziel + +Admin-Einsicht in Direktnachrichten ist eine Ausnahmehandlung, nicht normales Browsing. + +## Was das Tool können muss + +- Zeigt betroffene DM, Mitglieder und Zweck der Einsicht +- Pflichtfeld: Begründung +- Hinweis: Einsicht wird audit-geloggt und im DM sichtbar markiert +- Optional: Session-Dauer anzeigen +- Bestätigen öffnet S02 mit Admin-Audit-Banner +- Abbrechen zurück zu S08 +- Validierung: Begründung fehlt/zu kurz, keine `read_all_channels`-Berechtigung + +## Pattern / Archetyp + +A5 Confirm-/Reason-Modal. + +## Subframe + +- (erst nach explizitem OK) diff --git a/02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/Screens/S01 Chat-Index.md b/02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/Screens/S01 Chat-Index.md new file mode 100644 index 00000000..9ab40389 --- /dev/null +++ b/02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/Screens/S01 Chat-Index.md @@ -0,0 +1,40 @@ +--- +kind: screen +project: HiveGrid Screen Project +ticket: D-CHAT + AK-P15b +package: WF-P15 Team-Chat +id: S01 +screen: Chat-Index +typ: BESPOKE +umsetzung: Bauen +status: Entwurf +route: "/chat" +subframe: "" +--- + +# S01 — Chat-Index + +## Ziel + +Der Nutzer sieht sofort, wo etwas Neues passiert ist: ungelesene Channels, @Mentions, Projekt-Chats und DMs. + +## Was das Tool können muss + +- Channel-Liste mit Name, Avatar/Typ, Snippet, Zeitstempel, Unread-Badge, Mention-Marker, Pin-/Mute-Indikator +- Typen: Projekt-Channel, öffentliche Gruppe, private Gruppe, DM +- Projekt-Channels zeigen Projekt-Status/Stage und Link zum Projekt +- Filter: Alle, Ungelesen, Mentions, Projekt, Gruppen, DMs, Archiviert +- Suche innerhalb der Channel-Liste + Link zur globalen Suche S06 +- Channel-Browser/Discover für öffentliche Gruppen +- Schnell-Aktionen je Zeile: anpinnen, stummschalten, als gelesen markieren, archivieren/verlassen je Berechtigung +- CTA: Neuer Channel / DM → M01 +- Link: Meine Mentions → S05 +- Zustände: keine Channels, nur archivierte Channels, laden, offline/reconnect, keine Berechtigung + +## Pattern / Archetyp + +BESPOKE-Chat-Index; kompakter, scannbarer Arbeitsbereich statt Marketing-Layout. + +## Subframe + +- (erst nach explizitem OK) diff --git a/02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/Screens/S02 Channel-Detail.md b/02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/Screens/S02 Channel-Detail.md new file mode 100644 index 00000000..aa51e78e --- /dev/null +++ b/02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/Screens/S02 Channel-Detail.md @@ -0,0 +1,41 @@ +--- +kind: screen +project: HiveGrid Screen Project +ticket: D-CHAT + AK-P15b +package: WF-P15 Team-Chat +id: S02 +screen: Channel-Detail +typ: BESPOKE +umsetzung: Bauen +status: Entwurf +route: "/chat/:id" +subframe: "" +--- + +# S02 — Channel-Detail + +## Ziel + +Hier wird wirklich gearbeitet: Nachrichten lesen, antworten, Dateien teilen, Threads öffnen, Aufgaben erzeugen. + +## Was das Tool können muss + +- Header: Channel-Name, Typ, Member-Anzahl, Projekt-Link falls Projekt-Channel, Suche, Settings-Link S07 +- Pinned-Preview: max. 3 angepinnte Nachrichten inline + »alle anzeigen« → S04 +- Verlauf mit Datums-Trennern und System-Messages +- Nachricht: Avatar, Name, Zeit, Markdown-Body, Mentions, Anhänge, Link-Preview, Edited-/Lösch-Marker +- Reactions: Emoji + Anzahl + wer reagiert hat +- Thread-Indikator: Anzahl Antworten, letzter Reply, Klick → S03 +- Read-Receipt: »X von Y gelesen«; DM zeigt gelesen/zugestellt kompakt +- Composer: Markdown, Emoji, @Mention-Autocomplete, Datei-Dropzone, Foto/Anhang, Draft-Autosave +- Typing-Indikator und Reconnect-Banner +- Per-Message-Menü: reagieren, Thread, pinnen, als Aufgabe markieren (WF-P4 M01), Link kopieren, bearbeiten, löschen +- Rechte/States: read-only archiviert, muted, reconnect, Upload-Fehler, pending message, keine Berechtigung + +## Pattern / Archetyp + +BESPOKE Real-Time-Chat. Stable Composer und Verlauf ohne Layoutsprünge sind wichtiger als dekorative Flächen. + +## Subframe + +- (erst nach explizitem OK) diff --git a/02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/Screens/S03 Thread-Detail.md b/02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/Screens/S03 Thread-Detail.md new file mode 100644 index 00000000..1a691e4d --- /dev/null +++ b/02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/Screens/S03 Thread-Detail.md @@ -0,0 +1,37 @@ +--- +kind: screen +project: HiveGrid Screen Project +ticket: D-CHAT + AK-P15b +package: WF-P15 Team-Chat +id: S03 +screen: Thread-Detail +typ: BESPOKE Panel/Route +umsetzung: Bauen +status: Entwurf +route: "/chat/:id/thread/:thread_id" +subframe: "" +--- + +# S03 — Thread-Detail + +## Ziel + +Eine Nebendiskussion bleibt bei der Ausgangsnachricht, ohne den Hauptverlauf zu überfrachten. + +## Was das Tool können muss + +- Eltern-Nachricht oben vollständig anzeigen (inkl. Anhänge/Reactions) +- Replies chronologisch, mit denselben Message-Elementen wie S02 +- Eigener Composer für Replies mit @Mentions, Emoji, Anhängen, Draft +- Link zurück zum Kontext im Channel +- Aktionen: reagieren, pinnen, als Aufgabe markieren, Link kopieren, bearbeiten/löschen je Recht +- Read-Receipt/Seen-Info für Thread-Replies +- Zustände: Thread gelöscht, Ausgangsnachricht gelöscht, keine Berechtigung, reconnect, leerer Thread + +## Pattern / Archetyp + +BESPOKE-Teilfläche. Kann als Side-Panel oder eigene Route gebaut werden; für die Planung eigener Nachweis-Frame. + +## Subframe + +- (erst nach explizitem OK) diff --git a/02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/Screens/S04 Angepinnte Nachrichten.md b/02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/Screens/S04 Angepinnte Nachrichten.md new file mode 100644 index 00000000..bd2bd157 --- /dev/null +++ b/02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/Screens/S04 Angepinnte Nachrichten.md @@ -0,0 +1,36 @@ +--- +kind: screen +project: HiveGrid Screen Project +ticket: D-CHAT + AK-P15b +package: WF-P15 Team-Chat +id: S04 +screen: Angepinnte Nachrichten +typ: A2 Liste +umsetzung: Bauen +status: Entwurf +route: "/chat/:id/pinned" +subframe: "" +--- + +# S04 — Angepinnte Nachrichten + +## Ziel + +Wichtige Entscheidungen, Dateien und Hinweise im Channel sind schnell wieder auffindbar. + +## Was das Tool können muss + +- Liste aller Pins im Channel, max. 50 +- Zeile: Nachrichtenausschnitt, Autor, Datum, Pin-Ersteller, Anhänge, Link zum Kontext +- Filter/Suche innerhalb der Pins +- Aktionen: entpinnen, Link kopieren, als Aufgabe markieren +- Hinweis, wenn Pin-Limit erreicht ist +- Zustände: keine Pins, Nachricht gelöscht, keine Berechtigung + +## Pattern / Archetyp + +A2 Liste als Nebenfläche aus dem Channel. + +## Subframe + +- (erst nach explizitem OK) diff --git a/02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/Screens/S05 Meine Mentions.md b/02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/Screens/S05 Meine Mentions.md new file mode 100644 index 00000000..d60302cc --- /dev/null +++ b/02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/Screens/S05 Meine Mentions.md @@ -0,0 +1,36 @@ +--- +kind: screen +project: HiveGrid Screen Project +ticket: D-CHAT + AK-P15b +package: WF-P15 Team-Chat +id: S05 +screen: Meine Mentions +typ: A2 Liste +umsetzung: Bauen +status: Entwurf +route: "/chat/mentions" +subframe: "" +--- + +# S05 — Meine Mentions + +## Ziel + +Alle @Erwähnungen über Channels hinweg an einem Ort, damit nichts untergeht. + +## Was das Tool können muss + +- Liste kanalübergreifender Mentions an mich +- Spalten/Zeile: Channel, Projekt falls vorhanden, Nachrichtenausschnitt, Autor, Zeitpunkt, gelesen/ungelesen +- Filter: ungelesen, Projekt, Gruppe, DM, Zeitraum +- Aktionen: öffnen im Kontext, als gelesen markieren, als Aufgabe markieren +- Gruppierung nach Heute / Gestern / Älter +- Zustände: keine Mentions, keine Berechtigung für Channel inzwischen verloren, laden, Fehler + +## Pattern / Archetyp + +A2 Liste, ähnlich Benachrichtigungen/Aufgaben, aber mit Chat-Kontext. + +## Subframe + +- (erst nach explizitem OK) diff --git a/02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/Screens/S06 Chat-Suche.md b/02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/Screens/S06 Chat-Suche.md new file mode 100644 index 00000000..0a1f2c33 --- /dev/null +++ b/02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/Screens/S06 Chat-Suche.md @@ -0,0 +1,36 @@ +--- +kind: screen +project: HiveGrid Screen Project +ticket: D-CHAT + AK-P15b +package: WF-P15 Team-Chat +id: S06 +screen: Chat-Suche +typ: A2 Suche +umsetzung: Bauen +status: Entwurf +route: "/chat/search" +subframe: "" +--- + +# S06 — Chat-Suche + +## Ziel + +Nachrichten, Dateien und Entscheidungen über alle erlaubten Channels hinweg finden. + +## Was das Tool können muss + +- Suchfeld mit Ergebnisliste und Treffer-Kontext +- Filter: Channel, Typ, Projekt, Autor, Zeitraum, mit Anhang, nur Pins +- Ergebnis zeigt Snippet mit hervorgehobener Fundstelle, Channel, Autor, Datum, Anhänge +- Klick öffnet S02 an der richtigen Nachricht oder S03 im Thread +- Permission-sicher: nur erlaubte Channels durchsuchen +- Zustände: keine Treffer, zu kurze Suche, Suche läuft, Fehler + +## Pattern / Archetyp + +A2 Suche mit Chat-spezifischem Kontext. + +## Subframe + +- (erst nach explizitem OK) diff --git a/02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/Screens/S07 Channel-Einstellungen.md b/02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/Screens/S07 Channel-Einstellungen.md new file mode 100644 index 00000000..b39ac0bf --- /dev/null +++ b/02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/Screens/S07 Channel-Einstellungen.md @@ -0,0 +1,37 @@ +--- +kind: screen +project: HiveGrid Screen Project +ticket: D-CHAT + AK-P15b +package: WF-P15 Team-Chat +id: S07 +screen: Channel-Einstellungen +typ: A6 Settings +umsetzung: Bauen +status: Entwurf +route: "/chat/:id/settings" +subframe: "" +--- + +# S07 — Channel-Einstellungen + +## Ziel + +Channel-Mitglieder, Privatsphäre und Archivierung werden kontrolliert, ohne den Chatverlauf zu beschädigen. + +## Was das Tool können muss + +- Grunddaten: Name, Beschreibung, Typ (öffentlich/privat/Projekt/DM), Projekt-Verknüpfung falls vorhanden +- Mitgliederliste mit Rolle, Status, zuletzt aktiv; hinzufügen/entfernen je Recht +- Projekt-Channel: Hinweis, dass Mitglieder aus dem Projekt synchronisiert werden +- Privacy-Einstellung für Gruppen (public/private) wenn erlaubt +- Optionen: stummschalten, anpinnen, verlassen, archivieren, wiederherstellen +- Gefahrenbereich: Channel archivieren mit Grund; DM nicht manuell erweiterbar +- Zustände: keine Admin-Rechte, Projekt-Sync überschreibt manuelle Änderung, archivierter Channel read-only + +## Pattern / Archetyp + +A6 Settings. DM-Settings bewusst reduziert. + +## Subframe + +- (erst nach explizitem OK) diff --git a/02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/Screens/S08 Admin-Chat.md b/02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/Screens/S08 Admin-Chat.md new file mode 100644 index 00000000..7b1c5866 --- /dev/null +++ b/02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/Screens/S08 Admin-Chat.md @@ -0,0 +1,37 @@ +--- +kind: screen +project: HiveGrid Screen Project +ticket: D-CHAT + AK-P15b +package: WF-P15 Team-Chat +id: S08 +screen: Admin-Chat-Übersicht +typ: A2/A3 Admin +umsetzung: Bauen +status: Entwurf +route: "/admin/chat" +subframe: "" +--- + +# S08 — Admin-Chat-Übersicht + +## Ziel + +Admins können Channels verwalten und im Ausnahmefall auditierbar prüfen, ohne heimliche DM-Einsicht zu ermöglichen. + +## Was das Tool können muss + +- Tabelle aller Channels: Name, Typ, Mitglieder, Projekt, letzte Nachricht, Status, Archiviert +- Filter: Typ, Projekt, archiviert, private Gruppen, DMs +- Aktionen: öffnen, Mitglieder korrigieren, archivieren, Force-Delete nur mit Berechtigung +- DM öffnen immer nur über M02 (Begründungspflicht) +- Nach Audit-Freigabe: S02 mit sichtbarem **Admin-Einsicht aktiv**-Banner +- Audit-Info: wer, wann, welche Begründung, Session-Ablauf +- Zustände: keine Admin-Berechtigung, Audit-Session abgelaufen, Channel gelöscht + +## Pattern / Archetyp + +A2/A3 Admin-Fläche mit explizitem Audit-Guard. + +## Subframe + +- (erst nach explizitem OK) diff --git a/02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/Screens/V01 Monteur-Handy-Chat.md b/02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/Screens/V01 Monteur-Handy-Chat.md new file mode 100644 index 00000000..51d44377 --- /dev/null +++ b/02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/Screens/V01 Monteur-Handy-Chat.md @@ -0,0 +1,38 @@ +--- +kind: screen +project: HiveGrid Screen Project +ticket: D-CHAT + AK-P15b +package: WF-P15 Team-Chat +id: V01 +screen: Monteur-Handy-Chat +typ: BESPOKE-Mobile +umsetzung: Bauen +status: Entwurf +route: "Variante von S02" +subframe: "" +--- + +# V01 — Monteur-Handy-Chat 📱 + +## Ziel + +Monteurinnen und Monteure schreiben im Projekt-Chat schnell vom Handy: lesen, antworten, Foto senden. + +## Was das Tool können muss + +- Mobile Channel-Detail-Ansicht mit Fokus auf Projekt-Chat +- Vereinfachter Header: Projekt/Channel, Zurück, Info +- Verlauf touch-tauglich, Anhänge/Fotos groß genug +- Composer mit Text, Foto-Direktupload, @Mention, Senden +- Push-/Unread-Hinweis, Scroll-to-bottom, neue Nachrichten +- Offline/Reconnect: Entwurf bleibt erhalten, Foto wartet auf Upload +- Reduzierte Aktionen: reagieren, Thread öffnen, Foto senden, als Aufgabe markieren +- Keine Admin-/Settings-Komplexität im Mobile-Hauptframe + +## Pattern / Archetyp + +BESPOKE-Mobile. Dicht und robust, optimiert für Baustelle statt Desktop-Vollumfang. + +## Subframe + +- (erst nach explizitem OK) diff --git a/02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/WF-P15 Team-Chat.canvas b/02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/WF-P15 Team-Chat.canvas new file mode 100644 index 00000000..888844f0 --- /dev/null +++ b/02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/WF-P15 Team-Chat.canvas @@ -0,0 +1,35 @@ +{ + "nodes": [ + {"id":"md-paket","type":"file","file":"02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/WF-P15 Team-Chat.md","x":-80,"y":-440,"width":460,"height":280}, + {"id":"group-main","type":"group","x":-80,"y":-100,"width":1740,"height":660,"label":"Chat Hauptfläche"}, + {"id":"group-side","type":"group","x":1840,"y":-100,"width":1700,"height":660,"label":"Nebenflächen"}, + {"id":"group-admin","type":"group","x":3720,"y":-100,"width":1180,"height":660,"label":"Admin & Modals"}, + {"id":"note-entry","type":"text","text":"# Einstieg\n\nSidebar → Chat oder Projekt-Detail → Projekt-Channel.\n\nTelefonie bleibt eigenes Paket; dieser Flow ist nur Team-Chat.","x":0,"y":0,"width":360,"height":200}, + {"id":"s01","type":"file","file":"02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/Screens/S01 Chat-Index.md","x":560,"y":0,"width":420,"height":200}, + {"id":"s02","type":"file","file":"02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/Screens/S02 Channel-Detail.md","x":1120,"y":0,"width":420,"height":200}, + {"id":"s03","type":"file","file":"02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/Screens/S03 Thread-Detail.md","x":1120,"y":300,"width":420,"height":200}, + {"id":"v01","type":"file","file":"02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/Screens/V01 Monteur-Handy-Chat.md","x":560,"y":300,"width":420,"height":200}, + {"id":"s04","type":"file","file":"02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/Screens/S04 Angepinnte Nachrichten.md","x":1920,"y":0,"width":420,"height":200}, + {"id":"s05","type":"file","file":"02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/Screens/S05 Meine Mentions.md","x":2480,"y":0,"width":420,"height":200}, + {"id":"s06","type":"file","file":"02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/Screens/S06 Chat-Suche.md","x":3040,"y":0,"width":420,"height":200}, + {"id":"s07","type":"file","file":"02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/Screens/S07 Channel-Einstellungen.md","x":2480,"y":300,"width":420,"height":200}, + {"id":"note-task","type":"text","text":"# Wiederverwendet\n\n»Als Aufgabe markieren« öffnet WF-P4 Aufgaben M01 vorbefüllt mit Nachricht, Channel und Link zum Kontext.","x":3040,"y":300,"width":420,"height":200}, + {"id":"m01","type":"file","file":"02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/Screens/M01 Neuer Channel DM.md","x":3800,"y":0,"width":420,"height":200}, + {"id":"s08","type":"file","file":"02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/Screens/S08 Admin-Chat.md","x":4360,"y":0,"width":420,"height":200}, + {"id":"m02","type":"file","file":"02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/Screens/M02 DM-Audit-Begruendung.md","x":4360,"y":300,"width":420,"height":200} + ], + "edges": [ + {"id":"e-entry","fromNode":"note-entry","fromSide":"right","toNode":"s01","toSide":"left","toEnd":"arrow","styleAttributes":{"path":"dotted"}}, + {"id":"e-open","fromNode":"s01","fromSide":"right","toNode":"s02","toSide":"left","toEnd":"arrow","label":"öffnen"}, + {"id":"e-thread","fromNode":"s02","fromSide":"bottom","toNode":"s03","toSide":"top","toEnd":"arrow","label":"Thread"}, + {"id":"e-mobile","fromNode":"s02","fromSide":"bottom","toNode":"v01","toSide":"top","toEnd":"arrow","label":"Handy"}, + {"id":"e-pins","fromNode":"s02","fromSide":"right","toNode":"s04","toSide":"left","toEnd":"arrow","label":"Pins"}, + {"id":"e-mentions","fromNode":"s01","fromSide":"right","toNode":"s05","toSide":"left","toEnd":"arrow","label":"Mentions"}, + {"id":"e-search","fromNode":"s01","fromSide":"right","toNode":"s06","toSide":"left","toEnd":"arrow","label":"Suche"}, + {"id":"e-settings","fromNode":"s02","fromSide":"right","toNode":"s07","toSide":"left","toEnd":"arrow","label":"Settings"}, + {"id":"e-new","fromNode":"s01","fromSide":"right","toNode":"m01","toSide":"left","toEnd":"arrow","label":"neu"}, + {"id":"e-admin-reason","fromNode":"s08","fromSide":"bottom","toNode":"m02","toSide":"top","toEnd":"arrow","label":"DM öffnen"}, + {"id":"e-admin-open","fromNode":"m02","fromSide":"left","toNode":"s02","toSide":"right","toEnd":"arrow","label":"Audit-Banner"}, + {"id":"e-task-note","fromNode":"note-task","fromSide":"left","toNode":"s02","toSide":"right","toEnd":"arrow","styleAttributes":{"path":"dotted"}} + ] +} diff --git a/02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/WF-P15 Team-Chat.md b/02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/WF-P15 Team-Chat.md new file mode 100644 index 00000000..56655513 --- /dev/null +++ b/02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/WF-P15 Team-Chat.md @@ -0,0 +1,144 @@ +--- +kind: wireframe-package +project: HiveGrid Screen Project +ticket: D-CHAT + AK-P15b +package: WF-P15 Team-Chat +typ: Flow +status: Entwurf +subframe: "" +canvas: "WF-P15 Team-Chat.canvas" +--- + +# WF-P15 Team-Chat + +Status: Entwurf (nicht mit David validiert) +Subframe-Link: - +Canvas: [Canvas]() +Stand: 2026-07-09 + +--- + +## 1. Kurzfassung + +Der interne Team-Chat: Channel-Liste, Echtzeit-Verlauf, Threads, Reaktionen, Pins, Anhänge, @-Erwähnungen, kanalübergreifende Suche und eine vereinfachte Monteur-Handy-Ansicht. Projekt-Channels hängen am Projekt und synchronisieren die Projektmitglieder. Admins können DMs nur über einen begründeten Audit-Modus einsehen; der Zugriff wird sichtbar markiert und protokolliert. + +--- + +## 2. Intent Check + +| Frage | Antwort | +|---|---| +| Ticket/Modul | D-CHAT, Dev-Pendant AK-P15b | +| Nutzer-Ziel | Schnell im Team, Projekt oder 1:1 schreiben, Kontext finden und nichts durch Mentions/Ungelesenheit verpassen | +| Quellen geprüft | `ticket-docs/D-CHAT.md`, `ticket-docs/AK-P15b.md`, HiveGrid Map, Aufgaben-Muster für Mark-as-Task | +| Sicher im Scope | Chat-Index, Channel-Detail, Thread, Pins, Mentions, Suche, Channel anlegen/settings, Admin-Audit, Mobile-Monteur | +| Sicher nicht im Scope | Telefonie/AirCall (WF-P15 Telefonie), HalloPetra, WhatsApp/Unified Inbox, Whisper-Diktat | +| Unsichere Annahmen | Projekt-Chat ist im Projekt-Detail eingebettet, aber die eigentliche Channel-View bleibt dieser Flow | + +--- + +## 3. Scope + +| Punkt | Inhalt | +|---|---| +| Startpunkt | Sidebar »Chat« oder Projekt-Detail »Chat« | +| Ziel | Nachricht gesendet/gelesen, Thread beantwortet, Channel gefunden/angelegt, Admin-Audit sauber begründet | +| Haupt-Archetyp | **BESPOKE** Real-Time-Chat + A2/A6 Nebenflächen + A5 Modals | +| Out of scope | Telefonie, externe Kundenkommunikation, WhatsApp, KI-Hilfe-Chat | + +--- + +## 4. Entscheidungsentwurf (nicht validiert) + +| Entscheidung | Status | Notiz | +|---|---|---| +| Chat als eigener Bespoke-Flow | Entwurf | Real-Time-Layout ist kein Standard-A2/A3 | +| Channel-Detail ist die Hauptarbeitsfläche | Entwurf | Header, Pinned-Preview, Verlauf, Composer, Thread/Info-Seitenflächen | +| Thread als eigener nachweisbarer Screen | Entwurf | Kann später als Side-Panel oder Route umgesetzt werden; UX muss sichtbar sein | +| Pins, Mentions und Suche als eigene Nebenflächen | Entwurf | Diese sind schnelle Arbeitslisten, nicht nur Filter im Channel | +| Projekt-Channel ist entity-linked | Entwurf | Projektmitglieder werden synchronisiert; Einstieg im Projekt-Detail, View bleibt S02 | +| Admin-DM-Audit ist kein normaler Admin-Browse | Entwurf | Pflicht-Begründung je Session + Banner im geöffneten DM + Audit-Log | +| Monteur-Handy-Ansicht eigener Frame | Entwurf | Vereinfachte Navigation, Foto-Direktupload, Push/Offline-Hinweis | +| Telefonie bleibt eigenes Paket | Entwurf | D-CHAT enthält keinen AirCall-Setup/Calls-Flow | + +### Happy Path + +1. Nutzer öffnet `/chat`, sieht Channels nach Ungelesenheit, Mentions und Pins sortiert. +2. Er öffnet einen Projekt-Channel, liest neue Nachrichten, antwortet mit @Mention und hängt ein Foto an. +3. Eine Nachricht braucht Nacharbeit → Hover-Menü »als Aufgabe markieren« öffnet den P4-Aufgaben-Dialog vorbefüllt. +4. Auf dem Handy nutzt der Monteur die vereinfachte Ansicht: Projekt-Chat öffnen, Foto hochladen, kurze Antwort senden. + +### Meldungs-/Error-States (Entwurf) + +| Ereignis | UI-Form | Blockiert? | Bauen? | Copy/Notiz | +|---|---|---|---|---| +| Reconnect / WebSocket weg | Banner in S02/V01 | nein | Nur dokumentieren | »Verbindung unterbrochen — wir verbinden neu.« | +| Nachricht noch nicht gesendet | Optimistic-Zustand in S02/V01 | nein | Nur dokumentieren | Uhr/Retry, kein Layoutsprung | +| Upload fehlgeschlagen | Inline am Anhang | nein | Nur dokumentieren | erneut versuchen / entfernen | +| Keine Channel-Berechtigung | 404/Permission-State | ja | Wiederverwenden | keine Channel-Details leaken | +| Projekt-Channel ohne Projektzugriff | Permission-State | ja | Nur dokumentieren | Projektrechte gewinnen | +| Zu viele Pins | Inline-Hinweis in S02/S04 | nein | Nur dokumentieren | max. 50 Pins je Channel | +| DM-Audit ohne Begründung | M02 blockiert | ja | Bauen (M02) | Pflichtbegründung | +| Admin liest DM | sichtbarer Banner in S02/S08 | nein | Bauen in S08/S02 | »Admin-Einsicht aktiv« | +| Mobile offline | Banner in V01 | nein | Nur dokumentieren | Entwurf bleibt lokal | + +--- + +## 7. Screen-Liste (Entwurf) + +| ID | Screen/Zustand | Typ | Umsetzung | Ort | Status | +|---|---|---|---|---|---| +| S01 | Chat-Index | BESPOKE | Bauen | `/chat` | Entwurf | +| S02 | Channel-Detail | BESPOKE | Bauen | `/chat/:id` | Entwurf | +| S03 | Thread-Detail | BESPOKE Panel/Route | Bauen | `/chat/:id/thread/:thread_id` | Entwurf | +| S04 | Angepinnte Nachrichten | A2 Liste | Bauen | `/chat/:id/pinned` | Entwurf | +| S05 | Meine Mentions | A2 Liste | Bauen | `/chat/mentions` | Entwurf | +| S06 | Chat-Suche | A2 Suche | Bauen | `/chat/search` | Entwurf | +| S07 | Channel-Einstellungen | A6 Settings | Bauen | `/chat/:id/settings` | Entwurf | +| S08 | Admin-Chat-Übersicht | A2/A3 Admin | Bauen | `/admin/chat` | Entwurf | +| V01 | Monteur-Handy-Chat | BESPOKE-Mobile | Bauen | Variante von S02 | Entwurf | +| M01 | Neuer Channel / DM | Modal | Bauen | aus S01/S02 | Entwurf | +| M02 | DM-Audit-Begründung | Modal | Bauen | aus S08 | Entwurf | +| D01–D09 | Reconnect / Pending / Upload / Permission / Pins / Offline (s. Tabelle) | Inline/Zustand | Nur dokumentieren | — | Entwurf | + +--- + +## 8. Grid + +```text +Sidebar -> S01 (Chat-Index) +S01 -> S02 (Channel öffnen) +S02 -> S03 (Thread öffnen) +S02 -> S04 (Pins) +S01/S02 -> S05 (Meine Mentions) +S01/S02 -> S06 (Suche) +S02 -> S07 (Channel-Einstellungen) +S01/S02 -> M01 (neuer Channel / DM) +S02 -> V01 (Handy-Variante) +S08 Admin -> M02 (Begründung) -> S02 mit Audit-Banner +S02 Hover-Menü -> WF-P4 Aufgaben M01 (Mark-as-Task, vorbefüllt) +Projekt-Detail -> S02 (entity-linked Projekt-Channel) +``` + +--- + +## 9. Screen-Pakete + +- [[S01 Chat-Index]] · [[S02 Channel-Detail]] · [[S03 Thread-Detail]] · [[S04 Angepinnte Nachrichten]] +- [[S05 Meine Mentions]] · [[S06 Chat-Suche]] · [[S07 Channel-Einstellungen]] · [[S08 Admin-Chat]] +- [[V01 Monteur-Handy-Chat]] · [[M01 Neuer Channel DM]] · [[M02 DM-Audit-Begruendung]] + +--- + +## 10. Offene Fragen + +- Thread-Detail als Side-Panel oder eigene Route: Paket lässt beides zu; Subframe entscheidet nach Layout-Test. +- Channel-Browser/Discover: v1 im Chat-Index als Bereich/Overlay, kein eigener Screen. +- Read-Receipt-Kopie: »X von Y gelesen« reicht im Verlauf; Detail-Liste erst bei Bedarf. +- Push-Verhalten für Monteure: mit Benachrichtigungen P3 abstimmen. + +## Handoff-Check + +- [ ] Screen-Liste mit David validieren. +- [ ] Meldungs-/Error-State-Matrix mit David validieren. +- [ ] Subframe-Link ist eingetragen. *(Subframe erst nach explizitem OK)* diff --git a/02 Wireframes/P16 Termine/WF-P16 E-Signatur/Screens/M01 Zurueckziehen.md b/02 Wireframes/P16 Termine/WF-P16 E-Signatur/Screens/M01 Zurueckziehen.md new file mode 100644 index 00000000..a3914824 --- /dev/null +++ b/02 Wireframes/P16 Termine/WF-P16 E-Signatur/Screens/M01 Zurueckziehen.md @@ -0,0 +1,36 @@ +--- +kind: screen +project: HiveGrid Screen Project +ticket: D-P16 (Teil) + AK-P16d +package: WF-P16 E-Signatur +id: M01 +screen: Signatur zurückziehen +typ: Confirm-Modal +umsetzung: Bauen +status: Validiert +route: "aus S03" +subframe: "" +--- + +# M01 — Signatur zurückziehen + +## Ziel + +Das Büro kann einen laufenden Signaturvorgang bewusst stoppen, ohne den Verlauf zu verlieren. + +## Was das Tool können muss + +- Zeigt Dokumenttitel, betroffene Unterzeichner und aktuellen Status +- Pflichtfeld: Grund für das Zurückziehen +- Hinweis: Signatur-Link wird ungültig; bereits erzeugte Nachweise/Verläufe bleiben sichtbar +- Option: Unterzeichner per E-Mail informieren +- Bestätigen/Abbrechen; danach Status »zurückgezogen« in S03 und Link-Zustand in S04 +- Bulk nicht nötig in v1 + +## Pattern / Archetyp + +A5 Confirm-Modal. + +## Subframe + +- (erst nach explizitem OK) diff --git a/02 Wireframes/P16 Termine/WF-P16 E-Signatur/Screens/S01 Signaturen im Projekt.md b/02 Wireframes/P16 Termine/WF-P16 E-Signatur/Screens/S01 Signaturen im Projekt.md new file mode 100644 index 00000000..18006d3b --- /dev/null +++ b/02 Wireframes/P16 Termine/WF-P16 E-Signatur/Screens/S01 Signaturen im Projekt.md @@ -0,0 +1,38 @@ +--- +kind: screen +project: HiveGrid Screen Project +ticket: D-P16 (Teil) + AK-P16d +package: WF-P16 E-Signatur +id: S01 +screen: Signaturen im Projekt +typ: A3/A2 eingebettet +umsetzung: Bauen +status: Validiert +route: "Projekt-Detail, Reiter/Aktion" +subframe: "" +--- + +# S01 — Signaturen im Projekt + +## Ziel + +Im Projekt ist sofort sichtbar, welche Dokumente zur Unterschrift raus sind, was offen ist und was abgeschlossen wurde. + +## Was das Tool können muss + +- Reiter/Sektion im Projekt-Detail: **Signaturen** +- Liste der Signatur-Dokumente: Titel, Status, Unterzeichner-Fortschritt, zuletzt geändert, Ablaufdatum +- Primäraktion: **Zur Signatur senden** → S02 +- Zeilen-Klick → S03 (Detail & Status) +- Schnell-Aktionen: erinnern, zurückziehen (M01), Downloads wenn abgeschlossen +- Status-Badges: Entwurf, gesendet, angesehen, teilweise signiert, signiert, abgelehnt, zurückgezogen, abgelaufen, Fehler +- Hinweis: Artefakte liegen in den Projekt-Dateien (Original, signiertes PDF, Abschluss-Nachweis) +- Zustände: leer (CTA »Erstes Dokument senden«), laden, Fehler, keine Berechtigung + +## Pattern / Archetyp + +A3 Projekt-Detail-Sektion mit A2-Listenmuster; passt in das Karteikarten-Muster aus WF-P9 Projekt-Akte. + +## Subframe + +- (erst nach explizitem OK) diff --git a/02 Wireframes/P16 Termine/WF-P16 E-Signatur/Screens/S02 Signatur-Wizard.md b/02 Wireframes/P16 Termine/WF-P16 E-Signatur/Screens/S02 Signatur-Wizard.md new file mode 100644 index 00000000..a37583c1 --- /dev/null +++ b/02 Wireframes/P16 Termine/WF-P16 E-Signatur/Screens/S02 Signatur-Wizard.md @@ -0,0 +1,39 @@ +--- +kind: screen +project: HiveGrid Screen Project +ticket: D-P16 (Teil) + AK-P16d +package: WF-P16 E-Signatur +id: S02 +screen: Signatur-Wizard +typ: A5 Wizard +umsetzung: Bauen +status: Validiert +route: "aus Projekt-Detail / S01" +subframe: "" +--- + +# S02 — Signatur-Wizard + +## Ziel + +Das Büro verschickt ein PDF zur Unterschrift, ohne sich mit Technik, Provider-Details oder manueller Ablage zu beschäftigen. + +## Was das Tool können muss + +- Schritt 1: **PDF wählen** aus Projekt-Dateien ODER neues PDF hochladen (Dropzone), Dateiname/Titel prüfen +- Schritt 2: **Unterzeichner**: Name, E-Mail, Rolle, Kontakt-Verknüpfung, Reihenfolge; Kunde aus Projekt/Kontakt vorausgefüllt +- Reihenfolge: parallel oder sequenziell; bei sequenziell klar zeigen, wer zuerst dran ist +- Schritt 3: **Signaturmodus**: Fern-Link + Code (OTP) ODER Vor Ort auf diesem Gerät (V01) +- Optional: PandaDoc BYOK nur anzeigen, wenn verbunden; sonst Default **SES-Eigenbau** +- Nachricht an Unterzeichner: Betreff, kurzer Text, Ablaufdatum +- Zusammenfassung vor dem Senden: Dokument, Unterzeichner, Modus, Reihenfolge, Ablauf +- Senden → S03; Vor-Ort-Modus → V01 +- Validierung: PDF fehlt, Fern-Link ohne E-Mail, doppelte E-Mail, ungültige Reihenfolge, Datei nicht PDF + +## Pattern / Archetyp + +A5 Wizard, bewusst kompakt; technische Provider-Details bleiben im Hintergrund. + +## Subframe + +- (erst nach explizitem OK) diff --git a/02 Wireframes/P16 Termine/WF-P16 E-Signatur/Screens/S03 Signatur-Detail.md b/02 Wireframes/P16 Termine/WF-P16 E-Signatur/Screens/S03 Signatur-Detail.md new file mode 100644 index 00000000..546e0a0a --- /dev/null +++ b/02 Wireframes/P16 Termine/WF-P16 E-Signatur/Screens/S03 Signatur-Detail.md @@ -0,0 +1,38 @@ +--- +kind: screen +project: HiveGrid Screen Project +ticket: D-P16 (Teil) + AK-P16d +package: WF-P16 E-Signatur +id: S03 +screen: Signatur-Detail & Status +typ: A3 Detail +umsetzung: Bauen +status: Validiert +route: "Signatur öffnen" +subframe: "" +--- + +# S03 — Signatur-Detail & Status + +## Ziel + +Alles zu einem Signaturvorgang an einem Ort: wer hat schon unterschrieben, was ist offen, welche Nachweise gibt es. + +## Was das Tool können muss + +- Kopf: Dokument-Titel, Projekt, Gesamtstatus, Ablaufdatum, Provider (SES Default / PandaDoc BYOK) +- Linke Spalte: Dokumentdaten, Versandtext, Routing-Modus, Datei-Verknüpfungen +- Rechte Spalte: **Unterzeichner-Status** je Person (gesendet, angesehen, signiert, abgelehnt, zurückgezogen), Zeitpunkte, letzte Erinnerung +- Aktionen: Erinnerung senden, Link erneut senden, zurückziehen (M01), Download Original-PDF, signiertes PDF, Abschluss-Nachweis +- Verlauf: erstellt, gesendet, Link geöffnet, OTP bestätigt, signiert, abgelehnt, zurückgezogen, Versiegelung abgeschlossen/fehlgeschlagen +- Wenn abgeschlossen: klare Download-Leiste + Hinweis, dass das signierte PDF in den Projekt-Dateien liegt +- Wenn abgelehnt: Grund anzeigen, erneutes Senden als neuer Vorgang +- Zustände: wartend auf Unterzeichner, abgelaufen, zurückgezogen, Versiegelung fehlgeschlagen, keine Berechtigung + +## Pattern / Archetyp + +A3 Detail nach Karteikarten-Muster: Daten links, Status/Verlauf rechts, Aktionen im Kopf. + +## Subframe + +- (erst nach explizitem OK) diff --git a/02 Wireframes/P16 Termine/WF-P16 E-Signatur/Screens/S04 Signieren per Link.md b/02 Wireframes/P16 Termine/WF-P16 E-Signatur/Screens/S04 Signieren per Link.md new file mode 100644 index 00000000..8caa471f --- /dev/null +++ b/02 Wireframes/P16 Termine/WF-P16 E-Signatur/Screens/S04 Signieren per Link.md @@ -0,0 +1,38 @@ +--- +kind: screen +project: HiveGrid Screen Project +ticket: D-P16 (Teil) + AK-P16d +package: WF-P16 E-Signatur +id: S04 +screen: Signieren per Link +typ: BESPOKE-Public +umsetzung: Bauen +status: Validiert (Delta) +route: "/sign/:token" +subframe: "" +--- + +# S04 — 🌐 Signieren per Link + +## Ziel + +Der Kunde kann das Dokument sicher und verständlich unterschreiben, ohne Login und ohne App-Installation. + +## Was das Tool können muss + +- Firmen-Branding oben, Dokumenttitel, Projekt-/Angebotsbezug in Klartext ohne interne Details +- Schritt 1: Code-Bestätigung (OTP aus Mail) mit »Code erneut senden« +- Schritt 2: PDF ansehen/prüfen, Seiten-Navigation, Download des Original-PDF optional +- Schritt 3: Zustimmung/Intent-Text bestätigen + Unterschrift zeichnen oder tippen +- Abschluss: klare Bestätigung, Hinweis auf E-Mail/Download, keine internen Projektlinks +- Ablehnen: optionaler Grund, Status geht zurück nach S03, Büro bekommt Benachrichtigung +- Zustände: Link ungültig, Link abgelaufen, bereits signiert, zurückgezogen, falscher/abgelaufener Code, PDF lädt nicht, Signatur-Abschluss fehlgeschlagen +- Mobile-first: Unterschrift muss auf Handy sauber funktionieren + +## Pattern / Archetyp + +BESPOKE-Public. Das ist der öffentliche Trust-Touchpoint; höherer Polish als interne Verwaltungsflächen. + +## Subframe + +- (erst nach explizitem OK) diff --git a/02 Wireframes/P16 Termine/WF-P16 E-Signatur/Screens/V01 Vor Ort unterschreiben.md b/02 Wireframes/P16 Termine/WF-P16 E-Signatur/Screens/V01 Vor Ort unterschreiben.md new file mode 100644 index 00000000..cf65bcde --- /dev/null +++ b/02 Wireframes/P16 Termine/WF-P16 E-Signatur/Screens/V01 Vor Ort unterschreiben.md @@ -0,0 +1,38 @@ +--- +kind: screen +project: HiveGrid Screen Project +ticket: D-P16 (Teil) + AK-P16d +package: WF-P16 E-Signatur +id: V01 +screen: Vor Ort unterschreiben +typ: BESPOKE-Mobile +umsetzung: Bauen +status: Validiert +route: "aus S02, Touch-Modus" +subframe: "" +--- + +# V01 — Vor Ort unterschreiben 📱 + +## Ziel + +Der Mitarbeiter kann dem Kunden vor Ort das Gerät geben und die Unterschrift direkt aufnehmen. + +## Was das Tool können muss + +- Vollbild-/Tablet-tauglicher Modus ohne interne Navigation +- Dokumenttitel + kurze Bestätigung, dass der Kunde gerade unterschreibt +- PDF-Vorschau oder Pflicht-Hinweis »Dokument wurde geprüft« je nach Platz +- Name des Unterzeichners editierbar, Rolle sichtbar +- Touch-Signaturfeld groß genug für Finger/Stift, mit Löschen/Neu zeichnen +- Abschlussknopf mit bewusstem Intent-Text +- Nach Abschluss: Bestätigung und Rückgabe-Hinweis an Mitarbeiter; zurück zu S03 +- Zustände: offline/Sync steht aus, Signaturfeld leer, Versiegelung noch ausstehend + +## Pattern / Archetyp + +BESPOKE-Mobile, verwandt mit WF-P9 Abnahme-Unterschriften, aber für Dokument-Signatur am Projekt. + +## Subframe + +- (erst nach explizitem OK) diff --git a/02 Wireframes/P16 Termine/WF-P16 E-Signatur/WF-P16 E-Signatur.canvas b/02 Wireframes/P16 Termine/WF-P16 E-Signatur/WF-P16 E-Signatur.canvas new file mode 100644 index 00000000..a9123cb7 --- /dev/null +++ b/02 Wireframes/P16 Termine/WF-P16 E-Signatur/WF-P16 E-Signatur.canvas @@ -0,0 +1,29 @@ +{ + "nodes": [ + {"id":"md-paket","type":"file","file":"02 Wireframes/P16 Termine/WF-P16 E-Signatur/WF-P16 E-Signatur.md","x":-80,"y":-440,"width":460,"height":280}, + {"id":"group-intern","type":"group","x":-80,"y":-100,"width":1740,"height":660,"label":"Projekt-Team (intern)"}, + {"id":"group-public","type":"group","x":1840,"y":-100,"width":1180,"height":660,"label":"Kunde unterschreibt"}, + {"id":"group-actions","type":"group","x":3200,"y":-100,"width":620,"height":380,"label":"Aktionen"}, + {"id":"note-entry","type":"text","text":"# Einstieg\n\nProjekt-Detail → Reiter »Signaturen« oder Aktion »Zur Signatur senden«.\n\nSignatur hängt am Projekt-Kern, nicht an Deal.","x":0,"y":0,"width":360,"height":200}, + {"id":"s01","type":"file","file":"02 Wireframes/P16 Termine/WF-P16 E-Signatur/Screens/S01 Signaturen im Projekt.md","x":560,"y":0,"width":420,"height":200}, + {"id":"s02","type":"file","file":"02 Wireframes/P16 Termine/WF-P16 E-Signatur/Screens/S02 Signatur-Wizard.md","x":1120,"y":0,"width":420,"height":200}, + {"id":"s03","type":"file","file":"02 Wireframes/P16 Termine/WF-P16 E-Signatur/Screens/S03 Signatur-Detail.md","x":560,"y":300,"width":420,"height":200}, + {"id":"note-assets","type":"text","text":"# Artefakte\n\nOriginal-PDF, signiertes PDF und Abschluss-Nachweis landen in den Projekt-Dateien.\n\nDefault: SES-Eigenbau. PandaDoc nur BYOK-Opt-in.","x":1120,"y":300,"width":420,"height":200}, + {"id":"s04","type":"file","file":"02 Wireframes/P16 Termine/WF-P16 E-Signatur/Screens/S04 Signieren per Link.md","x":1920,"y":0,"width":420,"height":200}, + {"id":"v01","type":"file","file":"02 Wireframes/P16 Termine/WF-P16 E-Signatur/Screens/V01 Vor Ort unterschreiben.md","x":2480,"y":0,"width":420,"height":200}, + {"id":"note-public","type":"text","text":"# Public-Delta\n\nD-P16 nennt den Wizard, aber nicht den Kunden-Signierframe. S04 ist trotzdem Pflicht, weil hier Vertrauen + OTP + Signatur passieren.","x":1920,"y":300,"width":460,"height":200}, + {"id":"m01","type":"file","file":"02 Wireframes/P16 Termine/WF-P16 E-Signatur/Screens/M01 Zurueckziehen.md","x":3280,"y":0,"width":420,"height":200} + ], + "edges": [ + {"id":"e-entry","fromNode":"note-entry","fromSide":"right","toNode":"s01","toSide":"left","toEnd":"arrow","styleAttributes":{"path":"dotted"}}, + {"id":"e-new","fromNode":"s01","fromSide":"right","toNode":"s02","toSide":"left","toEnd":"arrow","label":"Zur Signatur senden"}, + {"id":"e-open","fromNode":"s01","fromSide":"bottom","toNode":"s03","toSide":"left","toEnd":"arrow","label":"öffnen"}, + {"id":"e-send","fromNode":"s02","fromSide":"right","toNode":"s04","toSide":"left","toEnd":"arrow","label":"Fern-Link + OTP"}, + {"id":"e-local","fromNode":"s02","fromSide":"right","toNode":"v01","toSide":"left","toEnd":"arrow","label":"Vor Ort"}, + {"id":"e-status","fromNode":"s04","fromSide":"bottom","toNode":"s03","toSide":"right","toEnd":"arrow","label":"Status"}, + {"id":"e-local-status","fromNode":"v01","fromSide":"bottom","toNode":"s03","toSide":"right","toEnd":"arrow","label":"Status"}, + {"id":"e-assets","fromNode":"note-assets","fromSide":"left","toNode":"s03","toSide":"right","toEnd":"arrow","styleAttributes":{"path":"dotted"}}, + {"id":"e-public-note","fromNode":"note-public","fromSide":"top","toNode":"s04","toSide":"bottom","toEnd":"arrow","styleAttributes":{"path":"dotted"}}, + {"id":"e-revoke","fromNode":"s03","fromSide":"right","toNode":"m01","toSide":"left","toEnd":"arrow","label":"zurückziehen"} + ] +} diff --git a/02 Wireframes/P16 Termine/WF-P16 E-Signatur/WF-P16 E-Signatur.md b/02 Wireframes/P16 Termine/WF-P16 E-Signatur/WF-P16 E-Signatur.md new file mode 100644 index 00000000..81d84506 --- /dev/null +++ b/02 Wireframes/P16 Termine/WF-P16 E-Signatur/WF-P16 E-Signatur.md @@ -0,0 +1,132 @@ +--- +kind: wireframe-package +project: HiveGrid Screen Project +ticket: D-P16 (Teil) + AK-P16d +package: WF-P16 E-Signatur +typ: Flow +status: Validiert +subframe: "" +canvas: "WF-P16 E-Signatur.canvas" +--- + +# WF-P16 E-Signatur + +Status: Validiert (Screen-Liste) +Subframe-Link: - +Canvas: [Canvas]() +Stand: 2026-07-09 + +--- + +## 1. Kurzfassung + +Digital unterschreiben am Projekt: Das Büro wählt ein PDF aus den Projekt-Dateien oder lädt eines hoch, legt Unterzeichner, Reihenfolge und Versandtext fest und sendet den Signatur-Link. Kunden unterschreiben per 🌐 öffentlichem Link mit Code-Bestätigung oder direkt vor Ort auf dem Gerät. Intern sieht das Team pro Unterzeichner den Status, kann erinnern oder zurückziehen und lädt am Ende Original-PDF, signiertes PDF und Abschluss-Nachweis herunter. Default ist der gehärtete **SES-Eigenbau**; PandaDoc bleibt nur optionaler BYOK-Adapter. + +--- + +## 2. Intent Check + +| Frage | Antwort | +|---|---| +| Ticket/Modul | D-P16 (Signatur-Wizard), Dev-Pendant AK-P16d | +| Nutzer-Ziel | Angebot/Projekt-Dokument ohne Medienbruch zur Unterschrift bringen und den Nachweis sauber am Projekt ablegen | +| Quellen geprüft | `ticket-docs/D-P16.md`, `ticket-docs/AK-P16d.md`, Signature-Modelle, HiveGrid Map | +| Sicher im Scope | Projekt-Einstieg, Signatur-Wizard, Status/Detail, 🌐 Kunden-Signierseite, Vor-Ort-Signatur, Erinnerung, Widerruf, Downloads | +| Sicher nicht im Scope | PDF-Editor/Vorlagenbau, qualifizierte elektronische Signatur (QES), PandaDoc als Pflichtprovider, Deal-Objekt | +| Unsichere Annahmen | Öffentliche Signierseite ist als eigener Screen nötig: Dev hat den Flow, D-P16 nennt ihn nicht als separates Screen-Delta | + +--- + +## 3. Scope + +| Punkt | Inhalt | +|---|---| +| Startpunkt | Projekt-Detail, Aktion »Zur Signatur senden« oder Reiter »Signaturen« | +| Ziel | Dokument ist versendet oder abgeschlossen; Artefakte liegen am Projekt in Dateien | +| Haupt-Archetyp | A5 Wizard + A3 Detail + BESPOKE-Public | +| Out of scope | Dokument inhaltlich bearbeiten, Unterschriftenfelder frei im PDF platzieren, QES/D-Trust | + +--- + +## 4. Entscheidungen (validiert 2026-07-09) + +| Entscheidung | Status | Notiz | +|---|---|---| +| Signatur hängt am Projekt-Kern | genommen | `ProjektSignatureDocument`/`Signer`; kein Deal-Objekt | +| Eigener SES-Flow ist Default | genommen | Fern-Link mit OTP + Touch vor Ort + PAdES/Certificate; PandaDoc nur BYOK-Opt-in | +| Eigene 🌐 Kunden-Signierseite | genommen (Delta) | Als eigener Frame S04, weil sonst der wichtigste externe Touchpoint fehlt | +| Projekt-Reiter/Liste vor Wizard | genommen | Nutzer braucht Überblick über laufende und abgeschlossene Signaturen am Projekt | +| Status-Detail als eigene Karteikarte | genommen | Download, Signer-Status, Reminder, Widerruf und Verlauf gehören an einen Ort | +| Vor-Ort-Signatur als eigener Mobile-/Tablet-Nachweis | genommen | Gleiche Signatur-Engine, aber ohne Mail-Link als Übergabe auf dem Gerät | +| PandaDoc nur optional sichtbar | genommen | Nur zeigen, wenn BYOK verbunden; sonst kein UI-Zwang | + +### Happy Path + +1. Mitarbeiter öffnet ein Projekt und klickt »Zur Signatur senden«. +2. Im Wizard: PDF auswählen/hochladen, Unterzeichner prüfen, Reihenfolge wählen, Modus »Fern-Link + Code« oder »Vor Ort« einstellen, Nachricht senden. +3. Kunde öffnet den Link, bestätigt den Code, prüft das PDF, setzt die Signatur und schließt ab. +4. Im Projekt springt der Status auf signiert; Original, signiertes PDF und Abschluss-Nachweis sind herunterladbar und in Dateien abgelegt. + +### Meldungs-/Error-States (validiert) + +| Ereignis | UI-Form | Blockiert? | Bauen? | Copy/Notiz | +|---|---|---|---|---| +| Kein PDF gewählt | Wizard-Validierung in S02 | ja | Nur dokumentieren | »Wähle ein PDF aus oder lade eins hoch.« | +| Unterzeichner ohne E-Mail bei Fern-Link | Inline-Fehler in S02 | ja | Nur dokumentieren | Vor-Ort-Modus darf ohne Mail gehen, Fern-Link nicht | +| Link abgelaufen/ungültig | 🌐 Klartext-Zustand in S04 | ja | Nur dokumentieren | Keine Projekt-Interna verraten | +| OTP falsch/abgelaufen | Inline-Fehler in S04 | ja | Nur dokumentieren | Neuer Code anfordern | +| Unterzeichner lehnt ab | Status in S03 + Benachrichtigung | nein | Nur dokumentieren | Grund optional, Dokument läuft nicht weiter | +| Dokument zurückgezogen | M01 + Status in S03/S04 | ja | Bauen (M01) | Link wird ungültig, Verlauf bleibt | +| PandaDoc nicht verbunden | Hinweis in S02/S03 | nein | Nur dokumentieren | Default SES bleibt nutzbar | +| PAdES/Versiegelung schlägt fehl | Fehlerzustand in S03 | ja (Abschluss) | Nur dokumentieren | Klartext + Support/erneut versuchen | + +--- + +## 7. Validierte Screen-Liste + +| ID | Screen/Zustand | Typ | Umsetzung | Ort | Status | +|---|---|---|---|---|---| +| S01 | Signaturen im Projekt | A3/A2 eingebettet | Bauen | Projekt-Detail, Reiter/Aktion | Validiert | +| S02 | Signatur-Wizard | A5 Wizard | Bauen | aus S01 / Projekt-Aktion | Validiert | +| S03 | Signatur-Detail & Status | A3 Detail | Bauen | Signatur öffnen | Validiert | +| S04 | 🌐 Signieren per Link | BESPOKE-Public | Bauen | `/sign/:token` | Validiert (Delta) | +| V01 | Vor Ort unterschreiben 📱 | BESPOKE-Mobile | Bauen | aus S02, Touch-Modus | Validiert | +| M01 | Signatur zurückziehen | Confirm-Modal | Bauen | aus S03 | Validiert | +| D01–D07 | PDF fehlt / Mail fehlt / Link ungültig / OTP / Ablehnung / Provider / Versiegelung | Inline/Zustand | Nur dokumentieren | — | Validiert | + +--- + +## 8. Grid + +```text +Projekt-Detail -> S01 (Signaturen im Projekt) -> S02 (Wizard: PDF, Unterzeichner, Reihenfolge, Modus, Nachricht) +S02 -> S04 (Fern-Link + OTP: PDF prüfen, Signatur, Abschluss) +S02 -> V01 (Vor Ort: Gerät übergeben, Touch-Signatur, Abschluss) +S01 -> S03 (Detail: Signer-Status, Verlauf, Downloads) +S03 -> M01 (zurückziehen) +S03: erinnern / signed PDF / Completion Certificate herunterladen +Artefakte: Original-PDF + signiertes PDF + Abschluss-Nachweis -> Projekt-Dateien +Optional: PandaDoc BYOK nur anzeigen, wenn verbunden; Default bleibt SES-Eigenbau +``` + +--- + +## 9. Screen-Pakete + +- [[S01 Signaturen im Projekt]] · [[S02 Signatur-Wizard]] · [[S03 Signatur-Detail]] · [[S04 Signieren per Link]] +- [[V01 Vor Ort unterschreiben]] · [[M01 Zurueckziehen]] + +--- + +## 10. Offene Fragen + +- **Design-Delta:** D-P16 nennt nur den internen Signatur-Wizard; die 🌐 Kunden-Signierseite S04 ist als eigener Screen ergänzt, weil sie für den Flow zwingend sichtbar sein muss. +- PDF-Feldplatzierung: v1 platziert Signatur/Certificate standardisiert; ein frei editierbarer PDF-Tagger bleibt v2. +- Erinnerungsregeln: manuell in v1, automatische Reminder später mit Benachrichtigungen/Automationen klären. +- PandaDoc BYOK: nur zeigen, wenn eine Verbindung existiert; genauer Einstiegsort über WF-P5 Integrationen prüfen. + +## Handoff-Check + +- [x] Jeder Screen ist mit Umsetzung markiert. +- [x] Meldungs-/Error-State-Matrix ist ausgefüllt. +- [ ] Subframe-Link ist eingetragen. *(Subframe erst nach explizitem OK)* diff --git a/02 Wireframes/P16 Termine/WF-P16 Kalender/Screens/M01 Termin-Dialog.md b/02 Wireframes/P16 Termine/WF-P16 Kalender/Screens/M01 Termin-Dialog.md new file mode 100644 index 00000000..23289004 --- /dev/null +++ b/02 Wireframes/P16 Termine/WF-P16 Kalender/Screens/M01 Termin-Dialog.md @@ -0,0 +1,39 @@ +--- +kind: screen +project: HiveGrid Screen Project +ticket: D-BOOK + D-P16 (Teil) +package: WF-P16 Kalender +id: M01 +screen: Termin anlegen/bearbeiten +typ: Modal +umsetzung: Bauen +status: Validiert +route: "aus S01 (Slot-Klick / + Termin)" +subframe: "" +--- + +# M01 — Termin anlegen/bearbeiten + +## Ziel + +Einen Termin planen wie in Google Calendar — mit Teilnehmern, und ohne jemanden doppelt zu verplanen. + +## Was das Tool können muss + +- **Grunddaten:** Titel, Datum + Uhrzeit (von–bis), optional ganztägig, Ort, Beschreibung/Notiz +- **Teilnehmer-Feld** (ein Feld, drei Arten): + - **Team-Mitglieder** (@-Vorschläge) — sehen den Termin sofort in ihrem HiveGrid-Kalender + - **Kontakte** (aus dem CRM) — bekommen E-Mail mit Kalender-Datei (.ics) + - **freie E-Mail-Adressen** — ebenfalls Mail + .ics, ohne Kontakt-Anlage +- **Verfügbarkeits-Check:** beim Hinzufügen interner Teilnehmer zeigt der Dialog belegt/frei im gewählten Zeitraum (»Max ist 9–12 Uhr belegt«) — Warnung, kein Block; Link »Kalender einblenden« aktiviert das Overlay in S01 +- Optionale **Projekt-Verknüpfung** (Termin erscheint dann auch in der Projekt-Akte) +- Bearbeiten nutzt denselben Dialog (nur bei eigenen Terminen); Änderung informiert Teilnehmer +- Speichern → zurück in S01, Termin sichtbar; bei Google-Sync geht der Termin auch extern raus + +## Pattern / Archetyp + +A5 Dialog + A4 Formular-Felder. Teilnehmer-Feld = @-Erwähnungs-Muster aus dem UI-Baukasten. + +## Subframe + +- (erst nach explizitem OK) diff --git a/02 Wireframes/P16 Termine/WF-P16 Kalender/Screens/M02 Termin-Detail.md b/02 Wireframes/P16 Termine/WF-P16 Kalender/Screens/M02 Termin-Detail.md new file mode 100644 index 00000000..4199a9d2 --- /dev/null +++ b/02 Wireframes/P16 Termine/WF-P16 Kalender/Screens/M02 Termin-Detail.md @@ -0,0 +1,35 @@ +--- +kind: screen +project: HiveGrid Screen Project +ticket: D-BOOK + D-P16 (Teil) +package: WF-P16 Kalender +id: M02 +screen: Termin-Detail +typ: Popover +umsetzung: Bauen +status: Validiert +route: "Termin-Klick in S01" +subframe: "" +--- + +# M02 — Termin-Detail (Popover) + +## Ziel + +Schnell sehen, was das für ein Termin ist, wer kommt — und mit einem Klick zur Quelle springen. + +## Was das Tool können muss + +- Titel, Zeit, Ort, Beschreibung, **Quelle-Badge** (Google / Buchung / Plantafel / Projekt / eigener Termin) +- **Teilnehmer-Liste mit Zu-/Absage-Status** (zugesagt / abgesagt / offen); Bounce-Marker wenn Einladungs-Mail unzustellbar (+ erneut senden) +- **Absprung zur Quelle:** Buchung → Buchungs-Detail · Einsatz → Plantafel · Projekt-Termin → Projekt-Akte · Google-Termin → nur lesen (Hinweis »kommt aus Google«) +- **Aktionen je nach Quelle:** eigene Termine → Bearbeiten (M01) + Löschen (mit Teilnehmer-Info); fremde Quellen → nur Absprung +- Für eingeladene interne Nutzer: **Zusagen/Absagen** direkt im Popover + +## Pattern / Archetyp + +Popover (leichtgewichtig, kein volles Modal) — Muster: Termin-Klick wie in Google Calendar. + +## Subframe + +- (erst nach explizitem OK) diff --git a/02 Wireframes/P16 Termine/WF-P16 Kalender/Screens/S01 Kalender.md b/02 Wireframes/P16 Termine/WF-P16 Kalender/Screens/S01 Kalender.md new file mode 100644 index 00000000..922ceaf9 --- /dev/null +++ b/02 Wireframes/P16 Termine/WF-P16 Kalender/Screens/S01 Kalender.md @@ -0,0 +1,40 @@ +--- +kind: screen +project: HiveGrid Screen Project +ticket: D-BOOK + D-P16 (Teil) +package: WF-P16 Kalender +id: S01 +screen: Kalender +typ: BESPOKE Kalender-Grid +umsetzung: Bauen +status: Validiert +route: "/calendar" +subframe: "" +--- + +# S01 — Kalender (Monat/Woche) ⭐ + +## Ziel + +Alle Termine an einem Ort — egal ob aus Google, einer Online-Buchung, der Plantafel, einem Projekt oder von Hand angelegt. Und: neue Termine direkt hier planen. + +## Was das Tool können muss + +- **Monats- und Wochen-Ansicht** mit Umschalter, Heute-Knopf, Zeitraum-Navigation (Pfeile) +- **Quell-Marker je Termin** (Farbe/Icon): Google · Buchung · Plantafel-Einsatz · Projekt-Termin · eigener Termin +- **Kollegen einblenden (Overlay):** Mitarbeiter-Auswahl (Avatare/Checkboxen) — deren HiveGrid-Termine erscheinen normal, private Google-Termine nur als **»belegt«** ohne Titel/Details +- **Termin anlegen:** Klick auf freien Slot oder »+ Termin«-Knopf → M01 +- **Termin öffnen:** Klick auf Termin → M02 (Detail-Popover) +- **Verbinden-Hinweis** solange kein Google-Konto verbunden ist (dismissbar, mit »Verbinden«-Knopf → S02); Kalender ist auch ohne Verbindung voll nutzbar +- **Re-Connect-Banner** bei kaputter Verbindung (Klartext + Lösungs-Knopf) +- Dezenter Hinweis bei frischen Sync-Konflikten mit Link zum Konflikt-Verlauf (S02) +- Zahnrad → Kalender-Einstellungen (S02) +- Zustände: leer (CTA Termin anlegen / Google verbinden), laden, Fehler + +## Pattern / Archetyp + +BESPOKE (Kalender-Grid, mittlerer Aufwand laut Ticket). Zustands-Set A7. + +## Subframe + +- (erst nach explizitem OK) diff --git a/02 Wireframes/P16 Termine/WF-P16 Kalender/Screens/S02 Kalender-Einstellungen.md b/02 Wireframes/P16 Termine/WF-P16 Kalender/Screens/S02 Kalender-Einstellungen.md new file mode 100644 index 00000000..6161327e --- /dev/null +++ b/02 Wireframes/P16 Termine/WF-P16 Kalender/Screens/S02 Kalender-Einstellungen.md @@ -0,0 +1,38 @@ +--- +kind: screen +project: HiveGrid Screen Project +ticket: D-BOOK + D-P16 (Teil) +package: WF-P16 Kalender +id: S02 +screen: Kalender-Einstellungen & Sync +typ: Settings (A6) +umsetzung: Bauen +status: Validiert +route: "/calendar/settings" +subframe: "" +--- + +# S02 — Kalender-Einstellungen & Sync + +## Ziel + +Sein eigenes Google-Konto verbinden und den Abgleich steuern — pro Mitarbeiter, ohne Admin-Umweg. Erreichbar direkt aus dem Kalender (Zahnrad / Verbinden-Hinweis). + +## Was das Tool können muss + +- **Provider-Auswahl:** Google (aktiv) · Outlook (**ausgegraut, »bald verfügbar«** — v2); UI bleibt provider-neutral +- **Verbinden:** startet den OAuth-Assistenten im WF-P5-Muster (M01) → danach Connect-Card mit Status-Ampel, verbundenem Konto (E-Mail), letzter Sync-Zeit +- **Kalender-Auswahl:** welche Google-Kalender abgeglichen werden (Checkbox-Liste) +- **Sync-Richtung:** beide Richtungen · nur HiveGrid → Google · nur Google → HiveGrid +- **Trennen** mit »auch extern entfernen?«-Wahl (Trennen-Modal aus WF-P5 M03, erweitert um die Extern-Wahl) — kein Hard-Delete +- **Re-Connect-Banner** bei kaputter Verbindung (Token abgelaufen o. ä.) +- **Konflikt-Verlauf-Sektion:** letzte 50 automatische Auflösungen in Klartext — »Termin ›Wartung Müller‹ wurde in Google auf 14:00 verschoben — HiveGrid hat übernommen« (Last-Write-Wins); jede Auflösung erzeugt zusätzlich eine Benachrichtigung +- Hinweis: Verbindung ist **pro Mitarbeiter** — jeder verwaltet hier nur sein eigenes Konto; fremde Termin-Inhalte bleiben privat (nur »belegt«) + +## Pattern / Archetyp + +A6 Settings/Connection — Connection-Card-Muster aus WF-P5 Integrationen; Konflikt-Verlauf analog Abgleich-Verlauf (WF-P5 S03), hier nur als Sektion. + +## Subframe + +- (erst nach explizitem OK) diff --git a/02 Wireframes/P16 Termine/WF-P16 Kalender/WF-P16 Kalender.canvas b/02 Wireframes/P16 Termine/WF-P16 Kalender/WF-P16 Kalender.canvas new file mode 100644 index 00000000..06ba0863 --- /dev/null +++ b/02 Wireframes/P16 Termine/WF-P16 Kalender/WF-P16 Kalender.canvas @@ -0,0 +1,42 @@ +{ + "nodes":[ + {"id":"md-paket","type":"file","file":"02 Wireframes/P16 Termine/WF-P16 Kalender/WF-P16 Kalender.md","x":-80,"y":-440,"width":460,"height":280}, + {"id":"group-kalender","type":"group","x":-80,"y":-100,"width":1140,"height":380,"label":"Kalender (Desktop, Bespoke)"}, + {"id":"group-dialoge","type":"group","x":-80,"y":460,"width":1140,"height":380,"label":"Termin-Dialoge"}, + {"id":"group-sync","type":"group","x":1300,"y":-100,"width":1120,"height":380,"label":"Einstellungen & Sync (pro Nutzer)"}, + {"id":"note-entry","type":"text","text":"# Einstieg\n\nSidebar → Kalender.\n\nQuellen: Google · Buchung · Plantafel · Projekt · eigene Termine. Kollegen als Overlay einblendbar (fremde private Google-Termine nur »belegt«).","x":0,"y":0,"width":360,"height":200}, + {"id":"s01","type":"file","file":"02 Wireframes/P16 Termine/WF-P16 Kalender/Screens/S01 Kalender.md","x":560,"y":0,"width":420,"height":200}, + {"id":"m01","type":"file","file":"02 Wireframes/P16 Termine/WF-P16 Kalender/Screens/M01 Termin-Dialog.md","x":0,"y":560,"width":420,"height":200}, + {"id":"m02","type":"file","file":"02 Wireframes/P16 Termine/WF-P16 Kalender/Screens/M02 Termin-Detail.md","x":580,"y":560,"width":420,"height":200}, + {"id":"s02","type":"file","file":"02 Wireframes/P16 Termine/WF-P16 Kalender/Screens/S02 Kalender-Einstellungen.md","x":1340,"y":0,"width":420,"height":200}, + {"id":"note-sync","type":"text","text":"# Wiederverwendet aus WF-P5\n\nOAuth-Assistent (M01) + Trennen-Modal (M03, + »auch extern entfernen?«).\n\nOutlook als Teaser »bald verfügbar« (v2). Konflikt-Verlauf als Sektion hier.","x":1900,"y":0,"width":460,"height":200} + ], + "edges":[ + { + "id":"e-entry", + "fromNode":"note-entry", + "fromSide":"right", + "toNode":"s01", + "toSide":"left", + "toEnd":"arrow", + "styleAttributes":{"path":"dotted"} + }, + {"id":"e-new","fromNode":"s01","fromSide":"bottom","toNode":"m01","toSide":"top","toEnd":"arrow","label":"Slot-Klick / + Termin"}, + {"id":"e-open","fromNode":"s01","fromSide":"bottom","toNode":"m02","toSide":"top","toEnd":"arrow","label":"Termin-Klick"}, + {"id":"e-edit","fromNode":"m02","fromSide":"left","toNode":"m01","toSide":"right","toEnd":"arrow","label":"Bearbeiten"}, + {"id":"e-settings","fromNode":"s01","fromSide":"right","toNode":"s02","toSide":"left","toEnd":"arrow","label":"Zahnrad / Verbinden"}, + { + "id":"e-sync-note", + "fromNode":"note-sync", + "fromSide":"left", + "toNode":"s02", + "toSide":"right", + "toEnd":"arrow", + "styleAttributes":{"path":"dotted"} + } + ], + "metadata":{ + "version":"1.0-1.0", + "frontmatter":{} + } +} \ No newline at end of file diff --git a/02 Wireframes/P16 Termine/WF-P16 Kalender/WF-P16 Kalender.md b/02 Wireframes/P16 Termine/WF-P16 Kalender/WF-P16 Kalender.md new file mode 100644 index 00000000..cc607cb3 --- /dev/null +++ b/02 Wireframes/P16 Termine/WF-P16 Kalender/WF-P16 Kalender.md @@ -0,0 +1,130 @@ +--- +kind: wireframe-package +project: HiveGrid Screen Project +ticket: D-BOOK + D-P16 (Teil) +package: WF-P16 Kalender +typ: Flow +status: Validiert +subframe: "" +canvas: "WF-P16 Kalender.canvas" +--- + +# WF-P16 Kalender + +Status: Validiert (Screen-Liste) +Subframe-Link: - +Canvas: [Canvas]() +Stand: 2026-07-09 + +--- + +## 1. Kurzfassung + +Der HiveGrid-Kalender: Monats-/Wochen-Ansicht mit allen Terminen (Google-Sync, Buchungen, Plantafel, Projekt-Termine — je mit Herkunfts-Marker). **Davids Delta: ein vollwertiger Kalender**, nicht nur eine Lese-Sicht — Termine anlegen, Personen einladen (Team, Kontakte, freie E-Mails), Kollegen-Kalender als Verfügbarkeits-Overlay einblenden. Funktioniert eigenständig ohne Google; Google-Sync ist optional (Outlook → v2, in der UI als »bald verfügbar« sichtbar). + +--- + +## 2. Intent Check + +| Frage | Antwort | +|---|---| +| Ticket/Modul | D-BOOK (Kalender-View) + D-P16 (Kalender-Verbindungen), Dev-Pendant AK-P16a | +| Nutzer-Ziel | Alle Termine an einem Ort sehen, neue Termine planen ohne Doppelbelegung, Google bleibt synchron | +| Quellen geprüft | `ticket-docs/D-BOOK.md`, `ticket-docs/D-P16.md`, `designer-briefs/P16-…md`, HiveGrid Map | +| Sicher im Scope | Kalender-Grid (Monat/Woche), Termin anlegen/bearbeiten + Einladen, Termin-Detail, Sync-Einstellungen inkl. Konflikt-Verlauf | +| Sicher nicht im Scope | Buchungsseiten + Verfügbarkeits-Editor (WF-P16 Terminbuchung); Plantafel (WF-P9); Outlook-Sync (v2) | +| Unsichere Annahmen | Termin-Objekt + Einladungen sind NICHT im Dev-Ticket — Change-Request nötig (s. Offene Fragen) | + +--- + +## 3. Scope + +| Punkt | Inhalt | +|---|---| +| Startpunkt | Sidebar »Kalender« | +| Ziel | Termin geplant (mit Teilnehmern, ohne Kollision) · Google verbunden und synchron | +| Haupt-Archetyp | **BESPOKE** (Kalender-Grid) + A6 (Einstellungen) | +| Out of scope | Booking-Pages, Plantafel-Einsätze (nur als Quelle sichtbar), Outlook | + +--- + +## 4. Entscheidungen (validiert 2026-07-09) + +| Entscheidung | Status | Notiz | +|---|---|---| +| Vollwertiger Kalender statt reiner Lese-Sicht | genommen (Delta) | Termine anlegen/bearbeiten/löschen, »alles was man für einen Kalender braucht« — **Change-Request an Dev** (Ticket sah nur Grid + Marker vor) | +| Einladen: Team + Kontakte + freie E-Mails | genommen (Delta) | Interne sehen den Termin in ihrem HiveGrid-Kalender; Externe bekommen E-Mail mit Kalender-Datei (.ics); Zu-/Absagen sichtbar | +| Kollegen-Kalender einblendbar (Overlay) | genommen (Delta) | Vor dem Einladen prüfen, ob jemand Zeit hat; fremde private Google-Termine nur als »belegt« ohne Details | +| Eigener + fremde Kalender in einer Ansicht | genommen | Plantafel bleibt die Baustellen-/Einsatz-Sicht — der Kalender ist die Termin-Sicht | +| Verbinden-Einstieg direkt im Kalender | genommen | Hinweis-Karte solange nichts verbunden ist; danach über Zahnrad → Einstellungen. Kalender ist auch ohne Google voll nutzbar | +| Provider-Auswahl mit Outlook-Teaser | genommen | Google aktiv (v1), Outlook ausgegraut »bald verfügbar« — UI bleibt provider-neutral | +| Sync-Konflikt-Verlauf in den Einstellungen | genommen | Kleine Sektion (letzte 50, Klartext: was kollidierte, wie gelöst) — Muster wie Abgleich-Verlauf WF-P5; im Kalender nur dezenter Hinweis bei frischen Konflikten | +| OAuth-Assistent + Trennen-Modal aus WF-P5 wiederverwenden | genommen | Trennen mit »auch extern entfernen?«-Wahl | + +### Happy Path + +1. Mitarbeiter öffnet Kalender (Woche), sieht eigene Termine quell-farbig (Google / Buchung / Plantafel / Projekt / eigene). +2. Klick auf freien Slot → Termin-Dialog: Titel, Zeit, Teilnehmer (Kollege @Max, Kontakt Müller, freie E-Mail) — beim Hinzufügen zeigt der Dialog, wer belegt ist; optional Kollegen-Kalender als Overlay einblenden. +3. Speichern → Interne sehen den Termin sofort, Externe bekommen Mail mit .ics; Zu-/Absagen laufen am Termin auf. +4. Einmalig: »Google verbinden« → OAuth (P5-Muster) → Kalender auswählen, Sync-Richtung — ab da läuft alles synchron. + +### Meldungs-/Error-States (validiert) + +| Ereignis | UI-Form | Blockiert? | Bauen? | Copy/Notiz | +|---|---|---|---|---| +| Kein Google verbunden | Hinweis-Karte in S01 (dismissbar) + Bereich in S02 | nein | Nur dokumentieren | »Verbinde deinen Google-Kalender …« — Kalender funktioniert trotzdem | +| Verbindung kaputt (Token abgelaufen) | Re-Connect-Banner in S01 + S02 | nein | Nur dokumentieren | Klartext + »Neu verbinden«-Knopf | +| Sync-Konflikt (beidseitige Änderung) | Benachrichtigung (Glocke) + Eintrag im Konflikt-Verlauf (S02) | nein | Nur dokumentieren | »Termin X wurde in Google auf 14:00 verschoben — HiveGrid hat übernommen« (Last-Write-Wins) | +| Teilnehmer belegt | Inline-Warnung im Termin-Dialog | nein (bewusst buchbar) | in M01 enthalten | »Max ist 9–12 Uhr belegt« — analog Plantafel-Konflikt, aber ohne Block | +| Fremder privater Google-Termin | nur »belegt«-Block ohne Titel/Details | nein | Nur dokumentieren | Datenschutz-Vorgabe aus dem Ticket | +| Einladungs-Mail unzustellbar | Hinweis am Teilnehmer in M02 | nein | Nur dokumentieren | Bounce-Marker + erneut senden | +| Leerer Kalender | Leer-Zustand + CTA (Termin anlegen / Google verbinden) | nein | Nur dokumentieren | — | + +--- + +## 7. Validierte Screen-Liste + +| ID | Screen/Zustand | Typ | Umsetzung | Ort | Status | +|---|---|---|---|---|---| +| S01 | Kalender (Monat/Woche, Quell-Marker, Kollegen-Overlay) ⭐ | BESPOKE | Bauen | `/calendar` | Validiert | +| M01 | Termin anlegen/bearbeiten (Teilnehmer + Verfügbarkeits-Check) | Modal | Bauen | aus S01 (Slot-Klick / + Termin) | Validiert | +| M02 | Termin-Detail (Quelle + Absprung, Zu-/Absagen) | Popover | Bauen | Termin-Klick in S01 | Validiert | +| S02 | Kalender-Einstellungen & Sync (Provider, Kalender-Auswahl, Richtung, Konflikt-Verlauf) | Settings (A6) | Bauen | `/calendar/settings` | Validiert | +| D01 | OAuth-Assistent | Wiederverwenden | Wiederverwenden | WF-P5 M01 | Validiert | +| D02 | Trennen-Bestätigung (»auch extern entfernen?«) | Wiederverwenden | Wiederverwenden | WF-P5 M03 (+ Extern-Wahl) | Validiert | +| D03–D07 | Verbinden-Hinweis / Re-Connect / Konflikt / »belegt« / Leer (s. Tabelle) | Inline | Nur dokumentieren | — | Validiert | + +--- + +## 8. Grid + +```text +Sidebar -> S01 (Monat/Woche, Quellen: Google + Buchung + Plantafel + Projekt + eigene) +S01 -> M01 (Slot-Klick / »+ Termin«: anlegen mit Teilnehmern + Verfügbarkeits-Check) +S01 -> M02 (Termin-Klick: Detail-Popover, Quelle-Absprung, Zu-/Absagen) +M02 -> M01 (Bearbeiten, nur bei eigenen Terminen) +S01 -> S02 (Zahnrad / Verbinden-Hinweis: Provider, Kalender-Auswahl, Sync-Richtung, Konflikt-Verlauf) +S02 nutzt WF-P5: OAuth-Assistent (M01) + Trennen-Modal (M03) +Quell-Absprünge: Buchung -> WF-P16 Terminbuchung · Einsatz -> WF-P9 Plantafel · Projekt-Termin -> WF-P9 Projekt-Akte +``` + +--- + +## 9. Screen-Pakete + +- [[S01 Kalender]] · [[M01 Termin-Dialog]] · [[M02 Termin-Detail]] · [[S02 Kalender-Einstellungen]] + +--- + +## 10. Offene Fragen + +- **Change-Request an Dev (groß):** Eigenes Termin-Objekt + Einladungen (Team/Kontakt/freie E-Mail, .ics-Versand, Zu-/Absage-Tracking) + Free/Busy-Overlay — Ticket AK-P16a sah nur Lese-Grid + Sync vor; ENTITY-MODEL Domäne 9 braucht Ergänzung +- Zu-/Absage-Tracking bei internen Teilnehmern: eigener Status oder reicht angenommen/abgelehnt wie extern? → Dev +- Erinnerungen vor Terminen (Push/Mail)? → v2-Kandidat, mit Benachrichtigungen (P3) klären +- Tages-Ansicht zusätzlich zu Monat/Woche? → beim Subframe-Bau entscheiden + +## Handoff-Check + +- [x] Jeder Screen ist mit Umsetzung markiert. +- [x] Meldungs-/Error-State-Matrix ist ausgefüllt. +- [ ] Subframe-Link ist eingetragen. *(Subframe erst nach explizitem OK)* diff --git a/02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/M01 Verschieben.md b/02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/M01 Verschieben.md new file mode 100644 index 00000000..08c447b2 --- /dev/null +++ b/02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/M01 Verschieben.md @@ -0,0 +1,34 @@ +--- +kind: screen +project: HiveGrid Screen Project +ticket: D-BOOK + D-P16 (Teil) +package: WF-P16 Terminbuchung +id: M01 +screen: Verschieben (interner Slot-Picker) +typ: Modal +umsetzung: Bauen +status: Validiert +route: "aus S06" +subframe: "" +--- + +# M01 — Verschieben (interner Slot-Picker) + +## Ziel + +Das Büro verschiebt eine Buchung auf einen neuen freien Slot — der Kunde wird automatisch informiert. + +## Was das Tool können muss + +- Kompakter Slot-Picker (gleiche Logik wie die öffentliche Slot-Wahl: nur echte freie Zeiten) +- Alter Termin sichtbar durchgestrichen, neuer Termin zur Bestätigung +- Hinweis: »Der Kunde bekommt automatisch eine E-Mail mit dem neuen Termin« +- Optional: anderer Mitarbeiter zusammen mit neuem Slot (wenn der bisherige nicht kann) + +## Pattern / Archetyp + +A5 Modal; Reschedule-Slot-Picker aus dem Ticket. + +## Subframe + +- (erst nach explizitem OK) diff --git a/02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/M02 Stornieren.md b/02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/M02 Stornieren.md new file mode 100644 index 00000000..63de9eba --- /dev/null +++ b/02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/M02 Stornieren.md @@ -0,0 +1,34 @@ +--- +kind: screen +project: HiveGrid Screen Project +ticket: D-BOOK + D-P16 (Teil) +package: WF-P16 Terminbuchung +id: M02 +screen: Stornieren +typ: Modal +umsetzung: Bauen +status: Validiert +route: "aus S05/S06 (auch Bulk)" +subframe: "" +--- + +# M02 — Stornieren (Grund + Auto-Mail) + +## Ziel + +Bewusst absagen: mit Grund, und der Kunde erfährt es automatisch. + +## Was das Tool können muss + +- **Grund** (Pflicht, Freitext oder Auswahl) — landet im Verlauf der Buchung +- Hinweis: »Der Kunde bekommt automatisch eine Absage-Mail« (mit Grund-Vorschau) +- **Bulk-Variante** aus S05: zeigt Anzahl der Buchungen, ein Grund für alle, eine Mail je Kunde +- Bestätigen/Abbrechen; danach Status »storniert« + Termin verschwindet aus dem Kalender + +## Pattern / Archetyp + +A5 Confirm-Modal. + +## Subframe + +- (erst nach explizitem OK) diff --git a/02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/S01 Termin waehlen.md b/02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/S01 Termin waehlen.md new file mode 100644 index 00000000..ce44a910 --- /dev/null +++ b/02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/S01 Termin waehlen.md @@ -0,0 +1,37 @@ +--- +kind: screen +project: HiveGrid Screen Project +ticket: D-BOOK + D-P16 (Teil) +package: WF-P16 Terminbuchung +id: S01 +screen: Termin waehlen (Slot-Picker) +typ: BESPOKE-Public +umsetzung: Bauen +status: Validiert +route: "/b/{firma}/{slug}" +subframe: "" +--- + +# S01 — 🌐 Termin wählen (Slot-Picker) ⭐ + +## Ziel + +Der Kunde findet in Sekunden einen freien Termin — Calendly-Erlebnis, höchster Polish, mit dem Branding des Betriebs. + +## Was das Tool können muss + +- **Monats-Kalender** mit freien/belegten Tagen + **Zeitslot-Liste** zum gewählten Tag +- Nur echte freie Slots: berechnet aus Verfügbarkeit (S09), bestehenden Buchungen, Google-Kalender der Mitarbeiter, Puffer und Vorlauf — Vergabe v1: »erster freier Mitarbeiter« +- **Zeitzonen-fest** + Zeitzonen-Wechsler (Anzeige »Zeiten in deiner Zeitzone«) +- Kopf: Firmen-Logo/Branding, Name der Buchungsseite, Dauer, Beschreibung +- **iFrame-Embed-Variante** (gleiche Strecke ohne Seiten-Rahmen) +- Slot wählen → weiter zu S02 (Fragen) +- Zustände: keine freien Termine (Klartext), laden, Fehler, Seite nicht veröffentlicht (404 mit Branding) + +## Pattern / Archetyp + +BESPOKE-Public (höchster Polish laut Ticket). Bekommt in W4 die finale Politur (D-POLISH). + +## Subframe + +- (erst nach explizitem OK) diff --git a/02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/S02 Fragen.md b/02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/S02 Fragen.md new file mode 100644 index 00000000..b2f7c843 --- /dev/null +++ b/02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/S02 Fragen.md @@ -0,0 +1,35 @@ +--- +kind: screen +project: HiveGrid Screen Project +ticket: D-BOOK + D-P16 (Teil) +package: WF-P16 Terminbuchung +id: S02 +screen: Fragen (Kunden-Formular) +typ: BESPOKE-Public +umsetzung: Bauen +status: Validiert +route: "Schritt 2 nach Slot-Wahl" +subframe: "" +--- + +# S02 — 🌐 Fragen (Kunden-Formular) + +## Ziel + +Der Kunde gibt seine Daten ein — genau die Felder, die der Betrieb im Editor (S08) festgelegt hat. + +## Was das Tool können muss + +- Zusammenfassung des gewählten Slots oben (Datum, Zeit, Dauer) + »Termin ändern«-Link zurück zu S01 +- **Felder aus dem Editor** (Name, E-Mail Pflicht; weitere je Konfiguration — Text, Telefon, Auswahl, …), Pflichtfeld-Markierung, Klartext-Validierung +- **Spam-Schutz** (mCaptcha) vor dem Absenden +- Absenden = verbindlich buchen → S03; **Race-Schutz:** ist der Slot inzwischen weg → Klartext-Hinweis + zurück zur Slot-Wahl (kein Datenverlust der Eingaben) +- Datenschutz-Hinweis (kurz, Klartext) + +## Pattern / Archetyp + +BESPOKE-Public. Feld-Rendering v1 als einfache Feld-Liste (D-FORM liegt als Review-Draft vor und wird später nur eingesteckt). + +## Subframe + +- (erst nach explizitem OK) diff --git a/02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/S03 Bestaetigung.md b/02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/S03 Bestaetigung.md new file mode 100644 index 00000000..5ba88d89 --- /dev/null +++ b/02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/S03 Bestaetigung.md @@ -0,0 +1,35 @@ +--- +kind: screen +project: HiveGrid Screen Project +ticket: D-BOOK + D-P16 (Teil) +package: WF-P16 Terminbuchung +id: S03 +screen: Bestaetigung +typ: BESPOKE-Public +umsetzung: Bauen +status: Validiert +route: "Schritt 3 nach Buchung" +subframe: "" +--- + +# S03 — 🌐 Bestätigung + +## Ziel + +Der Kunde weiß: Termin steht. Und er kriegt ihn in seinen eigenen Kalender. + +## Was das Tool können muss + +- Große Bestätigung mit allen Termin-Daten (Datum, Zeit, Dauer, ggf. Ort/Hinweise aus der Buchungsseite) +- **Add-to-Calendar-Knopf (.ics)** — funktioniert auf Handy und Desktop +- Hinweis auf die **Bestätigungs-Mail** (mit Absage-/Verschiebe-Link → S04) +- Firmen-Branding wie in S01/S02 +- Was im Hintergrund passiert (nicht sichtbar, aber geplant): Anfrage im Lead-Eingang **oder** Aktivität am bekannten Kontakt (E-Mail-Match), Termin im Mitarbeiter-Kalender, Bestätigungs-Mail raus + +## Pattern / Archetyp + +BESPOKE-Public. + +## Subframe + +- (erst nach explizitem OK) diff --git a/02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/S04 Absagen Verschieben.md b/02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/S04 Absagen Verschieben.md new file mode 100644 index 00000000..aee9f90f --- /dev/null +++ b/02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/S04 Absagen Verschieben.md @@ -0,0 +1,35 @@ +--- +kind: screen +project: HiveGrid Screen Project +ticket: D-BOOK + D-P16 (Teil) +package: WF-P16 Terminbuchung +id: S04 +screen: Absagen/Verschieben per Link +typ: BESPOKE-Public +umsetzung: Bauen +status: Validiert (Davids Delta) +route: "Link aus der Bestaetigungs-Mail" +subframe: "" +--- + +# S04 — 🌐 Absagen/Verschieben per Link (Davids Delta) + +## Ziel + +»Wie bei Calendly«: Der Kunde verwaltet seinen Termin selbst — ohne anzurufen. **Change-Request an Dev** (steht nicht in AK-P16b). + +## Was das Tool können muss + +- Zeigt die Buchung (Datum, Zeit, Buchungsseite, Firmen-Branding) +- **Verschieben:** öffnet den Slot-Picker (wie S01) → neuer Slot → Bestätigung; alte Zeit wird frei, Büro + Kalender automatisch aktuell +- **Absagen:** mit optionalem Grund → Bestätigung; Büro bekommt Benachrichtigung, Termin verschwindet aus dem Kalender +- Zustände: Link abgelaufen/ungültig, Termin liegt in der Vergangenheit, Termin wurde bereits storniert — je Klartext ohne Interna +- Sicherheit: signierter Link aus der Mail (kein Login nötig) + +## Pattern / Archetyp + +BESPOKE-Public, nutzt den Slot-Picker aus S01 wieder. + +## Subframe + +- (erst nach explizitem OK) diff --git a/02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/S05 Buchungen-Liste.md b/02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/S05 Buchungen-Liste.md new file mode 100644 index 00000000..87c600c7 --- /dev/null +++ b/02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/S05 Buchungen-Liste.md @@ -0,0 +1,37 @@ +--- +kind: screen +project: HiveGrid Screen Project +ticket: D-BOOK + D-P16 (Teil) +package: WF-P16 Terminbuchung +id: S05 +screen: Buchungen-Liste +typ: A2 Liste +umsetzung: Bauen +status: Validiert +route: "/bookings" +subframe: "" +--- + +# S05 — Buchungen-Liste + +## Ziel + +Der Überblick: Was ist gebucht, was war, was wurde abgesagt. + +## Was das Tool können muss + +- **Reiter:** Anstehend · Vergangen · Storniert · Alle +- Spalten: Termin (Datum/Zeit), Endkunde (Name/E-Mail), Buchungsseite, zuständiger Mitarbeiter, Status +- **Filter:** Status, Mitarbeiter, Buchungsseite, Zeitraum +- **Mehrfach-Aktion:** Bulk-Storno (öffnet M02, eine Auto-Mail je Kunde) +- **CSV-Export** +- Zeilen-Klick → S06 (Detail); Schnell-Aktionen in der Zeile (stornieren, verschieben) +- Zustände: leer (Klartext + Link zu den Buchungsseiten), laden, Fehler + +## Pattern / Archetyp + +A2 Liste (sortierbar, paginiert) + Filter-Bar + Bulk-Toolbar — wie Lead-Inbox/Aufgaben-Liste. + +## Subframe + +- (erst nach explizitem OK) diff --git a/02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/S06 Buchungs-Detail.md b/02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/S06 Buchungs-Detail.md new file mode 100644 index 00000000..dc9256b4 --- /dev/null +++ b/02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/S06 Buchungs-Detail.md @@ -0,0 +1,34 @@ +--- +kind: screen +project: HiveGrid Screen Project +ticket: D-BOOK + D-P16 (Teil) +package: WF-P16 Terminbuchung +id: S06 +screen: Buchungs-Detail +typ: A3 Detail (Karteikarten-Muster) +umsetzung: Bauen +status: Validiert +route: "/bookings/:id" +subframe: "" +--- + +# S06 — Buchungs-Detail (Karteikarte) + +## Ziel + +Alles zu einer Buchung an einem Ort — nach dem validierten P6-Karteikarten-Muster. + +## Was das Tool können muss + +- **Links:** Termin-Daten (Datum, Zeit, Dauer, Buchungsseite), zuständiger Mitarbeiter, **verknüpfter Kontakt/Anfrage** (klickbar), Kalender-Sync-Badge (synchronisiert / steht aus / Fehler) +- **Rechts:** die **Antworten des Kunden** (Frage → Antwort, in Reihenfolge), **interne Notizen**, Verlauf (gebucht/verschoben/storniert mit wann + von wem) +- **Aktionen im Kopf:** als »stattgefunden« / »nicht erschienen« (No-Show) markieren · **Stornieren** (M02) · **Verschieben** (M01) · **anderem Mitarbeiter zuweisen** (Auswahl mit Verfügbarkeits-Hinweis) +- Status-Anzeige: gebucht / verschoben / storniert (mit Grund) / stattgefunden / No-Show + +## Pattern / Archetyp + +A3 Detail — Karteikarten-Grundgerüst aus WF-P6 (Zweispalter + Aktionen im Kopf). + +## Subframe + +- (erst nach explizitem OK) diff --git a/02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/S07 Buchungsseiten-Liste.md b/02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/S07 Buchungsseiten-Liste.md new file mode 100644 index 00000000..910ccda4 --- /dev/null +++ b/02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/S07 Buchungsseiten-Liste.md @@ -0,0 +1,34 @@ +--- +kind: screen +project: HiveGrid Screen Project +ticket: D-BOOK + D-P16 (Teil) +package: WF-P16 Terminbuchung +id: S07 +screen: Buchungsseiten-Liste +typ: A2 Liste +umsetzung: Bauen +status: Validiert +route: "/bookings/pages" +subframe: "" +--- + +# S07 — Buchungsseiten-Liste + +## Ziel + +Alle Buchungslinks des Betriebs auf einen Blick — mit dem Link zum Kopieren direkt in der Zeile. + +## Was das Tool können muss + +- Spalten: Titel, Link (Slug) mit **Kopier-Knopf**, zugewiesene Mitarbeiter, Buchungen (gesamt / letzte 30 Tage), Status (veröffentlicht / Entwurf) +- »+ Neue Buchungsseite« → S08 (Editor) +- Zeilen-Aktionen: bearbeiten (S08), Link kopieren, veröffentlichen/zurückziehen, duplizieren +- Zustände: leer (Klartext + CTA »Erste Buchungsseite anlegen«), laden, Fehler + +## Pattern / Archetyp + +A2 Liste. + +## Subframe + +- (erst nach explizitem OK) diff --git a/02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/S08 Buchungsseiten-Editor.md b/02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/S08 Buchungsseiten-Editor.md new file mode 100644 index 00000000..b22e80ee --- /dev/null +++ b/02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/S08 Buchungsseiten-Editor.md @@ -0,0 +1,38 @@ +--- +kind: screen +project: HiveGrid Screen Project +ticket: D-BOOK + D-P16 (Teil) +package: WF-P16 Terminbuchung +id: S08 +screen: Buchungsseiten-Editor +typ: A6 + A4 +umsetzung: Bauen +status: Validiert +route: "/bookings/pages/:id + /new" +subframe: "" +--- + +# S08 — Buchungsseiten-Editor + +## Ziel + +Hier entsteht der Buchungslink: alles einstellen, »was da auch bei Calendly stehen würde« (Davids Vorgabe). + +## Was das Tool können muss + +- **Grunddaten:** Name, Link (Slug), Beschreibung (sieht der Kunde) +- **Termin-Logik:** Dauer · **Puffer** (Mindestabstand zwischen Terminen) · **Vorlauf** (frühestens buchbar ab) · wie weit im Voraus buchbar +- **Mitarbeiter-Auswahl:** wer kommt in Frage (v1-Vergabe: erster freier — Hinweis im UI) +- **Kunden-Fragen:** Felder festlegen (Name + E-Mail Pflicht, weitere frei: Text, Telefon, Auswahl, …) und je Feld die **Zuordnung zum Lead-Eingang** (welches Kunden-Feld → welches Lead-Feld) — Davids Punkt: nichts abtippen +- **Bestätigungs-Mail:** Text bearbeiten (Markdown + Platzhalter wie {Vorname}, {Termin}); enthält automatisch den Absage-/Verschiebe-Link (S04) +- **Veröffentlichen:** Publish-Schalter · öffentliche URL mit Kopier-Knopf · **Embed-Code (iFrame)** · **QR-Code** +- Hinweis, welche Verfügbarkeits-Zeiten (S09) greifen +- Zustände: unveröffentlichte Änderungen, Validierungs-Fehler in Klartext + +## Pattern / Archetyp + +A6 Settings + A4 Formular; Embed/QR/Copy-Muster wie im Ticket. Fragen-Felder v1 als einfache Feld-Liste (D-FORM liegt als Review-Draft vor und wird später eingesteckt). + +## Subframe + +- (erst nach explizitem OK) diff --git a/02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/S09 Verfuegbarkeit.md b/02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/S09 Verfuegbarkeit.md new file mode 100644 index 00000000..9a0aa5bb --- /dev/null +++ b/02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/S09 Verfuegbarkeit.md @@ -0,0 +1,37 @@ +--- +kind: screen +project: HiveGrid Screen Project +ticket: D-BOOK + D-P16 (Teil) +package: WF-P16 Terminbuchung +id: S09 +screen: Verfuegbarkeit +typ: A6 + A4 +umsetzung: Bauen +status: Validiert +route: "/availability" +subframe: "" +--- + +# S09 — Verfügbarkeit (»Wann bin ich buchbar?«) + +## Ziel + +Jeder Mitarbeiter pflegt seine eigenen buchbaren Zeiten; Admins können alle einsehen und ändern. + +## Was das Tool können muss + +- **Wochen-Regeln:** je Wochentag mehrere Zeitfenster (z. B. Mo 8–12 + 13–17), an/aus je Tag +- **Ausnahme-Tage:** Datum wählen → ganz frei ODER Sonder-Zeitfenster (z. B. 24.12. nur 8–12) +- **Zeitzone** der Zeiten +- **Admin-Sicht:** Mitarbeiter-Wechsler oben — Admin sieht und bearbeitet alle, jeder andere nur sich selbst +- Hinweis: **welche Buchungsseiten** diese Zeiten nutzen (mit Links) +- Klartext-Zusammenspiel: »Buchbar = diese Zeiten MINUS bestehende Termine (inkl. Google-Kalender) MINUS Puffer« +- Zustände: noch nichts gepflegt (Leer-Zustand mit Standard-Vorschlag Mo–Fr 8–17), Validierungs-Fehler + +## Pattern / Archetyp + +A6 Settings + A4; Wochen-Verfügbarkeits-Editor + Ausnahmen-Editor (UI-Elemente aus dem Ticket). + +## Subframe + +- (erst nach explizitem OK) diff --git a/02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/V01 Termin waehlen Handy.md b/02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/V01 Termin waehlen Handy.md new file mode 100644 index 00000000..912668af --- /dev/null +++ b/02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/V01 Termin waehlen Handy.md @@ -0,0 +1,33 @@ +--- +kind: screen +project: HiveGrid Screen Project +ticket: D-BOOK + D-P16 (Teil) +package: WF-P16 Terminbuchung +id: V01 +screen: Termin waehlen — Handy +typ: BESPOKE-Public 📱 +umsetzung: Bauen +status: Validiert +route: "Variante von S01" +subframe: "" +--- + +# V01 — 🌐 Termin wählen — Handy 📱 + +## Ziel + +Kunden buchen oft am Handy — die wichtigste öffentliche Seite muss dort genauso gut funktionieren. + +## Was das Tool können muss + +- Gleiche Logik wie S01, gestapelt: erst Tag wählen (kompakter Monats-Kalender), dann Zeitslot-Liste +- Zeitzonen-Wechsler erreichbar, Branding oben +- Fragen (S02) und Bestätigung (S03) sind auf dem Handy dieselben Screens (einspaltig) — nur der Slot-Picker braucht den eigenen Nachweis-Frame + +## Pattern / Archetyp + +BESPOKE-Public, Mobile-Variante von S01. + +## Subframe + +- (erst nach explizitem OK) diff --git a/02 Wireframes/P16 Termine/WF-P16 Terminbuchung/WF-P16 Terminbuchung.canvas b/02 Wireframes/P16 Termine/WF-P16 Terminbuchung/WF-P16 Terminbuchung.canvas new file mode 100644 index 00000000..6b44b652 --- /dev/null +++ b/02 Wireframes/P16 Termine/WF-P16 Terminbuchung/WF-P16 Terminbuchung.canvas @@ -0,0 +1,34 @@ +{ + "nodes":[ + {"id":"md-paket","type":"file","file":"02 Wireframes/P16 Termine/WF-P16 Terminbuchung/WF-P16 Terminbuchung.md","x":-80,"y":-440,"width":460,"height":280}, + {"id":"group-public","type":"group","x":-80,"y":-100,"width":2260,"height":660,"label":"Kunde 🌐 (Calendly-Stil, höchster Polish)"}, + {"id":"group-intern","type":"group","x":2360,"y":-100,"width":1700,"height":940,"label":"Büro (intern)"}, + {"id":"group-modals","type":"group","x":4160,"y":-100,"width":1180,"height":380,"label":"Aktionen (Modals)"}, + {"id":"note-entry","type":"text","text":"# Einstieg Kunde\n\nBuchungslink / QR / iFrame-Embed.\n\nJede Buchung → Anfrage im Lead-Eingang ODER Aktivität am bekannten Kontakt (E-Mail-Match) + Termin im Mitarbeiter-Kalender.","x":0,"y":0,"width":360,"height":200}, + {"id":"s01","type":"file","file":"02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/S01 Termin waehlen.md","x":560,"y":0,"width":420,"height":200}, + {"id":"s02","type":"file","file":"02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/S02 Fragen.md","x":1120,"y":0,"width":420,"height":200}, + {"id":"s03","type":"file","file":"02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/S03 Bestaetigung.md","x":1680,"y":0,"width":420,"height":200}, + {"id":"v01","type":"file","file":"02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/V01 Termin waehlen Handy.md","x":560,"y":280,"width":420,"height":200}, + {"id":"s04","type":"file","file":"02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/S04 Absagen Verschieben.md","x":1680,"y":280,"width":420,"height":200}, + {"id":"s05","type":"file","file":"02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/S05 Buchungen-Liste.md","x":2440,"y":0,"width":420,"height":200}, + {"id":"s06","type":"file","file":"02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/S06 Buchungs-Detail.md","x":3000,"y":0,"width":420,"height":200}, + {"id":"s07","type":"file","file":"02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/S07 Buchungsseiten-Liste.md","x":2440,"y":280,"width":420,"height":200}, + {"id":"s08","type":"file","file":"02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/S08 Buchungsseiten-Editor.md","x":3000,"y":280,"width":420,"height":200}, + {"id":"s09","type":"file","file":"02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/S09 Verfuegbarkeit.md","x":2440,"y":560,"width":420,"height":200}, + {"id":"note-intern","type":"text","text":"# Slot-Berechnung\n\nBuchbar = Verfügbarkeit (S09) − bestehende Termine (inkl. Google-Kalender aus WF-P16 Kalender) − Puffer/Vorlauf.\n\nVergabe v1: erster freier Mitarbeiter.","x":3000,"y":560,"width":460,"height":200}, + {"id":"m01","type":"file","file":"02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/M01 Verschieben.md","x":4240,"y":0,"width":420,"height":200}, + {"id":"m02","type":"file","file":"02 Wireframes/P16 Termine/WF-P16 Terminbuchung/Screens/M02 Stornieren.md","x":4820,"y":0,"width":420,"height":200} + ], + "edges":[ + {"id":"e-entry","fromNode":"note-entry","fromSide":"right","toNode":"s01","toSide":"left","toEnd":"arrow","styleAttributes":{"path":"dotted"}}, + {"id":"e-s1s2","fromNode":"s01","fromSide":"right","toNode":"s02","toSide":"left","toEnd":"arrow","label":"weiter"}, + {"id":"e-s2s3","fromNode":"s02","fromSide":"right","toNode":"s03","toSide":"left","toEnd":"arrow","label":"buchen"}, + {"id":"e-handy","fromNode":"s01","fromSide":"bottom","toNode":"v01","toSide":"top","toEnd":"arrow","label":"Handy"}, + {"id":"e-mail","fromNode":"s03","fromSide":"bottom","toNode":"s04","toSide":"top","toEnd":"arrow","label":"Link in der Mail"}, + {"id":"e-open","fromNode":"s05","fromSide":"right","toNode":"s06","toSide":"left","toEnd":"arrow","label":"öffnen"}, + {"id":"e-editor","fromNode":"s07","fromSide":"right","toNode":"s08","toSide":"left","toEnd":"arrow","label":"öffnen"}, + {"id":"e-m01","fromNode":"s06","fromSide":"right","toNode":"m01","toSide":"left","toEnd":"arrow","label":"verschieben"}, + {"id":"e-m02","fromNode":"s06","fromSide":"right","toNode":"m02","toSide":"left","toEnd":"arrow","label":"stornieren"}, + {"id":"e-avail","fromNode":"note-intern","fromSide":"left","toNode":"s09","toSide":"right","toEnd":"arrow","styleAttributes":{"path":"dotted"}} + ] +} diff --git a/02 Wireframes/P16 Termine/WF-P16 Terminbuchung/WF-P16 Terminbuchung.md b/02 Wireframes/P16 Termine/WF-P16 Terminbuchung/WF-P16 Terminbuchung.md new file mode 100644 index 00000000..3885a626 --- /dev/null +++ b/02 Wireframes/P16 Termine/WF-P16 Terminbuchung/WF-P16 Terminbuchung.md @@ -0,0 +1,136 @@ +--- +kind: wireframe-package +project: HiveGrid Screen Project +ticket: D-BOOK + D-P16 (Teil) +package: WF-P16 Terminbuchung +typ: Flow +status: Validiert +subframe: "" +canvas: "WF-P16 Terminbuchung.canvas" +--- + +# WF-P16 Terminbuchung + +Status: Validiert (Screen-Liste) +Subframe-Link: - +Canvas: [Canvas]() +Stand: 2026-07-09 + +--- + +## 1. Kurzfassung + +Online-Terminbuchung »wie bei Calendly« (Davids Vorgabe): Der Betrieb erstellt Buchungslinks (Dauer, Puffer, Vorlauf, Mitarbeiter, Kunden-Fragen inkl. **Feld-Zuordnung zum Lead-Eingang**), der Kunde bucht sich über eine 🌐 öffentliche Seite selbst einen freien Slot (Zeitzonen-fest, einbettbar, höchster Polish) und kann per Link in der Bestätigungs-Mail **selbst absagen/verschieben** (Davids Delta). Intern: Buchungen-Liste + Detail mit Notizen, Stornieren mit Grund + Auto-Mail, Verschieben, Neu-Zuweisen. Jede Buchung landet als Anfrage im Lead-Eingang oder hängt sich an den bestehenden Kontakt. + +--- + +## 2. Intent Check + +| Frage | Antwort | +|---|---| +| Ticket/Modul | D-BOOK (öffentliche Booking-Page) + D-P16 (Buchungen-Verwaltung, Editor, Verfügbarkeit), Dev-Pendant AK-P16b | +| Nutzer-Ziel | Kunde: Termin buchen ohne Anruf · Büro: Buchungslinks pflegen, Buchungen verwalten, nichts abtippen | +| Quellen geprüft | `ticket-docs/D-BOOK.md`, `ticket-docs/D-P16.md`, `designer-briefs/P16-…md`, HiveGrid Map | +| Sicher im Scope | 🌐 Buchungsstrecke (3 Schritte + Handy), 🌐 Absagen/Verschieben (Delta), Buchungen-Liste/-Detail, Buchungsseiten-Liste/-Editor, Verfügbarkeit | +| Sicher nicht im Scope | Formular-Baukasten selbst (D-FORM liegt als Review-Draft vor; Fragen hier bleiben als einfache Feld-Liste); Kalender-Grid (WF-P16 Kalender); Round-Robin/Wunsch-Mitarbeiter (v2) | +| Unsichere Annahmen | Self-Service-Absage/Verschieben ist NICHT im Dev-Ticket — Change-Request nötig (s. Offene Fragen) | + +--- + +## 3. Scope + +| Punkt | Inhalt | +|---|---| +| Startpunkt | 🌐 Buchungslink (`/b/{firma}/{slug}`) · Sidebar »Buchungen« (intern) | +| Ziel | Kunde hat gebucht (ohne Doppel-Buchung) · Buchung ist im Lead-Eingang/am Kontakt · Büro hat den Überblick | +| Haupt-Archetyp | **BESPOKE-Public** (höchster Polish) + A2/A3 Listen/Detail + A6/A4 Editor/Settings | +| Out of scope | v2: Wunsch-Mitarbeiter/Round-Robin, personalisierte Links | + +--- + +## 4. Entscheidungen (validiert 2026-07-09) + +| Entscheidung | Status | Notiz | +|---|---|---| +| Buchungsstrecke = 3 eigene Screens + 1 Handy-Frame | genommen | Termin wählen → Fragen → Bestätigung; Handy-Frame vom Slot-Picker (Kunden buchen oft mobil) | +| Kunde kann per Mail-Link selbst absagen/verschieben | genommen (Delta) | »Wie Calendly« — eigene 🌐 Seite S04; **Change-Request an Dev** (AK-P16b kennt nur internes Cancel/Reschedule) | +| Buchungs-Detail als eigene Detailseite | genommen | Karteikarten-Muster (P6): links Termin + Verknüpfungen, rechts Antworten/Notizen, Aktionen im Kopf | +| Verfügbarkeit pflegt jeder selbst, Admin alle | genommen | Passt zur Pro-Nutzer-Kalender-Verbindung; Admin hat Mitarbeiter-Auswahl in S09 | +| Fragen-Felder mit Feld-Zuordnung zum Lead-Eingang | genommen | Davids Punkt: im Editor festlegen, welches Kunden-Feld in welches Lead-Feld wandert — nichts abtippen | +| Slot-Vergabe v1: »erster freier Mitarbeiter« | genommen | Ticket-Schnitt; Kunde wählt keinen Mitarbeiter | +| Slot-Berechnung nutzt Kalender + Verfügbarkeit | genommen | Arbeitszeiten (S09) + bestehende Buchungen + Google-Sync (WF-P16 Kalender) + Puffer + Vorlauf | + +### Happy Path + +1. Büro legt Buchungsseite »Wartungstermin (30 Min)« an: Dauer, Puffer, Vorlauf, Mitarbeiter, 3 Kunden-Fragen (+ Zuordnung zum Lead), Bestätigungs-Mail — veröffentlichen, Link/QR teilen. +2. Kunde öffnet den Link (oft am Handy): freie Slots, wählt einen, beantwortet die Fragen, bucht — Bestätigung + Kalender-Datei. +3. Die Buchung erscheint intern (Liste + Lead-Eingang bzw. am bekannten Kontakt); der Termin steht im Kalender des Mitarbeiters. +4. Kunde kann's doch nicht einrichten → klickt »verschieben« in seiner Mail, wählt neuen Slot — Büro und Kalender bleiben automatisch aktuell. + +### Meldungs-/Error-States (validiert) + +| Ereignis | UI-Form | Blockiert? | Bauen? | Copy/Notiz | +|---|---|---|---|---| +| Slot gerade vergeben (Race-Schutz) | Klartext-Hinweis in S02/S03-Übergang + zurück zur Slot-Wahl | ja | Nur dokumentieren | »Dieser Termin wurde gerade vergeben — bitte wähle einen anderen.« | +| Keine freien Termine | Leer-Zustand in S01 mit Klartext | nein | Nur dokumentieren | »Aktuell sind keine Termine frei …« + ggf. Kontakt-Hinweis | +| Seite nicht veröffentlicht / Link falsch | 🌐 404-Seite mit Branding | ja | Nur dokumentieren | keine Interna verraten | +| Spam-Schutz | mCaptcha in S02 | ja (Submit) | Nur dokumentieren | wie öffentliche Formulare | +| Absage-/Verschiebe-Link abgelaufen oder Termin vorbei | Klartext-Zustand in S04 | ja | Nur dokumentieren | »Dieser Termin liegt in der Vergangenheit …« | +| Storno intern | M02 mit Pflicht-Grund + Hinweis auf Auto-Mail | ja (Aktion) | Bauen (M02) | auch als Bulk aus S05 | +| Kalender-Sync-Status je Buchung | Badge in S06 | nein | Nur dokumentieren | synchronisiert / steht aus / Fehler | + +--- + +## 7. Validierte Screen-Liste + +| ID | Screen/Zustand | Typ | Umsetzung | Ort | Status | +|---|---|---|---|---|---| +| S01 | 🌐 Termin wählen (Slot-Picker) ⭐ | BESPOKE-Public | Bauen | `/b/{firma}/{slug}` | Validiert | +| S02 | 🌐 Fragen (Kunden-Formular) | BESPOKE-Public | Bauen | Schritt 2 | Validiert | +| S03 | 🌐 Bestätigung (+ Kalender-Datei) | BESPOKE-Public | Bauen | Schritt 3 | Validiert | +| V01 | 🌐 Termin wählen — Handy | BESPOKE-Public 📱 | Bauen | Variante von S01 | Validiert | +| S04 | 🌐 Absagen/Verschieben per Link | BESPOKE-Public | Bauen | Link aus Bestätigungs-Mail | Validiert (Delta) | +| S05 | Buchungen-Liste | A2 Liste | Bauen | `/bookings` | Validiert | +| S06 | Buchungs-Detail (Karteikarte) | A3 Detail | Bauen | `/bookings/:id` | Validiert | +| S07 | Buchungsseiten-Liste | A2 Liste | Bauen | `/bookings/pages` | Validiert | +| S08 | Buchungsseiten-Editor | A6 + A4 | Bauen | `/bookings/pages/:id`, `/new` | Validiert | +| S09 | Verfügbarkeit (Wochen-Zeiten + Ausnahmen) | A6 + A4 | Bauen | `/availability` | Validiert | +| M01 | Verschieben (interner Slot-Picker) | Modal | Bauen | aus S06 | Validiert | +| M02 | Stornieren (Grund + Auto-Mail) | Modal | Bauen | aus S05/S06 | Validiert | +| D01–D05 | Race/leer/404/Spam/Link-abgelaufen (s. Tabelle) | Inline/Zustand | Nur dokumentieren | — | Validiert | + +--- + +## 8. Grid + +```text +🌐 Kunde: Link/QR/Embed -> S01 (Slots, Zeitzone) -> S02 (Fragen + Spam-Schutz) -> S03 (Bestätigung + .ics) + S01 -> V01 (Handy-Variante) · Bestätigungs-Mail -> S04 (absagen/verschieben -> zurück zu S01-Slots) +Buchung erzeugt: Anfrage im Lead-Eingang ODER Activity am bekannten Kontakt (E-Mail-Match) + Termin im Kalender (WF-P16 Kalender) +Intern: Sidebar -> S05 (Liste, Tabs) -> S06 (Detail: Antworten, Notizen, stattgefunden/No-Show) + S06 -> M01 (verschieben) / M02 (stornieren, Grund + Auto-Mail) / neu zuweisen + S07 (Buchungsseiten) -> S08 (Editor: Dauer/Puffer/Vorlauf/Fragen+Lead-Zuordnung/Mail/Publish/QR+Embed) + S09 Verfügbarkeit (jeder selbst, Admin alle) — speist die Slot-Berechnung +``` + +--- + +## 9. Screen-Pakete + +- [[S01 Termin waehlen]] · [[S02 Fragen]] · [[S03 Bestaetigung]] · [[V01 Termin waehlen Handy]] · [[S04 Absagen Verschieben]] +- [[S05 Buchungen-Liste]] · [[S06 Buchungs-Detail]] · [[S07 Buchungsseiten-Liste]] · [[S08 Buchungsseiten-Editor]] · [[S09 Verfuegbarkeit]] · [[M01 Verschieben]] · [[M02 Stornieren]] + +--- + +## 10. Offene Fragen + +- **Change-Request an Dev:** 🌐 Self-Service-Absage/Verschieben per Mail-Link (S04) — AK-P16b kennt nur internes Cancel/Reschedule +- Fragen-Formular: v1 als einfache Feld-Liste im Editor (12 Feld-Typen) — der volle Formular-Baukasten (D-FORM) liegt als Review-Draft vor und wird hier später nur eingesteckt → mit Dev klären, dass die Schnittstelle passt +- Bulk-Storno: eine Mail je Kunde (automatisch) — Grenzen/Limits? → Dev +- Vorlauf-Feinheiten (max. wie weit im Voraus buchbar) → Standardwerte beim Subframe-Bau festlegen + +## Handoff-Check + +- [x] Jeder Screen ist mit Umsetzung markiert. +- [x] Meldungs-/Error-State-Matrix ist ausgefüllt. +- [ ] Subframe-Link ist eingetragen. *(Subframe erst nach explizitem OK)* diff --git a/02 Wireframes/P2 Auth/WF-P2 2FA Verwalten/WF-P2 2FA Verwalten.canvas b/02 Wireframes/P2 Auth/WF-P2 2FA Verwalten/WF-P2 2FA Verwalten.canvas index b69a42ed..ebb402f3 100644 --- a/02 Wireframes/P2 Auth/WF-P2 2FA Verwalten/WF-P2 2FA Verwalten.canvas +++ b/02 Wireframes/P2 Auth/WF-P2 2FA Verwalten/WF-P2 2FA Verwalten.canvas @@ -18,4 +18,4 @@ {"id":"edge-m02-inline","fromNode":"m02","fromSide":"bottom","toNode":"note-inline","toSide":"top","label":"Codefehler"}, {"id":"edge-m03-inline","fromNode":"m03","fromSide":"bottom","toNode":"note-inline","toSide":"top","label":"Codefehler"} ] -} +} \ No newline at end of file diff --git a/02 Wireframes/P2 Auth/WF-P2 Admin RBAC/WF-P2 Admin RBAC.canvas b/02 Wireframes/P2 Auth/WF-P2 Admin RBAC/WF-P2 Admin RBAC.canvas index e7b5b02b..83210cfd 100644 --- a/02 Wireframes/P2 Auth/WF-P2 Admin RBAC/WF-P2 Admin RBAC.canvas +++ b/02 Wireframes/P2 Auth/WF-P2 Admin RBAC/WF-P2 Admin RBAC.canvas @@ -4,18 +4,18 @@ {"id":"group-notes","type":"group","x":-120,"y":900,"width":4280,"height":430,"label":"Dokumentierte Zustaende und Backlog"}, {"id":"s02","type":"file","file":"02 Wireframes/P2 Auth/WF-P2 Admin RBAC/Screens/S02 Rollen und Berechtigungen.md","x":1560,"y":440,"width":440,"height":230}, {"id":"s01","type":"file","file":"02 Wireframes/P2 Auth/WF-P2 Admin RBAC/Screens/S01 Mitarbeiterliste.md","x":0,"y":45,"width":440,"height":230}, - {"id":"note-seed","type":"text","text":"# Rollenmodell\n\nAdmin ist die einzige Start-/Systemrolle.\n\nWeitere Rollen werden vom Admin selbst angelegt.\n\nRollen-Vorlagen sind Work in Progress und werden nicht als feste Rollen in der Liste gezeigt.","x":1520,"y":1020,"width":520,"height":240}, {"id":"m04a","type":"file","file":"02 Wireframes/P2 Auth/WF-P2 Admin RBAC/Screens/M04A Rolle anlegen.md","x":2120,"y":-110,"width":440,"height":230}, {"id":"m04b","type":"file","file":"02 Wireframes/P2 Auth/WF-P2 Admin RBAC/Screens/M04B Rolle duplizieren.md","x":2980,"y":-110,"width":440,"height":230}, {"id":"d03","type":"file","file":"02 Wireframes/P2 Auth/WF-P2 Admin RBAC/Screens/D03 Rolle in Nutzung blockiert.md","x":3600,"y":440,"width":440,"height":230}, - {"id":"note-navigation","type":"text","text":"# Navigation\n\nS01 ist der Einstieg unter `Settings -> Mitarbeiter`.\n\nDer Rollen-Tab S02 ist direkt ueber die Settings-Tabs erreichbar und zusaetzlich ueber den Dropdown-Eintrag `Rollen und Berechtigungen bearbeiten`.","x":-40,"y":1020,"width":520,"height":240}, {"id":"m02","type":"file","file":"02 Wireframes/P2 Auth/WF-P2 Admin RBAC/Screens/M02 Rolle bearbeiten.md","x":2680,"y":440,"width":440,"height":230}, - {"id":"note-d01","type":"text","text":"# D01 Rollen-Dropdown\n\nKein eigener Screen.\n\nDas Dropdown ist direkt in S01 enthalten:\n- Rolle wechseln\n- `Rollen und Berechtigungen bearbeiten` fuehrt zu S02\n- letzter Admin wird ueber V01 blockiert","x":720,"y":1015,"width":500,"height":250}, - {"id":"note-backlog","type":"text","text":"# Backlog / V2\n\nReassign beim Deaktivieren, Rollen-Loeschen mit Ersatzrolle, Gruppen, Rechtekatalog, Rollen-Audit, Review-Zyklus, Rechte-Matrix, Bulk-Aktionen, erweiterter Admin-Schutz.","x":3520,"y":1020,"width":600,"height":240}, - {"id":"note-permission","type":"text","text":"# Permission-Muster\n\nIm Rollen-Modal wird eine konkrete Permission-Zeile vorgebaut:\n\n`Mitarbeiter ansehen`\nBeschreibung darunter\nControl: Toggle `An/Aus`\n\nRead-only/Bearbeiten bleibt spaeteres Advanced Control, falls ein Recht nicht binaer ist.\n\nFinaler Rechtekatalog liegt bei Development.","x":2620,"y":1000,"width":560,"height":280}, {"id":"v01","type":"file","file":"02 Wireframes/P2 Auth/WF-P2 Admin RBAC/Screens/V01 Letzter Admin blockiert.md","x":2120,"y":-440,"width":440,"height":230}, {"id":"m01","type":"file","file":"02 Wireframes/P2 Auth/WF-P2 Admin RBAC/Screens/M01 Mitarbeiter einladen.md","x":1000,"y":-360,"width":440,"height":230}, - {"id":"m03","type":"file","file":"02 Wireframes/P2 Auth/WF-P2 Admin RBAC/Screens/M03 Mitarbeiter deaktivieren.md","x":1000,"y":5,"width":440,"height":230} + {"id":"m03","type":"file","file":"02 Wireframes/P2 Auth/WF-P2 Admin RBAC/Screens/M03 Mitarbeiter deaktivieren.md","x":1000,"y":5,"width":440,"height":230}, + {"id":"note-seed","type":"text","text":"# Rollenmodell\n\nAdmin ist die einzige Start-/Systemrolle.\n\nWeitere Rollen werden vom Admin selbst angelegt.\n\nRollen-Vorlagen sind Work in Progress und werden nicht als feste Rollen in der Liste gezeigt.","x":1520,"y":1020,"width":520,"height":240}, + {"id":"note-navigation","type":"text","text":"# Navigation\n\nS01 ist der Einstieg unter `Settings -> Mitarbeiter`.\n\nDer Rollen-Tab S02 ist direkt ueber die Settings-Tabs erreichbar und zusaetzlich ueber den Dropdown-Eintrag `Rollen und Berechtigungen bearbeiten`.","x":-40,"y":1020,"width":520,"height":240}, + {"id":"note-d01","type":"text","text":"# D01 Rollen-Dropdown\n\nKein eigener Screen.\n\nDas Dropdown ist direkt in S01 enthalten:\n- Rolle wechseln\n- `Rollen und Berechtigungen bearbeiten` fuehrt zu S02\n- letzter Admin wird ueber V01 blockiert","x":720,"y":1015,"width":500,"height":250}, + {"id":"note-backlog","type":"text","text":"# Backlog / V2\n\nReassign beim Deaktivieren, Rollen-Loeschen mit Ersatzrolle, Gruppen, Rechtekatalog, Rollen-Audit, Review-Zyklus, Rechte-Matrix, Bulk-Aktionen, erweiterter Admin-Schutz.","x":3520,"y":1020,"width":600,"height":240}, + {"id":"note-permission","type":"text","text":"# Permission-Muster\n\nIm Rollen-Modal wird eine konkrete Permission-Zeile vorgebaut:\n\n`Mitarbeiter ansehen`\nBeschreibung darunter\nControl: Toggle `An/Aus`\n\nRead-only/Bearbeiten bleibt spaeteres Advanced Control, falls ein Recht nicht binaer ist.\n\nFinaler Rechtekatalog liegt bei Development.","x":2620,"y":1000,"width":560,"height":280} ], "edges":[ {"id":"edge-s01-m01","fromNode":"s01","fromSide":"right","toNode":"m01","toSide":"left","label":"Mitarbeiter einladen"}, diff --git a/02 Wireframes/P2 Auth/WF-P2 Passwort Reset/WF-P2 Passwort Reset.md b/02 Wireframes/P2 Auth/WF-P2 Passwort Reset/WF-P2 Passwort Reset.md index 5ed30a27..fb4d3549 100644 --- a/02 Wireframes/P2 Auth/WF-P2 Passwort Reset/WF-P2 Passwort Reset.md +++ b/02 Wireframes/P2 Auth/WF-P2 Passwort Reset/WF-P2 Passwort Reset.md @@ -161,15 +161,15 @@ Wie viel Sicherheit braucht der Passwort-Reset, bevor der Nutzer ein neues Passw ## 7. Validierte Screen-Liste -| ID | Screen/Zustand | Typ | Umsetzung | Ort | Status | -|---|---|---|---|---|---| -| S01 | [[Screens/S01 Passwort Reset anfordern]] | Auth Form | Bauen | `/forgot-password` | Validiert | -| S02 | [[Screens/S02 Reset-Link gesendet]] | Auth Feedback | Bauen | nach S01 | Validiert | -| S03 | [[Screens/S03 2FA bestaetigen]] | Auth Security | Bauen | nach Reset-Link, falls 2FA aktiv | Validiert | -| V01 | [[Varianten/V01 Reset-Link ungueltig]] | Auth Blocker | Bauen | Link-Fehler | Validiert | -| V02 | [[Varianten/V02 2FA nicht verfuegbar]] | Auth Blocker | Bauen | Variante von S03 | In Subframe | -| M01 | [[Screens/M01 Admin kontaktieren]] | Auth Modal | Bauen | von V02 | In Subframe | -| M02 | [[Screens/M02 Wiederherstellungscode eingeben]] | Auth Modal | Bauen | von V02 | In Subframe | +| ID | Screen/Zustand | Typ | Umsetzung | Ort | Status | +| --- | ----------------------------------------------- | ------------- | --------- | -------------------------------- | ----------- | +| S01 | [[Screens/S01 Passwort Reset anfordern]] | Auth Form | Bauen | `/forgot-password` | Validiert | +| S02 | [[Screens/S02 Reset-Link gesendet]] | Auth Feedback | Bauen | nach S01 | Validiert | +| S03 | [[Screens/S03 2FA bestaetigen]] | Auth Security | Bauen | nach Reset-Link, falls 2FA aktiv | Validiert | +| V01 | [[Varianten/V01 Reset-Link ungueltig]] | Auth Blocker | Bauen | Link-Fehler | Validiert | +| V02 | [[Varianten/V02 2FA nicht verfuegbar]] | Auth Blocker | Bauen | Variante von S03 | In Subframe | +| M01 | [[Screens/M01 Admin kontaktieren]] | Auth Modal | Bauen | von V02 | In Subframe | +| M02 | [[Screens/M02 Wiederherstellungscode eingeben]] | Auth Modal | Bauen | von V02 | In Subframe | --- diff --git a/02 Wireframes/P3 Querschnitt/WF-P3 Audit Log/WF-P3 Audit Log.canvas b/02 Wireframes/P3 Querschnitt/WF-P3 Audit Log/WF-P3 Audit Log.canvas index 20f27878..0f72d898 100644 --- a/02 Wireframes/P3 Querschnitt/WF-P3 Audit Log/WF-P3 Audit Log.canvas +++ b/02 Wireframes/P3 Querschnitt/WF-P3 Audit Log/WF-P3 Audit Log.canvas @@ -1,161 +1,36 @@ { - "nodes": [ - { - "id": "group-main", - "type": "group", - "x": -80, - "y": -100, - "width": 1875, - "height": 420, - "label": "Hauptflow (ein Screen, zwei Reiter)" - }, - { - "id": "group-drawer", - "type": "group", - "x": -80, - "y": 460, - "width": 1875, - "height": 420, - "label": "Drawer und Modals" - }, - { - "id": "group-notes", - "type": "group", - "x": 1935, - "y": -100, - "width": 460, - "height": 980, - "label": "Regeln" - }, - { - "id": "note-entry", - "type": "text", - "text": "# Einstieg\n\nVerwaltung → Aktivitäts-Protokoll (`/admin/audit`).\n\nNur mit Recht `audit.read` sichtbar.", - "x": 0, - "y": 0, - "width": 360, - "height": 200 - }, - { - "id": "s01", - "type": "file", - "file": "02 Wireframes/P3 Querschnitt/WF-P3 Audit Log/Screens/S01 Protokoll-Ansicht.md", - "x": 595, - "y": 0, - "width": 420, - "height": 200 - }, - { - "id": "s02", - "type": "file", - "file": "02 Wireframes/P3 Querschnitt/WF-P3 Audit Log/Screens/S02 Exporte.md", - "x": 1275, - "y": 0, - "width": 420, - "height": 200 - }, - { - "id": "m01", - "type": "file", - "file": "02 Wireframes/P3 Querschnitt/WF-P3 Audit Log/Screens/M01 Detail-Drawer.md", - "x": 0, - "y": 560, - "width": 420, - "height": 200 - }, - { - "id": "m03", - "type": "file", - "file": "02 Wireframes/P3 Querschnitt/WF-P3 Audit Log/Screens/M03 Objekt-Verlauf-Drawer.md", - "x": 655, - "y": 560, - "width": 420, - "height": 200 - }, - { - "id": "m02", - "type": "file", - "file": "02 Wireframes/P3 Querschnitt/WF-P3 Audit Log/Screens/M02 Export anlegen.md", - "x": 1275, - "y": 560, - "width": 420, - "height": 200 - }, - { - "id": "note-mask", - "type": "text", - "text": "# Regeln (nur Doku)\n\n- Sensible Werte im Diff: nur „Wert geändert\"\n- Zeitraum max. 24 Monate — ältere nur per Export\n- Sortierung/Filter: Mitarbeiter, Zeitraum, Bereich, Änderungs-Art (validiert)\n- Actor-Marker: Mensch / Automation / MCP-Agent / Admin\n- M03 ist das wiederverwendbare „Verlauf\"-Muster für alle Detailseiten (Einstieg je Feature-Paket)", - "x": 1975, - "y": 0, - "width": 380, - "height": 560 - }, - { - "id": "md-paket", - "type": "file", - "file": "02 Wireframes/P3 Querschnitt/WF-P3 Audit Log/WF-P3 Audit Log.md", - "x": -80, - "y": -440, - "width": 460, - "height": 280 - } - ], - "edges": [ - { - "id": "e-entry", - "fromNode": "note-entry", - "fromSide": "right", - "toNode": "s01", - "toSide": "left", - "toEnd": "arrow" - }, - { - "id": "e-tab", - "fromNode": "s01", - "fromSide": "right", - "toNode": "s02", - "toSide": "left", - "toEnd": "arrow", - "label": "Reiter Exporte" - }, - { - "id": "e-row", - "fromNode": "s01", - "fromSide": "bottom", - "toNode": "m01", - "toSide": "top", - "toEnd": "arrow", - "label": "Zeile öffnen" - }, - { - "id": "e-verlauf", - "fromNode": "m01", - "fromSide": "right", - "toNode": "m03", - "toSide": "left", - "toEnd": "arrow", - "label": "Verlauf dieses Objekts" - }, - { - "id": "e-export", - "fromNode": "s02", - "fromSide": "bottom", - "toNode": "m02", - "toSide": "top", - "toEnd": "arrow", - "label": "Export anlegen" - }, - { - "id": "e-job", - "fromNode": "m02", - "fromSide": "right", - "toNode": "s02", - "toSide": "right", - "toEnd": "arrow", - "label": "Job angelegt", - "styleAttributes": { - "path": "long-dashed" - } - } - ] + "nodes":[ + {"id":"group-main","type":"group","x":-80,"y":-100,"width":1875,"height":420,"label":"Hauptflow (ein Screen, zwei Reiter)"}, + {"id":"group-drawer","type":"group","x":-80,"y":460,"width":1875,"height":420,"label":"Drawer und Modals"}, + {"id":"group-notes","type":"group","x":1935,"y":-100,"width":460,"height":980,"label":"Regeln"}, + {"id":"note-entry","type":"text","text":"# Einstieg\n\nVerwaltung → Aktivitäts-Protokoll (`/admin/audit`).\n\nNur mit Recht `audit.read` sichtbar.","x":0,"y":0,"width":360,"height":200}, + {"id":"s01","type":"file","file":"02 Wireframes/P3 Querschnitt/WF-P3 Audit Log/Screens/S01 Protokoll-Ansicht.md","x":595,"y":0,"width":420,"height":200}, + {"id":"s02","type":"file","file":"02 Wireframes/P3 Querschnitt/WF-P3 Audit Log/Screens/S02 Exporte.md","x":1275,"y":0,"width":420,"height":200}, + {"id":"m01","type":"file","file":"02 Wireframes/P3 Querschnitt/WF-P3 Audit Log/Screens/M01 Detail-Drawer.md","x":0,"y":560,"width":420,"height":200}, + {"id":"m03","type":"file","file":"02 Wireframes/P3 Querschnitt/WF-P3 Audit Log/Screens/M03 Objekt-Verlauf-Drawer.md","x":655,"y":560,"width":420,"height":200}, + {"id":"m02","type":"file","file":"02 Wireframes/P3 Querschnitt/WF-P3 Audit Log/Screens/M02 Export anlegen.md","x":1275,"y":560,"width":420,"height":200}, + {"id":"note-mask","type":"text","text":"# Regeln (nur Doku)\n\n- Sensible Werte im Diff: nur „Wert geändert\"\n- Zeitraum max. 24 Monate — ältere nur per Export\n- Sortierung/Filter: Mitarbeiter, Zeitraum, Bereich, Änderungs-Art (validiert)\n- Actor-Marker: Mensch / Automation / MCP-Agent / Admin\n- M03 ist das wiederverwendbare „Verlauf\"-Muster für alle Detailseiten (Einstieg je Feature-Paket)","x":1975,"y":0,"width":380,"height":560}, + {"id":"md-paket","type":"file","file":"02 Wireframes/P3 Querschnitt/WF-P3 Audit Log/WF-P3 Audit Log.md","x":-80,"y":-440,"width":460,"height":280} + ], + "edges":[ + {"id":"e-entry","fromNode":"note-entry","fromSide":"right","toNode":"s01","toSide":"left","toEnd":"arrow"}, + {"id":"e-tab","fromNode":"s01","fromSide":"right","toNode":"s02","toSide":"left","toEnd":"arrow","label":"Reiter Exporte"}, + {"id":"e-row","fromNode":"s01","fromSide":"bottom","toNode":"m01","toSide":"top","toEnd":"arrow","label":"Zeile öffnen"}, + {"id":"e-verlauf","fromNode":"m01","fromSide":"right","toNode":"m03","toSide":"left","toEnd":"arrow","label":"Verlauf dieses Objekts"}, + {"id":"e-export","fromNode":"s02","fromSide":"bottom","toNode":"m02","toSide":"top","toEnd":"arrow","label":"Export anlegen"}, + { + "id":"e-job", + "fromNode":"m02", + "fromSide":"right", + "toNode":"s02", + "toSide":"right", + "toEnd":"arrow", + "label":"Job angelegt", + "styleAttributes":{"path":"long-dashed"} + } + ], + "metadata":{ + "version":"1.0-1.0", + "frontmatter":{} + } } \ No newline at end of file diff --git a/02 Wireframes/P7 Leads/WF-P7 Leads/WF-P7 Leads.canvas b/02 Wireframes/P7 Leads/WF-P7 Leads/WF-P7 Leads.canvas index 46ec3ae8..5d7e8dba 100644 --- a/02 Wireframes/P7 Leads/WF-P7 Leads/WF-P7 Leads.canvas +++ b/02 Wireframes/P7 Leads/WF-P7 Leads/WF-P7 Leads.canvas @@ -16,11 +16,23 @@ {"id":"s05","type":"file","file":"02 Wireframes/P7 Leads/WF-P7 Leads/Screens/S05 Spam-Schutz.md","x":2500,"y":560,"width":420,"height":200} ], "edges":[ - {"id":"e-entry","fromNode":"note-entry","fromSide":"right","toNode":"s01","toSide":"left","toEnd":"arrow","styleAttributes":{"path":"dotted"}}, + { + "id":"e-entry", + "fromNode":"note-entry", + "fromSide":"right", + "toNode":"s01", + "toSide":"left", + "toEnd":"arrow", + "styleAttributes":{"path":"dotted"} + }, {"id":"e-open","fromNode":"s01","fromSide":"right","toNode":"s02","toSide":"left","toEnd":"arrow","label":"Anfrage öffnen"}, {"id":"e-stats","fromNode":"s01","fromSide":"top","toNode":"s03","toSide":"top","toEnd":"arrow","label":"Statistiken · Drill-Down zurück in gefilterte Inbox"}, {"id":"e-convert","fromNode":"s02","fromSide":"bottom","toNode":"m01","toSide":"top","toEnd":"arrow","label":"Zu Projekt (Freigabe = ja; aus S01 auch als Bulk)"}, {"id":"e-new","fromNode":"s01","fromSide":"bottom","toNode":"m02","toSide":"top","toEnd":"arrow","label":"+ Anfrage"}, {"id":"e-import","fromNode":"s01","fromSide":"bottom","toNode":"m03","toSide":"top","toEnd":"arrow","label":"Importieren (CSV)"} - ] -} + ], + "metadata":{ + "version":"1.0-1.0", + "frontmatter":{} + } +} \ No newline at end of file diff --git a/02 Wireframes/P8 Pipelines/WF-P8 Pipelines/WF-P8 Pipelines.canvas b/02 Wireframes/P8 Pipelines/WF-P8 Pipelines/WF-P8 Pipelines.canvas index 7866035b..175b744c 100644 --- a/02 Wireframes/P8 Pipelines/WF-P8 Pipelines/WF-P8 Pipelines.canvas +++ b/02 Wireframes/P8 Pipelines/WF-P8 Pipelines/WF-P8 Pipelines.canvas @@ -19,14 +19,43 @@ {"id":"s06","type":"file","file":"02 Wireframes/P8 Pipelines/WF-P8 Pipelines/Screens/S06 Verlust-Gruende.md","x":1240,"y":1220,"width":420,"height":200} ], "edges":[ - {"id":"e-entry","fromNode":"note-entry","fromSide":"right","toNode":"s01","toSide":"left","toEnd":"arrow","styleAttributes":{"path":"dotted"}}, + { + "id":"e-entry", + "fromNode":"note-entry", + "fromSide":"right", + "toNode":"s01", + "toSide":"left", + "toEnd":"arrow", + "styleAttributes":{"path":"dotted"} + }, {"id":"e-open","fromNode":"s01","fromSide":"right","toNode":"s02","toSide":"left","toEnd":"arrow","label":"Karte öffnen"}, {"id":"e-liste","fromNode":"s01","fromSide":"top","toNode":"s03","toSide":"top","toEnd":"arrow","label":"Umschalter Board ↔ Liste"}, {"id":"e-lost","fromNode":"s01","fromSide":"bottom","toNode":"m01","toSide":"top","toEnd":"arrow","label":"Verloren-Spalte (Grund Pflicht)"}, {"id":"e-won","fromNode":"s01","fromSide":"bottom","toNode":"m02","toSide":"top","toEnd":"arrow","label":"Gewonnen ohne Spalten-Regel"}, {"id":"e-verw","fromNode":"s02","fromSide":"bottom","toNode":"s04","toSide":"top","toEnd":"arrow","label":"Board verwalten (aus S01/S02)"}, {"id":"e-edit","fromNode":"s04","fromSide":"right","toNode":"s05","toSide":"left","toEnd":"arrow","label":"Edit-Mode öffnen"}, - {"id":"e-link","fromNode":"s07","fromSide":"top","toNode":"s04","toSide":"bottom","toEnd":"arrow","label":"öffnet dieselbe Seite","styleAttributes":{"path":"long-dashed"}}, - {"id":"e-set","fromNode":"note-settings","fromSide":"right","toNode":"s07","toSide":"left","toEnd":"arrow","styleAttributes":{"path":"dotted"}} - ] -} + { + "id":"e-link", + "fromNode":"s07", + "fromSide":"top", + "toNode":"s04", + "toSide":"bottom", + "toEnd":"arrow", + "label":"öffnet dieselbe Seite", + "styleAttributes":{"path":"long-dashed"} + }, + { + "id":"e-set", + "fromNode":"note-settings", + "fromSide":"right", + "toNode":"s07", + "toSide":"left", + "toEnd":"arrow", + "styleAttributes":{"path":"dotted"} + } + ], + "metadata":{ + "version":"1.0-1.0", + "frontmatter":{} + } +} \ No newline at end of file diff --git a/02 Wireframes/Screen Paket Tracker.md b/02 Wireframes/Screen Paket Tracker.md index 697f1086..b0a61598 100644 --- a/02 Wireframes/Screen Paket Tracker.md +++ b/02 Wireframes/Screen Paket Tracker.md @@ -46,9 +46,13 @@ Diese Datei ist der erste Ort, um zu sehen: | WF-P9 Projekt-Akte | D-P9 | Auftrag | Validiert | [MD]() | [Canvas]() | - | Subframe nach explizitem OK | Screens validiert 2026-07-09; Rechnung v1 neu; Board-Auftragsoptionen erweitern WF-P8 S04 | | WF-P9 Plantafel | D-PLAN | Auftrag | Validiert | [MD]() | [Canvas]() | - | Subframe nach explizitem OK | Screens validiert 2026-07-09; Monteur-Modus = 1 Frame Heute<->Woche; Konflikt-Dialog wird gebaut | | WF-P9 Abnahme | D-ABN | Auftrag | Validiert | [MD]() | [Canvas]() | - | Subframe nach explizitem OK | Screens validiert 2026-07-09; mehrstufiger Mobile-Workflow; immutable nach Abschluss | -| WF-P12 Flow-Editor | D-FLOW | Automationen | Validiert | [MD]() | [Canvas]() | - | Subframe nach explizitem OK | Screens validiert 2026-07-09; Katalog links/Canvas Mitte/Konfig rechts; Start-Auswahl mit Vorlagen; Langlaeufer | -| WF-P12 Verwaltung | D-P12 | Automationen | Validiert | [MD]() | [Canvas]() | - | Subframe nach explizitem OK | Screens validiert 2026-07-09; Klartext-Timeline + 24h-Undo; Konfig-Panels hier, Editor bettet ein | +| WF-P12 Flow-Editor | D-FLOW | Automationen | Subframe Draft | [MD]() | [Canvas]() | [Subframe]() | David Review / Varianten-Entscheidung | Screens validiert 2026-07-09; Subframe-Abgleich 2026-07-10: 2 S01-Varianten (Dreispalten kompakt vs. Gefuehrter Inspector), Entscheidung offen; Langlaeufer | +| WF-P12 Verwaltung | D-P12 | Automationen | Subframe Draft | [MD]() | [Canvas]() | [Subframe]() | David Review | Screens validiert 2026-07-09; Subframe-Abgleich 2026-07-10: alle 5 Screens gebaut, Details in Screen-MDs; Klartext-Timeline + 24h-Undo | | WF-D-FORM Form Builder | D-FORM | Termine & Formulare | Review | [MD]() | [Canvas]() | [Subframe]() | David Review / Handoff | Mitarbeiterinnen-Paket eingebaut 2026-07-09; S02 Simple Builder ist empfohlener Hauptfluss; Advanced-Canvas bleibt Referenz | +| WF-P16 Kalender | D-BOOK + D-P16 | Termine | Validiert | [MD]() | [Canvas]() | - | Subframe nach explizitem OK | Screens validiert 2026-07-09; Davids Delta: vollwertiger Kalender (anlegen + einladen inkl. freie E-Mails, Kollegen-Overlay) = CR an Dev; Sync im P5-Muster | +| WF-P16 Terminbuchung | D-BOOK + D-P16 | Termine | Validiert | [MD]() | [Canvas]() | - | Subframe nach explizitem OK | Screens validiert 2026-07-09; Calendly-Stil; Davids Delta: Kunde kann per Mail-Link absagen/verschieben (S04) = CR an Dev; Feld-Zuordnung zum Lead im Editor | +| WF-P16 E-Signatur | D-P16 + AK-P16d | Termine | Validiert | [MD]() | [Canvas]() | - | Subframe nach explizitem OK | Screens validiert 2026-07-09; Projekt-Kern statt Deal; SES-Eigenbau Default; Davids Delta: eigene Kunden-Signierseite S04 | +| WF-P15 Team-Chat | D-CHAT + AK-P15b | Kommunikation | Entwurf | [MD]() | [Canvas]() | - | Mit David validieren | Entwurf angelegt 2026-07-09; noch nicht gemeinsam geplant/validiert | --- @@ -180,15 +184,15 @@ Diese Datei ist der erste Ort, um zu sehen: | S02 | Abnahme Schritt 1: Pruefungen | WF-P9 Abnahme | BESPOKE-Mobile | Bauen | Validiert | `/projects/:id/acceptance/new` | [MD]() | - | [Canvas]() | | S03 | Abnahme Schritt 2: Maengel | WF-P9 Abnahme | BESPOKE-Mobile | Bauen | Validiert | Workflow | [MD]() | - | [Canvas]() | | S04 | Abnahme Schritt 3: Unterschriften | WF-P9 Abnahme | BESPOKE-Mobile | Bauen | Validiert | Workflow | [MD]() | - | [Canvas]() | -| S01 | Flow-Editor (Katalog/Canvas/Konfig) | WF-P12 Flow-Editor | BESPOKE | Bauen | Validiert | `/automations/:id/edit` | [MD]() | - | [Canvas]() | -| M01 | Probelauf (Dry-Run) | WF-P12 Flow-Editor | Modal | Bauen | Validiert | aus S01 | [MD]() | - | [Canvas]() | -| M02 | Schleifen-Warnung | WF-P12 Flow-Editor | Modal | Bauen | Validiert | aus S01 | [MD]() | - | [Canvas]() | -| M03 | Start-Auswahl (leer/Vorlage) | WF-P12 Flow-Editor | Modal | Bauen | Validiert | + Neue Automation | [MD]() | - | [Canvas]() | -| S01 | Automation-Liste | WF-P12 Verwaltung | Liste | Bauen | Validiert | `/automations` | [MD]() | - | [Canvas]() | -| S02 | Lauf-Liste | WF-P12 Verwaltung | Liste | Bauen | Validiert | `.../executions` | [MD]() | - | [Canvas]() | -| S03 | Lauf-Detail (Timeline, 24h-Undo) | WF-P12 Verwaltung | Detail | Bauen | Validiert | `.../executions/:id` | [MD]() | - | [Canvas]() | -| S04 | Vorlagen-Katalog | WF-P12 Verwaltung | Karten-Liste | Bauen | Validiert | `/automations/templates` | [MD]() | - | [Canvas]() | -| S05 | Baustein-Konfig-Panels (Muster) | WF-P12 Verwaltung | Compose-Panel | Bauen | Validiert | im Flow-Editor | [MD]() | - | [Canvas]() | +| S01 | Flow-Editor (Katalog/Canvas/Konfig) | WF-P12 Flow-Editor | BESPOKE | Bauen | Subframe Draft / Varianten-Entscheidung offen | `/automations/:id/edit` | [MD]() | [Subframe A]() · [B]() | [Canvas]() | +| M01 | Probelauf (Dry-Run) | WF-P12 Flow-Editor | Modal | Bauen | Subframe Draft / Review offen | aus S01 | [MD]() | [Subframe]() | [Canvas]() | +| M02 | Schleifen-Warnung | WF-P12 Flow-Editor | Modal | Bauen | Subframe Draft / Review offen | aus S01 | [MD]() | [Subframe]() | [Canvas]() | +| M03 | Start-Auswahl (leer/Vorlage) | WF-P12 Flow-Editor | Modal | Bauen | Subframe Draft / Review offen | + Neue Automation | [MD]() | [Subframe]() | [Canvas]() | +| S01 | Automation-Liste | WF-P12 Verwaltung | Liste | Bauen | Subframe Draft / Review offen | `/automations` | [MD]() | [Subframe]() | [Canvas]() | +| S02 | Lauf-Liste | WF-P12 Verwaltung | Liste | Bauen | Subframe Draft / Review offen | `.../executions` | [MD]() | [Subframe]() | [Canvas]() | +| S03 | Lauf-Detail (Timeline, 24h-Undo) | WF-P12 Verwaltung | Detail | Bauen | Subframe Draft / Review offen | `.../executions/:id` | [MD]() | [Subframe]() | [Canvas]() | +| S04 | Vorlagen-Katalog | WF-P12 Verwaltung | Karten-Liste | Bauen | Subframe Draft / Review offen | `/automations/templates` | [MD]() | [Subframe]() | [Canvas]() | +| S05 | Baustein-Konfig-Panels (Muster) | WF-P12 Verwaltung | Compose-Panel | Bauen | Subframe Draft / Review offen | im Flow-Editor | [MD]() | [Subframe]() | [Canvas]() | | S00 | Formulare verwalten | WF-D-FORM Form Builder | Formular-Zentrale | Bauen | Subframe Draft / Review offen | `/forms` | [MD]() | [Subframe]() | [Canvas]() | | E01 | S00 Keine Formulare | WF-D-FORM Form Builder | Empty State | Bauen | Subframe Draft / Review offen | `/forms` | [MD]() | [Subframe]() | [Canvas]() | | E02 | S00 Keine Treffer | WF-D-FORM Form Builder | Empty State | Bauen | Subframe Draft / Review offen | `/forms` | [MD]() | [Subframe]() | [Canvas]() | @@ -204,24 +208,53 @@ Diese Datei ist der erste Ort, um zu sehen: | S05 | Antworten Tabelle | WF-D-FORM Form Builder | Builder Tab | Bauen | Subframe Draft / Review offen | `/forms/:id/edit` | [MD]() | [Subframe]() | [Canvas]() | | E03 | S05 Keine Antworten | WF-D-FORM Form Builder | Empty State | Bauen | Subframe Draft / Review offen | `/forms/:id/edit` | [MD]() | [Subframe]() | [Canvas]() | | E04 | S05 Export Fehler | WF-D-FORM Form Builder | Error State | Bauen | Subframe Draft / Review offen | `/forms/:id/edit` | [MD]() | [Subframe]() | [Canvas]() | -| S01B | Gefuehrter Fragen Editor | WF-D-FORM Form Builder | Simple Builder Alternative | Alternative bauen | Subframe Draft / Review offen | `/forms/:id/edit` | [MD]() | [Subframe]() | [Canvas]() | | S01C | Website Formular Editor | WF-D-FORM Form Builder | Simple Builder Beispiel | Alternative bauen | Subframe Draft / Review offen | `/forms/:id/edit` | [MD]() | [Subframe]() | [Canvas]() | | S01D | Umfrage Editor | WF-D-FORM Form Builder | Simple Builder Beispiel | Alternative bauen | Subframe Draft / Review offen | `/forms/:id/edit` | [MD]() | [Subframe]() | [Canvas]() | | S01E | Internes Formular Editor | WF-D-FORM Form Builder | Simple Builder Beispiel | Alternative bauen | Subframe Draft / Review offen | `/forms/:id/edit` | [MD]() | [Subframe]() | [Canvas]() | -| ADV-S01 | Form-Builder Canvas | WF-D-FORM Form Builder | Bespoke Builder | Bauen | Subframe Draft / Review offen | `/forms/:id/edit` | [MD]() | [Subframe]() | [Canvas]() | -| ADV-S01-2 | Form-Builder Canvas 2 | WF-D-FORM Form Builder | Advanced / Referenz | Referenz behalten | Subframe Draft / aktuell nicht Hauptfluss | `/forms/:id/edit` | [MD]() | [Subframe]() | [Canvas]() | -| D07 | Logic Regel Detail | WF-D-FORM Form Builder | Panel-Zustand in S01 | Bauen | Subframe Draft / Review offen | in S01 Logic-Modus, kein eigener Screen | [MD]() | [Subframe]() | [Canvas]() | -| D08 | Action Detail mit Mapping | WF-D-FORM Form Builder | Panel-Zustand in S01 | Bauen | Subframe Draft / Review offen | in S01 Actions-Modus, kein eigener Screen | [MD]() | [Subframe]() | [Canvas]() | -| D09 | Publish Blocker Summary | WF-D-FORM Form Builder | Inline-Zustand in S01 | Bauen | Subframe Draft / Review offen | in S01 nach Klick auf Publish | [MD]() | [Subframe]() | [Canvas]() | -| D01 | Sharing Panel URL QR iFrame | WF-D-FORM Form Builder | Panel-Zustand in S01 | Nur dokumentieren | Vor Subframe / dokumentiert | in ADV-S01 Form-Builder Canvas, kein eigener Screen | [MD]() | - | [Canvas]() | -| D02 | Post-Submit Actions | WF-D-FORM Form Builder | Panel-Zustand in S01 | Nur dokumentieren | Vor Subframe / dokumentiert | in ADV-S01 Form-Builder Canvas, kein eigener Screen | [MD]() | - | [Canvas]() | -| D03 | Conditional Logic Editor | WF-D-FORM Form Builder | Panel-Zustand in S01 | Nur dokumentieren | Vor Subframe / dokumentiert | in ADV-S01 Form-Builder Canvas, kein eigener Screen | [MD]() | - | [Canvas]() | -| D06 | Feldkarten Menue | WF-D-FORM Form Builder | Inline-Zustand in S01 | Nur dokumentieren | Vor Subframe / dokumentiert | in ADV-S01 Form-Builder Canvas, kein eigener Screen | [MD]() | - | [Canvas]() | -| D10 | Builder Interaktionsfaelle | WF-D-FORM Form Builder | Case-Matrix in S01 | Nur dokumentieren | Vor Subframe / dokumentiert | in ADV-S01 Form-Builder Canvas, kein eigener Screen | [MD]() | - | [Canvas]() | -| M01 | Feld loeschen Confirm | WF-D-FORM Form Builder | Confirm Modal | Nur dokumentieren | Vor Subframe / dokumentiert | von S01 Feldkarte oder Inspector | [MD]() | - | [Canvas]() | -| M02 | Unpublish Confirm | WF-D-FORM Form Builder | Confirm Modal | Nur dokumentieren | Vor Subframe / dokumentiert | von S01 Header oder More-Menue | [MD]() | - | [Canvas]() | -| M03 | Archive Confirm | WF-D-FORM Form Builder | Confirm Modal | Nur dokumentieren | Vor Subframe / dokumentiert | von S01 More-Menue | [MD]() | - | [Canvas]() | -| M04 | Unsaved Changes Confirm | WF-D-FORM Form Builder | Confirm Modal / Browser Guard | Nur dokumentieren | Vor Subframe / dokumentiert | beim Verlassen von S01 mit Dirty State | [MD]() | - | [Canvas]() | +| D01 | Sharing Panel URL QR iFrame | WF-D-FORM Form Builder | Detailnotiz | Nur dokumentieren | Dokumentiert / nicht im Review-Canvas | Public-Link/Teilen-Kontext, kein eigener Screen | [MD]() | - | - | +| D02 | Post-Submit Actions | WF-D-FORM Form Builder | Detailnotiz | Nur dokumentieren | Dokumentiert / nicht im Review-Canvas | Nach-Absenden-Kontext, kein eigener Screen | [MD]() | - | - | +| D03 | Conditional Logic Editor | WF-D-FORM Form Builder | Detailnotiz | Nur dokumentieren | Dokumentiert / nicht im Review-Canvas | einfache Bedingungen, kein eigener Screen | [MD]() | - | - | +| D06 | Feldkarten Menue | WF-D-FORM Form Builder | Detailnotiz | Nur dokumentieren | Dokumentiert / nicht im Review-Canvas | Fragekarten-Menue, kein eigener Screen | [MD]() | - | - | +| D10 | Builder Interaktionsfaelle | WF-D-FORM Form Builder | Case-Matrix | Nur dokumentieren | Dokumentiert / nicht im Review-Canvas | Klick-/Edge-Cases, kein eigener Screen | [MD]() | - | - | +| M01 | Feld loeschen Confirm | WF-D-FORM Form Builder | Confirm Modal | Nur dokumentieren | Dokumentiert / nicht im Review-Canvas | von Fragekarte oder Inspector | [MD]() | - | - | +| M02 | Unpublish Confirm | WF-D-FORM Form Builder | Confirm Modal | Nur dokumentieren | Dokumentiert / nicht im Review-Canvas | von Publish/Public-Link-Kontext | [MD]() | - | - | +| M03 | Archive Confirm | WF-D-FORM Form Builder | Confirm Modal | Nur dokumentieren | Dokumentiert / nicht im Review-Canvas | von More-Menue | [MD]() | - | - | +| M04 | Unsaved Changes Confirm | WF-D-FORM Form Builder | Confirm Modal / Browser Guard | Nur dokumentieren | Dokumentiert / nicht im Review-Canvas | beim Verlassen mit Dirty State | [MD]() | - | - | +| S01 | Kalender (Monat/Woche, Kollegen-Overlay) | WF-P16 Kalender | BESPOKE Grid | Bauen | Validiert | `/calendar` | [MD]() | - | [Canvas]() | +| M01 | Termin anlegen/bearbeiten | WF-P16 Kalender | Modal | Bauen | Validiert | Slot-Klick / + Termin in S01 | [MD]() | - | [Canvas]() | +| M02 | Termin-Detail (Popover) | WF-P16 Kalender | Popover | Bauen | Validiert | Termin-Klick in S01 | [MD]() | - | [Canvas]() | +| S02 | Kalender-Einstellungen & Sync | WF-P16 Kalender | Settings (A6) | Bauen | Validiert | `/calendar/settings` | [MD]() | - | [Canvas]() | +| D01 | OAuth-Assistent (aus WF-P5 M01) | WF-P16 Kalender | Wizard | Wiederverwenden | Validiert | aus S02 | [MD]() | - | - | +| D02 | Trennen-Bestaetigung + Extern-Wahl (aus WF-P5 M03) | WF-P16 Kalender | Modal | Wiederverwenden | Validiert | aus S02 | [MD]() | - | - | +| S01 | Termin waehlen (Slot-Picker) 🌐 | WF-P16 Terminbuchung | BESPOKE-Public | Bauen | Validiert | `/b/{firma}/{slug}` | [MD]() | - | [Canvas]() | +| S02 | Fragen (Kunden-Formular) 🌐 | WF-P16 Terminbuchung | BESPOKE-Public | Bauen | Validiert | Schritt 2 | [MD]() | - | [Canvas]() | +| S03 | Bestaetigung (+ .ics) 🌐 | WF-P16 Terminbuchung | BESPOKE-Public | Bauen | Validiert | Schritt 3 | [MD]() | - | [Canvas]() | +| V01 | Termin waehlen — Handy 🌐📱 | WF-P16 Terminbuchung | BESPOKE-Public Mobile | Bauen | Validiert | Variante von S01 | [MD]() | - | [Canvas]() | +| S04 | Absagen/Verschieben per Link 🌐 | WF-P16 Terminbuchung | BESPOKE-Public | Bauen | Validiert (Delta) | Link aus Bestaetigungs-Mail | [MD]() | - | [Canvas]() | +| S05 | Buchungen-Liste | WF-P16 Terminbuchung | Liste | Bauen | Validiert | `/bookings` | [MD]() | - | [Canvas]() | +| S06 | Buchungs-Detail (Karteikarte) | WF-P16 Terminbuchung | Detail | Bauen | Validiert | `/bookings/:id` | [MD]() | - | [Canvas]() | +| S07 | Buchungsseiten-Liste | WF-P16 Terminbuchung | Liste | Bauen | Validiert | `/bookings/pages` | [MD]() | - | [Canvas]() | +| S08 | Buchungsseiten-Editor | WF-P16 Terminbuchung | Settings/Form | Bauen | Validiert | `/bookings/pages/:id` + `/new` | [MD]() | - | [Canvas]() | +| S09 | Verfuegbarkeit (Wochen-Zeiten + Ausnahmen) | WF-P16 Terminbuchung | Settings/Form | Bauen | Validiert | `/availability` | [MD]() | - | [Canvas]() | +| M01 | Verschieben (interner Slot-Picker) | WF-P16 Terminbuchung | Modal | Bauen | Validiert | aus S06 | [MD]() | - | [Canvas]() | +| M02 | Stornieren (Grund + Auto-Mail) | WF-P16 Terminbuchung | Modal | Bauen | Validiert | aus S05/S06 (auch Bulk) | [MD]() | - | [Canvas]() | +| S01 | Signaturen im Projekt | WF-P16 E-Signatur | Projekt-Sektion | Bauen | Validiert | Projekt-Detail | [MD]() | - | [Canvas]() | +| S02 | Signatur-Wizard | WF-P16 E-Signatur | Wizard | Bauen | Validiert | aus Projekt-Detail/S01 | [MD]() | - | [Canvas]() | +| S03 | Signatur-Detail & Status | WF-P16 E-Signatur | Detail | Bauen | Validiert | Signatur öffnen | [MD]() | - | [Canvas]() | +| S04 | Signieren per Link 🌐 | WF-P16 E-Signatur | BESPOKE-Public | Bauen | Validiert (Delta) | `/sign/:token` | [MD]() | - | [Canvas]() | +| V01 | Vor Ort unterschreiben 📱 | WF-P16 E-Signatur | BESPOKE-Mobile | Bauen | Validiert | Touch-Modus aus S02 | [MD]() | - | [Canvas]() | +| M01 | Signatur zurueckziehen | WF-P16 E-Signatur | Confirm-Modal | Bauen | Validiert | aus S03 | [MD]() | - | [Canvas]() | +| S01 | Chat-Index | WF-P15 Team-Chat | BESPOKE | Bauen | Entwurf | `/chat` | [MD]() | - | [Canvas]() | +| S02 | Channel-Detail | WF-P15 Team-Chat | BESPOKE | Bauen | Entwurf | `/chat/:id` | [MD]() | - | [Canvas]() | +| S03 | Thread-Detail | WF-P15 Team-Chat | BESPOKE Panel/Route | Bauen | Entwurf | `/chat/:id/thread/:thread_id` | [MD]() | - | [Canvas]() | +| S04 | Angepinnte Nachrichten | WF-P15 Team-Chat | Liste | Bauen | Entwurf | `/chat/:id/pinned` | [MD]() | - | [Canvas]() | +| S05 | Meine Mentions | WF-P15 Team-Chat | Liste | Bauen | Entwurf | `/chat/mentions` | [MD]() | - | [Canvas]() | +| S06 | Chat-Suche | WF-P15 Team-Chat | Suche/Liste | Bauen | Entwurf | `/chat/search` | [MD]() | - | [Canvas]() | +| S07 | Channel-Einstellungen | WF-P15 Team-Chat | Settings | Bauen | Entwurf | `/chat/:id/settings` | [MD]() | - | [Canvas]() | +| S08 | Admin-Chat-Uebersicht | WF-P15 Team-Chat | Admin Liste/Detail | Bauen | Entwurf | `/admin/chat` | [MD]() | - | [Canvas]() | +| V01 | Monteur-Handy-Chat 📱 | WF-P15 Team-Chat | BESPOKE-Mobile | Bauen | Entwurf | Variante von S02 | [MD]() | - | [Canvas]() | +| M01 | Neuer Channel / DM | WF-P15 Team-Chat | Modal | Bauen | Entwurf | aus S01/S02 | [MD]() | - | [Canvas]() | +| M02 | DM-Audit-Begruendung | WF-P15 Team-Chat | Modal | Bauen | Entwurf | aus S08 | [MD]() | - | [Canvas]() | --- @@ -308,9 +341,30 @@ Diese Datei ist der erste Ort, um zu sehen: | WF-P12 Flow-Editor | Editor-Anordnung | Genommen | Katalog links, Canvas Mitte, Konfig-Panel rechts (n8n-Muster); Katalog einklappbar. | Validiert 2026-07-09 | | WF-P12 Flow-Editor | Anlegen-Flow | Genommen | Start-Auswahl-Modal: leer ODER Vorlage (Katalog eingebettet). | Validiert 2026-07-09 | | WF-D-FORM Form Builder | Paket eingebaut | Genommen | Mitarbeiterinnen-Paket wurde als `WF-D-FORM Form Builder` in den Vault uebernommen. | Ursprungsname war `WF-P16 Form Builder`; wegen bestehendem `P16 Termine` bewusst auf D-FORM normalisiert | -| WF-D-FORM Form Builder | Simple Builder als Hauptfluss | Review | S02 ist der empfohlene Google-Forms-nahe Haupteditor fuer Handwerksbetriebe; ADV-S01/ADV-S01-2 bleiben Referenz. | David prueft Richtung und Subframe-Feinschliff | +| WF-D-FORM Form Builder | Simple Builder als Hauptfluss | Review | S02 ist der empfohlene Google-Forms-nahe Haupteditor fuer Handwerksbetriebe; S01B und die alten Advanced-Screens wurden aus dem Obsidian-Handoff entfernt. | David prueft Richtung und Subframe-Feinschliff | | WF-D-FORM Form Builder | Lead-Feldzuweisung | Review | S02L ordnet Formularfragen per Dropdown Lead-Feldern zu; E05 deckt fehlende oder unmoegliche Zuweisungen ab. | Davids Feedback aus dem Mitarbeiterinnen-Paket uebernommen | | WF-D-FORM Form Builder | Scope-Abgrenzung | Genommen | D-FORM umfasst Builder, Public Form, Public States, Design und Antworten-Tabelle; Booking, Kalender, Signatur und HR bleiben eigene Pakete. | Handoff-Datei angelegt | +| WF-P16 Kalender | Vollwertiger Kalender | Genommen (Delta) | Termine anlegen/bearbeiten/loeschen statt reiner Lese-Sicht; eigenes Termin-Objekt noetig. | Change-Request an Dev (AK-P16a); validiert 2026-07-09 | +| WF-P16 Kalender | Einladen: Team + Kontakte + freie E-Mails | Genommen (Delta) | Interne sehen Termin im HiveGrid-Kalender, Externe bekommen Mail + .ics; Zu-/Absagen sichtbar. | Teil des CR an Dev | +| WF-P16 Kalender | Kollegen-Overlay (Verfuegbarkeit) | Genommen (Delta) | Andere Kalender einblendbar; fremde private Google-Termine nur als »belegt«. | Teil des CR an Dev | +| WF-P16 Kalender | Verbinden-Einstieg im Kalender | Genommen | Hinweis-Karte solange nichts verbunden, danach Zahnrad -> `/calendar/settings`; kein Admin-Umweg (Verbindung pro Nutzer). | OAuth/Trennen aus WF-P5 wiederverwendet | +| WF-P16 Kalender | Konflikt-Verlauf-Ort | Genommen | Sektion in den Kalender-Einstellungen (letzte 50, Klartext); zusaetzlich Benachrichtigung je Aufloesung. | Muster wie Abgleich-Verlauf WF-P5 | +| WF-P16 Kalender | Outlook-Teaser | Genommen | Provider-Auswahl zeigt Outlook ausgegraut »bald verfuegbar« (v1 nur Google aktiv). | UI bleibt provider-neutral | +| WF-P16 Terminbuchung | Kunden-Strecke als 3 Screens + Handy-Frame | Genommen | Termin waehlen -> Fragen -> Bestaetigung als eigene Frames; Handy-Frame vom Slot-Picker. | Validiert 2026-07-09 | +| WF-P16 Terminbuchung | Self-Service absagen/verschieben per Mail-Link | Genommen (Delta) | »Wie Calendly«: eigene oeffentliche Seite S04, signierter Link, kein Login. | Change-Request an Dev (AK-P16b) | +| WF-P16 Terminbuchung | Buchungs-Detail als eigene Detailseite | Genommen | Karteikarten-Muster (P6): links Termin + Verknuepfungen, rechts Antworten + Notizen. | Validiert 2026-07-09 | +| WF-P16 Terminbuchung | Verfuegbarkeit: jeder selbst + Admin alle | Genommen | Passt zur Pro-Nutzer-Kalender-Verbindung; Admin-Mitarbeiter-Wechsler in S09. | Validiert 2026-07-09 | +| WF-P16 Terminbuchung | Kunden-Fragen mit Feld-Zuordnung zum Lead | Genommen | Im Editor je Feld festlegen, wohin es im Lead-Eingang wandert (nichts abtippen). | Davids Punkt 2026-07-09 | +| WF-P16 Terminbuchung | Fragen-Felder v1 ohne Formular-Baukasten | Genommen | Einfache Feld-Liste im Editor; D-FORM liegt jetzt als Review-Draft vor und wird spaeter ueber eine klare Schnittstelle eingesteckt. | Schnittstelle mit Dev klaeren | +| WF-P16 E-Signatur | Signatur haengt am Projekt-Kern | Genommen | `ProjektSignatureDocument`/`Signer`; kein Deal-Objekt. | Pivot aus AK-P16d | +| WF-P16 E-Signatur | SES-Eigenbau als Default | Genommen | Fern-Link + OTP und Touch vor Ort; PandaDoc nur BYOK-Opt-in, wenn verbunden. | Validiert 2026-07-09 | +| WF-P16 E-Signatur | Kunden-Signierseite als eigener Screen | Genommen (Delta) | S04 `/sign/:token` mit Branding, OTP, PDF-Pruefung, Intent und Signatur. | D-P16 Screen-Luecke geschlossen | +| WF-P16 E-Signatur | Vor-Ort-Signatur als eigener Mobile-Frame | Genommen | Geraet wird dem Kunden uebergeben; keine interne Navigation sichtbar. | Verwandschaft zu WF-P9 Abnahme | +| WF-P15 Team-Chat | Chat als eigener Bespoke-Flow | Entwurf | Real-Time-Verlauf, Composer, Presence, Threads und Reconnect passen nicht in Standard-A2/A3. | Mit David validieren | +| WF-P15 Team-Chat | Thread als eigener Nachweis-Frame | Entwurf | Umsetzung spaeter als Side-Panel oder Route moeglich; Planung braucht einen Frame. | Mit David validieren | +| WF-P15 Team-Chat | Projekt-Channels entity-linked | Entwurf | Projektmitglieder werden synchronisiert; Einstieg im Projekt-Detail, View bleibt S02. | Schnittstelle zu P8/P9 klaeren | +| WF-P15 Team-Chat | Admin-DM-Audit mit Begruendung | Entwurf | DM-Einsicht nur ueber M02, Banner im geoeffneten DM, Audit-Log. | AK-P15b Vorgabe; mit David validieren | +| WF-P15 Team-Chat | Monteur-Handy-Frame | Entwurf | Vereinfachte mobile View mit Foto-Direktupload, Push/Offline-Hinweis. | Mit David validieren | --- @@ -337,6 +391,17 @@ Diese Liste sammelt Punkte, die in einzelnen Paketen als `Backlog`, `Spaeter` od | WF-P2 Konto/Workspace loeschen | Voller DSGVO-/Export-Wizard | Backlog | Datenexport-Step, Reauth, Confirm Phrase, Countdown und Legal/Billing-Details spaeter | | WF-P2 Workspace Settings | Workspace-Settings ausbauen | Backlog | Voraussetzung fuer Konto-/Workspace-Loeschflows; Bereich ist noch nicht gebaut | | WF-P2 Admin RBAC | V2-Backlog | Dokumentiert | Reassign, Rollen-Loeschen mit Ersatzrolle, Gruppen, Rechtekatalog, Rollen-Audit, Rollen-Review, Templates, Rechte-Matrix, Bulk-Aktionen, erweiterter Admin-Schutz | +| WF-P16 Kalender | Outlook-Sync | Backlog (v2) | Interface bleibt provider-agnostisch; in der UI als Teaser sichtbar | +| WF-P16 Kalender | Termin-Erinnerungen (Push/Mail) | Backlog/Klaeren | Mit Benachrichtigungen (P3) klaeren; v2-Kandidat | +| WF-P16 Kalender | Tages-Ansicht | Backlog/Klaeren | Zusaetzlich zu Monat/Woche? Beim Subframe-Bau entscheiden | +| WF-P16 Terminbuchung | Wunsch-Mitarbeiter / Round-Robin | Backlog (v2) | v1 nur »erster freier Mitarbeiter«; Kunde waehlt keinen Mitarbeiter | +| WF-P16 Terminbuchung | Personalisierte Buchungslinks | Backlog (v2) | v1 nur URL + QR + iFrame | +| WF-P16 E-Signatur | Freier PDF-Feldplatzierer | Backlog (v2) | v1 nutzt standardisierte Signatur-/Certificate-Platzierung; kein PDF-Tagger | +| WF-P16 E-Signatur | QES/D-Trust | Backlog (v2) | v1 SES reicht fuer SHK; QES nur spaeter fuer Sonderfaelle | +| WF-P16 E-Signatur | Automatische Reminder-Regeln | Backlog/Klaeren | v1 manuelle Erinnerung; spaeter mit Benachrichtigungen/Automationen klaeren | +| WF-P15 Team-Chat | WhatsApp / Unified Inbox | Backlog (v2) | Team-Chat ist intern; externe Kundenkommunikation bleibt ausserhalb v1 | +| WF-P15 Team-Chat | HalloPetra / KI-Chat | Backlog (v2) | Nicht Teil von D-CHAT; Hilfe-Chat/Telefonassistent sind eigene Themen | +| WF-P15 Team-Chat | Voller Read-Receipt-Detaildialog | Backlog/Klaeren | v1 zeigt »X von Y gelesen«; Detail-Liste nur bei Bedarf | --- diff --git a/05 HiveGrid Map/HiveGrid Map.canvas b/05 HiveGrid Map/HiveGrid Map.canvas index 77d76a62..f057d464 100644 --- a/05 HiveGrid Map/HiveGrid Map.canvas +++ b/05 HiveGrid Map/HiveGrid Map.canvas @@ -2,57 +2,53 @@ "nodes":[ {"id":"904c5ece127e2e4c","type":"group","x":5497,"y":3020,"width":1540,"height":1400,"label":"VERWALTUNG (WORKSPACE-EINSTELLUNGEN)"}, {"id":"88792c50986c93db","type":"group","x":1950,"y":960,"width":2587,"height":740,"color":"2","label":"VERTRIEB — vom Anruf zum Auftrag"}, - {"id":"77c19412215a977c","type":"group","x":5037,"y":960,"width":2260,"height":740,"color":"2","label":"AUFTRAG & BAUSTELLE"}, - {"id":"41b8d85d3bb8d47d","type":"group","x":-160,"y":0,"width":1850,"height":740,"color":"6","label":"BASIS — gilt für alle Features"}, {"id":"ca7223940f6e1d29","type":"group","x":2520,"y":2080,"width":2317,"height":740,"label":"TERMINE & FORMULARE"}, + {"id":"77c19412215a977c","type":"group","x":5037,"y":960,"width":2260,"height":740,"color":"2","label":"AUFTRAG & BAUSTELLE"}, {"id":"e97e13901e405fcd","type":"group","x":5037,"y":2080,"width":1540,"height":740,"label":"KOMMUNIKATION"}, {"id":"8acc5d7d61317076","type":"group","x":2110,"y":3020,"width":1450,"height":740,"label":"AUTOMATIONEN"}, {"id":"070f258760fab4e9","type":"group","x":760,"y":3020,"width":1190,"height":740,"label":"AUFGABEN & DATEIEN — stecken in jedem Feature"}, - {"id":"db7d9db3cd7939b0","type":"group","x":3997,"y":0,"width":1040,"height":740,"label":"SUCHE & DASHBOARD"}, - {"id":"c3172228336521df","type":"group","x":7197,"y":3020,"width":540,"height":1400,"label":"PLATTFORM-ADMIN — unsere interne Verwaltung"}, {"id":"00d6b92d3845c185","type":"group","x":1160,"y":2080,"width":1190,"height":520,"label":"E-MAIL-MARKETING"}, + {"id":"41b8d85d3bb8d47d","type":"group","x":-160,"y":0,"width":1850,"height":740,"color":"6","label":"BASIS — gilt für alle Features"}, + {"id":"db7d9db3cd7939b0","type":"group","x":3997,"y":0,"width":1040,"height":740,"label":"SUCHE & DASHBOARD"}, + {"id":"c3172228336521df","type":"group","x":7297,"y":3020,"width":540,"height":1400,"label":"PLATTFORM-ADMIN — unsere interne Verwaltung"}, {"id":"7a26f88c260892eb","type":"group","x":1950,"y":0,"width":557,"height":740,"color":"3","label":"WEBSITE 🌐"}, {"id":"2025c7621bb63c98","type":"group","x":2737,"y":0,"width":540,"height":740,"color":"4","label":"LOGIN & ZUGÄNGE"}, - {"id":"3c6e5b1ea4a27913","type":"group","x":7977,"y":3020,"width":540,"height":740,"label":"HR (MITARBEITER)"}, - {"id":"d9868dadb782f253","type":"text","text":"## Kontakte & Organisationen\n**Ticket:** `D-CRM` · **Status:** 🟡 vorgeplant (Screens validiert, Subframe offen)\n**Wo:** `/contacts`\n\n**Was drin ist:** Kontakt-Liste, Organisations-Liste, Kontakt-Detailseite (die Vorlage für ALLE Detailseiten), eigene Felder, Tags, Duplikate zusammenführen\n\n**So funktioniert's:** Ein Kontakt ist Privatperson oder Organisation und hat einen Status (Kontakt → Kunde → Bestandskunde). Auf der Detailseite sieht man alles: Projekte, Mails, Anrufe, Aufgaben, Dateien, Notizen.","x":3077,"y":1040,"width":460,"height":360,"color":"5"}, - {"id":"a-kontakte-pkg","type":"file","file":"02 Wireframes/P6 Kontakte/WF-P6 Kontakte und Organisationen/WF-P6 Kontakte und Organisationen.canvas","x":3077,"y":1440,"width":460,"height":220}, - {"id":"e10a35560a461111","type":"text","text":"## Kanban-Board & Pipelines (Projekte)\n**Ticket:** `D-P8` + `D-KANBAN` · **Status:** 🟡 vorgeplant (Screens validiert, Subframe offen)\n**Wo:** `/projects` + Admin\n\n**Was drin ist:** Kanban-Board mit Spalten je Status (Karten ziehen = Status wechseln), Projekt-Liste, Projekt-Detailseite, Pipeline-Einstellungen (Spalten, Rechte, Pflichtfelder), Verlust-Gründe\n\n**So funktioniert's:** Ein Projekt wandert durch Boards — »Vertrieb« und »Auftrag« sind nur zwei Boards für denselben Datensatz. Gewonnen = wechselt aufs Auftrags-Board. Wer welche Felder sieht (z. B. Umsatz), ist einstellbar.","x":4037,"y":1040,"width":460,"height":360,"color":"2"}, - {"id":"a-pipelines-pkg","type":"file","file":"02 Wireframes/P8 Pipelines/WF-P8 Pipelines/WF-P8 Pipelines.canvas","x":4037,"y":1440,"width":460,"height":220}, + {"id":"3c6e5b1ea4a27913","type":"group","x":7297,"y":2080,"width":540,"height":740,"label":"HR (MITARBEITER)"}, {"id":"8272764aa65537b0","type":"text","text":"## Lead-Eingang (Anfragen)\n**Ticket:** `D-P7` · **Status:** 🟡 vorgeplant (Screens validiert, Subframe offen)\n**Wo:** `/leads`\n\n**Was drin ist:** Eingangs-Liste aller Anfragen (mit Quelle, Spam-Verdacht, Duplikat-Hinweis), Anfrage-Detail, »Zu Projekt machen«, Statistiken, Quellen-Verwaltung, Spam-Schutz-Einstellungen\n\n**So funktioniert's:** Alle Anfragen (Website, Formulare, Anrufe, Buchungen) landen hier. Wer sie bearbeiten darf (Freigabe = ja), macht daraus ein Projekt und hängt den Kontakt dran.","x":2007,"y":1040,"width":460,"height":360,"color":"5"}, {"id":"a-leads-pkg","type":"file","file":"02 Wireframes/P7 Leads/WF-P7 Leads/WF-P7 Leads.canvas","x":2007,"y":1440,"width":460,"height":220}, - {"id":"77ebfde5a3a05b8a","type":"text","text":"## Online-Terminbuchung 🌐\n**Ticket:** `D-BOOK` + `D-P16` (Teil) · **Status:** ⬜ offen\n**Wo:** 🌐 Buchungslink + `/bookings`\n\n**Was drin ist:** 🌐 Buchungs-Seite für Kunden (freien Termin wählen → Formular → Bestätigung mit Kalender-Datei), intern: Buchungs-Liste (verschieben, stornieren, No-Show), Buchungsseiten-Editor, Verfügbarkeits-Zeiten\n\n**So funktioniert's:** Kunde bucht selbst, der erste freie Mitarbeiter bekommt den Termin, die Anfrage landet im Lead-Eingang. Muss am schönsten aussehen — ist öffentlich.","x":3067,"y":2160,"width":460,"height":360,"color":"2"}, - {"id":"28bd46ecb4eaa069","type":"text","text":"## Kalender\n**Ticket:** `D-BOOK` + `D-P16` (Teil) · **Status:** ⬜ offen\n**Wo:** `/calendar` + Einstellungen\n\n**Was drin ist:** Monats-/Wochen-Ansicht mit allen Terminen (Google-Kalender + Buchungen + Plantafel, je mit Herkunfts-Marker), Google-Kalender verbinden, Konflikt-Übersicht\n\n**So funktioniert's:** Läuft in beide Richtungen mit Google (Outlook kommt in v2). Fremde private Termine erscheinen nur als »belegt«.","x":3837,"y":2160,"width":460,"height":360,"color":"2"}, + {"id":"d9868dadb782f253","type":"text","text":"## Kontakte & Organisationen\n**Ticket:** `D-CRM` · **Status:** 🟡 vorgeplant (Screens validiert, Subframe offen)\n**Wo:** `/contacts`\n\n**Was drin ist:** Kontakt-Liste, Organisations-Liste, Kontakt-Detailseite (die Vorlage für ALLE Detailseiten), eigene Felder, Tags, Duplikate zusammenführen\n\n**So funktioniert's:** Ein Kontakt ist Privatperson oder Organisation und hat einen Status (Kontakt → Kunde → Bestandskunde). Auf der Detailseite sieht man alles: Projekte, Mails, Anrufe, Aufgaben, Dateien, Notizen.","x":3077,"y":1040,"width":460,"height":360,"color":"5"}, + {"id":"a-kontakte-pkg","type":"file","file":"02 Wireframes/P6 Kontakte/WF-P6 Kontakte und Organisationen/WF-P6 Kontakte und Organisationen.canvas","x":3077,"y":1440,"width":460,"height":220}, + {"id":"77ebfde5a3a05b8a","type":"text","text":"## Online-Terminbuchung 🌐\n**Ticket:** `D-BOOK` + `D-P16` (Teil) · **Status:** 🟡 vorgeplant (Screens validiert, Subframe offen)\n**Wo:** 🌐 Buchungslink + `/bookings`\n\n**Was drin ist:** 🌐 Buchungs-Seite für Kunden (freien Termin wählen → Formular → Bestätigung mit Kalender-Datei), intern: Buchungs-Liste (verschieben, stornieren, No-Show), Buchungsseiten-Editor (Fragen mit Feld-Zuordnung zum Lead), Verfügbarkeits-Zeiten — Davids Delta: Kunde kann per Mail-Link selbst absagen/verschieben\n\n**So funktioniert's:** Kunde bucht selbst, der erste freie Mitarbeiter bekommt den Termin, die Anfrage landet im Lead-Eingang. Muss am schönsten aussehen — ist öffentlich.","x":3067,"y":2160,"width":460,"height":360,"color":"2"}, {"id":"6b8c7d3bc3ee7449","type":"text","text":"## Formulare (Baukasten) 🌐\n**Ticket:** `D-FORM` + `D-P16` (Teil) · **Status:** 🟡 Review-Draft (Mitarbeiterinnen-Paket eingebaut, David Review offen)\n**Wo:** `/forms` + öffentlicher Link\n\n**Was drin ist:** Formular-Zentrale, einfacher Google-Forms-naher Builder S02, Lead-Feldzuweisung S02L/E05, Design, Antworten-Tabelle, 🌐 öffentliches Formular mit Erfolg/Blocker/Submit-Fehler\n\n**So funktioniert's:** S02 ist der Hauptfluss für Handwerksbetriebe; Advanced-Canvas, Logic/Actions und Publish-Blocker bleiben als Referenz/Power-User-Richtung. D-FORM kann später in Terminbuchung, HR und Abnahme-Vorlagen eingesteckt werden.","x":2560,"y":2160,"width":460,"height":360,"color":"2"}, - {"id":"a-d-form-pkg","type":"file","file":"02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/WF-D-FORM Form Builder.canvas","x":2560,"y":2560,"width":460,"height":220}, {"id":"4cce52e2e3bd8823","type":"text","text":"## Versand-Technik & Statistiken 🌐\n**Ticket:** `D-P14` · **Status:** ⬜ offen\n**Wo:** `/email-marketing` + Einstellungen\n\n**Was drin ist:** Eigene Absender-Domain einrichten (mit Copy-Paste-Anleitung), Kampagnen-Übersicht + Statistiken (zugestellt/geöffnet/geklickt), gesperrte Empfänger, Vorlagen, 🌐 Abmelde-Seite\n\n**So funktioniert's:** Mails an Kunden tauchen automatisch am Kontakt auf (BCC-Trick, kein Postfach-Anschluss nötig — der kommt in v2).","x":1850,"y":2160,"width":460,"height":360,"color":"2"}, - {"id":"b789fc765f833f45","type":"text","text":"## Digital unterschreiben (E-Signatur)\n**Ticket:** `D-P16` (Teil) · **Status:** ⬜ offen\n**Wo:** im Projekt-Detail\n\n**Was drin ist:** Angebot als PDF hochladen → Unterzeichner festlegen → Kunde unterschreibt per Link (mit SMS/Mail-Code) oder direkt auf dem Gerät → Status je Unterzeichner, Erinnerung, Widerruf\n\n**So funktioniert's:** Hängt direkt am Projekt (»Angebot zur Signatur senden«). Eigene Lösung, kein Pflicht-Drittanbieter.","x":4337,"y":2160,"width":460,"height":360,"color":"5"}, - {"id":"903e3384b0a5e623","type":"text","text":"**Detail-Flows:** 9 Wireframe-Pakete mit eigenen Canvases in `02 Wireframes/P2 Auth/` (Login, Signup, Reset, Set Password, 2FA, RBAC …) — siehe Tracker","x":2777,"y":480,"width":460,"height":220}, {"id":"ab16fe58ff28c0ac","type":"text","text":"## Kampagnen & Empfängerlisten\n**Ticket:** `D-KAMP` · **Status:** ⬜ offen\n**Wo:** `/email-marketing`\n\n**Was drin ist:** Kampagne erstellen in 6 Schritten (Absender → Inhalt → Empfänger → Test-Mail → Zeitpunkt → Check) · Empfängerlisten (Segmente) mit Filtern bauen\n\n**So funktioniert's:** Vor dem Versand prüft eine Checkliste alles Rechtliche (Domain bestätigt? Abmelde-Link? Impressum?) — bei Rot wird nicht gesendet.","x":1200,"y":2160,"width":460,"height":360,"color":"2"}, - {"id":"401d8270854bebac","type":"text","text":"## Team-Chat 📱\n**Ticket:** `D-CHAT` · **Status:** ⬜ offen\n**Wo:** `/chat`\n\n**Was drin ist:** Channel-Liste, Chat-Verlauf (Antwort-Threads, Emoji-Reaktionen, gelesen-von, Anhänge), Erwähnungen mit @, angepinnte Nachrichten, Suche, Direktnachrichten\n\n**So funktioniert's:** Jedes Projekt kann seinen eigenen Channel haben (Team wird automatisch synchron gehalten). Monteure: vereinfachte Handy-Ansicht mit Foto-Direktupload. Admin kann DMs nur mit Begründung einsehen.","x":5077,"y":2160,"width":460,"height":360,"color":"2"}, - {"id":"4bb807ef5e3c8dee","type":"text","text":"## Projekt-Akte (Auftrag) 📱\n**Ticket:** `D-P9` · **Status:** 🟡 vorgeplant (Screens validiert, Subframe offen)\n**Wo:** im Projekt-Detail\n\n**Was drin ist:** Reiter am Projekt: Team-Mitglieder, Termine, **Rechnungen (neu in v1)**, Checklisten & Mängel, Aufgaben, Dateien, Abnahmen · dazu Projekt-Vorlagen und Abnahme-Formular-Vorlagen\n\n**So funktioniert's:** Alles zum Auftrag an einem Ort. Am Handy reduziert: Stammdaten, heutige Aufgaben, Abnahme-Knopf, Foto direkt hochladen.","x":5077,"y":1040,"width":460,"height":360,"color":"2"}, + {"id":"28bd46ecb4eaa069","type":"text","text":"## Kalender\n**Ticket:** `D-BOOK` + `D-P16` (Teil) · **Status:** 🟡 vorgeplant (Screens validiert, Subframe offen)\n**Wo:** `/calendar` + Einstellungen\n\n**Was drin ist:** Monats-/Wochen-Ansicht mit allen Terminen (Google-Kalender + Buchungen + Plantafel, je mit Herkunfts-Marker), Google-Kalender verbinden, Konflikt-Übersicht — Davids Delta: Termine anlegen + einladen (Team/Kontakte/freie E-Mails), Kollegen-Overlay\n\n**So funktioniert's:** Läuft in beide Richtungen mit Google (Outlook kommt in v2). Fremde private Termine erscheinen nur als »belegt«.","x":3837,"y":2160,"width":460,"height":360,"color":"2"}, + {"id":"903e3384b0a5e623","type":"text","text":"**Detail-Flows:** 9 Wireframe-Pakete mit eigenen Canvases in `02 Wireframes/P2 Auth/` (Login, Signup, Reset, Set Password, 2FA, RBAC …) — siehe Tracker","x":2777,"y":480,"width":460,"height":220}, + {"id":"d48196ed3e229be5","type":"text","text":"**Standards:** `04 Standards/Subframe UI Standards/` — validierte Layout-Regeln","x":690,"y":480,"width":460,"height":220}, + {"id":"e10a35560a461111","type":"text","text":"## Kanban-Board & Pipelines (Projekte)\n**Ticket:** `D-P8` + `D-KANBAN` · **Status:** 🟡 vorgeplant (Screens validiert, Subframe offen)\n**Wo:** `/projects` + Admin\n\n**Was drin ist:** Kanban-Board mit Spalten je Status (Karten ziehen = Status wechseln), Projekt-Liste, Projekt-Detailseite, Pipeline-Einstellungen (Spalten, Rechte, Pflichtfelder), Verlust-Gründe\n\n**So funktioniert's:** Ein Projekt wandert durch Boards — »Vertrieb« und »Auftrag« sind nur zwei Boards für denselben Datensatz. Gewonnen = wechselt aufs Auftrags-Board. Wer welche Felder sieht (z. B. Umsatz), ist einstellbar.","x":4037,"y":1040,"width":460,"height":360,"color":"2"}, + {"id":"a-pipelines-pkg","type":"file","file":"02 Wireframes/P8 Pipelines/WF-P8 Pipelines/WF-P8 Pipelines.canvas","x":4037,"y":1440,"width":460,"height":220}, + {"id":"a-d-form-pkg","type":"file","file":"02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/WF-D-FORM Form Builder.canvas","x":2560,"y":2560,"width":460,"height":220}, + {"id":"a-kalender-pkg","type":"file","file":"02 Wireframes/P16 Termine/WF-P16 Kalender/WF-P16 Kalender.canvas","x":3837,"y":2560,"width":460,"height":220}, + {"id":"a-buchung-pkg","type":"file","file":"02 Wireframes/P16 Termine/WF-P16 Terminbuchung/WF-P16 Terminbuchung.canvas","x":3067,"y":2560,"width":460,"height":220}, + {"id":"b789fc765f833f45","type":"text","text":"## Digital unterschreiben (E-Signatur)\n**Ticket:** `D-P16` (Teil) · **Status:** 🟡 vorgeplant (Screens validiert, Subframe offen)\n**Wo:** im Projekt-Detail\n\n**Was drin ist:** PDF aus Projekt-Dateien wählen/hochladen → Unterzeichner + Reihenfolge → Kunde unterschreibt per Link mit Code oder direkt vor Ort → Status je Unterzeichner, Erinnerung, Zurückziehen, Downloads\n\n**So funktioniert's:** Hängt direkt am Projekt (»Zur Signatur senden«). SES-Eigenbau ist Default; PandaDoc nur optional BYOK. Davids Delta: eigene Kunden-Signierseite als Frame.","x":4337,"y":2160,"width":460,"height":360,"color":"5"}, {"id":"3e27875daca5713d","type":"text","text":"## Login, Registrierung & Workspace\n**Ticket:** `D-P2` · **Status:** ✅ validiert / in Subframe\n**Wo:** Login-Seiten + Einstellungen\n\n**Was drin ist:** Registrierung, Login + 2FA, Passwort vergessen/ändern, Workspace-Auswahl + neuer Workspace, Profil & Sicherheit, Mitarbeiter & Rollen (Admin)\n\n**Stand:** 9 Wireframe-Pakete fertig gebaut. Konto/Workspace löschen bewusst geparkt. → `02 Wireframes/P2 Auth/`","x":2777,"y":80,"width":460,"height":360,"color":"4"}, {"id":"89c0813f7e139a88","type":"text","text":"## Dashboard (Startseite)\n**Ticket:** `D-DASH` (Teil) · **Status:** ⬜ offen\n**Wo:** `/dashboard`\n\n**Was drin ist:** 3 feste Ansichten je Rolle: Chef (Umsatz, Projekte, Auslastung) · Büro (Projekte, Aufgaben, Anrufe) · Monteur 📱 (meine Termine, Projekte, Aufgaben)\n\n**So funktioniert's:** Kacheln mit Zahlen, Listen und Trend-Charts, live aktualisiert. Kein freies Umbauen in v1 — nur Filter/Zeitraum einstellbar.","x":4037,"y":80,"width":460,"height":360,"color":"2"}, {"id":"4a4e5be847a54e5f","type":"text","text":"## Suche (Cmd+K)\n**Ticket:** `D-DASH` (Teil) · **Status:** ⬜ offen\n**Wo:** überall + `/search`\n\n**Was drin ist:** Such-Fenster per Tastenkürzel: tippen, Treffer nach Typ gruppiert, mit Pfeiltasten navigieren · dazu große Such-Seite mit Filtern\n\n**So funktioniert's:** Findet Kontakte, Projekte, Dateien usw. — zeigt nur, was man sehen darf.","x":4537,"y":80,"width":460,"height":360,"color":"1"}, {"id":"ce5632682989a6ca","type":"text","text":"## Website (Landing & Demo-Anfrage)\n**Ticket:** `D-WEB` · **Status:** ⬜ offen\n**Wo:** 🌐 hivegrid.de\n\n**Was drin ist:** Startseite, Features-Seite, Preise-Teaser, Demo-Anfrage-Formular\n\n**Warum wichtig:** Der erste Eindruck nach außen. Farben/Schriften sind dieselben wie in der App. Demo-Anfragen landen im Lead-Eingang.","x":2007,"y":80,"width":460,"height":360,"color":"2"}, {"id":"c02969250252a3ab","type":"text","text":"## Feinschliff vor Launch\n**Ticket:** `D-POLISH` · **Status:** ⬜ offen\n**Wo:** 🌐 alles öffentlich Sichtbare\n\n**Was drin ist:** Letzter Polier-Durchgang: Terminbuchungs-Seite, öffentliche Formulare, Marketing-Mails · danach Screenshots als »So muss es aussehen«-Referenz einfrieren\n\n**Warum wichtig:** Kommt ganz am Ende, direkt vor dem Launch. Braucht Terminbuchung + Dashboard fertig.","x":1190,"y":80,"width":460,"height":360,"color":"3"}, - {"id":"d48196ed3e229be5","type":"text","text":"**Standards:** `04 Standards/Subframe UI Standards/` — validierte Layout-Regeln","x":690,"y":480,"width":460,"height":220}, {"id":"d53e7ea48a9b1e6c","type":"text","text":"## UI-Baukasten (Listen, Formulare, Modals …)\n**Ticket:** `D-D0b` · **Status:** ✅ validiert\n**Wo:** überall in der App\n\n**Was drin ist:** Fertige Bausteine: Listen mit Filter, Detail-Seiten, Formular-Felder (12 Typen), Dialoge/Wizards, Settings-Seiten, Leer-/Lade-/Fehler-Zustände + Kleinteile (Buttons, Tags, Charts, Unterschriften-Feld …)\n\n**Warum wichtig:** Einmal designen, überall wiederverwenden. Layout-Standards liegen in `04 Standards`. ✅ Bereits erledigt.","x":690,"y":80,"width":460,"height":360,"color":"4"}, + {"id":"b3cad562971d30a5","type":"text","text":"## App-Gerüst (Sidebar, Topbar & Design)\n**Ticket:** `D-D0a` · **Status:** ✅ gebaut (David, in Subframe)\n**Wo:** überall in der App\n\n**Was drin ist:** Sidebar (Module ein-/ausblendbar je Abo, Handy-Variante 📱), Topbar (Workspace-Wechsler, Suche, Glocke, Profil-Menü), Breadcrumb, Warn-Banner (z. B. »Zahlung offen«), Erfolgs-/Fehlermeldungen\n\n**Stand:** Bereits gebaut und validiert. Der Rahmen, in dem jedes Feature steckt — inkl. Farben, Schriften, Hell/Dunkel.","x":-100,"y":80,"width":460,"height":360,"color":"4"}, + {"id":"a-signatur-pkg","type":"file","file":"02 Wireframes/P16 Termine/WF-P16 E-Signatur/WF-P16 E-Signatur.canvas","x":4337,"y":2560,"width":460,"height":220}, {"id":"e21930b7073823f8","type":"text","text":"## Aufgaben\n**Ticket:** `D-P4` (Teil) · **Status:** 🟡 vorgeplant (Screens validiert, Subframe offen)\n**Wo:** `/tasks` + überall als Reiter\n\n**Was drin ist:** Aufgaben-Liste (Meine / von mir erstellt / Alle), Aufgabe mit Frist, Priorität, Zuständigem (Person oder Gruppe), Beobachtern und Verknüpfung (Kontakt/Projekt/…)\n\n**So funktioniert's:** Einmal gebaut, überall eingebettet — jede Detailseite hat einen Aufgaben-Reiter. Aufgaben können auch aus Chat, Formularen oder Automationen entstehen (Herkunft sichtbar). Erinnerung vor/nach Frist.","x":800,"y":3100,"width":460,"height":360,"color":"5"}, {"id":"036e2a807223753d","type":"text","text":"## Dateien\n**Ticket:** `D-P4` (Teil) · **Status:** 🟡 vorgeplant (Screens validiert, Subframe offen)\n**Wo:** `/files` + überall als Reiter\n\n**Was drin ist:** Datei-Übersicht (Tabelle oder Kacheln, Filter, Mehrfach-Aktionen inkl. ZIP), Upload per Drag&Drop (mehrere gleichzeitig, mit Fortschritt), Datei-Detail mit Vorschau\n\n**So funktioniert's:** Dateien hängen an Kontakten/Projekten/… mit Rolle (z. B. »Angebot«). iPhone-Fotos (HEIC) werden automatisch zu JPG. Versionen kommen in v2.","x":1450,"y":3100,"width":460,"height":360,"color":"5"}, {"id":"8f3127f03a5bab96","type":"text","text":"## Automations-Baukasten (Flow-Editor)\n**Ticket:** `D-FLOW` · **Status:** 🟡 vorgeplant (Screens validiert, Subframe offen)\n**Wo:** `/automations/:id/edit`\n\n**Was drin ist:** Zeichenfläche mit Bausteinen: Auslöser (neue Anfrage, Formular abgeschickt, Zeitplan …) und Aktionen (Aufgabe anlegen, Mail senden, Wenn-Dann, Warten …) per Ziehen verbinden\n\n**So funktioniert's:** Alles in verständlichem Deutsch beschriftet. Vor dem Aktivieren zeigt eine Probe-Vorschau »trifft X Datensätze« — erst bestätigen, dann läuft's. Warnt vor Endlos-Schleifen. **Aufwendigste UI der ganzen App.**","x":2150,"y":3100,"width":460,"height":360,"color":"1"}, {"id":"e6d8c36a0f89d954","type":"text","text":"## Automationen: Übersicht & Verläufe\n**Ticket:** `D-P12` · **Status:** 🟡 vorgeplant (Screens validiert, Subframe offen)\n**Wo:** `/automations`\n\n**Was drin ist:** Liste aller Automationen (aktiv/pausiert/Entwurf), Lauf-Protokolle Schritt für Schritt in Klartext, Vorlagen-Katalog (SHK-Vorlagen + eigene)\n\n**So funktioniert's:** Jeder Lauf ist nachvollziehbar; 24 h lang rückgängig machbar (Countdown-Knopf), fehlgeschlagene Schritte neu anstoßen.","x":3067,"y":3100,"width":460,"height":360,"color":"2"}, + {"id":"401d8270854bebac","type":"text","text":"## Team-Chat 📱\n**Ticket:** `D-CHAT` · **Status:** ⬜ offen (Entwurf angelegt, nicht validiert)\n**Wo:** `/chat`\n\n**Entwurf enthält:** Channel-Liste, Chat-Verlauf mit Threads/Reactions/Anhängen/Read-Receipts, Composer mit @Mentions, Pins, Mentions-Aggregat, Suche, Channel-Settings, Admin-Audit, Monteur-Handy-Frame\n\n**Noch zu klären:** Paket-Schnitt gemeinsam mit David validieren. Projekt-Channel, Admin-DM-Audit und Mobile-Frame sind Vorschläge, nicht final entschieden.","x":5077,"y":2160,"width":460,"height":360,"color":"2"}, + {"id":"4bb807ef5e3c8dee","type":"text","text":"## Projekt-Akte (Auftrag) 📱\n**Ticket:** `D-P9` · **Status:** 🟡 vorgeplant (Screens validiert, Subframe offen)\n**Wo:** im Projekt-Detail\n\n**Was drin ist:** Reiter am Projekt: Team-Mitglieder, Termine, **Rechnungen (neu in v1)**, Checklisten & Mängel, Aufgaben, Dateien, Abnahmen · dazu Projekt-Vorlagen und Abnahme-Formular-Vorlagen\n\n**So funktioniert's:** Alles zum Auftrag an einem Ort. Am Handy reduziert: Stammdaten, heutige Aufgaben, Abnahme-Knopf, Foto direkt hochladen.","x":5077,"y":1040,"width":460,"height":360,"color":"2"}, + {"id":"a-akte-pkg","type":"file","file":"02 Wireframes/P9 Auftrag/WF-P9 Projekt-Akte/WF-P9 Projekt-Akte.canvas","x":5077,"y":1440,"width":460,"height":220}, + {"id":"a-chat-pkg","type":"file","file":"02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/WF-P15 Team-Chat.canvas","x":5077,"y":2560,"width":460,"height":220}, {"id":"5c72e7b04059503a","type":"text","text":"## Integrationen (Apps verbinden)\n**Ticket:** `D-P5` · **Status:** 🟡 vorgeplant (Screens validiert, Subframe offen)\n**Wo:** `/admin/integrations`\n\n**Was drin ist:** App-Katalog (nach Kategorie, mit Status-Ampel), Verbindung einrichten per Schritt-für-Schritt-Assistent, Verbindungs-Zustand + Sync-Verlauf in Klartext\n\n**So funktioniert's:** Das gemeinsame Gerüst für alle Anbindungen — E-Mail, Telefonie, Kalender, Signatur nutzen alle dieselben Verbindungs-Karten.","x":5537,"y":3100,"width":460,"height":360,"color":"5"}, {"id":"29ca06733a9b243b","type":"text","text":"## Telefonie (Anrufe)\n**Ticket:** `D-P15` · **Status:** ⬜ offen\n**Wo:** `/calls` + Einstellungen\n\n**Was drin ist:** AirCall verbinden (Telefon-Nutzer zuordnen), Anruf-Liste + Detail (Notiz, Aufnahme), Anruf-Knopf direkt an Kontakt/Anfrage/Projekt\n\n**So funktioniert's:** Anrufe werden automatisch dem richtigen Kontakt zugeordnet. Unbekannte Nummer ruft an → wird automatisch eine Anfrage im Lead-Eingang.","x":5577,"y":2160,"width":460,"height":360,"color":"5"}, - {"id":"3f83fa1a16ae69df","type":"text","text":"## Plantafel (Einsatzplanung) 📱\n**Ticket:** `D-PLAN` · **Status:** 🟡 vorgeplant (Screens validiert, Subframe offen)\n**Wo:** `/projects/plantafel`\n\n**Was drin ist:** Planungs-Raster: Zeilen = Mitarbeiter, Spalten = Zeit. Einsätze per Ziehen verschieben/verlängern, Klick auf freien Slot = neuer Einsatz\n\n**So funktioniert's:** Warnt bei Konflikten (doppelt verplant, Urlaub/krank, außerhalb Arbeitszeit) — zuweisen geht trotzdem. Monteure sehen am Handy »Meine Termine heute« mit Adresse + Navi, auch offline.","x":5897,"y":1040,"width":460,"height":360,"color":"1"}, - {"id":"0f6acf7087283c0c","type":"text","text":"## Aktivitäts-Protokoll (Audit-Log)\n**Ticket:** `D-P3` (Teil) · **Status:** 🟡 vorgeplant (Screens validiert, Subframe offen)\n**Wo:** `/admin/audit`\n\n**Was drin ist:** Wer hat wann was geändert — als deutscher Klartext-Satz, mit Filter und Vorher/Nachher-Vergleich, Export mit Zweck-Angabe\n\n**So funktioniert's:** Der gleiche Verlauf ist auch an jedem Datensatz einblendbar (»Verlauf dieses Objekts«). Unterscheidet Mensch / Automation / Agent / Admin.","x":6037,"y":3100,"width":460,"height":360,"color":"5"}, - {"id":"50945ed6e43505ce","type":"text","text":"## Benachrichtigungen\n**Ticket:** `D-P3` (Teil) · **Status:** 🟡 vorgeplant (Screens validiert, Subframe offen)\n**Wo:** Glocke oben + Einstellungen\n\n**Was drin ist:** Glocke mit Ungelesen-Zähler + aufklappbare Liste (Klick springt zur Sache), eigene Seite mit allen Meldungen, Einstellungen: pro Ereignis-Typ wählen ob In-App und/oder E-Mail\n\n**So funktioniert's:** Wird von allen Features gespeist (neue Anfrage, Aufgabe fällig, Antrag genehmigt …). Master-Schalter »alle E-Mails aus«.","x":6077,"y":2160,"width":460,"height":360,"color":"5"}, - {"id":"43a26561f03f72da","type":"file","file":"02 Wireframes/P3 Querschnitt/WF-P3 Benachrichtigungen/WF-P3 Benachrichtigungen.canvas","x":6077,"y":2560,"width":460,"height":220}, - {"id":"a1328943a7cef3b2","type":"file","file":"02 Wireframes/P4 Aufgaben und Dateien/WF-P4 Dateien/WF-P4 Dateien.canvas","x":1450,"y":3500,"width":460,"height":220}, - {"id":"98798c9044c1d4bc","type":"file","file":"02 Wireframes/P4 Aufgaben und Dateien/WF-P4 Aufgaben/WF-P4 Aufgaben.canvas","x":800,"y":3500,"width":460,"height":220}, - {"id":"a-integrationen-pkg","type":"file","file":"02 Wireframes/P5 Integrationen/WF-P5 Integrationen/WF-P5 Integrationen.canvas","x":5537,"y":3500,"width":460,"height":220}, - {"id":"3864f1b74d665e1a","type":"file","file":"02 Wireframes/P3 Querschnitt/WF-P3 Audit Log/WF-P3 Audit Log.canvas","x":6037,"y":3500,"width":460,"height":220}, - {"id":"6b3c0929759a607b","type":"text","text":"## Abo & Rechnungen (für HiveGrid)\n**Ticket:** `D-P20` (Teil) · **Status:** ⚠️ Ticket braucht Recut\n**Wo:** `/admin/subscription`\n\n**Was drin ist:** Abo-Übersicht (Plan, Plätze, gebuchte Module, Test-Countdown), Verbrauchs-Balken (Automations-Läufe, Speicher), Rechnungen + Zahlungsstatus\n\n**Achtung:** Das Ticket wird gerade umgeschnitten (neuer Abrechnungs-Unterbau) — erst planen, wenn der Brief neu steht. Kein Selbst-Upgrade in v1.","x":6537,"y":3100,"width":460,"height":360,"color":"5"}, - {"id":"b3cad562971d30a5","type":"text","text":"## App-Gerüst (Sidebar, Topbar & Design)\n**Ticket:** `D-D0a` · **Status:** ✅ gebaut (David, in Subframe)\n**Wo:** überall in der App\n\n**Was drin ist:** Sidebar (Module ein-/ausblendbar je Abo, Handy-Variante 📱), Topbar (Workspace-Wechsler, Suche, Glocke, Profil-Menü), Breadcrumb, Warn-Banner (z. B. »Zahlung offen«), Erfolgs-/Fehlermeldungen\n\n**Stand:** Bereits gebaut und validiert. Der Rahmen, in dem jedes Feature steckt — inkl. Farben, Schriften, Hell/Dunkel.","x":-100,"y":80,"width":460,"height":360,"color":"4"}, {"id":"d5d3db1765287eef","type":"text","text":"# 🗺 HiveGrid Feature Map (v1)\nJede Karte = **ein Feature**. Das Ticket steht nur als Referenz dran. Übergreifende Sachen (App-Gerüst, UI-Baukasten, Feinschliff) liegen unter **Basis**.\n\n**Farben:**\n🟢 fertig/validiert · 🔴 Sonder-UI, wird von Hand designt (Tage) · 🔵 wird aus dem UI-Baukasten zusammengesteckt (Stunden) · 🟠 Mischung aus beidem · 🟣 Basis · 🟡 Feinschliff\n\n**Marker:** 🌐 öffentlich sichtbar (höchster Anspruch) · 📱 Monteur-Handy, teils offline\n\n**Unter jeder Karte:** Slot für die verknüpften Paket-Canvases (Detail-Flows aus `02 Wireframes`) — Karte anklicken öffnet den Flow.\n\n**Pfeile:** orange = Hauptkette (Anfrage → Auftrag → Geld) · grau = klare Querverbindung. Gilt etwas für **alle** Features (Automationen, Aufgaben/Dateien, Benachrichtigungen, Verbindungs-Gerüst), steht es als Text auf der Karte statt als Pfeil.\n\nAbhaklisten & Details: [[HiveGrid Map]]","x":-163,"y":-600,"width":1140,"height":380}, { "id":"a4f86f7bfc24194b", @@ -64,19 +60,27 @@ "width":820, "height":420 }, + {"id":"md-hivegrid-map","type":"file","file":"05 HiveGrid Map/HiveGrid Map.md","x":-683,"y":-600,"width":460,"height":300}, + {"id":"3f83fa1a16ae69df","type":"text","text":"## Plantafel (Einsatzplanung) 📱\n**Ticket:** `D-PLAN` · **Status:** 🟡 vorgeplant (Screens validiert, Subframe offen)\n**Wo:** `/projects/plantafel`\n\n**Was drin ist:** Planungs-Raster: Zeilen = Mitarbeiter, Spalten = Zeit. Einsätze per Ziehen verschieben/verlängern, Klick auf freien Slot = neuer Einsatz\n\n**So funktioniert's:** Warnt bei Konflikten (doppelt verplant, Urlaub/krank, außerhalb Arbeitszeit) — zuweisen geht trotzdem. Monteure sehen am Handy »Meine Termine heute« mit Adresse + Navi, auch offline.","x":5897,"y":1040,"width":460,"height":360,"color":"1"}, + {"id":"a1328943a7cef3b2","type":"file","file":"02 Wireframes/P4 Aufgaben und Dateien/WF-P4 Dateien/WF-P4 Dateien.canvas","x":1450,"y":3500,"width":460,"height":220}, + {"id":"98798c9044c1d4bc","type":"file","file":"02 Wireframes/P4 Aufgaben und Dateien/WF-P4 Aufgaben/WF-P4 Aufgaben.canvas","x":800,"y":3500,"width":460,"height":220}, + {"id":"a-integrationen-pkg","type":"file","file":"02 Wireframes/P5 Integrationen/WF-P5 Integrationen/WF-P5 Integrationen.canvas","x":5537,"y":3500,"width":460,"height":220}, + {"id":"a-plantafel-pkg","type":"file","file":"02 Wireframes/P9 Auftrag/WF-P9 Plantafel/WF-P9 Plantafel.canvas","x":5897,"y":1440,"width":460,"height":220}, + {"id":"a-flow-pkg","type":"file","file":"02 Wireframes/P12 Automationen/WF-P12 Flow-Editor/WF-P12 Flow-Editor.canvas","x":2150,"y":3500,"width":460,"height":220}, + {"id":"a-autoverw-pkg","type":"file","file":"02 Wireframes/P12 Automationen/WF-P12 Verwaltung/WF-P12 Verwaltung.canvas","x":3067,"y":3500,"width":460,"height":220}, + {"id":"0f6acf7087283c0c","type":"text","text":"## Aktivitäts-Protokoll (Audit-Log)\n**Ticket:** `D-P3` (Teil) · **Status:** 🟡 vorgeplant (Screens validiert, Subframe offen)\n**Wo:** `/admin/audit`\n\n**Was drin ist:** Wer hat wann was geändert — als deutscher Klartext-Satz, mit Filter und Vorher/Nachher-Vergleich, Export mit Zweck-Angabe\n\n**So funktioniert's:** Der gleiche Verlauf ist auch an jedem Datensatz einblendbar (»Verlauf dieses Objekts«). Unterscheidet Mensch / Automation / Agent / Admin.","x":6037,"y":3100,"width":460,"height":360,"color":"5"}, + {"id":"50945ed6e43505ce","type":"text","text":"## Benachrichtigungen\n**Ticket:** `D-P3` (Teil) · **Status:** 🟡 vorgeplant (Screens validiert, Subframe offen)\n**Wo:** Glocke oben + Einstellungen\n\n**Was drin ist:** Glocke mit Ungelesen-Zähler + aufklappbare Liste (Klick springt zur Sache), eigene Seite mit allen Meldungen, Einstellungen: pro Ereignis-Typ wählen ob In-App und/oder E-Mail\n\n**So funktioniert's:** Wird von allen Features gespeist (neue Anfrage, Aufgabe fällig, Antrag genehmigt …). Master-Schalter »alle E-Mails aus«.","x":6077,"y":2160,"width":460,"height":360,"color":"5"}, + {"id":"43a26561f03f72da","type":"file","file":"02 Wireframes/P3 Querschnitt/WF-P3 Benachrichtigungen/WF-P3 Benachrichtigungen.canvas","x":6077,"y":2560,"width":460,"height":220}, + {"id":"3864f1b74d665e1a","type":"file","file":"02 Wireframes/P3 Querschnitt/WF-P3 Audit Log/WF-P3 Audit Log.canvas","x":6037,"y":3500,"width":460,"height":220}, {"id":"b334f15bdd19d9ad","type":"text","text":"## Erste Schritte & Demo-Daten\n**Ticket:** `D-P20` (Teil) · **Status:** ⬜ offen\n**Wo:** `/onboarding` + Admin\n\n**Was drin ist:** Erste-Schritte-Checkliste (mit Direkt-Links, abhakbar, ausblendbar), Beispieldaten-Knopf (anlegen, zählen, wieder löschen, »Demo ausblenden«-Filter), Hell/Dunkel-Umschalter\n\n**So funktioniert's:** Kein Einrichtungs-Assistent — beim Registrieren wird alles SHK-typisch vorbefüllt, die Checkliste führt danach durch.","x":5537,"y":3760,"width":460,"height":360,"color":"5"}, {"id":"6533d410b309a48f","type":"text","text":"## Datenschutz (Export & Löschung)\n**Ticket:** `D-P21` (Teil) · **Status:** ⬜ offen\n**Wo:** `/admin/privacy` + Einstellungen\n\n**Was drin ist:** Daten-Export als ZIP (für einen Nutzer oder den ganzen Workspace), Workspace-Löschung (mehrstufig, 30 Tage Schonfrist, Passwort + 2FA), Konto-Löschung, Liste der Unterauftragsverarbeiter, AVV-Vertrag\n\n**So funktioniert's:** Letzter Baustein vor dem Launch. DSGVO-Auskunftsanfragen laufen in v1 als manuelle Anleitung, ohne eigenen Assistenten.","x":6037,"y":3760,"width":460,"height":360,"color":"5"}, + {"id":"6b3c0929759a607b","type":"text","text":"## Abo & Rechnungen (für HiveGrid)\n**Ticket:** `D-P20` (Teil) · **Status:** ⚠️ Ticket braucht Recut\n**Wo:** `/admin/subscription`\n\n**Was drin ist:** Abo-Übersicht (Plan, Plätze, gebuchte Module, Test-Countdown), Verbrauchs-Balken (Automations-Läufe, Speicher), Rechnungen + Zahlungsstatus\n\n**Achtung:** Das Ticket wird gerade umgeschnitten (neuer Abrechnungs-Unterbau) — erst planen, wenn der Brief neu steht. Kein Selbst-Upgrade in v1.","x":6537,"y":3100,"width":460,"height":360,"color":"5"}, {"id":"db76161d24fb0c1d","type":"text","text":"## Feedback & Hilfe\n**Ticket:** `D-P21` (Teil) · **Status:** ⬜ offen\n**Wo:** überall + `/help`\n\n**Was drin ist:** Schwebender Feedback-Knopf (Fehler melden / Idee / Frage, optional mit Screenshot), kleine FAQ-Seite\n\n**So funktioniert's:** Meldungen landen direkt in unserer Support-Inbox im Plattform-Admin.","x":6537,"y":3760,"width":460,"height":360,"color":"5"}, {"id":"d9ad572ae83b4d0a","type":"text","text":"## Abnahme (Baustellen-Protokoll) 📱\n**Ticket:** `D-ABN` · **Status:** 🟡 vorgeplant (Screens validiert, Subframe offen)\n**Wo:** im Projekt-Detail\n\n**Was drin ist:** Abnahme-Formular (Druckprobe, Funktionsprüfung, Zählerstände, übergebene Dokumente), Mängel mit Foto festhalten, Unterschrift Kunde + Monteur direkt am Handy, Liste aller Abnahmen je Projekt\n\n**So funktioniert's:** Nach dem Speichern unveränderbar (6 Jahre Aufbewahrung), PDF geht automatisch per Mail an den Kunden. Funktioniert offline, lädt später hoch. Mängel werden automatisch Aufgaben.","x":6767,"y":1040,"width":460,"height":360,"color":"1"}, - {"id":"md-hivegrid-map","type":"file","file":"05 HiveGrid Map/HiveGrid Map.md","x":-683,"y":-600,"width":460,"height":300}, - {"id":"c8d8914e45e1497e","type":"text","text":"## Plattform-Admin\n**Ticket:** `D-ADMIN` · **Status:** ⬜ offen\n**Wo:** admin.hivegrid.de\n\n**Was drin ist:** Eigener Login (2FA Pflicht), Übersichts-Dashboard (Kunden, Nutzer, Umsatz, System-Zustand), Kunden-Verwaltung (Module freischalten = Abo-Baukasten, Test verlängern, »als Kunde einloggen« mit Begründung), Nutzer-Hilfen (2FA-Reset), Feature-Schalter, Warteschlangen-Monitor, Support-Inbox, Protokoll\n\n**So funktioniert's:** Komplett eigene Welt für uns als Betreiber — Kunden sehen davon nichts. Muss funktionieren, nicht hübsch sein.","x":7237,"y":3100,"width":460,"height":360,"color":"5"}, - {"id":"abf5f779c1463715","type":"text","text":"## Hinweis\nDieses eine Ticket enthält viele kleine Features (Kunden, Schalter, Support …).\n\nBeim atomaren Durchgehen behandeln wir es als eigenen Bereich und teilen es bei Bedarf in Unter-Pakete auf. Priorität liegt aber hinter der Vertriebs-/Auftrags-Kette.","x":7237,"y":3760,"width":460,"height":360,"color":"5"}, - {"id":"03e8e03ef11606a0","type":"text","text":"## HR-Bereich 📱\n**Ticket:** `D-HR` · **Status:** ⬜ offen\n**Wo:** `/hr` + HR-Admin\n\n**Was drin ist:** Startseite (Ankündigungen, offene Anträge, wer ist heute da), Urlaub & Krankmeldung (mit Genehmigung), Lohnabrechnungen, Anträge (Spesen, Hardware …), Passwort-Safe, Umfragen, Mitarbeiter-Verzeichnis, Standorte, eigene Dokumente\n\n**So funktioniert's:** Mitarbeiter-Sicht und HR-Verwaltung sind strikt getrennt; sensible Daten verschlüsselt, jedes Aufdecken protokolliert. Urlaub/krank fließt direkt in die Plantafel-Konflikte.","x":8017,"y":3100,"width":460,"height":360,"color":"5"}, - {"id":"a-akte-pkg","type":"file","file":"02 Wireframes/P9 Auftrag/WF-P9 Projekt-Akte/WF-P9 Projekt-Akte.canvas","x":5077,"y":1440,"width":460,"height":220}, - {"id":"a-plantafel-pkg","type":"file","file":"02 Wireframes/P9 Auftrag/WF-P9 Plantafel/WF-P9 Plantafel.canvas","x":5897,"y":1440,"width":460,"height":220}, {"id":"a-abnahme-pkg","type":"file","file":"02 Wireframes/P9 Auftrag/WF-P9 Abnahme/WF-P9 Abnahme.canvas","x":6767,"y":1440,"width":460,"height":220}, - {"id":"a-flow-pkg","type":"file","file":"02 Wireframes/P12 Automationen/WF-P12 Flow-Editor/WF-P12 Flow-Editor.canvas","x":2150,"y":3500,"width":460,"height":220}, - {"id":"a-autoverw-pkg","type":"file","file":"02 Wireframes/P12 Automationen/WF-P12 Verwaltung/WF-P12 Verwaltung.canvas","x":3067,"y":3500,"width":460,"height":220} + {"id":"03e8e03ef11606a0","type":"text","text":"## HR-Bereich 📱\n**Ticket:** `D-HR` · **Status:** ⬜ offen\n**Wo:** `/hr` + HR-Admin\n\n**Was drin ist:** Startseite (Ankündigungen, offene Anträge, wer ist heute da), Urlaub & Krankmeldung (mit Genehmigung), Lohnabrechnungen, Anträge (Spesen, Hardware …), Passwort-Safe, Umfragen, Mitarbeiter-Verzeichnis, Standorte, eigene Dokumente\n\n**So funktioniert's:** Mitarbeiter-Sicht und HR-Verwaltung sind strikt getrennt; sensible Daten verschlüsselt, jedes Aufdecken protokolliert. Urlaub/krank fließt direkt in die Plantafel-Konflikte.","x":7337,"y":2160,"width":460,"height":360,"color":"5"}, + {"id":"c8d8914e45e1497e","type":"text","text":"## Plattform-Admin\n**Ticket:** `D-ADMIN` · **Status:** ⬜ offen\n**Wo:** admin.hivegrid.de\n\n**Was drin ist:** Eigener Login (2FA Pflicht), Übersichts-Dashboard (Kunden, Nutzer, Umsatz, System-Zustand), Kunden-Verwaltung (Module freischalten = Abo-Baukasten, Test verlängern, »als Kunde einloggen« mit Begründung), Nutzer-Hilfen (2FA-Reset), Feature-Schalter, Warteschlangen-Monitor, Support-Inbox, Protokoll\n\n**So funktioniert's:** Komplett eigene Welt für uns als Betreiber — Kunden sehen davon nichts. Muss funktionieren, nicht hübsch sein.","x":7337,"y":3100,"width":460,"height":360,"color":"5"}, + {"id":"abf5f779c1463715","type":"text","text":"## Hinweis\nDieses eine Ticket enthält viele kleine Features (Kunden, Schalter, Support …).\n\nBeim atomaren Durchgehen behandeln wir es als eigenen Bereich und teilen es bei Bedarf in Unter-Pakete auf. Priorität liegt aber hinter der Vertriebs-/Auftrags-Kette.","x":7337,"y":3760,"width":460,"height":360,"color":"5"} ], "edges":[ { @@ -303,6 +307,7 @@ ], "metadata":{ "version":"1.0-1.0", - "frontmatter":{} + "frontmatter":{}, + "startNode":"306e93d5a09ecb25" } } \ No newline at end of file diff --git a/05 HiveGrid Map/HiveGrid Map.md b/05 HiveGrid Map/HiveGrid Map.md index a2920b5e..186d9728 100644 --- a/05 HiveGrid Map/HiveGrid Map.md +++ b/05 HiveGrid Map/HiveGrid Map.md @@ -3,7 +3,7 @@ kind: map project: HiveGrid Screen Project area: HiveGrid Map status: active -stand: 2026-07-08 +stand: 2026-07-09 --- # HiveGrid Map @@ -52,9 +52,9 @@ Gesamtkarte aller HiveGrid-v1-Features. Eine Karte = **ein Feature** (so benannt | Kampagnen & Empfängerlisten | E-Mail-Marketing | D-KAMP | 🟠 | ⬜ offen | | Versand-Technik & Statistiken 🌐 | E-Mail-Marketing | D-P14 | 🟠 | ⬜ offen | | Formulare (Baukasten) 🌐 | Termine & Formulare | D-FORM + D-P16 (Teil) | 🔴 | 🟡 Review-Draft | -| Online-Terminbuchung 🌐 | Termine & Formulare | D-BOOK + D-P16 (Teil) | 🟠 | ⬜ offen | -| Kalender | Termine & Formulare | D-BOOK + D-P16 (Teil) | 🔴 mittel | ⬜ offen | -| Digital unterschreiben (E-Signatur) | Termine & Formulare | D-P16 (Teil) | 🔵 | ⬜ offen | +| Online-Terminbuchung 🌐 | Termine & Formulare | D-BOOK + D-P16 (Teil) | 🟠 | 🟡 vorgeplant | +| Kalender | Termine & Formulare | D-BOOK + D-P16 (Teil) | 🔴 mittel | 🟡 vorgeplant | +| Digital unterschreiben (E-Signatur) | Termine & Formulare | D-P16 (Teil) | 🔵 | 🟡 vorgeplant | | Team-Chat 📱 | Kommunikation | D-CHAT | 🟠 | ⬜ offen | | Telefonie (Anrufe) | Kommunikation | D-P15 | 🔵 | ⬜ offen | | Benachrichtigungen | Kommunikation | D-P3 (Teil) | 🔵 | 🟡 vorgeplant | @@ -293,39 +293,48 @@ Abhaken = in der groben Planung berücksichtigt (Screens/Flow verortet). Quelle: - [ ] David Review und Feinschliff der Subframe-Frames abschließen. - Paket: `02 Wireframes/D-FORM Formulare/WF-D-FORM Form Builder/` (Mitarbeiterinnen-Paket eingebaut; Handoff erstellt) -#### Online-Terminbuchung 🌐 — `D-BOOK` + `D-P16` (Teil) — ⬜ offen -- [ ] 🌐 Buchungs-Seite: freien Termin wählen (Zeitzonen-fest) → Fragen-Formular → Bestätigung + Kalender-Datei (.ics); einbettbar -- [ ] Buchung erzeugt Anfrage oder hängt sich an bestehenden Kontakt + Bestätigungs-Mail; kein Doppel-Buchen möglich -- [ ] Intern: Buchungs-Liste (Reiter anstehend/vergangen/storniert, CSV) + Detail (stattgefunden/No-Show, stornieren mit Grund + Auto-Mail, verschieben, anderen Mitarbeiter zuweisen) -- [ ] Buchungsseiten-Editor (Dauer, Puffer, Vorlauf, Mitarbeiter, Fragen-Formular, Bestätigungs-Mail, Einbett-Code) -- [ ] Verfügbarkeits-Zeiten (Wochen-Regeln + Ausnahme-Tage) -- v1: Vergabe nur »erster freier Mitarbeiter«. +#### Online-Terminbuchung 🌐 — `D-BOOK` + `D-P16` (Teil) — 🟡 vorgeplant (2026-07-09) +- [x] 🌐 Buchungs-Seite »wie Calendly«: freien Termin wählen (Zeitzonen-fest) → Fragen-Formular → Bestätigung + Kalender-Datei (.ics); einbettbar (iFrame + QR); eigener Handy-Frame für den Slot-Picker +- [x] Buchung erzeugt Anfrage oder hängt sich an bestehenden Kontakt + Bestätigungs-Mail; kein Doppel-Buchen möglich (Race-Schutz) +- [x] Intern: Buchungs-Liste (Reiter anstehend/vergangen/storniert, Bulk-Storno, CSV) + Detail nach Karteikarten-Muster (stattgefunden/No-Show, stornieren mit Grund + Auto-Mail, verschieben, anderen Mitarbeiter zuweisen, Notizen) +- [x] Buchungsseiten-Editor (Dauer, Puffer, Vorlauf, Mitarbeiter, Fragen-Formular **mit Feld-Zuordnung zum Lead-Eingang**, Bestätigungs-Mail, Einbett-Code + QR) + Buchungsseiten-Liste +- [x] Verfügbarkeits-Zeiten (Wochen-Regeln + Ausnahme-Tage) — jeder Mitarbeiter pflegt seine eigenen, Admin alle +- Davids Delta (Change-Request an Dev): 🌐 **Kunde kann per Mail-Link selbst absagen/verschieben** (eigene öffentliche Seite) +- v1: Vergabe nur »erster freier Mitarbeiter«; Fragen-Felder als einfache Liste. D-FORM liegt jetzt als Review-Draft vor und kann später über die Lead-Feldzuweisungs-Schnittstelle eingesteckt werden. +- Paket: `02 Wireframes/P16 Termine/WF-P16 Terminbuchung/` (Screens validiert; Subframe steht aus) -#### Kalender — `D-BOOK` + `D-P16` (Teil) — ⬜ offen -- [ ] Monats-/Wochen-Ansicht mit allen Terminen (Google + Buchungen + Plantafel, je mit Herkunfts-Marker) -- [ ] Google-Kalender verbinden (Kalender auswählen, Richtung des Abgleichs, sauber trennen) -- [ ] Konflikt-Übersicht in Klartext (was kollidierte, wie gelöst) -- [ ] Fremde private Termine nur als »belegt« sichtbar -- v2: Outlook. +#### Kalender — `D-BOOK` + `D-P16` (Teil) — 🟡 vorgeplant (2026-07-09) +- [x] Monats-/Wochen-Ansicht mit allen Terminen (Google + Buchungen + Plantafel + Projekt-Termine, je mit Herkunfts-Marker) +- [x] Google-Kalender verbinden (Einstieg direkt im Kalender, pro Mitarbeiter; Kalender auswählen, Richtung des Abgleichs, sauber trennen) — OAuth + Trennen im P5-Muster +- [x] Konflikt-Übersicht in Klartext (was kollidierte, wie gelöst) — als Sektion in den Kalender-Einstellungen + Benachrichtigung +- [x] Fremde private Termine nur als »belegt« sichtbar (Kollegen-Overlay) +- Davids Delta (Change-Request an Dev): **vollwertiger Kalender** — Termine anlegen/bearbeiten, Einladungen an Team/Kontakte/freie E-Mails (.ics, Zu-/Absagen), Kollegen-Kalender als Verfügbarkeits-Overlay; funktioniert auch ohne Google +- v2: Outlook (in der Provider-Auswahl als »bald verfügbar« sichtbar). +- Paket: `02 Wireframes/P16 Termine/WF-P16 Kalender/` (Screens validiert; Subframe steht aus) -#### Digital unterschreiben (E-Signatur) — `D-P16` (Teil) — ⬜ offen -- [ ] Assistent am Projekt: PDF hochladen → Unterzeichner + Reihenfolge → senden -- [ ] Kunde unterschreibt per Link (mit Code-Bestätigung) oder direkt auf dem Gerät vor Ort -- [ ] Status je Unterzeichner (gesendet/angesehen/unterschrieben/abgelehnt), Erinnerung, Zurückziehen -- [ ] Original-PDF, unterschriebenes PDF + Abschluss-Nachweis herunterladbar -- Eigene Lösung; PandaDoc nur optional mit eigenem Schlüssel. +#### Digital unterschreiben (E-Signatur) — `D-P16` (Teil) — 🟡 vorgeplant (2026-07-09) +- [x] Projekt-Einstieg/Reiter »Signaturen« + Assistent: PDF aus Projekt-Dateien wählen oder hochladen → Unterzeichner + Reihenfolge → Signaturmodus → senden +- [x] 🌐 Kunde unterschreibt per Link mit Code-Bestätigung (OTP), PDF-Prüfung, Intent und Signatur; direkter Vor-Ort-Modus auf dem Gerät als eigener Mobile-Frame +- [x] Status je Unterzeichner (gesendet/angesehen/unterschrieben/abgelehnt/zurückgezogen), manuelle Erinnerung, Zurückziehen mit Grund +- [x] Original-PDF, unterschriebenes PDF + Abschluss-Nachweis herunterladbar und in Projekt-Dateien abgelegt +- [x] Eigene Lösung als Default: gehärteter SES-Eigenbau; PandaDoc nur optional mit eigenem Schlüssel (BYOK) +- Davids Delta: 🌐 **Kunden-Signierseite als eigener Screen** ergänzt, weil D-P16 nur den internen Wizard nennt. +- v2: freier PDF-Feldplatzierer, automatische Reminder-Regeln, QES/D-Trust. +- Paket: `02 Wireframes/P16 Termine/WF-P16 E-Signatur/` (Screens validiert; Subframe steht aus) ### Kommunikation -#### Team-Chat 📱 — `D-CHAT` — ⬜ offen -- [ ] Channel-Liste (Ungelesen-Zähler, @-Marker, anpinnen/stummschalten, Projekt-Status bei Projekt-Chats) -- [ ] Chat-Verlauf (Antwort-Threads, Emoji-Reaktionen, Anhänge, bearbeitet-Marker, »gelesen von X von Y«) -- [ ] Schreibfeld (Formatierung, Emoji, @-Erwähnung mit Vorschlägen, Anhänge, Entwurf bleibt erhalten), »tippt gerade …« -- [ ] Angepinnte Nachrichten, alle meine Erwähnungen, Suche über alle Channels -- [ ] Neuer Channel / Direktnachricht; Channel-Einstellungen (Mitglieder, archivieren) -- [ ] Projekt-Channels: Team wird automatisch synchron gehalten, Chat erscheint im Projekt -- [ ] Admin-Einsicht in DMs nur mit Pflicht-Begründung + sichtbarem Hinweis-Banner -- [ ] Monteur-Handy-Ansicht (vereinfacht, Foto-Direktupload, Push) +#### Team-Chat 📱 — `D-CHAT` — ⬜ offen (Entwurf angelegt, nicht validiert) +- [ ] Channel-Liste (Ungelesen-Zähler, @-Marker, anpinnen/stummschalten, Projekt-Status bei Projekt-Chats) + Channel-Browser +- [ ] Chat-Verlauf (Antwort-Threads, Emoji-Reaktionen, Anhänge, bearbeitet-Marker, »gelesen von X von Y«), Reconnect/Pending/Upload-Zustände +- [ ] Schreibfeld (Markdown, Emoji, @-Erwähnung mit Vorschlägen, Anhänge, Entwurf bleibt erhalten), »tippt gerade …« +- [ ] Angepinnte Nachrichten, alle meine Erwähnungen, Suche über alle Channels als eigene Nebenflächen +- [ ] Neuer Channel / Direktnachricht; Channel-Einstellungen (Mitglieder, Privacy, archivieren) +- [ ] Projekt-Channels: Team wird automatisch synchron gehalten, Chat erscheint im Projekt; View bleibt `/chat/:id` +- [ ] Admin-Einsicht in DMs nur mit Pflicht-Begründung + sichtbarem Hinweis-Banner + Audit-Log +- [ ] Monteur-Handy-Ansicht (vereinfacht, Foto-Direktupload, Push/Offline-Hinweis) +- Entwurf vorhanden: `02 Wireframes/P15 Kommunikation/WF-P15 Team-Chat/` — muss mit David gemeinsam geplant/validiert werden. +- Noch nicht zählen als vorgeplant; Subframe steht aus. #### Telefonie (Anrufe) — `D-P15` — ⬜ offen - [ ] AirCall verbinden (Status-Ampel, Telefon-Nutzer ↔ HiveGrid-Nutzer zuordnen mit Auto-Vorschlag, »Verbindung testen«) diff --git a/Tickets/Fertige Tickets/D-FORM Form Builder.md b/Tickets/Fertige Tickets/D-FORM Form Builder.md index e6bc3427..9273fa4f 100644 --- a/Tickets/Fertige Tickets/D-FORM Form Builder.md +++ b/Tickets/Fertige Tickets/D-FORM Form Builder.md @@ -28,7 +28,7 @@ Subframe Flow: [WF-D-FORM Form Builder](https://app.subframe.com/f023064a5ca0/fl Das D-FORM-Mitarbeiterinnen-Paket wurde in den Haupt-Vault uebernommen und auf `WF-D-FORM Form Builder` normalisiert. Es enthaelt einen reviewfaehigen Subframe-Draft fuer Formular-Zentrale, einfachen Form-Builder, Lead-Feldzuweisung, Public Form, Public States, Design und Antworten-Tabelle. -Der empfohlene Hauptfluss ist der einfache Google-Forms-nahe Editor `S02`. Der alte Advanced-Canvas bleibt als Referenz/Power-User-Richtung erhalten. +Der empfohlene Hauptfluss ist der einfache Google-Forms-nahe Editor `S02`. S01B und die alten Advanced-Screens wurden im Obsidian-Handoff entfernt, damit der Review fokussiert bleibt. ## Gemacht @@ -44,7 +44,7 @@ Der empfohlene Hauptfluss ist der einfache Google-Forms-nahe Editor `S02`. Der a - Pruefen, ob `S02 Einfacher Fragen Editor` als Haupteditor fuer Handwerksbetriebe die richtige Richtung ist. - Pruefen, ob `S02L Lead Felder zuweisen` und `E05 S02 Lead Zuweisung pruefen` fuer Davids Lead-Mapping-Anforderung reichen. - Public-Strecke `S04`, `V01`, `V02`, `V03` auf Copy, mobile Nutzbarkeit und Trust pruefen. -- Entscheiden, ob `ADV-S01`, `ADV-S01-2`, `D07`, `D08`, `D09` nur Referenz bleiben oder spaeter als Power-User-Flow nachgezogen werden. +- Entscheiden, ob die spaeteren Beispielvarianten `S01C`, `S01D`, `S01E` im Subframe-Flow liegen bleiben sollen. - Klaeren, ob die dokumentierten Modale `M01` bis `M04` als echte Dialog-Screens gebaut werden muessen. ## Flow-Paket