From 4876ccfa1d7c1feca68831faae39744513ff3ec2 Mon Sep 17 00:00:00 2001 From: Ihor Aleksandrychiev Date: Mon, 12 Oct 2026 17:22:48 +0300 Subject: [PATCH 1/9] Files and folders can be dragged into another folder or the root Co-Authored-By: Claude Opus 5.5 Signed-off-by: Ihor Aleksandrychiev --- .../components/PolicyFileExplorer.test.tsx | 3 + .../src/components/PolicyFileExplorer.tsx | 131 +++++++++++++++++- src/renderer/src/pages/ProjectView.tsx | 7 + src/renderer/src/store/filesSlice/index.ts | 21 +++ 4 files changed, 156 insertions(+), 6 deletions(-) diff --git a/src/renderer/src/components/PolicyFileExplorer.test.tsx b/src/renderer/src/components/PolicyFileExplorer.test.tsx index a01c8f9..fa24baf 100644 --- a/src/renderer/src/components/PolicyFileExplorer.test.tsx +++ b/src/renderer/src/components/PolicyFileExplorer.test.tsx @@ -8,10 +8,12 @@ import { fileConditionEnabled, fileConditionModeChanged, fileDescriptionChanged, + fileMoved, fileRemoved, fileRenamed, fileSelected, folderAdded, + folderMoved, folderRemoved, folderRenamed } from '../store/filesSlice'; @@ -37,6 +39,7 @@ function ConnectedExplorer() { onAddFolder={(name, parentId) => dispatch(folderAdded(name, parentId))} onRenameFile={(fileId, name) => dispatch(fileRenamed({ fileId, name }))} onRenameFolder={(folderId, name) => dispatch(folderRenamed({ folderId, name }))} + onMove={(kind, id, parentId) => dispatch(kind === 'file' ? fileMoved({ fileId: id, parentId }) : folderMoved({ folderId: id, parentId }))} onDeleteFile={fileId => inOneStep(dispatch, () => { dispatch(fileRemoved({ fileId })); diff --git a/src/renderer/src/components/PolicyFileExplorer.tsx b/src/renderer/src/components/PolicyFileExplorer.tsx index 8b575a3..008726d 100644 --- a/src/renderer/src/components/PolicyFileExplorer.tsx +++ b/src/renderer/src/components/PolicyFileExplorer.tsx @@ -1,4 +1,4 @@ -import { type ReactNode, useEffect, useRef, useState } from 'react'; +import { type HTMLAttributes, type ReactNode, useEffect, useRef, useState } from 'react'; import CreateNewFolderOutlinedIcon from '@mui/icons-material/CreateNewFolderOutlined'; import DeleteOutlineIcon from '@mui/icons-material/DeleteOutlineOutlined'; @@ -26,6 +26,7 @@ interface PolicyFileExplorerProps { onAddFolder: (name: string, parentId: string | null) => void; onDeleteFile: (fileId: string) => void; onDeleteFolder: (folderId: string) => void; + onMove: (kind: 'file' | 'folder', id: string, parentId: string | null) => void; onRenameFile: (fileId: string, name: string) => void; onRenameFolder: (folderId: string, name: string) => void; onSelectFile: (fileId: string) => void; @@ -53,6 +54,14 @@ interface DeleteTarget { kind: 'file' | 'folder'; } +type DragProps = Pick, 'draggable' | 'onDragEnd' | 'onDragOver' | 'onDragStart' | 'onDrop'>; + +// The drop target meaning the project's root (null is "no target"). +const ROOT_TARGET = '__root__'; +const DRAG_MIME_TYPE = 'application/x-cfpb-tree-item'; +// Hovering a collapsed folder this long while dragging opens it. +const EXPAND_DELAY_MS = 600; + function ancestorFolderIds(folders: PolicyFolder[], parentId: string | null): string[] { const ids: string[] = []; let current = parentId; @@ -128,6 +137,7 @@ function RowActions({ onRename, onDelete, disabled, name }: { disabled?: boolean interface FileRowProps { blockCount: number; canDelete: boolean; + dragProps: DragProps; editingName: string; file: PolicyFile; isEditing: boolean; @@ -154,11 +164,13 @@ function FileRow({ onCommitRename, onCancelRename, onContextMenu, - onDeleteRequest + onDeleteRequest, + dragProps }: FileRowProps) { const gate = describeFileCondition(file.condition); return ( !isEditing && onSelect()} onDoubleClick={onStartRename} onContextMenu={onContextMenu} @@ -220,6 +232,8 @@ function FileRow({ interface FolderRowProps { canDelete: boolean; + dragProps: DragProps; + dropTarget: boolean; editingName: string; expanded: boolean; folder: PolicyFolder; @@ -245,14 +259,21 @@ function FolderRow({ onCommitRename, onCancelRename, onContextMenu, - onDeleteRequest + onDeleteRequest, + dragProps, + dropTarget }: FolderRowProps) { return ( !isEditing && onToggleExpanded()} onDoubleClick={onStartRename} onContextMenu={onContextMenu} - sx={{ ...rowSx, cursor: isEditing ? 'default' : 'pointer' }} + sx={{ + ...rowSx, + cursor: isEditing ? 'default' : 'pointer', + ...(dropTarget && { bgcolor: 'action.selected', outline: '1px solid', outlineColor: 'primary.main' }) + }} > {expanded ? ( @@ -358,7 +379,8 @@ export function PolicyFileExplorer({ onRenameFile, onRenameFolder, onDeleteFile, - onDeleteFolder + onDeleteFolder, + onMove }: PolicyFileExplorerProps) { const [expandedItems, setExpandedItems] = useState([]); const [editing, setEditing] = useState(null); @@ -367,6 +389,9 @@ export function PolicyFileExplorer({ const [draftName, setDraftName] = useState(''); const [contextMenu, setContextMenu] = useState(null); const [deleteTarget, setDeleteTarget] = useState(null); + const [dragging, setDragging] = useState(null); + const [dropTarget, setDropTarget] = useState(null); + const expandTimer = useRef<{ folderId: string; timer: ReturnType } | null>(null); // Keeps the current file's folder visible after switching files (e.g. // loading a project, or a cross-file jump) — same idea as an editor's @@ -457,6 +482,71 @@ export function PolicyFileExplorer({ setDeleteTarget(null); }; + const cancelExpandTimer = () => { + if (expandTimer.current) clearTimeout(expandTimer.current.timer); + expandTimer.current = null; + }; + + const endDrag = () => { + cancelExpandTimer(); + setDragging(null); + setDropTarget(null); + }; + + // A file can't land where it already is; a folder neither, nor inside itself. + const canDropInto = (parentId: string | null) => { + if (!dragging) return false; + if (dragging.kind === 'file') return files.find(file => file.id === dragging.id)?.parentId !== parentId; + if (folders.find(folder => folder.id === dragging.id)?.parentId === parentId) return false; + return parentId !== dragging.id && !ancestorFolderIds(folders, parentId).includes(dragging.id); + }; + + // Hovering a folder opens it after a moment, so a drag can reach its nested folders. + const scheduleExpand = (folderId: string) => { + if (expandTimer.current?.folderId === folderId) return; + cancelExpandTimer(); + if (expandedItems.includes(folderId)) return; + expandTimer.current = { + folderId, + timer: setTimeout(() => setExpandedItems(current => (current.includes(folderId) ? current : [...current, folderId])), EXPAND_DELAY_MS) + }; + }; + + // `item` is what the row drags; `parentId` is where a drop on the row moves things. + const dragPropsFor = (item: DeleteTarget, parentId: string | null, isEditing: boolean): DragProps => ({ + draggable: !isEditing, + onDragStart: event => { + event.stopPropagation(); + event.dataTransfer.setData(DRAG_MIME_TYPE, item.id); + event.dataTransfer.effectAllowed = 'move'; + setDragging(item); + }, + onDragEnd: endDrag, + onDragOver: event => { + if (!dragging) return; + event.stopPropagation(); + if (item.kind === 'folder' && item.id !== dragging.id) scheduleExpand(item.id); + else cancelExpandTimer(); + if (!canDropInto(parentId)) { + event.dataTransfer.dropEffect = 'none'; + setDropTarget(null); + return; + } + event.preventDefault(); + event.dataTransfer.dropEffect = 'move'; + setDropTarget(parentId ?? ROOT_TARGET); + }, + onDrop: event => { + event.preventDefault(); + event.stopPropagation(); + if (dragging && canDropInto(parentId)) { + onMove(dragging.kind, dragging.id, parentId); + if (parentId) setExpandedItems(current => [...new Set([...current, ...ancestorFolderIds(folders, parentId)])]); + } + endDrag(); + } + }); + function renderChildren(parentId: string | null, nodes: FileTreeNode[]): ReactNode { return ( <> @@ -496,6 +586,8 @@ export function PolicyFileExplorer({ setContextMenu({ mouseX: event.clientX, mouseY: event.clientY, target: { kind: 'folder', id: folder.id } }); }} onDeleteRequest={() => setDeleteTarget({ kind: 'folder', id: folder.id })} + dragProps={dragPropsFor({ kind: 'folder', id: folder.id }, folder.id, isEditing)} + dropTarget={dropTarget === folder.id} /> } > @@ -530,6 +622,7 @@ export function PolicyFileExplorer({ setContextMenu({ mouseX: event.clientX, mouseY: event.clientY, target: { kind: 'file', id: file.id } }); }} onDeleteRequest={() => setDeleteTarget({ kind: 'file', id: file.id })} + dragProps={dragPropsFor({ kind: 'file', id: file.id }, file.parentId, isEditing)} /> } /> @@ -562,7 +655,33 @@ export function PolicyFileExplorer({ - + { + if (!dragging) return; + cancelExpandTimer(); + if (!canDropInto(null)) { + setDropTarget(null); + return; + } + event.preventDefault(); + event.dataTransfer.dropEffect = 'move'; + setDropTarget(ROOT_TARGET); + }} + onDrop={event => { + event.preventDefault(); + if (dragging && canDropInto(null)) onMove(dragging.kind, dragging.id, null); + endDrag(); + }} + sx={{ + flex: 1, + overflowY: 'auto', + px: 1, + borderRadius: '4px', + ...(dropTarget === ROOT_TARGET && { outline: '1px solid', outlineColor: 'primary.main', outlineOffset: -1 }) + }} + > setExpandedItems(itemIds)} diff --git a/src/renderer/src/pages/ProjectView.tsx b/src/renderer/src/pages/ProjectView.tsx index 4ff810b..c345bb8 100644 --- a/src/renderer/src/pages/ProjectView.tsx +++ b/src/renderer/src/pages/ProjectView.tsx @@ -103,10 +103,12 @@ import { fileConditionModeChanged, fileConditionRemoved, fileDescriptionChanged, + fileMoved, fileRemoved, fileRenamed, fileSelected, folderAdded, + folderMoved, folderRemoved, folderRenamed } from '../store/filesSlice'; @@ -447,6 +449,10 @@ export default function ProjectView({ dirty, locked, onConnectAgent, onOpenSetti dispatch(folderRenamed({ folderId, name })); }; + const handleMove = (kind: 'file' | 'folder', id: string, parentId: string | null) => { + dispatch(kind === 'file' ? fileMoved({ fileId: id, parentId }) : folderMoved({ folderId: id, parentId })); + }; + const handleDeleteFile = (fileId: string) => { asOneStep(() => { dispatch(fileRemoved({ fileId })); @@ -1001,6 +1007,7 @@ export default function ProjectView({ dirty, locked, onConnectAgent, onOpenSetti onRenameFolder={handleRenameFolder} onDeleteFile={handleDeleteFile} onDeleteFolder={handleDeleteFolder} + onMove={handleMove} /> diff --git a/src/renderer/src/store/filesSlice/index.ts b/src/renderer/src/store/filesSlice/index.ts index c2525f7..7ddc6e8 100644 --- a/src/renderer/src/store/filesSlice/index.ts +++ b/src/renderer/src/store/filesSlice/index.ts @@ -110,6 +110,25 @@ const filesSlice = createSlice({ if (!sanitized) return; folder.name = uniqueSiblingName(siblingFolderNames(state, folder.parentId, folder.id), sanitized); }, + // A name already taken in the target folder gets a " 2" suffix, as on create. + fileMoved(state, action: PayloadAction<{ fileId: string; parentId: string | null }>) { + const file = state.files.find(item => item.id === action.payload.fileId); + const { parentId } = action.payload; + if (!file || file.parentId === parentId || (parentId && !state.folders.some(folder => folder.id === parentId))) return; + file.name = uniqueSiblingName(siblingFileNames(state, parentId, file.id), file.name); + file.parentId = parentId; + }, + // Ignored when the target is the folder itself or nested inside it. + folderMoved(state, action: PayloadAction<{ folderId: string; parentId: string | null }>) { + const folder = state.folders.find(item => item.id === action.payload.folderId); + const { parentId } = action.payload; + if (!folder || folder.parentId === parentId) return; + if (parentId && !state.folders.some(item => item.id === parentId)) return; + const { folderIds } = collectFolderDescendants(state.files, state.folders, folder.id); + if (parentId === folder.id || (parentId && folderIds.includes(parentId))) return; + folder.name = uniqueSiblingName(siblingFolderNames(state, parentId, folder.id), folder.name); + folder.parentId = parentId; + }, // The caller is responsible for also dispatching // canvasSlice's blocksRemovedForFile — this slice doesn't know about blocks. fileRemoved(state, action: PayloadAction<{ fileId: string }>) { @@ -150,6 +169,8 @@ export const { fileSelected, fileRenamed, folderRenamed, + fileMoved, + folderMoved, fileRemoved, folderRemoved } = filesSlice.actions; From 017d16e0a98832a802da7337af3aa1a210a84f3a Mon Sep 17 00:00:00 2001 From: Ihor Aleksandrychiev Date: Tue, 13 Oct 2026 10:11:45 +0300 Subject: [PATCH 2/9] The MCP SDK loads on the first agent request, not at every launch Loading the SDK and zod took ~300 ms before the window could be created, even with the MCP server turned off. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Ihor Aleksandrychiev --- src/main/mcp.ts | 17 +++-------------- src/main/mcpTools/serve.ts | 27 +++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 14 deletions(-) create mode 100644 src/main/mcpTools/serve.ts diff --git a/src/main/mcp.ts b/src/main/mcp.ts index 0caffb6..cf63335 100644 --- a/src/main/mcp.ts +++ b/src/main/mcp.ts @@ -1,5 +1,3 @@ -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import { randomBytes, timingSafeEqual } from 'crypto'; import { BrowserWindow, type IpcMainInvokeEvent, type WebContents, type WebFrameMain, app, ipcMain } from 'electron'; import { promises as fs } from 'fs'; @@ -7,7 +5,6 @@ import { type IncomingMessage, type Server, type ServerResponse, createServer } import type { AddressInfo } from 'net'; import { join } from 'path'; -import { MCP_INSTRUCTIONS, registerBuilderTools } from './mcpTools/index'; import { type McpPermissions, checkedPermissions, isToolAllowed } from './mcpTools/permissions'; /** @@ -18,6 +15,7 @@ import { type McpPermissions, checkedPermissions, isToolAllowed } from './mcpToo const HOST = '127.0.0.1'; const TOOL_TIMEOUT_MS = 30_000; +const SERVER_NAME = 'cfengine-policy-builder'; // Tools that wait on cfbs, Docker or the agent: a test run builds, starts hosts and runs cf-agent. const SLOW_TOOLS: Record = { create_project: 180_000, @@ -27,7 +25,6 @@ const SLOW_TOOLS: Record = { run_tests: 900_000, stop_test_environment: 300_000 }; -const SERVER_NAME = 'cfengine-policy-builder'; interface McpSettings { enabled: boolean; @@ -137,16 +134,8 @@ async function handle(req: IncomingMessage, res: ServerResponse, port: number, t res.writeHead(404).end(); return; } - // Stateless: a server and transport per request. - const mcp = new McpServer({ name: SERVER_NAME, version: app.getVersion() }, { instructions: MCP_INSTRUCTIONS }); - registerBuilderTools(mcp, callWindowTool, permissions); - const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined }); - res.on('close', () => { - void transport.close(); - void mcp.close(); - }); - await mcp.connect(transport); - await transport.handleRequest(req, res); + const { answerMcpRequest } = await import('./mcpTools/serve'); + await answerMcpRequest(req, res, { name: SERVER_NAME, version: app.getVersion() }, callWindowTool, permissions); } function listen(port: number, token: string): Promise { diff --git a/src/main/mcpTools/serve.ts b/src/main/mcpTools/serve.ts new file mode 100644 index 0000000..6a6a906 --- /dev/null +++ b/src/main/mcpTools/serve.ts @@ -0,0 +1,27 @@ +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import type { IncomingMessage, ServerResponse } from 'http'; + +import { MCP_INSTRUCTIONS, registerBuilderTools } from './index'; +import type { McpPermissions } from './permissions'; +import type { CallWindow } from './shared'; + +// Loaded on the first request (mcp.ts): the SDK and zod take ~300 ms, too much for every launch. +// Stateless: a server and transport per request. +export async function answerMcpRequest( + req: IncomingMessage, + res: ServerResponse, + info: { name: string; version: string }, + call: CallWindow, + permissions: McpPermissions +) { + const mcp = new McpServer(info, { instructions: MCP_INSTRUCTIONS }); + registerBuilderTools(mcp, call, permissions); + const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined }); + res.on('close', () => { + void transport.close(); + void mcp.close(); + }); + await mcp.connect(transport); + await transport.handleRequest(req, res); +} From a50e05966c09922e87b480d67e1ed1a3d526fc6d Mon Sep 17 00:00:00 2001 From: Ihor Aleksandrychiev Date: Tue, 13 Oct 2026 10:13:35 +0300 Subject: [PATCH 3/9] Built renderer is minified, halving it to 1.8 MB Co-Authored-By: Claude Opus 5.5 Signed-off-by: Ihor Aleksandrychiev --- electron.vite.config.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/electron.vite.config.ts b/electron.vite.config.ts index 4660798..29f24de 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -29,6 +29,8 @@ export default defineConfig({ allow: [resolve('.')] } }, + // electron-vite leaves the renderer unminified: 3.9 MB vs 1.8 MB, ~60 ms later first paint. + build: { minify: true }, plugins: [react()] } }); From 5b17884e62a7b0c9ab047c25a17fc116ec9bc42b Mon Sep 17 00:00:00 2001 From: Ihor Aleksandrychiev Date: Tue, 13 Oct 2026 10:18:23 +0300 Subject: [PATCH 4/9] app.asar holds only the built app, not 43k node_modules files Every package is bundled by electron-vite, the MCP SDK and zod into main too, so all of them are devDependencies now. The asar went from 91.5 MB to 3.2 MB, and its header Electron parses at launch from 10.9 MB to a few KB. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Ihor Aleksandrychiev --- package-lock.json | 265 +++++++++++++++++++++++++++++++++++++++++++--- package.json | 22 ++-- 2 files changed, 262 insertions(+), 25 deletions(-) diff --git a/package-lock.json b/package-lock.json index f21ac9b..8316b7f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.0", "hasInstallScript": true, "license": "SEE LICENSE IN LICENSE", - "dependencies": { + "devDependencies": { "@codemirror/autocomplete": "^6.20.3", "@codemirror/language": "^6.12.4", "@codemirror/state": "^6.7.4", @@ -25,26 +25,20 @@ "@mui/icons-material": "^9.4.0", "@mui/material": "^9.2.0", "@mui/x-tree-view": "^9.15.0", - "@northern.tech/themes": "^0.17.0", - "@reduxjs/toolkit": "^2.12.0", - "@uiw/react-codemirror": "^4.25.11", - "@xyflow/react": "^12.12.0", - "react": "^19.3.0", - "react-dom": "^19.3.0", - "react-redux": "^9.3.0", - "zod": "^4.6.5" - }, - "devDependencies": { "@northern.tech/eslint-config": "^0.7.2", "@northern.tech/prettier-config": "^0.2.1", + "@northern.tech/themes": "^0.17.0", "@playwright/test": "^1.63.0", + "@reduxjs/toolkit": "^2.12.0", "@testing-library/jest-dom": "^7.0.1", "@testing-library/react": "^16.3.3", "@types/node": "^26.6.4", "@types/react": "^19.3.0", "@types/react-dom": "^19.3.0", + "@uiw/react-codemirror": "^4.25.11", "@vitejs/plugin-react": "^5.2.0", "@vitest/coverage-v8": "^5.0.3", + "@xyflow/react": "^12.12.0", "electron": "^44.5.1", "electron-builder": "^26.17.0", "electron-vite": "^5.0.0", @@ -52,9 +46,13 @@ "globals": "^17.13.0", "jsdom": "^30.1.2", "prettier": "^3.9.9", + "react": "^19.3.0", + "react-dom": "^19.3.0", + "react-redux": "^9.3.0", "typescript": "^6.0.3", "vite": "^7.3.7", - "vitest": "^5.0.1" + "vitest": "^5.0.1", + "zod": "^4.6.5" }, "engines": { "node": ">=22.12.0" @@ -124,6 +122,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", @@ -196,6 +195,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.29.7", @@ -239,6 +239,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -248,6 +249,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.29.7", @@ -289,6 +291,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -298,6 +301,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -331,6 +335,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.29.7" @@ -394,6 +399,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -403,6 +409,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.7", @@ -417,6 +424,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.7", @@ -435,6 +443,7 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.29.7", @@ -448,6 +457,7 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/@base-ui/utils/-/utils-0.4.0.tgz", "integrity": "sha512-bO9fz25kKtPf+aZVyfQrC0PDmJdmVni31W2hCS5/Owb+inwdIL3XU26pCPRPlt4LSxZrBgLwubXQXQlKaFEZzw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.29.7", @@ -554,6 +564,7 @@ "version": "6.20.3", "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.3.tgz", "integrity": "sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -566,6 +577,7 @@ "version": "6.11.1", "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.11.1.tgz", "integrity": "sha512-O/4hG3SC1YwcmQ0d2UVNDs+AsaNWd1iHVxbTeEBuqH+6bExAiPK3iS/BvpY6rZGURALv4ZD3sIgcCmRvw3ehBg==", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -578,6 +590,7 @@ "version": "6.12.4", "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.4.tgz", "integrity": "sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", @@ -592,6 +605,7 @@ "version": "6.9.7", "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.7.tgz", "integrity": "sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", @@ -603,6 +617,7 @@ "version": "6.7.2", "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.2.tgz", "integrity": "sha512-gUYkYhT2+n/+VGZ+8EzE5WFkYZUZYm1VOKDudIsNqh42uRVQJ0a6Yss9sdKT3MeOYfuL1N6AZA57oza0Oyr0LA==", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", @@ -614,6 +629,7 @@ "version": "6.7.6", "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.7.6.tgz", "integrity": "sha512-kAz+AncRtKuIknedxT1bq4XwXv4UowhbkHU1myPrtVb/jZtImWuV5BXzv5vK6i3kYACsdiZiQKFQQ5Mq7elW8w==", + "dev": true, "license": "MIT", "dependencies": { "@marijn/find-cluster-break": "^1.0.0" @@ -623,6 +639,7 @@ "version": "6.1.3", "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz", "integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -635,6 +652,7 @@ "version": "6.43.13", "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.13.tgz", "integrity": "sha512-sihaFrUzAsYBQsL9J2t69y8nfMQGwcYmggAZsk+kjPbjYZMyuf2hU8tUNTZ+P+isb6XRr8JE22TZlJxBoVdH1A==", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/state": "^6.7.0", @@ -787,6 +805,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/@dagrejs/dagre/-/dagre-3.1.1.tgz", "integrity": "sha512-zroZB1dFOFiGgv4Xcrn1DckB1o4aOikPqD2NDQPV0WM//CXGcS6xiD0rNkqHmw6FEg4tabt4nxPLwgCWT+Vb2A==", + "dev": true, "license": "MIT", "dependencies": { "@dagrejs/graphlib": "4.0.5" @@ -796,12 +815,14 @@ "version": "4.0.5", "resolved": "https://registry.npmjs.org/@dagrejs/graphlib/-/graphlib-4.0.5.tgz", "integrity": "sha512-7xrBTqIts3o+PMUZX97wSc+7TUbW+/rULzGNCTP6yooNVDXbzw4Wutg/H/xOutTB/c/k0YqOAavgPh4/Zk9PFA==", + "dev": true, "license": "MIT" }, "node_modules/@dnd-kit/accessibility": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "dev": true, "license": "MIT", "dependencies": { "tslib": "^2.0.0" @@ -814,6 +835,7 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "dev": true, "license": "MIT", "dependencies": { "@dnd-kit/accessibility": "^3.1.1", @@ -829,6 +851,7 @@ "version": "10.0.0", "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "dev": true, "license": "MIT", "dependencies": { "@dnd-kit/utilities": "^3.2.2", @@ -843,6 +866,7 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "dev": true, "license": "MIT", "dependencies": { "tslib": "^2.0.0" @@ -1195,6 +1219,7 @@ "version": "11.13.5", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.16.7", @@ -1214,6 +1239,7 @@ "version": "11.14.0", "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "dev": true, "license": "MIT", "dependencies": { "@emotion/memoize": "^0.9.0", @@ -1227,12 +1253,14 @@ "version": "0.9.2", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "dev": true, "license": "MIT" }, "node_modules/@emotion/is-prop-valid": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", + "dev": true, "license": "MIT", "dependencies": { "@emotion/memoize": "^0.9.0" @@ -1242,12 +1270,14 @@ "version": "0.9.0", "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "dev": true, "license": "MIT" }, "node_modules/@emotion/react": { "version": "11.14.0", "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.18.3", @@ -1272,6 +1302,7 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "dev": true, "license": "MIT", "dependencies": { "@emotion/hash": "^0.9.2", @@ -1285,12 +1316,14 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "dev": true, "license": "MIT" }, "node_modules/@emotion/styled": { "version": "11.14.1", "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.18.3", @@ -1314,12 +1347,14 @@ "version": "0.10.0", "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "dev": true, "license": "MIT" }, "node_modules/@emotion/use-insertion-effect-with-fallbacks": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "dev": true, "license": "MIT", "peerDependencies": { "react": ">=16.8.0" @@ -1329,12 +1364,14 @@ "version": "1.4.2", "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "dev": true, "license": "MIT" }, "node_modules/@emotion/weak-memoize": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "dev": true, "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { @@ -1895,12 +1932,14 @@ "version": "0.2.12", "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "dev": true, "license": "MIT" }, "node_modules/@fontsource/red-hat-text": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/@fontsource/red-hat-text/-/red-hat-text-5.3.0.tgz", "integrity": "sha512-/kgNIUqdoc9uyd4zK+T7gEwY6bYF+hSCZeQTl8Ni/oKtv+a7YeCyQwAE94BeLwud6UfLqn90gBFKa3gCpiZnRA==", + "dev": true, "license": "OFL-1.1", "funding": { "url": "https://github.com/sponsors/ayuhito" @@ -1910,6 +1949,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.3.tgz", "integrity": "sha512-TA//nWMqPhbfdfneACk6t5a9eqbS9lABEPyKn0/xZTah3H3U2XaVg85rJFl0/Fyit0I552YDHgXGVSf3GwqbUw==", + "dev": true, "license": "MIT", "engines": { "node": ">=20" @@ -2104,6 +2144,7 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -2125,6 +2166,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -2134,12 +2176,14 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -2157,12 +2201,14 @@ "version": "1.5.3", "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.3.tgz", "integrity": "sha512-H0iErY4e43LpXbYDyBci5W4v/RwTgGV3YzYOlJPiGZ8RY8w51kVE+xLn1tg3Z6UmSfyETvtsUK3r7YsgDQEI7Q==", + "dev": true, "license": "MIT" }, "node_modules/@lezer/highlight": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.5.tgz", "integrity": "sha512-O1GMVKgtf5YspFaRzpmqkVgMtIk2HG9uMAbH6wdAIdNh3TVzCsch/+cRi+6l2//UGDKe6NB9BW3cvuR5ezMQ0w==", + "dev": true, "license": "MIT", "dependencies": { "@lezer/common": "^1.3.0" @@ -2172,6 +2218,7 @@ "version": "1.4.10", "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz", "integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==", + "dev": true, "license": "MIT", "dependencies": { "@lezer/common": "^1.0.0" @@ -2236,12 +2283,14 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.4.tgz", "integrity": "sha512-Wy0V7+SGUjnF9/TkiM1hKVDPj7jKXduPNboMVtHTA8dySMURWqfg/JZ9E2Sq8JgSJmkl7k7Qe9FLeMSrSraWmQ==", + "dev": true, "license": "MIT" }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.32.1", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.32.1.tgz", "integrity": "sha512-2DdE+SJDtzLEEWzY1ZjY7Q+VcPhcV1KisD3zI4u0XZyktsjHum1mwbMI+JaulUBi2OZk+KJAi2uPXzxichPkdw==", + "dev": true, "license": "MIT", "dependencies": { "@hono/node-server": "^1.19.9 || ^2.0.5", @@ -2282,6 +2331,7 @@ "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -2298,12 +2348,14 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, "license": "MIT" }, "node_modules/@mui/core-downloads-tracker": { "version": "9.4.0", "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-9.4.0.tgz", "integrity": "sha512-d+gIYM1raJP87yrVQcVg+Q3vycVMArMkgu/TjJLi2vTvO9FHKWNRR1xs9nnN4eLpM9nGxxm02UMvc9BrmcUKvQ==", + "dev": true, "license": "MIT", "funding": { "type": "opencollective", @@ -2314,6 +2366,7 @@ "version": "9.4.0", "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-9.4.0.tgz", "integrity": "sha512-5PVgBYtLOXTk6u0YUAjoV9GoTUQDbeZ8g+pus2h0GphLT/rpmftRnB5D/Kw6QCAovB7EyBX7UxKdZqPBBAHUKw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.29.7" @@ -2340,6 +2393,7 @@ "version": "9.4.0", "resolved": "https://registry.npmjs.org/@mui/material/-/material-9.4.0.tgz", "integrity": "sha512-6KKUIvkQ2r/s48s9VZV9JEZ1W2dRa7RndVsF+Ipx4CYxDOQeozeRsS5r9NiheFf8A5hpbqJJIGoX7hjg5X4XUw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.29.7", @@ -2389,6 +2443,7 @@ "version": "9.4.0", "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-9.4.0.tgz", "integrity": "sha512-qQpX/2XPGDz5F2OIWzo4kkorURlDeXGKytRQudXCdLS5TuwKsMKaZyx77LZEVSB73c7o3UrLWwQ/goRuHBdtjw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.29.7", @@ -2416,6 +2471,7 @@ "version": "9.4.0", "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-9.4.0.tgz", "integrity": "sha512-SlqVEYqnyEgXMFW4JuIDMelyhKzoWBJ85v2mNCQd0XtU0uk7U9E1zaJZ06XqodhA8oJrb+4Q+3E/JlwBe4jWkw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.29.7", @@ -2450,6 +2506,7 @@ "version": "9.4.0", "resolved": "https://registry.npmjs.org/@mui/system/-/system-9.4.0.tgz", "integrity": "sha512-AZz9z13oGS1J0BYeAFHTbV7PegCtPiE/uMEIi7TJ1kTx3Vkt+jUpLyXF/2uV8B6Y79iTh98kVeOH9fX3x3zXXg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.29.7", @@ -2490,6 +2547,7 @@ "version": "9.4.0", "resolved": "https://registry.npmjs.org/@mui/types/-/types-9.4.0.tgz", "integrity": "sha512-13DH0Oniua4WmGqeYbQAZqmiN7r4nfd0zwIoGJ5QeJwyIHgEmf+LkRw/jV1HZb6AiUoHX8Oxf4xgalq3vYHcIw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.29.7" @@ -2507,6 +2565,7 @@ "version": "9.4.0", "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-9.4.0.tgz", "integrity": "sha512-WkzY8uYtqUDyGKPShrRQRBomfMQHddlsyKu/gRRESYO24tDPD2z0xRE8CBoUsWqpBa+YrFs9KixiS7kQRjsPAQ==", + "dev": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.29.7", @@ -2537,6 +2596,7 @@ "version": "9.15.0", "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-9.15.0.tgz", "integrity": "sha512-1ELNplhYtGt+54f6o3CHcP7Oz7tyLSgYt20D7WItCyD8ssui3fah7gzZdPGGPggSE/it0UbNwQNOHlqJfLAVgQ==", + "dev": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.29.7 || ^8.0.0", @@ -2560,6 +2620,7 @@ "version": "9.15.0", "resolved": "https://registry.npmjs.org/@mui/x-tree-view/-/x-tree-view-9.15.0.tgz", "integrity": "sha512-1Qsu/1oViKGFU/sF2w32c8OydY3GDozx85bNEsr0f8A53q9kzomrbOwPiW0EHKXjBZ/OeQuiBhxPn6D5m8YsDQ==", + "dev": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.29.7 || ^8.0.0", @@ -3138,6 +3199,7 @@ "version": "0.17.0", "resolved": "https://registry.npmjs.org/@northern.tech/themes/-/themes-0.17.0.tgz", "integrity": "sha512-BXOimCKEJ4mVBNgrcAfszM/yGk6zV4S7TceSR52b/LK32Ks+CJvLaFnWOYWWNNvlYCkV2sRmEmV2vTWdaNhiuA==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@mui/material": "^9.x" @@ -4296,6 +4358,7 @@ "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "dev": true, "license": "MIT", "funding": { "type": "opencollective", @@ -4306,6 +4369,7 @@ "version": "2.13.0", "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.13.0.tgz", "integrity": "sha512-f6jwVRfwW30+tnllz4YcsHHpPYYOE7abcHkyUCovD+P7DyhdAI5prz2Fh/8TLhakjpN+7aIEion1s5L/tvlYlA==", + "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", @@ -4709,12 +4773,14 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, "license": "MIT" }, "node_modules/@standard-schema/utils": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "dev": true, "license": "MIT" }, "node_modules/@storybook/global": { @@ -4986,12 +5052,14 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "dev": true, "license": "MIT" }, "node_modules/@types/d3-drag": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "dev": true, "license": "MIT", "dependencies": { "@types/d3-selection": "*" @@ -5001,6 +5069,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dev": true, "license": "MIT", "dependencies": { "@types/d3-color": "*" @@ -5010,12 +5079,14 @@ "version": "3.0.12", "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.12.tgz", "integrity": "sha512-Qe/KWYhEiIIxGs7HrAAjMfShxKldx19SJtr5zu53f3afPsdZNz7HHtdTLXo/kqeiWNXVycI24kSnfzBYkTzpgw==", + "dev": true, "license": "MIT" }, "node_modules/@types/d3-transition": { "version": "3.0.9", "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "dev": true, "license": "MIT", "dependencies": { "@types/d3-selection": "*" @@ -5025,6 +5096,7 @@ "version": "3.0.9", "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.9.tgz", "integrity": "sha512-0sE1406XBYJGiqD3AusTl9ZqC//2mIXix51tbom25gDCA8ri4xnSZg28CaSE8Srl6FClqABUKfsn0qghgdepMA==", + "dev": true, "license": "MIT", "dependencies": { "@types/d3-interpolate": "*", @@ -5161,18 +5233,21 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "dev": true, "license": "MIT" }, "node_modules/@types/prop-types": { "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, "license": "MIT" }, "node_modules/@types/react": { "version": "19.3.0", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -5182,7 +5257,7 @@ "version": "19.3.0", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", - "devOptional": true, + "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.3.0" @@ -5192,6 +5267,7 @@ "version": "4.4.12", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "*" @@ -5225,6 +5301,7 @@ "version": "0.0.6", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "dev": true, "license": "MIT" }, "node_modules/@typescript-eslint/scope-manager": { @@ -5294,6 +5371,7 @@ "version": "4.25.12", "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.25.12.tgz", "integrity": "sha512-wR5lz7jbVcBGyiiLEObAmDikcc39KNqrrd4bdUhav85WZu48ttgnSf1YV+grHuMsj5l/WCw6u+r9ZuSfURJgiA==", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", @@ -5321,6 +5399,7 @@ "version": "4.25.12", "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.25.12.tgz", "integrity": "sha512-RE0Gj/QesD7/JWPnJREG8Kq8brGv2NAH17iUECcBrLkqXzzEAgsj/4Qqvz/OlK8XWKmzlsenLRkObT9H+gYQ0w==", + "dev": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.18.6", @@ -5822,6 +5901,7 @@ "version": "12.12.0", "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.12.0.tgz", "integrity": "sha512-74oXI0Rgm1Eu33cHQAd74J6oVgCH9mbFyTVQbsGgp2kmWwh0HdtBr2VP04z8mbPD+M/xO69nqG5dDEt7/EN2rQ==", + "dev": true, "license": "MIT", "dependencies": { "@xyflow/system": "0.0.83", @@ -5847,6 +5927,7 @@ "version": "0.0.83", "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.83.tgz", "integrity": "sha512-a//eDjez3WZF47aaPTfVUDD0OINOq+WVwBZCRTqG+m0a5euX8EwlJ4jMY1Wxz0Ocj4xOeekJ70V22Pq0ll8RJw==", + "dev": true, "license": "MIT", "dependencies": { "@types/d3-drag": "^3.0.7", @@ -5874,6 +5955,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dev": true, "license": "MIT", "dependencies": { "mime-types": "^3.0.0", @@ -5887,6 +5969,7 @@ "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -5896,6 +5979,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, "license": "MIT", "dependencies": { "mime-db": "^1.54.0" @@ -5962,6 +6046,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, "license": "MIT", "dependencies": { "ajv": "^8.0.0" @@ -5979,6 +6064,7 @@ "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -5995,6 +6081,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, "license": "MIT" }, "node_modules/ansi-regex": { @@ -6542,6 +6629,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.5", @@ -6629,6 +6717,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "dev": true, "license": "MIT", "dependencies": { "bytes": "^3.1.2", @@ -6653,6 +6742,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -6799,6 +6889,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -6900,6 +6991,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -6913,6 +7005,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -6929,6 +7022,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -7085,6 +7179,7 @@ "version": "5.0.5", "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", + "dev": true, "license": "MIT" }, "node_modules/cliui": { @@ -7119,6 +7214,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -7128,6 +7224,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", + "dev": true, "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", @@ -7234,6 +7331,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -7247,6 +7345,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -7256,12 +7355,14 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true, "license": "MIT" }, "node_modules/cookie": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -7271,6 +7372,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.6.0" @@ -7287,6 +7389,7 @@ "version": "2.8.6", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "dev": true, "license": "MIT", "dependencies": { "object-assign": "^4", @@ -7304,6 +7407,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dev": true, "license": "MIT", "dependencies": { "@types/parse-json": "^4.0.0", @@ -7320,6 +7424,7 @@ "version": "1.10.3", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", + "dev": true, "license": "ISC", "engines": { "node": ">= 6" @@ -7329,6 +7434,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.7.tgz", "integrity": "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==", + "dev": true, "license": "MIT" }, "node_modules/cross-dirname": { @@ -7344,6 +7450,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -7379,12 +7486,14 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, "license": "MIT" }, "node_modules/d3-color": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "dev": true, "license": "ISC", "engines": { "node": ">=12" @@ -7394,6 +7503,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "dev": true, "license": "ISC", "engines": { "node": ">=12" @@ -7403,6 +7513,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dev": true, "license": "ISC", "dependencies": { "d3-dispatch": "1 - 3", @@ -7416,6 +7527,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=12" @@ -7425,6 +7537,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dev": true, "license": "ISC", "dependencies": { "d3-color": "1 - 3" @@ -7437,6 +7550,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "dev": true, "license": "ISC", "engines": { "node": ">=12" @@ -7446,6 +7560,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "dev": true, "license": "ISC", "engines": { "node": ">=12" @@ -7455,6 +7570,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dev": true, "license": "ISC", "dependencies": { "d3-color": "1 - 3", @@ -7474,6 +7590,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dev": true, "license": "ISC", "dependencies": { "d3-dispatch": "1 - 3", @@ -7558,6 +7675,7 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -7745,6 +7863,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -7862,6 +7981,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/runtime": "^7.8.7", @@ -7901,6 +8021,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -7932,6 +8053,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, "license": "MIT" }, "node_modules/ejs": { @@ -8635,6 +8757,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -8687,6 +8810,7 @@ "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" @@ -8784,6 +8908,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8793,6 +8918,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8837,6 +8963,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -8959,12 +9086,14 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, "license": "MIT" }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -9551,6 +9680,7 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -9560,6 +9690,7 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "dev": true, "license": "MIT", "dependencies": { "eventsource-parser": "^3.0.1" @@ -9572,6 +9703,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz", "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=18.0.0" @@ -9598,6 +9730,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "dev": true, "license": "MIT", "dependencies": { "accepts": "^2.0.0", @@ -9641,6 +9774,7 @@ "version": "8.7.1", "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.7.1.tgz", "integrity": "sha512-2FXbqkygil3mZiXzEtM0IjE/UBmBNot0J1GEVrJT4JbrvwOSoEtqtO+wHbiMQELWe1eNvHoYRg2dkjOid+6JkA==", + "dev": true, "license": "MIT", "dependencies": { "debug": "^4.4.3", @@ -9660,6 +9794,7 @@ "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -9669,6 +9804,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, "license": "MIT", "dependencies": { "mime-db": "^1.54.0" @@ -9692,6 +9828,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, "license": "MIT" }, "node_modules/fast-json-stable-stringify": { @@ -9712,6 +9849,7 @@ "version": "3.1.8", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", + "dev": true, "funding": [ { "type": "github", @@ -9796,6 +9934,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "dev": true, "license": "MIT", "dependencies": { "debug": "^4.4.0", @@ -9817,6 +9956,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "dev": true, "license": "MIT" }, "node_modules/find-up": { @@ -9922,6 +10062,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -9931,6 +10072,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -9977,6 +10119,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -10057,6 +10200,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -10081,6 +10225,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -10256,6 +10401,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -10353,6 +10499,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -10394,6 +10541,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -10423,6 +10571,7 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "react-is": "^16.7.0" @@ -10432,12 +10581,14 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, "license": "MIT" }, "node_modules/hono": { "version": "4.13.13", "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.13.tgz", "integrity": "sha512-CQ46U0ZkAGmbT/4UxdzzGJpacP2IeKgY4a5/tOI9AABbpOMfK739wfDXmv1usCk+3RkKj1hQy4/fjhiwa2xlrA==", + "dev": true, "license": "MIT", "engines": { "node": ">=16.9.0" @@ -10507,6 +10658,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, "license": "MIT", "dependencies": { "depd": "~2.0.0", @@ -10569,6 +10721,7 @@ "version": "0.7.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "dev": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -10585,6 +10738,7 @@ "version": "11.1.18", "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.18.tgz", "integrity": "sha512-EQyQtLiYW029lyoczMl/Hh4Xu7cDecSc58JRYpHyL4tIAu3eqd1yJzQX04d2BZHDkzFFvm6qJEJWOtfDSWAXbQ==", + "dev": true, "license": "MIT", "funding": { "type": "opencollective", @@ -10595,6 +10749,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -10654,6 +10809,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, "license": "ISC" }, "node_modules/ini": { @@ -10685,6 +10841,7 @@ "version": "10.7.3", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.3.tgz", "integrity": "sha512-A1kdq/tSb5QjvKvAMgIoEvDBIgL7qaqVP/jkvSwYYRZ9iEzvPpopxp2wQfu3SuZRHtpHNxMn8Fs0bS+gf5Xmwg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 12" @@ -10694,6 +10851,7 @@ "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.10" @@ -10747,6 +10905,7 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, "license": "MIT" }, "node_modules/is-async-function": { @@ -10829,6 +10988,7 @@ "version": "2.16.2", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.3" @@ -11093,6 +11253,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, "license": "MIT" }, "node_modules/is-regex": { @@ -11281,6 +11442,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, "license": "ISC" }, "node_modules/iterator.prototype": { @@ -11356,6 +11518,7 @@ "version": "6.2.12", "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz", "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" @@ -11365,6 +11528,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -11454,6 +11618,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -11473,6 +11638,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, "license": "MIT" }, "node_modules/json-schema-traverse": { @@ -11486,6 +11652,7 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "dev": true, "license": "BSD-2-Clause" }, "node_modules/json-stable-stringify-without-jsonify": { @@ -11598,6 +11765,7 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, "license": "MIT" }, "node_modules/load-plugin": { @@ -11660,6 +11828,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" @@ -11747,6 +11916,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -11920,6 +12090,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -11933,6 +12104,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -12688,6 +12860,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -12736,6 +12909,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", + "dev": true, "license": "MIT", "dependencies": { "content-type": "^2.1.0" @@ -12752,6 +12926,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -13054,6 +13229,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -13063,6 +13239,7 @@ "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -13189,6 +13366,7 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, "license": "MIT", "dependencies": { "ee-first": "1.1.1" @@ -13201,6 +13379,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -13386,6 +13565,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -13425,6 +13605,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", @@ -13456,6 +13637,7 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -13485,6 +13667,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -13494,6 +13677,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, "license": "MIT" }, "node_modules/path-scurry": { @@ -13524,6 +13708,7 @@ "version": "8.4.2", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "dev": true, "license": "MIT", "funding": { "type": "opencollective", @@ -13534,6 +13719,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -13569,6 +13755,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -13588,6 +13775,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=16.20.0" @@ -13843,6 +14031,7 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", @@ -13854,6 +14043,7 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, "license": "MIT" }, "node_modules/proper-lockfile": { @@ -13872,6 +14062,7 @@ "version": "2.0.8", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz", "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==", + "dev": true, "license": "MIT", "dependencies": { "forwarded": "0.2.0", @@ -13950,6 +14141,7 @@ "version": "6.16.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "es-define-property": "^1.0.1", @@ -13979,6 +14171,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -13992,6 +14185,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "dev": true, "license": "MIT", "dependencies": { "bytes": "~3.1.2", @@ -14007,6 +14201,7 @@ "version": "19.3.0", "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -14016,6 +14211,7 @@ "version": "19.3.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz", "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", + "dev": true, "license": "MIT", "dependencies": { "scheduler": "^0.28.0" @@ -14028,12 +14224,14 @@ "version": "19.3.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.3.0.tgz", "integrity": "sha512-UpMYezM4v5/18F28aC66AEsjXIgE02kyEMH6yLdgLXu/UTfa1Ntwck/nNLrbqJsEXW7gPb0coNO9FQse9WTovA==", + "dev": true, "license": "MIT" }, "node_modules/react-redux": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==", + "dev": true, "license": "MIT", "dependencies": { "@types/use-sync-external-store": "^0.0.6", @@ -14067,6 +14265,7 @@ "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "@babel/runtime": "^7.5.5", @@ -14186,12 +14385,14 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "dev": true, "license": "MIT" }, "node_modules/redux-thunk": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "dev": true, "license": "MIT", "peerDependencies": { "redux": "^5.0.0" @@ -14330,6 +14531,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -14357,12 +14559,14 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.3.0.tgz", "integrity": "sha512-XGoLeRAVzUTcJ1qkxPQhDJyIZ5d6zzZD9nT7AEZOaaU9UbWclhycElmhO+VD5bFeLuzhPBaOV2oXC8uG35ZSpg==", + "dev": true, "license": "MIT" }, "node_modules/resolve": { "version": "1.22.12", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -14391,6 +14595,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -14513,6 +14718,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dev": true, "license": "MIT", "dependencies": { "debug": "^4.4.0", @@ -14605,6 +14811,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, "license": "MIT" }, "node_modules/sanitize-filename": { @@ -14644,6 +14851,7 @@ "version": "0.28.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "dev": true, "license": "MIT" }, "node_modules/scslre": { @@ -14686,6 +14894,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "dev": true, "license": "MIT", "dependencies": { "debug": "^4.4.3", @@ -14712,6 +14921,7 @@ "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -14721,6 +14931,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, "license": "MIT", "dependencies": { "mime-db": "^1.54.0" @@ -14754,6 +14965,7 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "dev": true, "license": "MIT", "dependencies": { "encodeurl": "^2.0.0", @@ -14822,12 +15034,14 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, "license": "ISC" }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -14840,6 +15054,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -14849,6 +15064,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -14868,6 +15084,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -14884,6 +15101,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -14902,6 +15120,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -14941,6 +15160,7 @@ "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -15045,6 +15265,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -15450,12 +15671,14 @@ "version": "4.1.4", "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.4.tgz", "integrity": "sha512-XXWIQt633/EpAFx8aZDOTjBzrCaGmhvEQlQo6MVPfa2OzO2cWo+4hV9h+6UkHYlXGfy+ODXKUdP7Pthmcu5ATw==", + "dev": true, "license": "MIT" }, "node_modules/stylis": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "dev": true, "license": "MIT" }, "node_modules/sumchecker": { @@ -15488,6 +15711,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -15695,6 +15919,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.6" @@ -15790,6 +16015,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, "license": "0BSD" }, "node_modules/type-check": { @@ -15823,6 +16049,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "dev": true, "license": "MIT", "dependencies": { "content-type": "^2.0.0", @@ -15841,6 +16068,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -15854,6 +16082,7 @@ "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -15863,6 +16092,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, "license": "MIT", "dependencies": { "mime-db": "^1.54.0" @@ -16301,6 +16531,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -16418,6 +16649,7 @@ "version": "1.7.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.7.0.tgz", "integrity": "sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A==", + "dev": true, "license": "MIT", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -16462,6 +16694,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -16787,6 +17020,7 @@ "version": "2.2.8", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "dev": true, "license": "MIT" }, "node_modules/w3c-xmlserializer": { @@ -16862,6 +17096,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -17026,6 +17261,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, "license": "ISC" }, "node_modules/ws": { @@ -17174,6 +17410,7 @@ "version": "4.6.5", "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz", "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" @@ -17183,6 +17420,7 @@ "version": "3.25.2", "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "dev": true, "license": "ISC", "peerDependencies": { "zod": "^3.25.28 || ^4" @@ -17205,6 +17443,7 @@ "version": "4.5.7", "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "dev": true, "license": "MIT", "dependencies": { "use-sync-external-store": "^1.2.2" diff --git a/package.json b/package.json index f1cb40e..00825ec 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "test:watch": "vitest --project renderer --project main", "test:e2e": "electron-vite build && playwright test -c e2e/playwright.config.ts" }, - "dependencies": { + "devDependencies": { "@codemirror/autocomplete": "^6.20.3", "@codemirror/language": "^6.12.4", "@codemirror/state": "^6.7.4", @@ -59,26 +59,20 @@ "@mui/icons-material": "^9.4.0", "@mui/material": "^9.2.0", "@mui/x-tree-view": "^9.15.0", - "@northern.tech/themes": "^0.17.0", - "@reduxjs/toolkit": "^2.12.0", - "@uiw/react-codemirror": "^4.25.11", - "@xyflow/react": "^12.12.0", - "react": "^19.3.0", - "react-dom": "^19.3.0", - "react-redux": "^9.3.0", - "zod": "^4.6.5" - }, - "devDependencies": { "@northern.tech/eslint-config": "^0.7.2", "@northern.tech/prettier-config": "^0.2.1", + "@northern.tech/themes": "^0.17.0", "@playwright/test": "^1.63.0", + "@reduxjs/toolkit": "^2.12.0", "@testing-library/jest-dom": "^7.0.1", "@testing-library/react": "^16.3.3", "@types/node": "^26.6.4", "@types/react": "^19.3.0", "@types/react-dom": "^19.3.0", + "@uiw/react-codemirror": "^4.25.11", "@vitejs/plugin-react": "^5.2.0", "@vitest/coverage-v8": "^5.0.3", + "@xyflow/react": "^12.12.0", "electron": "^44.5.1", "electron-builder": "^26.17.0", "electron-vite": "^5.0.0", @@ -86,8 +80,12 @@ "globals": "^17.13.0", "jsdom": "^30.1.2", "prettier": "^3.9.9", + "react": "^19.3.0", + "react-dom": "^19.3.0", + "react-redux": "^9.3.0", "typescript": "^6.0.3", "vite": "^7.3.7", - "vitest": "^5.0.1" + "vitest": "^5.0.1", + "zod": "^4.6.5" } } From ef93330f17e51064dfc2636c38dc2fe55178e048 Mon Sep 17 00:00:00 2001 From: Ihor Aleksandrychiev Date: Tue, 13 Oct 2026 10:38:42 +0300 Subject: [PATCH 5/9] Canvas arrows, gates and data steps mount their menu on first opening A closed MUI Menu still renders a Popover and a Modal: with one per arrow, a 150-block file re-rendered ~600 of them on every drag move or keystroke. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Ihor Aleksandrychiev --- src/renderer/src/components/DataNode.tsx | 33 +++++----- src/renderer/src/components/GateNode.tsx | 67 +++++++++++---------- src/renderer/src/components/OutcomeEdge.tsx | 11 ++-- src/renderer/src/hooks/useMenuAnchor.ts | 20 ++++++ 4 files changed, 78 insertions(+), 53 deletions(-) create mode 100644 src/renderer/src/hooks/useMenuAnchor.ts diff --git a/src/renderer/src/components/DataNode.tsx b/src/renderer/src/components/DataNode.tsx index 71eebc3..29f1d45 100644 --- a/src/renderer/src/components/DataNode.tsx +++ b/src/renderer/src/components/DataNode.tsx @@ -1,4 +1,4 @@ -import { type ReactNode, useMemo, useState } from 'react'; +import { type ReactNode, useMemo } from 'react'; import CloseIcon from '@mui/icons-material/Close'; import DataObjectIcon from '@mui/icons-material/DataObject'; @@ -12,6 +12,7 @@ import { currentChainType, decoratorsById } from '../blocks/decorators'; import type { DecoratorValue } from '../blocks/evaluateDecorator'; import { runDecoratorChain } from '../blocks/runDecoratorChain'; import { DATA_NODE_WIDTH, type DataChain } from '../canvas/dataChains'; +import { useMenuAnchor } from '../hooks/useMenuAnchor'; import { AddDecoratorButton } from './PropertiesPanel'; import { stopCanvasEvents } from './stopCanvasEvents'; @@ -35,27 +36,29 @@ function formatValue(value: DecoratorValue): string { const oneLine = { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } as const; function StepMenu({ index, count, onMove, onRemove }: { count: number; index: number; onMove: (to: number) => void; onRemove: () => void }) { - const [anchor, setAnchor] = useState(null); + const menu = useMenuAnchor(); const close = (action: () => void) => () => { action(); - setAnchor(null); + menu.close(); }; return ( <> - setAnchor(event.currentTarget)} sx={{ p: 0 }}> + menu.open(event.currentTarget)} sx={{ p: 0 }}> - setAnchor(null)}> - onMove(index - 1))}> - - - onMove(index + 1))}> - - - - - - + {menu.mounted && ( + + onMove(index - 1))}> + + + onMove(index + 1))}> + + + + + + + )} ); } diff --git a/src/renderer/src/components/GateNode.tsx b/src/renderer/src/components/GateNode.tsx index 32109de..049a082 100644 --- a/src/renderer/src/components/GateNode.tsx +++ b/src/renderer/src/components/GateNode.tsx @@ -1,5 +1,3 @@ -import { useState } from 'react'; - import FilterAltOutlinedIcon from '@mui/icons-material/FilterAltOutlined'; import MoreHorizIcon from '@mui/icons-material/MoreHoriz'; import { Box, Divider, IconButton, ListItemText, Menu, MenuItem, Typography, useTheme } from '@mui/material'; @@ -8,6 +6,7 @@ import { Handle, type Node, type NodeProps, Position, useStore } from '@xyflow/r import type { FileConditionRelation } from '../canvas/fileCondition'; import { GATE_WIDTH, type Gate } from '../canvas/gates'; +import { useMenuAnchor } from '../hooks/useMenuAnchor'; import { COMPACT_ZOOM, type ConditionSource } from './BlockCard'; import { stopCanvasEvents } from './stopCanvasEvents'; @@ -43,7 +42,7 @@ function GateCaption({ fileRelation, fileLabel }: { fileLabel?: string; fileRela export function GateNode({ data, selected }: NodeProps) { const { gate, source, fileRelation, onHighlight, onModeChange, onRemove } = data; const theme = useTheme(); - const [menuAnchor, setMenuAnchor] = useState(null); + const menu = useMenuAnchor(); const { mode, className } = gate.condition; const blocks = gate.instanceIds.length; const compact = useStore(state => state.transform[2] < COMPACT_ZOOM); @@ -88,7 +87,7 @@ export function GateNode({ data, selected }: NodeProps) { {!compact && } - setMenuAnchor(event.currentTarget)} sx={{ p: 0.25 }}> + menu.open(event.currentTarget)} sx={{ p: 0.25 }}> ) { title="Drag onto another block's left edge to gate it too" style={{ width: 10, height: 10, background: theme.palette.text.secondary, border: `2px solid ${theme.palette.background.default}` }} /> - setMenuAnchor(null)}> - { - onModeChange('if'); - setMenuAnchor(null); - }} - > - - - { - onModeChange('unless'); - setMenuAnchor(null); - }} - > - - - - { - onRemove(); - setMenuAnchor(null); - }} - > - 1 ? `Remove from all ${blocks} blocks` : 'Remove condition'} /> - - + {menu.mounted && ( + + { + onModeChange('if'); + menu.close(); + }} + > + + + { + onModeChange('unless'); + menu.close(); + }} + > + + + + { + onRemove(); + menu.close(); + }} + > + 1 ? `Remove from all ${blocks} blocks` : 'Remove condition'} /> + + + )} ); } diff --git a/src/renderer/src/components/OutcomeEdge.tsx b/src/renderer/src/components/OutcomeEdge.tsx index 77c2cc9..c4dd861 100644 --- a/src/renderer/src/components/OutcomeEdge.tsx +++ b/src/renderer/src/components/OutcomeEdge.tsx @@ -1,10 +1,9 @@ -import { useState } from 'react'; - import { Box, useTheme } from '@mui/material'; import { BaseEdge, type Edge, EdgeLabelRenderer, type EdgeProps, type InternalNode, Position, getBezierPath, useInternalNode } from '@xyflow/react'; import { describeOutcomes } from '../canvas/outcomes'; +import { useMenuAnchor } from '../hooks/useMenuAnchor'; import type { BlockOutcome } from '../store/edgesSlice/types'; import { OutcomeMenu } from './OutcomeMenu'; @@ -68,7 +67,7 @@ function floatingEndpoints(source: ReturnType, target: ReturnType export function OutcomeEdge(props: EdgeProps) { const { id, source, target, data, selected, markerEnd } = props; const theme = useTheme(); - const [anchorEl, setAnchorEl] = useState(null); + const menu = useMenuAnchor(); const sourceNode = useInternalNode(source); const targetNode = useInternalNode(target); // Until both blocks are measured, fall back to React Flow's handle positions. @@ -84,7 +83,7 @@ export function OutcomeEdge(props: EdgeProps) { setAnchorEl(event.currentTarget)} + onClick={event => menu.open(event.currentTarget)} title="Change or remove this arrow" sx={{ position: 'absolute', @@ -105,7 +104,9 @@ export function OutcomeEdge(props: EdgeProps) { {port.symbol} {port.label} - setAnchorEl(null)} outcomes={data.outcomes} onChange={data.onOutcomesChange} onRemove={data.onDelete} /> + {menu.mounted && ( + + )} ); } diff --git a/src/renderer/src/hooks/useMenuAnchor.ts b/src/renderer/src/hooks/useMenuAnchor.ts new file mode 100644 index 0000000..a60bc81 --- /dev/null +++ b/src/renderer/src/hooks/useMenuAnchor.ts @@ -0,0 +1,20 @@ +import { useState } from 'react'; + +/** + * A menu's anchor, and whether to mount the menu at all: from its first opening on, so the + * closing transition still plays. A closed MUI Menu still renders a Popover and a Modal, which + * on every canvas block or arrow adds hundreds of components to each re-render. + */ +export function useMenuAnchor() { + const [anchor, setAnchor] = useState(null); + const [mounted, setMounted] = useState(false); + return { + anchor, + mounted, + open: (element: HTMLElement) => { + setMounted(true); + setAnchor(element); + }, + close: () => setAnchor(null) + }; +} From 5bc4fcfbb862c7e8164e3364243b0a5ffe13b477 Mon Sep 17 00:00:00 2001 From: Ihor Aleksandrychiev Date: Tue, 13 Oct 2026 10:48:06 +0300 Subject: [PATCH 6/9] Canvas re-renders only the blocks and arrows that changed Every node and edge was rebuilt with new data and callbacks on each render, and the click handlers React Flow hands to each wrapper were new too, so a drag move or keystroke re-rendered the whole canvas: ~340 ms per move with 150 blocks, ~67 ms now. Unchanged items keep their objects, their callbacks reach the canvas's handlers through a latest-ref, and duplicateKeys is a new Set only when its keys change. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Ihor Aleksandrychiev --- src/renderer/src/canvas/stableItems.ts | 32 +++++++++ src/renderer/src/components/FlowCanvas.tsx | 81 ++++++++++++++-------- src/renderer/src/hooks/useLatest.ts | 16 +++++ src/renderer/src/pages/ProjectView.tsx | 7 +- 4 files changed, 108 insertions(+), 28 deletions(-) create mode 100644 src/renderer/src/canvas/stableItems.ts create mode 100644 src/renderer/src/hooks/useLatest.ts diff --git a/src/renderer/src/canvas/stableItems.ts b/src/renderer/src/canvas/stableItems.ts new file mode 100644 index 0000000..010837c --- /dev/null +++ b/src/renderer/src/canvas/stableItems.ts @@ -0,0 +1,32 @@ +const isPlainObject = (value: unknown): value is Record => + typeof value === 'object' && value !== null && Object.getPrototypeOf(value) === Object.prototype; + +// Equal down to `depth` levels of plain objects and arrays; any two functions count as equal. +export function sameShape(a: unknown, b: unknown, depth: number): boolean { + if (Object.is(a, b)) return true; + if (typeof a === 'function' && typeof b === 'function') return true; + if (depth === 0) return false; + if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((item, index) => sameShape(item, b[index], depth - 1)); + if (!isPlainObject(a) || !isPlainObject(b)) return false; + const keys = Object.keys(a); + return keys.length === Object.keys(b).length && keys.every(key => key in b && sameShape(a[key], b[key], depth - 1)); +} + +/** + * React Flow re-renders a node or edge whenever its object changes. These are rebuilt on every + * canvas render, so each is swapped for last call's object when nothing in it changed (three + * levels deep, callbacks ignored). The callbacks must only capture what the item itself holds, + * and reach the canvas's handlers through a latest-ref (useLatest), or a reused one goes stale. + * Create one per list and keep it for the component's life: `useState(() => stableItems())`. + */ +export function stableItems(): (items: T[]) => T[] { + let previous = new Map(); + return items => { + const stable = items.map(item => { + const before = previous.get(item.id); + return before && sameShape(before, item, 3) ? before : item; + }); + previous = new Map(stable.map(item => [item.id, item])); + return stable; + }; +} diff --git a/src/renderer/src/components/FlowCanvas.tsx b/src/renderer/src/components/FlowCanvas.tsx index fdad3bf..a44e0f6 100644 --- a/src/renderer/src/components/FlowCanvas.tsx +++ b/src/renderer/src/components/FlowCanvas.tsx @@ -38,6 +38,8 @@ import { GATE_EDGE_PREFIX, GATE_WIDTH, type Gate, deriveGates } from '../canvas/ import { GROUP_NODE_PREFIX, type GroupFrame, type Rect, deriveGroupFrames, frameAt, settleGroup } from '../canvas/groupFrames'; import { GRID_SIZE, NODE_WIDTH, estimateNodeHeight } from '../canvas/layout'; import { defaultOutcomesFor, describeOutcomes } from '../canvas/outcomes'; +import { stableItems } from '../canvas/stableItems'; +import { useLatest, useStableCallback } from '../hooks/useLatest'; import { useBlockProblems } from '../project/testRuns'; import { useAppSelector } from '../store'; import type { BlockInstance } from '../store/canvasSlice/types'; @@ -339,6 +341,23 @@ export function FlowCanvas({ // Where dragged blocks started, to carry a resized frame along when its whole group moves. const dragStartRef = useRef>(new Map()); + // Unchanged nodes and edges keep their objects, so React Flow re-renders only the changed ones. + const [stableNodes] = useState(() => stableItems()); + const [stableEdges] = useState(() => stableItems()); + // What their callbacks call: a reused item keeps its first closures. + const handlers = useLatest({ + dataCallbacks, + onEdgeOutcomesChange, + onEdgeRemove, + onGateLinkRemove, + onGateModeChange, + onGateRemove, + onGroupResize, + onNodeDragStart, + onNodeDragStop, + onRemove + }); + const order = useMemo(() => executionOrder(instances, edges, blockDescriptorsById), [instances, edges]); // Blocks whose promises failed in the last test run, and on which hosts. const testEnvironments = useAppSelector(state => state.testEnvironments); @@ -374,7 +393,7 @@ export function FlowCanvas({ descriptor, duplicateKeys, orderNumber: orderNumbers.get(instance.instanceId), - onRemove: () => onRemove(instance.instanceId), + onRemove: () => handlers.current.onRemove(instance.instanceId), cutPending: instance.instanceId === cutPendingId, highlighted: instance.instanceId === highlightedId, hasDataInput: fedBlocks.has(instance.instanceId), @@ -406,8 +425,8 @@ export function FlowCanvas({ source: conditionSourceOf(gate.condition.className, allInstances, files, currentFileId), fileRelation: relationToFileCondition(gate.condition, fileCondition), onHighlight: setHighlightedId, - onModeChange: mode => onGateModeChange(gate, mode), - onRemove: () => onGateRemove(gate) + onModeChange: mode => handlers.current.onGateModeChange(gate, mode), + onRemove: () => handlers.current.onGateRemove(gate) } })); const gateEdges: GateFlowEdge[] = gates.flatMap(gate => @@ -419,7 +438,7 @@ export function FlowCanvas({ targetHandle: 'gate', type: 'gate' as const, selected: `${GATE_EDGE_PREFIX}${instanceId}` === selectedEdgeId, - data: { onRemove: () => onGateLinkRemove(instanceId) } + data: { onRemove: () => handlers.current.onGateLinkRemove(instanceId) } })) ); @@ -481,12 +500,12 @@ export function FlowCanvas({ dropTarget: frame.group.id === dropGroupId, onResizeStart: () => { resizingRef.current = true; - onNodeDragStart(); + handlers.current.onNodeDragStart(); }, - onResize: rect => onGroupResize(frame.group.id, rect), + onResize: rect => handlers.current.onGroupResize(frame.group.id, rect), onResizeEnd: () => { resizingRef.current = false; - onNodeDragStop(); + handlers.current.onNodeDragStop(); } } })); @@ -502,7 +521,11 @@ export function FlowCanvas({ type: 'outcome', selected: edge.id === selectedEdgeId, markerEnd: { type: MarkerType.ArrowClosed, color, width: 18, height: 18 }, - data: { outcomes: edge.outcomes, onOutcomesChange: outcomes => onEdgeOutcomesChange(edge.id, outcomes), onDelete: () => onEdgeRemove(edge.id) } + data: { + outcomes: edge.outcomes, + onOutcomesChange: outcomes => handlers.current.onEdgeOutcomesChange(edge.id, outcomes), + onDelete: () => handlers.current.onEdgeRemove(edge.id) + } }; }); const dataNodes: DataFlowNode[] = chains.map(chain => ({ @@ -514,11 +537,11 @@ export function FlowCanvas({ selected: chain.owner.instanceId === selectedInstanceId && chain.owner.entryId === selectedChainEntryId, data: { chain, - onAddStep: decoratorId => dataCallbacks.addStep(chain.owner, decoratorId), - onMoveStep: (fromIndex, toIndex) => dataCallbacks.moveStep(chain.owner, fromIndex, toIndex), - onRemove: () => dataCallbacks.remove(chain.owner), - onRemoveStep: stepId => dataCallbacks.removeStep(chain.owner, stepId), - onSampleChange: value => dataCallbacks.sampleChange(chain.owner, value) + onAddStep: decoratorId => handlers.current.dataCallbacks.addStep(chain.owner, decoratorId), + onMoveStep: (fromIndex, toIndex) => handlers.current.dataCallbacks.moveStep(chain.owner, fromIndex, toIndex), + onRemove: () => handlers.current.dataCallbacks.remove(chain.owner), + onRemoveStep: stepId => handlers.current.dataCallbacks.removeStep(chain.owner, stepId), + onSampleChange: value => handlers.current.dataCallbacks.sampleChange(chain.owner, value) } })); // Solid info-blue arrows: data flowing into what a chain produces — unlike @@ -535,8 +558,8 @@ export function FlowCanvas({ })); // Later nodes draw on top: a chain's controls stay usable even where it overlaps a block. - const nodes: CanvasNode[] = [...groupNodes, ...gateNodes, ...blockNodes, ...dataNodes]; - const flowEdges: CanvasEdge[] = [...gateEdges, ...dataEdges, ...outcomeEdges]; + const nodes = stableNodes([...groupNodes, ...gateNodes, ...blockNodes, ...dataNodes]); + const flowEdges = stableEdges([...gateEdges, ...dataEdges, ...outcomeEdges]); // Positions and measured sizes live outside React Flow (Redux / the // parent), so only those two change kinds matter; selection is driven by @@ -715,6 +738,20 @@ export function FlowCanvas({ return null; }; + // Stable: React Flow hands them to every node and edge wrapper, which re-render when they change. + const handleNodeClick = useStableCallback((event: React.MouseEvent, node: CanvasNode) => { + const frame = frameByNodeId.get(node.id); + if (frame) return onSelectGroup(frame.group.id); + const gate = gateByNodeId.get(node.id); + const chain = chainByNodeId.get(node.id); + if (event.shiftKey && byId.has(node.id)) toggleSelected(node.id); + else if (gate) onSelectGate(gate.key); + else if (chain) dataCallbacks.open(chain.owner); + else onSelectInstance(node.id); + }); + // Data arrows aren't selectable, but React Flow still reports their clicks. + const handleEdgeClick = useStableCallback((_event: React.MouseEvent, edge: CanvasEdge) => edge.selectable !== false && onSelectEdge(edge.id)); + return ( { - const frame = frameByNodeId.get(node.id); - if (frame) return onSelectGroup(frame.group.id); - const gate = gateByNodeId.get(node.id); - const chain = chainByNodeId.get(node.id); - if (event.shiftKey && byId.has(node.id)) toggleSelected(node.id); - else if (gate) onSelectGate(gate.key); - else if (chain) dataCallbacks.open(chain.owner); - else onSelectInstance(node.id); - }} - // Data arrows aren't selectable, but React Flow still reports their clicks. - onEdgeClick={(_event, edge) => edge.selectable !== false && onSelectEdge(edge.id)} + onNodeClick={handleNodeClick} + onEdgeClick={handleEdgeClick} onPaneClick={clearSelection} onConnect={connection => { if (connectionProblem(connection)) return; diff --git a/src/renderer/src/hooks/useLatest.ts b/src/renderer/src/hooks/useLatest.ts new file mode 100644 index 0000000..8b753df --- /dev/null +++ b/src/renderer/src/hooks/useLatest.ts @@ -0,0 +1,16 @@ +import { useCallback, useLayoutEffect, useRef } from 'react'; + +/** A ref always holding the last rendered `value`: for callbacks created once that must call the current handlers. */ +export function useLatest(value: T) { + const latest = useRef(value); + useLayoutEffect(() => { + latest.current = value; + }); + return latest; +} + +/** A function that keeps its identity across renders but always runs the last rendered `callback`. */ +export function useStableCallback(callback: (...args: A) => R): (...args: A) => R { + const latest = useLatest(callback); + return useCallback((...args: A) => latest.current(...args), [latest]); +} diff --git a/src/renderer/src/pages/ProjectView.tsx b/src/renderer/src/pages/ProjectView.tsx index c345bb8..eb6a618 100644 --- a/src/renderer/src/pages/ProjectView.tsx +++ b/src/renderer/src/pages/ProjectView.tsx @@ -701,7 +701,12 @@ export default function ProjectView({ dirty, locked, onConnectAgent, onOpenSetti useEffect(() => () => clearTimeout(statusMessageTimeoutRef.current), []); - const duplicateKeys = useMemo(() => duplicateDefinitionKeys(allInstances, blockDescriptorsById, currentFileId), [allInstances, currentFileId]); + // A new Set only when its keys change: every block card depends on it. + const duplicateList = useMemo( + () => [...duplicateDefinitionKeys(allInstances, blockDescriptorsById, currentFileId)].sort().join('\n'), + [allInstances, currentFileId] + ); + const duplicateKeys = useMemo(() => new Set(duplicateList ? duplicateList.split('\n') : []), [duplicateList]); const handleEscape = () => { if (selectedGroup) setSelectedGroupId(null); From 0aa261b8c343c00140f98beec26d613162b7fff7 Mon Sep 17 00:00:00 2001 From: Ihor Aleksandrychiev Date: Tue, 13 Oct 2026 10:55:51 +0300 Subject: [PATCH 7/9] Block palette and file explorer no longer re-render on canvas edits Both are memoized with stable callbacks, the explorer takes block counts per file instead of every block, and dnd-kit gets stable sensor options and handlers, whose change re-rendered every palette row. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Ihor Aleksandrychiev --- src/renderer/src/components/BlockPalette.tsx | 6 +- .../components/PolicyFileExplorer.test.tsx | 4 +- .../src/components/PolicyFileExplorer.tsx | 14 ++--- src/renderer/src/pages/ProjectView.tsx | 59 ++++++++++++++----- 4 files changed, 57 insertions(+), 26 deletions(-) diff --git a/src/renderer/src/components/BlockPalette.tsx b/src/renderer/src/components/BlockPalette.tsx index 2a12759..23801ca 100644 --- a/src/renderer/src/components/BlockPalette.tsx +++ b/src/renderer/src/components/BlockPalette.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from 'react'; +import { memo, useMemo, useState } from 'react'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import SearchIcon from '@mui/icons-material/Search'; @@ -75,7 +75,7 @@ function CategoryGroup({ category, blocks, onAdd }: { blocks: BlockDescriptor[]; ); } -export function BlockPalette({ onAddBlock }: { onAddBlock: (block: BlockDescriptor) => void }) { +export const BlockPalette = memo(function BlockPalette({ onAddBlock }: { onAddBlock: (block: BlockDescriptor) => void }) { const [search, setSearch] = useState(''); const groups = useMemo(() => { @@ -119,4 +119,4 @@ export function BlockPalette({ onAddBlock }: { onAddBlock: (block: BlockDescript ); -} +}); diff --git a/src/renderer/src/components/PolicyFileExplorer.test.tsx b/src/renderer/src/components/PolicyFileExplorer.test.tsx index fa24baf..642d40e 100644 --- a/src/renderer/src/components/PolicyFileExplorer.test.tsx +++ b/src/renderer/src/components/PolicyFileExplorer.test.tsx @@ -28,12 +28,14 @@ function ConnectedExplorer() { const dispatch = useAppDispatch(); const { files, folders, currentFileId } = useAppSelector(state => state.files); const instances = useAppSelector(state => state.canvas); + const blockCounts: Record = {}; + for (const instance of instances) blockCounts[instance.fileId] = (blockCounts[instance.fileId] ?? 0) + 1; return ( dispatch(fileSelected({ fileId }))} onAddFile={(name, parentId) => dispatch(fileAdded(name, parentId))} onAddFolder={(name, parentId) => dispatch(folderAdded(name, parentId))} diff --git a/src/renderer/src/components/PolicyFileExplorer.tsx b/src/renderer/src/components/PolicyFileExplorer.tsx index 008726d..e68891f 100644 --- a/src/renderer/src/components/PolicyFileExplorer.tsx +++ b/src/renderer/src/components/PolicyFileExplorer.tsx @@ -1,4 +1,4 @@ -import { type HTMLAttributes, type ReactNode, useEffect, useRef, useState } from 'react'; +import { type HTMLAttributes, type ReactNode, memo, useEffect, useRef, useState } from 'react'; import CreateNewFolderOutlinedIcon from '@mui/icons-material/CreateNewFolderOutlined'; import DeleteOutlineIcon from '@mui/icons-material/DeleteOutlineOutlined'; @@ -12,16 +12,16 @@ import { Box, IconButton, Menu, MenuItem, TextField, Tooltip, Typography } from import { SimpleTreeView, TreeItem } from '@mui/x-tree-view'; import { describeFileCondition } from '../canvas/fileCondition'; -import type { BlockInstance } from '../store/canvasSlice/types'; import { type FileTreeNode, MAX_NAME_LENGTH, buildFileTree, collectFolderDescendants } from '../store/filesSlice/fileTree'; import type { PolicyFile, PolicyFolder } from '../store/filesSlice/types'; import { ConfirmDialog } from './dialogs/ConfirmDialog'; interface PolicyFileExplorerProps { + // Blocks per file id. + blockCounts: Record; currentFileId: string | null; files: PolicyFile[]; folders: PolicyFolder[]; - instances: BlockInstance[]; onAddFile: (name: string, parentId: string | null) => void; onAddFolder: (name: string, parentId: string | null) => void; onDeleteFile: (fileId: string) => void; @@ -368,11 +368,11 @@ function DraftRow({ kind, name, onNameChange, onCommit, onCancel }: DraftRowProp * namespace (see architecture-plan.md's Namespaces section). Folders are * purely organizational — see PolicyFile.parentId. */ -export function PolicyFileExplorer({ +export const PolicyFileExplorer = memo(function PolicyFileExplorer({ files, folders, currentFileId, - instances, + blockCounts, onSelectFile, onAddFile, onAddFolder, @@ -422,7 +422,7 @@ export function PolicyFileExplorer({ return () => cancelAnimationFrame(frame); }, [currentFileId]); - const blockCountFor = (fileId: string) => instances.filter(instance => instance.fileId === fileId).length; + const blockCountFor = (fileId: string) => blockCounts[fileId] ?? 0; const startRename = (kind: 'file' | 'folder', id: string, name: string) => { setEditing({ kind, id, name }); @@ -750,4 +750,4 @@ export function PolicyFileExplorer({ /> ); -} +}); diff --git a/src/renderer/src/pages/ProjectView.tsx b/src/renderer/src/pages/ProjectView.tsx index eb6a618..3999718 100644 --- a/src/renderer/src/pages/ProjectView.tsx +++ b/src/renderer/src/pages/ProjectView.tsx @@ -38,6 +38,7 @@ import { ConfirmDialog } from '../components/dialogs/ConfirmDialog'; import { ConditionSection } from '../components/properties/ConditionSection'; import { RunsWhenSection } from '../components/properties/RunsWhenSection'; import { buildClassNameOptions } from '../components/properties/classOptions'; +import { useStableCallback } from '../hooks/useLatest'; import { LEFT_SIDEBAR_MAX, LEFT_SIDEBAR_MIN, @@ -283,6 +284,9 @@ interface ProjectViewProps { onSave: () => Promise; } +// A module constant: a new options object each render would make new sensors, and re-render every draggable. +const POINTER_SENSOR_OPTIONS = { activationConstraint: { distance: 8 } }; + /** The open project; saving it into cfbs.json is owned by App (project/useProjectSession.ts). */ export default function ProjectView({ dirty, locked, onConnectAgent, onOpenSettings, onReload, onSave }: ProjectViewProps) { const dispatch = useAppDispatch(); @@ -401,7 +405,7 @@ export default function ProjectView({ dirty, locked, onConnectAgent, onOpenSetti })); }; - const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 8 } })); + const sensors = useSensors(useSensor(PointerSensor, POINTER_SENSOR_OPTIONS)); const sizeOf = (instance: BlockInstance) => layoutSizes[instance.instanceId] ?? { width: NODE_WIDTH, height: estimateNodeHeight(instance, blockDescriptorsById.get(instance.blockId)) }; @@ -784,6 +788,34 @@ export default function ProjectView({ dirty, locked, onConnectAgent, onOpenSetti } }); + // New handlers re-render every draggable (the palette's rows) through dnd-kit's context. + const dndHandlers = { + onDragStart: useStableCallback(handleDragStart), + onDragEnd: useStableCallback(handleDragEnd), + onDragCancel: useStableCallback(() => setDragPreview(null)) + }; + // The sidebars are memoized: they get stable callbacks, and counts that change only with them. + const paletteAddBlock = useStableCallback((descriptor: BlockDescriptor) => addBlockToCanvas(descriptor)); + const blockCountsJson = useMemo(() => { + const counts: Record = {}; + for (const instance of allInstances) counts[instance.fileId] = (counts[instance.fileId] ?? 0) + 1; + return JSON.stringify(counts); + }, [allInstances]); + const blockCounts = useMemo(() => JSON.parse(blockCountsJson) as Record, [blockCountsJson]); + const explorer = { + onSelectFile: useStableCallback((fileId: string) => { + dispatch(fileSelected({ fileId })); + setSelectedInstanceId(null); + }), + onAddFile: useStableCallback(handleAddFile), + onAddFolder: useStableCallback(handleAddFolder), + onRenameFile: useStableCallback(handleRenameFile), + onRenameFolder: useStableCallback(handleRenameFolder), + onDeleteFile: useStableCallback(handleDeleteFile), + onDeleteFolder: useStableCallback(handleDeleteFolder), + onMove: useStableCallback(handleMove) + }; + if (!project) return null; const selectedInstance = instances.find(instance => instance.instanceId === selectedInstanceId); @@ -959,7 +991,7 @@ export default function ProjectView({ dirty, locked, onConnectAgent, onOpenSetti ); return ( - setDragPreview(null)}> + - + @@ -1001,18 +1033,15 @@ export default function ProjectView({ dirty, locked, onConnectAgent, onOpenSetti files={files} folders={folders} currentFileId={currentFileId} - instances={allInstances} - onSelectFile={fileId => { - dispatch(fileSelected({ fileId })); - setSelectedInstanceId(null); - }} - onAddFile={handleAddFile} - onAddFolder={handleAddFolder} - onRenameFile={handleRenameFile} - onRenameFolder={handleRenameFolder} - onDeleteFile={handleDeleteFile} - onDeleteFolder={handleDeleteFolder} - onMove={handleMove} + blockCounts={blockCounts} + onSelectFile={explorer.onSelectFile} + onAddFile={explorer.onAddFile} + onAddFolder={explorer.onAddFolder} + onRenameFile={explorer.onRenameFile} + onRenameFolder={explorer.onRenameFolder} + onDeleteFile={explorer.onDeleteFile} + onDeleteFolder={explorer.onDeleteFolder} + onMove={explorer.onMove} /> From 786647255b15725b9410a068f923124a214faebb Mon Sep 17 00:00:00 2001 From: Ihor Aleksandrychiev Date: Tue, 13 Oct 2026 10:58:26 +0300 Subject: [PATCH 8/9] Properties panel menus mount on first opening, not with every render Co-Authored-By: Claude Opus 5.5 Signed-off-by: Ihor Aleksandrychiev --- src/renderer/src/components/BlockGroupRow.tsx | 45 +++++----- .../src/components/properties/dataBinding.tsx | 37 +++++---- .../src/components/properties/decorators.tsx | 83 ++++++++++--------- 3 files changed, 86 insertions(+), 79 deletions(-) diff --git a/src/renderer/src/components/BlockGroupRow.tsx b/src/renderer/src/components/BlockGroupRow.tsx index b986128..763359b 100644 --- a/src/renderer/src/components/BlockGroupRow.tsx +++ b/src/renderer/src/components/BlockGroupRow.tsx @@ -1,7 +1,6 @@ -import { useState } from 'react'; - import { Box, Button, Divider, ListItemText, Menu, MenuItem, Typography, useTheme } from '@mui/material'; +import { useMenuAnchor } from '../hooks/useMenuAnchor'; import type { BlockInstance } from '../store/canvasSlice/types'; import type { BlockGroup } from '../store/groupsSlice/types'; @@ -23,7 +22,7 @@ export function BlockGroupRow({ onOpen: (groupId: string) => void; }) { const theme = useTheme(); - const [anchorEl, setAnchorEl] = useState(null); + const menu = useMenuAnchor(); const group = groups.find(candidate => candidate.id === instance.groupId); const others = groups.filter(candidate => candidate.id !== instance.groupId); @@ -41,33 +40,35 @@ export function BlockGroupRow({ ) : ( - )} - setAnchorEl(null)}> - {others.map(candidate => ( + {menu.mounted && ( + + {others.map(candidate => ( + { + onJoin(candidate.id); + menu.close(); + }} + > + + + + ))} + {others.length > 0 && } { - onJoin(candidate.id); - setAnchorEl(null); + onNewGroup(); + menu.close(); }} > - - + - ))} - {others.length > 0 && } - { - onNewGroup(); - setAnchorEl(null); - }} - > - - - + + )} ); } diff --git a/src/renderer/src/components/properties/dataBinding.tsx b/src/renderer/src/components/properties/dataBinding.tsx index 9942fc2..7c2cc27 100644 --- a/src/renderer/src/components/properties/dataBinding.tsx +++ b/src/renderer/src/components/properties/dataBinding.tsx @@ -1,8 +1,9 @@ -import { type ReactNode, useState } from 'react'; +import { type ReactNode } from 'react'; import { Box, Button, ListItemText, ListSubheader, Menu, MenuItem, Stack, Typography } from '@mui/material'; import type { BlockParameter } from '../../blocks/types'; +import { useMenuAnchor } from '../../hooks/useMenuAnchor'; // `nameParam`: the descriptor's entries.name_param — a defined name, never computed. export function isBindable(parameter: BlockParameter, nameParam?: string): boolean { @@ -21,13 +22,13 @@ export function BindToDataLink({ onBind: (valueSourceId: string) => void; sources: { badge?: string; id: string; label: string }[]; }) { - const [anchor, setAnchor] = useState(null); + const menu = useMenuAnchor(); return ( or - setAnchor(null)}> - Compute {fieldLabel} from: - {sources.map(source => ( - { - onBind(source.id); - setAnchor(null); - }} - > - - - ))} - + {menu.mounted && ( + + Compute {fieldLabel} from: + {sources.map(source => ( + { + onBind(source.id); + menu.close(); + }} + > + + + ))} + + )} ); } diff --git a/src/renderer/src/components/properties/decorators.tsx b/src/renderer/src/components/properties/decorators.tsx index 5b82974..a1c557c 100644 --- a/src/renderer/src/components/properties/decorators.tsx +++ b/src/renderer/src/components/properties/decorators.tsx @@ -7,6 +7,7 @@ import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; import { Box, Button, IconButton, ListItemText, Menu, MenuItem, Stack, TextField, Typography } from '@mui/material'; import { decorators } from '../../blocks/decorators'; +import { useMenuAnchor } from '../../hooks/useMenuAnchor'; import type { DecoratorInstance } from '../../store/canvasSlice/types'; import { ParameterField } from './paramFields'; @@ -71,7 +72,7 @@ const DECORATOR_GROUPS: { label: string; note: string; type: 'slist' | 'string' ]; export function AddDecoratorButton({ chainType, onAdd }: { chainType: 'slist' | 'string'; onAdd: (decoratorId: string) => void }) { - const [anchorEl, setAnchorEl] = useState(null); + const menu = useMenuAnchor(); const [search, setSearch] = useState(''); const query = search.trim().toLowerCase(); const matches = (decorator: (typeof decorators)[number]) => @@ -83,51 +84,53 @@ export function AddDecoratorButton({ chainType, onAdd }: { chainType: 'slist' | size="small" startIcon={} onClick={event => { - setAnchorEl(event.currentTarget); + menu.open(event.currentTarget); setSearch(''); }} sx={{ alignSelf: 'flex-start' }} > Add transform - setAnchorEl(null)}> - - setSearch(event.target.value)} - onKeyDown={event => event.stopPropagation()} - fullWidth - /> - - {DECORATOR_GROUPS.flatMap(group => { - const items = decorators.filter(decorator => decorator.input_type === group.type && matches(decorator)); - if (items.length === 0) return []; - const enabled = group.type === chainType; - return [ - - {group.label} - {!enabled && {group.note}} - , - ...items.map(decorator => ( - { - onAdd(decorator.id); - setAnchorEl(null); - }} - > - - {decorator.badge && {decorator.badge}} - - )) - ]; - })} - + {menu.mounted && ( + + + setSearch(event.target.value)} + onKeyDown={event => event.stopPropagation()} + fullWidth + /> + + {DECORATOR_GROUPS.flatMap(group => { + const items = decorators.filter(decorator => decorator.input_type === group.type && matches(decorator)); + if (items.length === 0) return []; + const enabled = group.type === chainType; + return [ + + {group.label} + {!enabled && {group.note}} + , + ...items.map(decorator => ( + { + onAdd(decorator.id); + menu.close(); + }} + > + + {decorator.badge && {decorator.badge}} + + )) + ]; + })} + + )} ); } From e984e764efeb7f36eb0c44e977418b431a92fcc4 Mon Sep 17 00:00:00 2001 From: Ihor Aleksandrychiev Date: Tue, 13 Oct 2026 11:04:29 +0300 Subject: [PATCH 9/9] Multi-line fields on the canvas view are sized by CSS, not measured in JS MUI's TextareaAutosize re-measures after every render, which forces a layout of the whole page: ~200 ms of opening a 150-block file. The file description, block note, text parameters and data samples now use a plain textarea with field-sizing: content and the same min/max rows. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Ihor Aleksandrychiev --- src/renderer/src/components/DataNode.tsx | 4 ++-- src/renderer/src/components/PropertiesPanel.tsx | 4 ++-- src/renderer/src/components/contentSized.ts | 12 ++++++++++++ .../src/components/properties/FileSettingsPanel.tsx | 4 ++-- .../src/components/properties/paramFields.tsx | 6 +++--- 5 files changed, 21 insertions(+), 9 deletions(-) create mode 100644 src/renderer/src/components/contentSized.ts diff --git a/src/renderer/src/components/DataNode.tsx b/src/renderer/src/components/DataNode.tsx index 29f1d45..2a4c081 100644 --- a/src/renderer/src/components/DataNode.tsx +++ b/src/renderer/src/components/DataNode.tsx @@ -14,6 +14,7 @@ import { runDecoratorChain } from '../blocks/runDecoratorChain'; import { DATA_NODE_WIDTH, type DataChain } from '../canvas/dataChains'; import { useMenuAnchor } from '../hooks/useMenuAnchor'; import { AddDecoratorButton } from './PropertiesPanel'; +import { contentSized, plainTextarea } from './contentSized'; import { stopCanvasEvents } from './stopCanvasEvents'; export interface DataNodeData extends Record { @@ -189,12 +190,11 @@ export function DataNode({ data, selected }: NodeProps) { className="nodrag nopan" size="small" multiline - maxRows={2} value={chain.sampleInput} onChange={event => onSampleChange(event.target.value)} placeholder="Sample input, to preview the steps…" sx={{ mb: 0.5, ml: 2.75, width: 'calc(100% - 22px)', '& .MuiInputBase-root': { py: 0.5, px: 0.75 } }} - slotProps={{ htmlInput: { style: { fontSize: 10, fontFamily: 'monospace', lineHeight: 1.3 } } }} + slotProps={{ input: plainTextarea, htmlInput: { style: { ...contentSized(1, 2), fontSize: 10, fontFamily: 'monospace', lineHeight: 1.3 } } }} /> )} {chain.steps.map((step, index) => { diff --git a/src/renderer/src/components/PropertiesPanel.tsx b/src/renderer/src/components/PropertiesPanel.tsx index 2fe5d4f..f689f7a 100644 --- a/src/renderer/src/components/PropertiesPanel.tsx +++ b/src/renderer/src/components/PropertiesPanel.tsx @@ -14,6 +14,7 @@ import { BINDING_TARGET_PREFIX } from '../store/canvasSlice'; import type { BlockInstance, Condition, DefinitionEntry, ParamBinding } from '../store/canvasSlice/types'; import type { BlockOutcome } from '../store/edgesSlice/types'; import type { PolicyFile } from '../store/filesSlice/types'; +import { contentSized, plainTextarea } from './contentSized'; import { ConditionSection, type ConditionSlot, FileConditionNotice } from './properties/ConditionSection'; import { EntryList } from './properties/EntryList'; import type { NewClassDefinition } from './properties/NewClassModal'; @@ -138,8 +139,7 @@ export function PropertiesPanel(props: PropertiesPanelProps) { placeholder="Why it's there, or a decision behind it" helperText="Kept as the generated promise's comment." multiline - minRows={1} - maxRows={6} + slotProps={{ input: plainTextarea, htmlInput: { style: contentSized(1, 6) } }} fullWidth size="small" sx={inputSx} diff --git a/src/renderer/src/components/contentSized.ts b/src/renderer/src/components/contentSized.ts new file mode 100644 index 0000000..80fe0cb --- /dev/null +++ b/src/renderer/src/components/contentSized.ts @@ -0,0 +1,12 @@ +import type { CSSProperties } from 'react'; + +// Multi-line fields sized by Chromium (CSS field-sizing) instead of MUI's TextareaAutosize, which +// re-measures after every render, forcing a layout of the page: ~200 ms opening a 150-block file. +// Use both: `slotProps={{ input: plainTextarea, htmlInput: { style: contentSized(1, 6) } }}`. +export const plainTextarea = { inputComponent: 'textarea' } as const; + +export const contentSized = (minRows: number, maxRows: number): CSSProperties => ({ + fieldSizing: 'content', + minHeight: `${minRows}lh`, + maxHeight: `${maxRows}lh` +}); diff --git a/src/renderer/src/components/properties/FileSettingsPanel.tsx b/src/renderer/src/components/properties/FileSettingsPanel.tsx index 50f775c..b86fe99 100644 --- a/src/renderer/src/components/properties/FileSettingsPanel.tsx +++ b/src/renderer/src/components/properties/FileSettingsPanel.tsx @@ -2,6 +2,7 @@ import { Box, Divider, Stack, TextField, Typography } from '@mui/material'; import type { BlockInstance } from '../../store/canvasSlice/types'; import type { PolicyFile } from '../../store/filesSlice/types'; +import { contentSized, plainTextarea } from '../contentSized'; import { ConditionSection } from './ConditionSection'; import { buildClassNameOptions, buildTemplateTokens } from './classOptions'; @@ -39,8 +40,7 @@ export function FileSettingsPanel({ label="Description" size="small" multiline - minRows={2} - maxRows={8} + slotProps={{ input: plainTextarea, htmlInput: { style: contentSized(2, 8) } }} placeholder="What this file is for" helperText="Shown on hovering the file's name." value={file.description ?? ''} diff --git a/src/renderer/src/components/properties/paramFields.tsx b/src/renderer/src/components/properties/paramFields.tsx index f5240f3..0943901 100644 --- a/src/renderer/src/components/properties/paramFields.tsx +++ b/src/renderer/src/components/properties/paramFields.tsx @@ -3,6 +3,7 @@ import { type ReactNode, useState } from 'react'; import { Autocomplete, Box, Checkbox, FormControlLabel, InputAdornment, MenuItem, TextField, Typography } from '@mui/material'; import { type BlockParameter, optionHelp, optionLabel, optionValue } from '../../blocks/types'; +import { contentSized, plainTextarea } from '../contentSized'; import type { TemplateToken } from '../editor/templateTokens'; import { CasesField } from './CasesField'; import type { NewClassDefinition } from './NewClassModal'; @@ -162,12 +163,11 @@ function renderTextField({ parameter, value, onOpenEditor, label, error }: Field helperText={error ?? (parameter.help && !parameter.mustache ? `${parameter.help} Click to edit.` : 'Click to edit.')} fullWidth multiline - minRows={3} - maxRows={3} size="small" onClick={onOpenEditor} slotProps={{ - htmlInput: { style: { fontFamily: 'monospace', cursor: 'pointer' }, readOnly: true } + input: plainTextarea, + htmlInput: { style: { ...contentSized(3, 3), fontFamily: 'monospace', cursor: 'pointer' }, readOnly: true } }} sx={inputSx} />