From d9b71b8884d385b30ae391f4f3c0e2c10292304d Mon Sep 17 00:00:00 2001 From: Lasse Diercks Date: Mon, 25 May 2026 00:11:57 +0200 Subject: [PATCH 1/3] docs: document Homebrew tap install Add `brew install lassediercks/flowgo/flowgo` as the recommended macOS/Linux path, linking the homebrew-flowgo tap repo. Keeps `go install` as the alternative for Go users. --- README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d5586d2..632737a 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,17 @@ The path of least resistance, in order: **1. In your browser, no install:** [flowgo-map.com](https://flowgo-map.com) -**2. On your machine, one command:** +**2. On macOS / Linux via Homebrew:** + +``` +brew install lassediercks/flowgo/flowgo +flowgo new +``` + +The tap lives at [lassediercks/homebrew-flowgo](https://github.com/lassediercks/homebrew-flowgo) +and tracks the latest release. `brew upgrade flowgo` pulls new versions. + +**3. On your machine, one command:** ``` go install github.com/lassediercks/flowgo@latest From 50bfa2f29b369f04e8e6913538da0799c340d554 Mon Sep 17 00:00:00 2001 From: Lasse Diercks Date: Tue, 26 May 2026 00:59:45 +0000 Subject: [PATCH 2/3] refactor: extract pkg/flowgo library + cmd/flowgo binary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Splits the single-package main into a reusable library so downstream consumers can compose flowgo's handlers onto their own HTTP mux without copying code. Layout: - pkg/flowgo (library) state.go — IndexHTML embed, Config, Configure, MCPHandler mcp.go — MCP JSON-RPC handler + tool actions (was main) workspace.go — Workspace, WorkspaceManager (was main) mcp_test.go - pkg/graph (library, expanded) validate.go (moved from main; renamed validateGraph → Validate) map.flowgo (test fixture) validate_test.go stroke_palette_test.go (moved from main; tests Parse/Serialize) version_directive_test.go - cmd/flowgo (binary) main.go — flag parsing, single-file editor mode, version serve.go — flowgo serve subcommand naming.go, upgrade.go, version_check.go and tests The MCP handler dropped its package-level serveMode / filePath / mu / workspaces / serveCfg globals. They are now fields on pkg/flowgo.Config, which the binary populates via flowgo.Configure. The handler reads through that struct so consumers can configure backend behaviour without touching pkg/flowgo internals. dist/index.html moved to pkg/flowgo/dist/ so the library can embed it directly via //go:embed. Toolchain updates: - vite.config.ts: outDir → pkg/flowgo/dist - justfile: go run ./cmd/flowgo, find ./pkg/flowgo/dist/index.html - .github/workflows/release-please.yml: go build … ./cmd/flowgo - version_check.go: install hint → github.com/lassediercks/flowgo/cmd/flowgo - package.json: now @flowgo/editor, exports map for downstream consumers - .gitignore: anchor /flowgo (binary) so it doesn't also ignore pkg/flowgo Behaviour-identical from a user's perspective: - CLI commands (flowgo new, flowgo , flowgo serve, flowgo upgrade, flowgo version) unchanged. - HTTP routes (/, /state, /save, /mcp, /version, /m/) unchanged. - .flowgo file format unchanged. - MCP tool surface unchanged. --- .github/workflows/release-please.yml | 2 +- .gitignore | 9 +- listen_test.go => cmd/flowgo/listen_test.go | 0 main.go => cmd/flowgo/main.go | 88 +++++++-------- naming.go => cmd/flowgo/naming.go | 0 serve.go => cmd/flowgo/serve.go | 45 ++++---- upgrade.go => cmd/flowgo/upgrade.go | 0 upgrade_test.go => cmd/flowgo/upgrade_test.go | 0 .../flowgo/version_check.go | 2 +- .../flowgo/version_check_test.go | 0 justfile | 10 +- package.json | 23 +++- {dist => pkg/flowgo/dist}/index.html | 0 mcp.go => pkg/flowgo/mcp.go | 44 ++++---- mcp_test.go => pkg/flowgo/mcp_test.go | 2 +- pkg/flowgo/state.go | 102 ++++++++++++++++++ workspace.go => pkg/flowgo/workspace.go | 4 +- map.flowgo => pkg/graph/map.flowgo | 0 .../graph/stroke_palette_test.go | 40 +++---- validate.go => pkg/graph/validate.go | 12 +-- .../graph/validate_test.go | 15 ++- .../graph/version_directive_test.go | 14 +-- vite.config.ts | 9 +- 23 files changed, 256 insertions(+), 165 deletions(-) rename listen_test.go => cmd/flowgo/listen_test.go (100%) rename main.go => cmd/flowgo/main.go (82%) rename naming.go => cmd/flowgo/naming.go (100%) rename serve.go => cmd/flowgo/serve.go (77%) rename upgrade.go => cmd/flowgo/upgrade.go (100%) rename upgrade_test.go => cmd/flowgo/upgrade_test.go (100%) rename version_check.go => cmd/flowgo/version_check.go (98%) rename version_check_test.go => cmd/flowgo/version_check_test.go (100%) rename {dist => pkg/flowgo/dist}/index.html (100%) rename mcp.go => pkg/flowgo/mcp.go (98%) rename mcp_test.go => pkg/flowgo/mcp_test.go (99%) create mode 100644 pkg/flowgo/state.go rename workspace.go => pkg/flowgo/workspace.go (96%) rename map.flowgo => pkg/graph/map.flowgo (100%) rename stroke_palette_test.go => pkg/graph/stroke_palette_test.go (92%) rename validate.go => pkg/graph/validate.go (95%) rename validate_test.go => pkg/graph/validate_test.go (88%) rename version_directive_test.go => pkg/graph/version_directive_test.go (90%) diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 7f646e1..25461f6 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -68,7 +68,7 @@ jobs: ext="" if [ "$GOOS" = "windows" ]; then ext=".exe"; fi out="flowgo-${TAG}-${GOOS}-${GOARCH}${ext}" - go build -trimpath -ldflags "-s -w -X main.version=${TAG}" -o "$out" . + go build -trimpath -ldflags "-s -w -X main.version=${TAG}" -o "$out" ./cmd/flowgo echo "OUT=$out" >> "$GITHUB_ENV" - name: Attach to release env: diff --git a/.gitignore b/.gitignore index 6d5bdeb..2f9cc3f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,12 @@ .claude-container -flowgo +.claude + +# Compiled binary at repo root. Anchored with `/` so it doesn't also +# ignore pkg/flowgo (the library package directory). +/flowgo + +# Local-only planning docs (not for public repo). +PLAN.md # MemPalace per-project files (issue #185) mempalace.yaml diff --git a/listen_test.go b/cmd/flowgo/listen_test.go similarity index 100% rename from listen_test.go rename to cmd/flowgo/listen_test.go diff --git a/main.go b/cmd/flowgo/main.go similarity index 82% rename from main.go rename to cmd/flowgo/main.go index 7b6baa0..3e92231 100644 --- a/main.go +++ b/cmd/flowgo/main.go @@ -1,7 +1,11 @@ +// Package main is the public flowgo CLI. It composes the +// pkg/flowgo library with a small flag parser, browser launcher, and +// version reporter; everything substantive lives in the library so +// downstream consumers can wire flowgo onto their own HTTP mux +// without copying code. package main import ( - _ "embed" "encoding/json" "fmt" "net" @@ -13,39 +17,20 @@ import ( "strings" "sync" + "github.com/lassediercks/flowgo/pkg/flowgo" "github.com/lassediercks/flowgo/pkg/graph" ) -// Re-export the graph types and parser/serializer under their original -// unqualified names so the rest of this binary (mcp.go, serve.go, -// workspace.go, validate*.go) keeps compiling without churn. External -// consumers should import github.com/lassediercks/flowgo/pkg/graph -// directly instead of relying on these aliases. -type ( - Box = graph.Box - Edge = graph.Edge - Text = graph.Text - Line = graph.Line - Stroke = graph.Stroke - NamedMap = graph.NamedMap - Graph = graph.Graph -) - -var ( - parse = graph.Parse - serialize = graph.Serialize -) - +// version is overwritten at release-build time via: +// +// go build -ldflags "-X main.version=" ./cmd/flowgo +// +// `go install ...@` also surfaces the module version via +// runtime/debug, which resolveVersionString falls back on. var version = "dev" -//go:embed dist/index.html -var indexHTML string - -//go:embed .release-please-manifest.json -var releasePleaseManifest []byte - var ( - mu sync.Mutex + fileMu sync.Mutex filePath string ) @@ -106,11 +91,11 @@ func main() { createdFile := false if _, err := os.Stat(filePath); os.IsNotExist(err) { - seed := serialize(Graph{ + seed := graph.Serialize(graph.Graph{ Version: resolveVersionString(), - Maps: []NamedMap{{ + Maps: []graph.NamedMap{{ Path: "/", - Boxes: []Box{{ID: "b1", Label: seedBoxLabel(filePath)}}, + Boxes: []graph.Box{{ID: "b1", Label: seedBoxLabel(filePath)}}, }}, }) if err := os.WriteFile(filePath, []byte(seed), 0644); err != nil { @@ -122,9 +107,16 @@ func main() { fmt.Printf("initialised the flowgo interface on a new file %s\n", filePath) } + flowgo.Configure(flowgo.Config{ + ServeMode: false, + LocalFile: filePath, + LocalFileMu: &fileMu, + Version: resolveVersionString, + }) + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.Write([]byte(indexHTML)) + w.Write([]byte(flowgo.IndexHTML)) }) http.HandleFunc("/state", handleState) http.HandleFunc("/save", handleSave) @@ -132,7 +124,7 @@ func main() { w.Header().Set("Content-Type", "text/plain; charset=utf-8") fmt.Fprintln(w, resolveVersionString()) }) - http.HandleFunc("/mcp", handleMCP) + http.HandleFunc("/mcp", flowgo.MCPHandler) // Walk forward from the canonical port so a second flowgo (or any // process holding 54041) doesn't fail to start. Range is bounded to @@ -144,9 +136,6 @@ func main() { } addr := ln.Addr().(*net.TCPAddr) displayHost := bindHost - // When binding to all interfaces, surface the host's real LAN IP - // in the printed URL — `0.0.0.0` isn't a thing the user can paste - // into another browser tab. if bindHost == "0.0.0.0" { if lan := pickLanIP(); lan != "" { displayHost = lan @@ -169,8 +158,6 @@ func main() { // RFC 1918 private ranges (10/8, 172.16/12, 192.168/16) over any other // non-loopback IPv4. Empty string when nothing usable is reachable — // callers should fall back to bindHost in that case. -// -// Pulled out of main so it can be unit-tested via a fake addr provider. func pickLanIP() string { addrs, err := net.InterfaceAddrs() if err != nil { @@ -204,9 +191,7 @@ func pickLanIPFromAddrs(addrs []net.Addr) string { } // listenFirstFree tries each port in [start, end] on host and returns the -// first listener that binds successfully. The window is small on purpose: -// "next free port" should still produce a predictable URL, not vanish into -// the ephemeral range. +// first listener that binds successfully. func listenFirstFree(host string, start, end int) (net.Listener, error) { var lastErr error for port := start; port <= end; port++ { @@ -220,14 +205,14 @@ func listenFirstFree(host string, start, end int) (net.Listener, error) { } func handleState(w http.ResponseWriter, r *http.Request) { - mu.Lock() - defer mu.Unlock() + fileMu.Lock() + defer fileMu.Unlock() data, err := os.ReadFile(filePath) if err != nil { http.Error(w, err.Error(), 500) return } - g, err := parse(string(data)) + g, err := graph.Parse(string(data)) if err != nil { http.Error(w, err.Error(), 500) return @@ -241,15 +226,15 @@ func handleSave(w http.ResponseWriter, r *http.Request) { http.Error(w, "POST only", 405) return } - var g Graph + var g graph.Graph if err := json.NewDecoder(r.Body).Decode(&g); err != nil { http.Error(w, err.Error(), 400) return } - mu.Lock() - defer mu.Unlock() + fileMu.Lock() + defer fileMu.Unlock() g.Version = resolveVersionString() - if err := os.WriteFile(filePath, []byte(serialize(g)), 0644); err != nil { + if err := os.WriteFile(filePath, []byte(graph.Serialize(g)), 0644); err != nil { http.Error(w, err.Error(), 500) return } @@ -294,10 +279,9 @@ func resolveVersionString() string { if version != "dev" { return version } - var m map[string]string - if err := json.Unmarshal(releasePleaseManifest, &m); err == nil { - if v := m["."]; v != "" { - return v + if info, ok := debug.ReadBuildInfo(); ok { + if v := info.Main.Version; v != "" && v != "(devel)" { + return strings.TrimPrefix(v, "v") } } return "dev" diff --git a/naming.go b/cmd/flowgo/naming.go similarity index 100% rename from naming.go rename to cmd/flowgo/naming.go diff --git a/serve.go b/cmd/flowgo/serve.go similarity index 77% rename from serve.go rename to cmd/flowgo/serve.go index d958361..32177ba 100644 --- a/serve.go +++ b/cmd/flowgo/serve.go @@ -7,27 +7,19 @@ import ( "os" "strings" "time" -) -// serveMode flips dispatchTool / mcpTools / route registration into multi- -// tenant workspace + snapshot-share territory. When false, flowgo is the -// single-file editor it's always been. -var serveMode bool + "github.com/lassediercks/flowgo/pkg/flowgo" +) -type ServeConfig struct { +type serveConfig struct { BindAddr string WebhookURL string WebhookSecret string WorkspaceTTL time.Duration } -var ( - serveCfg *ServeConfig - workspaces *WorkspaceManager -) - func runServe(args []string) { - cfg := &ServeConfig{ + cfg := &serveConfig{ BindAddr: "127.0.0.1:8080", WorkspaceTTL: time.Hour, } @@ -85,26 +77,26 @@ func runServe(args []string) { } } - // Allow secret via env so it doesn't appear in process listings. if cfg.WebhookSecret == "" { cfg.WebhookSecret = os.Getenv("FLOWGO_WEBHOOK_SECRET") } - // share is the only feature that strictly requires the webhook. Allow - // running without it for local testing; agents calling share will get a - // clear error if it's not configured. - serveCfg = cfg - serveMode = true - workspaces = newWorkspaceManager(cfg.WorkspaceTTL) + flowgo.Configure(flowgo.Config{ + ServeMode: true, + Workspaces: flowgo.NewWorkspaceManager(cfg.WorkspaceTTL), + ShareWebhookURL: cfg.WebhookURL, + ShareWebhookSecret: cfg.WebhookSecret, + Version: resolveVersionString, + }) mux := http.NewServeMux() - mux.HandleFunc("/mcp", handleMCP) + mux.HandleFunc("/mcp", flowgo.MCPHandler) mux.HandleFunc("/m/", func(w http.ResponseWriter, r *http.Request) { - // Editor HTML for shared snapshots. The website is expected to - // reverse-proxy /m/* here. The HTML detects snapshot mode from the - // pathname and bootstraps from /api/snapshot/. + // Editor HTML for shared snapshots. The website reverse-proxies + // /m/* here. The HTML detects snapshot mode from the pathname + // and bootstraps from /api/snapshot/. w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.Write([]byte(indexHTML)) + w.Write([]byte(flowgo.IndexHTML)) }) mux.HandleFunc("/version", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") @@ -117,7 +109,7 @@ func runServe(args []string) { } addr := ln.Addr().(*net.TCPAddr) fmt.Printf("flowgo serve\n") - fmt.Printf(" bind: %s (port %d)\n", cfg.BindAddr, addr.Port) + fmt.Printf(" bind: %s (port %d)\n", cfg.BindAddr, addr.Port) fmt.Printf(" workspace ttl: %s\n", cfg.WorkspaceTTL) if cfg.WebhookURL != "" { fmt.Printf(" share webhook: %s\n", cfg.WebhookURL) @@ -155,7 +147,6 @@ Flags: (default 1h) The website reverse-proxies /api/mcp* and /m/* to this binary on loopback; -flowgo never serves the public surface directly. See -docs/website-integration-memo.md. +flowgo never serves the public surface directly. `) } diff --git a/upgrade.go b/cmd/flowgo/upgrade.go similarity index 100% rename from upgrade.go rename to cmd/flowgo/upgrade.go diff --git a/upgrade_test.go b/cmd/flowgo/upgrade_test.go similarity index 100% rename from upgrade_test.go rename to cmd/flowgo/upgrade_test.go diff --git a/version_check.go b/cmd/flowgo/version_check.go similarity index 98% rename from version_check.go rename to cmd/flowgo/version_check.go index fd7d6a4..1053235 100644 --- a/version_check.go +++ b/cmd/flowgo/version_check.go @@ -72,7 +72,7 @@ func notifyIfNewer(current, latest string) { fmt.Fprintf(os.Stderr, " update available: flowgo %s (you have %s)\n"+ " run `flowgo upgrade` to update in place (or `brew upgrade flowgo` if installed via brew,\n"+ - " or `go install github.com/lassediercks/flowgo@latest` if installed via go)\n", + " or `go install github.com/lassediercks/flowgo/cmd/flowgo@latest` if installed via go)\n", latest, current) } diff --git a/version_check_test.go b/cmd/flowgo/version_check_test.go similarity index 100% rename from version_check_test.go rename to cmd/flowgo/version_check_test.go diff --git a/justfile b/justfile index 6dacbf9..f6cfd6c 100644 --- a/justfile +++ b/justfile @@ -66,9 +66,9 @@ _dev-run file: wait "$GO_PID" 2>/dev/null || true echo "── restarting flowgo ──────────────────────────────────" if (( started )); then - FLOWGO_NO_OPEN=1 go run . "{{file}}" --host & + FLOWGO_NO_OPEN=1 go run ./cmd/flowgo "{{file}}" --host & else - go run . "{{file}}" --host & + go run ./cmd/flowgo "{{file}}" --host & started=1 fi GO_PID=$! @@ -81,18 +81,18 @@ _dev-run file: changed=0 while IFS= read -r f; do if [[ "$f" -nt "$marker" ]]; then changed=1; break; fi - done < <(find . \( -name '*.go' -o -path './dist/index.html' \) -not -path './node_modules/*' 2>/dev/null) + done < <(find . \( -name '*.go' -o -path './pkg/flowgo/dist/index.html' \) -not -path './node_modules/*' 2>/dev/null) (( changed )) && start_go done -# One-shot frontend build (writes dist/index.html that main.go embeds). +# One-shot frontend build (writes pkg/flowgo/dist/index.html that the library embeds). build-frontend: pnpm install --silent pnpm exec vite build # Build the Go binary with the freshly built frontend embedded. build: build-frontend - go build -o flowgo . + go build -o flowgo ./cmd/flowgo # Run both test suites. test: diff --git a/package.json b/package.json index 5c16211..615ef24 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,25 @@ { - "name": "flowgo-frontend", - "private": true, - "version": "0.0.0", + "name": "@flowgo/editor", + "version": "0.1.2", + "description": "TypeScript editor + .flowgo helpers for the flowgo mind-map tool. Built as a single self-contained index.html by the public flowgo binary; also consumable as ESM modules by downstream projects.", "type": "module", "packageManager": "pnpm@10.33.2", - "description": "TypeScript pure-function modules + Vitest suite for the flowgo editor. Phase 1 mirrors helpers from index.html so future phases can swap them in.", + "license": "AGPL-3.0", + "repository": { + "type": "git", + "url": "https://github.com/lassediercks/flowgo.git" + }, + "files": [ + "src", + "vite.config.ts", + "tsconfig.json" + ], + "main": "./src/index.ts", + "exports": { + ".": "./src/index.ts", + "./editor": "./src/editor/main.ts", + "./editor/*": "./src/editor/*" + }, "scripts": { "dev": "vite", "build": "vite build", diff --git a/dist/index.html b/pkg/flowgo/dist/index.html similarity index 100% rename from dist/index.html rename to pkg/flowgo/dist/index.html diff --git a/mcp.go b/pkg/flowgo/mcp.go similarity index 98% rename from mcp.go rename to pkg/flowgo/mcp.go index 0ae4f3c..dd9eda6 100644 --- a/mcp.go +++ b/pkg/flowgo/mcp.go @@ -1,4 +1,4 @@ -package main +package flowgo import ( "bytes" @@ -164,7 +164,7 @@ func handleMCP(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]any{ "name": "flowgo", - "version": resolveVersionString(), + "version": cfg.Version(), "about": "POST JSON-RPC 2.0 to this endpoint per the MCP streamable-HTTP transport.", }) return @@ -194,7 +194,7 @@ func handleMCP(w http.ResponseWriter, r *http.Request) { }, "serverInfo": map[string]string{ "name": "flowgo", - "version": resolveVersionString(), + "version": cfg.Version(), }, "instructions": mcpInstructions, } @@ -280,9 +280,9 @@ func mcpToolError(msg string) map[string]any { // --------------------------------------------------------------------------- func updateFile(f func(g *Graph) error) (Graph, error) { - mu.Lock() - defer mu.Unlock() - data, err := os.ReadFile(filePath) + cfg.LocalFileMu.Lock() + defer cfg.LocalFileMu.Unlock() + data, err := os.ReadFile(cfg.LocalFile) if err != nil { return Graph{}, err } @@ -293,17 +293,17 @@ func updateFile(f func(g *Graph) error) (Graph, error) { if err := f(&g); err != nil { return Graph{}, err } - g.Version = resolveVersionString() - if err := os.WriteFile(filePath, []byte(serialize(g)), 0644); err != nil { + g.Version = cfg.Version() + if err := os.WriteFile(cfg.LocalFile, []byte(serialize(g)), 0644); err != nil { return Graph{}, err } return g, nil } func readFile() (Graph, error) { - mu.Lock() - defer mu.Unlock() - data, err := os.ReadFile(filePath) + cfg.LocalFileMu.Lock() + defer cfg.LocalFileMu.Unlock() + data, err := os.ReadFile(cfg.LocalFile) if err != nil { return Graph{}, err } @@ -936,10 +936,10 @@ func dispatchTool(name string, raw json.RawMessage) (any, error) { } } - if serveMode { + if cfg.ServeMode { switch name { case "start_workspace": - return mcpToolText(workspaces.Start()), nil + return mcpToolText(cfg.Workspaces.Start()), nil case "share": return shareWorkspace(args) } @@ -950,14 +950,14 @@ func dispatchTool(name string, raw json.RawMessage) (any, error) { return nil, fmt.Errorf("unknown tool: %s", name) } - if serveMode { + if cfg.ServeMode { wsID := stringArg(args, "workspace_id", "") if wsID == "" { return nil, fmt.Errorf("workspace_id is required (call start_workspace first)") } var result any var inner error - err := workspaces.With(wsID, func(ws *Workspace) error { + err := cfg.Workspaces.With(wsID, func(ws *Workspace) error { r, e := fn(&ws.Graph, args) result = r inner = e @@ -1006,7 +1006,7 @@ func dispatchTool(name string, raw json.RawMessage) (any, error) { func mcpTools() []mcpToolDef { var tools []mcpToolDef - if serveMode { + if cfg.ServeMode { tools = append(tools, mcpToolDef{ Name: "start_workspace", @@ -1024,7 +1024,7 @@ func mcpTools() []mcpToolDef { } wsArg := func(props map[string]any, required []string) (map[string]any, []string) { - if !serveMode { + if !cfg.ServeMode { return props, required } np := map[string]any{"workspace_id": schemaString("Workspace id from start_workspace.")} @@ -1292,12 +1292,12 @@ func shareWorkspace(args map[string]any) (any, error) { if wsID == "" { return nil, fmt.Errorf("workspace_id is required") } - if serveCfg == nil || serveCfg.WebhookURL == "" { + if cfg.ShareWebhookURL == "" { return nil, fmt.Errorf("share is unconfigured: --share-webhook missing") } var graphCopy Graph - if err := workspaces.With(wsID, func(ws *Workspace) error { + if err := cfg.Workspaces.With(wsID, func(ws *Workspace) error { graphCopy = ws.Graph return nil }); err != nil { @@ -1323,13 +1323,13 @@ func shareWorkspace(args map[string]any) (any, error) { return nil, fmt.Errorf("marshal payload: %v", err) } - req, err := http.NewRequest(http.MethodPost, serveCfg.WebhookURL, bytes.NewReader(payload)) + req, err := http.NewRequest(http.MethodPost, cfg.ShareWebhookURL, bytes.NewReader(payload)) if err != nil { return nil, fmt.Errorf("build request: %v", err) } req.Header.Set("Content-Type", "application/json") - if serveCfg.WebhookSecret != "" { - req.Header.Set("Authorization", "Bearer "+serveCfg.WebhookSecret) + if cfg.ShareWebhookSecret != "" { + req.Header.Set("Authorization", "Bearer "+cfg.ShareWebhookSecret) } client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Do(req) diff --git a/mcp_test.go b/pkg/flowgo/mcp_test.go similarity index 99% rename from mcp_test.go rename to pkg/flowgo/mcp_test.go index 166a598..cab91bf 100644 --- a/mcp_test.go +++ b/pkg/flowgo/mcp_test.go @@ -1,4 +1,4 @@ -package main +package flowgo import ( "encoding/json" diff --git a/pkg/flowgo/state.go b/pkg/flowgo/state.go new file mode 100644 index 0000000..782f11b --- /dev/null +++ b/pkg/flowgo/state.go @@ -0,0 +1,102 @@ +// Package flowgo is the library form of the flowgo editor server. It +// exposes the embedded editor bundle, the MCP HTTP handler, and the +// in-memory Workspace manager so consumers can wire flowgo's +// behaviour onto their own HTTP mux without copying code. The CLI +// binary in cmd/flowgo is the canonical consumer. +// +// The package is configured once per process via Configure. The MCP +// handler is exposed as MCPHandler and reads state through the +// configured Backend (single-file or multi-tenant workspace). +package flowgo + +import ( + _ "embed" + "net/http" + "sync" + + "github.com/lassediercks/flowgo/pkg/graph" +) + +// Type aliases keep the rest of this package (and tests written before +// the package extraction) compiling without the `graph.` qualifier on +// every reference. External consumers should import pkg/graph directly. +type ( + Box = graph.Box + Edge = graph.Edge + Text = graph.Text + Line = graph.Line + Stroke = graph.Stroke + NamedMap = graph.NamedMap + Graph = graph.Graph +) + +// parse / serialize / validate are kept as package-level shorthands for +// the same reason as the type aliases above. +var ( + parse = graph.Parse + serialize = graph.Serialize + validateGraph = graph.Validate +) + +// IndexHTML is the embedded single-file editor bundle. Consumers serve +// it at "/" (and at "/m/" for snapshot mode). +// +//go:embed dist/index.html +var IndexHTML string + +// Config holds the per-process configuration for the MCP handler and +// any helpers that need to know whether we're in single-file or +// multi-tenant mode. Configure must be called before MCPHandler serves +// any request. +type Config struct { + // ServeMode flips dispatch and tool listing into multi-tenant + // workspace mode. When false, MCP reads/writes the single .flowgo + // file at LocalFile. + ServeMode bool + + // LocalFile is the on-disk .flowgo path (single-file mode only). + LocalFile string + // LocalFileMu serializes reads and writes against LocalFile. If nil + // when ServeMode is false, Configure allocates one. + LocalFileMu *sync.Mutex + + // Workspaces is the in-memory workspace store (serve mode only). + Workspaces *WorkspaceManager + + // ShareWebhookURL is the POST target for the `share` MCP tool. Empty + // disables `share` (the tool still appears in tools/list but returns + // an error if called). + ShareWebhookURL string + ShareWebhookSecret string + + // Version returns the version string to stamp into serialized graphs + // and to report in initialize / serverInfo. If nil, "dev" is used. + Version func() string +} + +var cfg Config + +// init seeds cfg with safe defaults so the package can be imported and +// metadata-only MCP requests (initialize, tools/list, resources/list, +// resources/read) work without a Configure call. Tests rely on this. +func init() { + cfg = Config{Version: func() string { return "dev" }} +} + +// Configure sets the package-level configuration. Safe to call before +// any handler serves a request; not safe to call concurrently with +// active MCP traffic. +func Configure(c Config) { + if c.Version == nil { + c.Version = func() string { return "dev" } + } + if !c.ServeMode && c.LocalFileMu == nil { + c.LocalFileMu = &sync.Mutex{} + } + cfg = c +} + +// MCPHandler is the JSON-RPC MCP HTTP handler. Configure must run first. +func MCPHandler(w http.ResponseWriter, r *http.Request) { + handleMCP(w, r) +} diff --git a/workspace.go b/pkg/flowgo/workspace.go similarity index 96% rename from workspace.go rename to pkg/flowgo/workspace.go index d888089..84aff6e 100644 --- a/workspace.go +++ b/pkg/flowgo/workspace.go @@ -1,4 +1,4 @@ -package main +package flowgo import ( "crypto/rand" @@ -24,7 +24,7 @@ type WorkspaceManager struct { ttl time.Duration } -func newWorkspaceManager(ttl time.Duration) *WorkspaceManager { +func NewWorkspaceManager(ttl time.Duration) *WorkspaceManager { m := &WorkspaceManager{ items: map[string]*Workspace{}, ttl: ttl, diff --git a/map.flowgo b/pkg/graph/map.flowgo similarity index 100% rename from map.flowgo rename to pkg/graph/map.flowgo diff --git a/stroke_palette_test.go b/pkg/graph/stroke_palette_test.go similarity index 92% rename from stroke_palette_test.go rename to pkg/graph/stroke_palette_test.go index ae88d8d..f3f711b 100644 --- a/stroke_palette_test.go +++ b/pkg/graph/stroke_palette_test.go @@ -1,4 +1,4 @@ -package main +package graph import ( "strings" @@ -21,7 +21,7 @@ func TestParseStrokePalette(t *testing.T) { } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - g, err := parse(tc.line) + g, err := Parse(tc.line) if err != nil { t.Fatalf("parse %q: %v", tc.line, err) } @@ -43,7 +43,7 @@ func TestParseStrokePalette(t *testing.T) { // than a silent fallback — otherwise typos in hand-edited files would // silently drop the first point. func TestParseStrokeBadPalette(t *testing.T) { - _, err := parse("stroke s1 abc 100,200 200,300") + _, err := Parse("stroke s1 abc 100,200 200,300") if err == nil { t.Fatal("expected parse error for non-numeric palette token") } @@ -63,7 +63,7 @@ func TestSerializeStrokePalette(t *testing.T) { {ID: "c", Points: [][]float64{{0, 0}, {1, 1}}, Palette: 9}, }, }}} - out := serialize(g) + out := Serialize(g) wantLines := []string{ "stroke a 0,0 1,1", "stroke b 3 0,0 1,1", @@ -76,7 +76,7 @@ func TestSerializeStrokePalette(t *testing.T) { } } -// Round-trip pins parse(serialize(parse(x))) == parse(x) for every +// Round-trip pins Parse(Serialize(Parse(x))) == Parse(x) for every // palette index, including 0 (default — no token emitted). func TestStrokePaletteRoundTrip(t *testing.T) { for _, p := range []int{0, 2, 3, 4, 5, 6, 7, 8, 9} { @@ -88,7 +88,7 @@ func TestStrokePaletteRoundTrip(t *testing.T) { Palette: p, }}, }}} - out, err := parse(serialize(in)) + out, err := Parse(Serialize(in)) if err != nil { t.Fatalf("palette %d re-parse: %v", p, err) } @@ -99,7 +99,7 @@ func TestStrokePaletteRoundTrip(t *testing.T) { } // Edges accept an optional trailing palette token after the two -// endpoints. Round-tripping through parse(serialize(g)) preserves the +// endpoints. Round-tripping through Parse(Serialize(g)) preserves the // palette for every legal value, including 0 (no token emitted). func TestEdgePaletteRoundTrip(t *testing.T) { for _, p := range []int{0, 2, 5, 9} { @@ -108,7 +108,7 @@ func TestEdgePaletteRoundTrip(t *testing.T) { Boxes: []Box{{ID: "a", Label: "a"}, {ID: "b", Label: "b"}}, Edges: []Edge{{From: "a", To: "b", Palette: p}}, }}} - out, err := parse(serialize(in)) + out, err := Parse(Serialize(in)) if err != nil { t.Fatalf("palette %d re-parse: %v", p, err) } @@ -130,7 +130,7 @@ func TestSerializeEdgePalette(t *testing.T) { {From: "a", To: "b", FromHandle: "tl", ToHandle: "br", Palette: 7}, }, }}} - out := serialize(g) + out := Serialize(g) for _, want := range []string{ "edge a b\n", "edge a b 5\n", @@ -150,7 +150,7 @@ func TestLinePaletteRoundTrip(t *testing.T) { Path: "/", Lines: []Line{{ID: "l1", X1: 0, Y1: 0, X2: 10, Y2: 10, Palette: p}}, }}} - out, err := parse(serialize(in)) + out, err := Parse(Serialize(in)) if err != nil { t.Fatalf("palette %d re-parse: %v", p, err) } @@ -168,7 +168,7 @@ func TestSerializeLinePalette(t *testing.T) { {ID: "l2", X1: 0, Y1: 0, X2: 1, Y2: 1, Palette: 4}, }, }}} - out := serialize(g) + out := Serialize(g) for _, want := range []string{"line l1 0 0 1 1\n", "line l2 0 0 1 1 4\n"} { if !strings.Contains(out, want) { t.Fatalf("missing %q in:\n%s", want, out) @@ -176,7 +176,7 @@ func TestSerializeLinePalette(t *testing.T) { } } -// A line with control points round-trips through parse(serialize(g)) +// A line with control points round-trips through Parse(Serialize(g)) // with Mids preserved — for the palette-less case (sentinel "1" // position-holder emitted), the palette-bearing case, and the // multi-point case. @@ -199,7 +199,7 @@ func TestLineMidsRoundTrip(t *testing.T) { Palette: tc.palette, Mids: tc.mids, }}, }}} - out, err := parse(serialize(in)) + out, err := Parse(Serialize(in)) if err != nil { t.Fatalf("re-parse: %v", err) } @@ -229,7 +229,7 @@ func TestSerializeLineMids(t *testing.T) { {ID: "c", X1: 0, Y1: 0, X2: 10, Y2: 10, Mids: [][]float64{{3, 4}, {6, 7}}}, }, }}} - out := serialize(g) + out := Serialize(g) for _, want := range []string{ "line a 0 0 10 10 1 5 8\n", "line b 0 0 10 10 4 5 8\n", @@ -244,7 +244,7 @@ func TestSerializeLineMids(t *testing.T) { // Hand-edited files that emit an odd number of mid tokens should fail // loudly rather than silently dropping a coordinate. func TestParseLineMidsRequiresPairs(t *testing.T) { - _, err := parse("line l1 0 0 10 10 1 5") + _, err := Parse("line l1 0 0 10 10 1 5") if err == nil { t.Fatal("expected parse error for odd mid token count") } @@ -262,7 +262,7 @@ func TestLineStyleRoundTrip(t *testing.T) { Path: "/", Lines: []Line{{ID: "l1", X1: 0, Y1: 0, X2: 10, Y2: 10, Style: s}}, }}} - out, err := parse(serialize(in)) + out, err := Parse(Serialize(in)) if err != nil { t.Fatalf("style %d re-parse: %v", s, err) } @@ -288,7 +288,7 @@ func TestSerializeLineStyle(t *testing.T) { {ID: "c", X1: 0, Y1: 0, X2: 1, Y2: 1, Style: 3}, }, }}} - out := serialize(g) + out := Serialize(g) for _, want := range []string{ "line a 0 0 1 1\n", "line b 0 0 1 1\n", @@ -309,7 +309,7 @@ func TestSerializeLineStyle(t *testing.T) { // linestyle for an unknown line id is a parse error rather than a // silent drop so hand-edited files surface the typo immediately. func TestParseLineStyleUnknownID(t *testing.T) { - _, err := parse("line l1 0 0 10 10\nlinestyle nope 2\n") + _, err := Parse("line l1 0 0 10 10\nlinestyle nope 2\n") if err == nil { t.Fatal("expected parse error for linestyle pointing at unknown line") } @@ -329,7 +329,7 @@ func TestValidateStrokePalette(t *testing.T) { Path: "/", Strokes: []Stroke{{ID: "s1", Points: [][]float64{{0, 0}, {1, 1}}, Palette: p}}, }}} - if errs := validateGraph(g); len(errs) != 0 { + if errs := Validate(g); len(errs) != 0 { t.Fatalf("palette %d rejected unexpectedly: %v", p, errs) } } @@ -338,7 +338,7 @@ func TestValidateStrokePalette(t *testing.T) { Path: "/", Strokes: []Stroke{{ID: "s1", Points: [][]float64{{0, 0}, {1, 1}}, Palette: p}}, }}} - errs := validateGraph(g) + errs := Validate(g) var found bool for _, e := range errs { if strings.Contains(e.Error(), "palette") { diff --git a/validate.go b/pkg/graph/validate.go similarity index 95% rename from validate.go rename to pkg/graph/validate.go index 9ff3240..281428b 100644 --- a/validate.go +++ b/pkg/graph/validate.go @@ -1,20 +1,14 @@ -package main +package graph import ( "fmt" "strings" - - "github.com/lassediercks/flowgo/pkg/graph" ) -// MaxLabelLen mirrors the JS-side cap. Single source of truth for the -// editor + validator + MCP lives in pkg/graph. -const MaxLabelLen = graph.MaxLabelLen - -// validateGraph runs semantic checks the .flowgo parser doesn't perform. +// Validate runs semantic checks the .flowgo parser doesn't perform. // Returns every violation it finds rather than stopping at the first one, // so a single CI run surfaces all problems at once. -func validateGraph(g Graph) []error { +func Validate(g Graph) []error { var errs []error if len(g.Maps) == 0 { diff --git a/validate_test.go b/pkg/graph/validate_test.go similarity index 88% rename from validate_test.go rename to pkg/graph/validate_test.go index cbe7364..3a10e43 100644 --- a/validate_test.go +++ b/pkg/graph/validate_test.go @@ -1,4 +1,4 @@ -package main +package graph import ( "os" @@ -15,12 +15,12 @@ func TestMapFlowgoIsValid(t *testing.T) { t.Fatalf("read map.flowgo: %v", err) } - g, err := parse(string(raw)) + g, err := Parse(string(raw)) if err != nil { t.Fatalf("parse map.flowgo: %v", err) } - if errs := validateGraph(g); len(errs) > 0 { + if errs := Validate(g); len(errs) > 0 { var b strings.Builder for _, e := range errs { b.WriteString(" - ") @@ -30,19 +30,16 @@ func TestMapFlowgoIsValid(t *testing.T) { t.Fatalf("map.flowgo failed validation (%d issue(s)):\n%s", len(errs), b.String()) } - // parse → serialize → parse must yield an equivalent graph; otherwise - // we have a lossy round-trip somewhere (e.g., a new field that the - // serializer forgot to emit, or the parser dropped on the way in). - round, err := parse(serialize(g)) + round, err := Parse(Serialize(g)) if err != nil { t.Fatalf("re-parse after serialize: %v", err) } - if errs := validateGraph(round); len(errs) > 0 { + if errs := Validate(round); len(errs) > 0 { t.Fatalf("round-tripped graph failed validation: %v", errs) } if !graphsEquivalent(g, round) { t.Fatalf("parse(serialize(g)) != g — lossy round-trip\noriginal: %s\nround-trip: %s", - serialize(g), serialize(round)) + Serialize(g), Serialize(round)) } } diff --git a/version_directive_test.go b/pkg/graph/version_directive_test.go similarity index 90% rename from version_directive_test.go rename to pkg/graph/version_directive_test.go index eb7a666..4cddfe1 100644 --- a/version_directive_test.go +++ b/pkg/graph/version_directive_test.go @@ -1,4 +1,4 @@ -package main +package graph import ( "strings" @@ -23,7 +23,7 @@ func TestParseVersionDirective(t *testing.T) { } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - g, err := parse(tc.input) + g, err := Parse(tc.input) if err != nil { t.Fatalf("parse: %v", err) } @@ -37,7 +37,7 @@ func TestParseVersionDirective(t *testing.T) { // `version` on its own (no value) is a parse error — silently dropping // it would mask a corrupted file from the user. func TestParseVersionMissingValue(t *testing.T) { - _, err := parse("version\n") + _, err := Parse("version\n") if err == nil { t.Fatal("expected error for bare `version` directive") } @@ -52,13 +52,13 @@ func TestParseVersionMissingValue(t *testing.T) { // writer's version. func TestSerializeVersionDirective(t *testing.T) { t.Run("omitted when empty", func(t *testing.T) { - out := serialize(Graph{Maps: []NamedMap{{Path: "/", Boxes: []Box{{ID: "b1", Label: "hi"}}}}}) + out := Serialize(Graph{Maps: []NamedMap{{Path: "/", Boxes: []Box{{ID: "b1", Label: "hi"}}}}}) if strings.HasPrefix(out, "version") { t.Fatalf("unexpected version line in output:\n%s", out) } }) t.Run("first line when set", func(t *testing.T) { - out := serialize(Graph{ + out := Serialize(Graph{ Version: "0.0.23", Maps: []NamedMap{{Path: "/", Boxes: []Box{{ID: "b1", Label: "hi"}}}}, }) @@ -68,7 +68,7 @@ func TestSerializeVersionDirective(t *testing.T) { }) } -// parse(serialize(g)) must preserve Version verbatim so consumers can +// Parse(Serialize(g)) must preserve Version verbatim so consumers can // trust it as a stable record. func TestVersionRoundTrip(t *testing.T) { for _, v := range []string{"", "0.0.23", "1.2.3-rc.4", "dev"} { @@ -76,7 +76,7 @@ func TestVersionRoundTrip(t *testing.T) { Version: v, Maps: []NamedMap{{Path: "/", Boxes: []Box{{ID: "b1", Label: "hi"}}}}, } - out, err := parse(serialize(g)) + out, err := Parse(Serialize(g)) if err != nil { t.Fatalf("version %q re-parse: %v", v, err) } diff --git a/vite.config.ts b/vite.config.ts index cfa0113..2596356 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -6,16 +6,17 @@ import { viteSingleFile } from "vite-plugin-singlefile"; const here = fileURLToPath(new URL(".", import.meta.url)); // Vite root is src/editor (the source HTML lives there). The build -// emits a single self-contained index.html into dist/, which the Go -// binary embeds via //go:embed dist/index.html. Devs run `npm run dev` -// for an HMR server and `npm run build` before `go build`. +// emits a single self-contained index.html into pkg/flowgo/dist/, +// which the Go library embeds via //go:embed dist/index.html in +// pkg/flowgo/state.go. Devs run `pnpm dev` for an HMR server and +// `pnpm build` before `go build ./cmd/flowgo`. export default defineConfig({ root: resolve(here, "src/editor"), publicDir: false, server: { port: 54041, strictPort: true }, preview: { port: 54041, strictPort: true }, build: { - outDir: resolve(here, "dist"), + outDir: resolve(here, "pkg/flowgo/dist"), emptyOutDir: true, assetsInlineLimit: Number.MAX_SAFE_INTEGER, cssCodeSplit: false, From 2515cf8628661d01e08a942d6805a45996528ea6 Mon Sep 17 00:00:00 2001 From: Lasse Diercks Date: Tue, 26 May 2026 01:01:07 +0000 Subject: [PATCH 3/3] refactor(editor): route mutations through mutations.ts wrapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Behaviour-unchanged refactor that funnels every mutation seam in the editor through a typed wrapper. Each call site now invokes a typed mutator (mutatedBox, mutatedEdge, mutatedText, mutatedLine, mutatedStroke, mutatedCurrentMap, mutatedDoc) instead of scheduleSave() directly. Each mutator forwards to the wired scheduleSave, so observable behaviour is identical. The wrapper exists as a single injection point: downstream wiring can replace what mutators do without having to audit the 26 mutation seams individually. Migrated 26 call sites across 9 modules (align, attach, brush, clipboard, edit, factories, keys, mouse, touch). Each site picks the most specific mutator for what it actually changed: deleteSelection → mutatedDoc (touches submaps), createBoxAt → mutatedBox, paste → mutatedCurrentMap, edge add/remove → mutatedEdge, and so on. The scheduleSave field is dropped from 8 wireX binding interfaces; main.ts wires the new mutations module once with wireMutations({ scheduleSave: () => scheduleSave() }). clipboard.test.ts switches to calling wireMutations directly in its setup with a no-op scheduleSave; no other test surgery needed. Bundle size: 103.41 KB (−243 bytes vs main). All 120 vitest tests pass; tsc --noEmit clean; go test ./... clean; binary serves /state and /save unchanged end-to-end. --- pkg/flowgo/dist/index.html | 4 ++-- src/editor/align.ts | 5 +++-- src/editor/attach.ts | 6 +++--- src/editor/brush.ts | 4 ++-- src/editor/clipboard.test.ts | 3 ++- src/editor/clipboard.ts | 7 +++--- src/editor/edit.ts | 8 +++---- src/editor/factories.ts | 15 ++++++++----- src/editor/keys.ts | 18 ++++++++++------ src/editor/main.ts | 16 ++++++-------- src/editor/mouse.ts | 14 +++++++----- src/editor/mutations.ts | 41 ++++++++++++++++++++++++++++++++++++ src/editor/touch.ts | 8 +++---- 13 files changed, 102 insertions(+), 47 deletions(-) create mode 100644 src/editor/mutations.ts diff --git a/pkg/flowgo/dist/index.html b/pkg/flowgo/dist/index.html index bfd0d5b..0cede78 100644 --- a/pkg/flowgo/dist/index.html +++ b/pkg/flowgo/dist/index.html @@ -588,8 +588,8 @@ `}let o=a||(e.texts?.length??0)>0;o&&(e.lines?.length??0)&&(r+=` `);for(let t of e.lines??[]){let e=`line ${t.id} ${v(t.x1)} ${v(t.y1)} ${v(t.x2)} ${v(t.y2)}`,n=t.mids??[];if(y(t.palette)||n.length>0){let n=y(t.palette)?t.palette:1;e+=` `+n}for(let[t,r]of n)e+=` ${v(t)} ${v(r)}`;r+=e+` `}for(let t of e.lines??[])typeof t.style==`number`&&t.style>=2&&t.style<=9&&(r+=`linestyle ${t.id} ${t.style}\n`);(o||(e.lines?.length??0)>0)&&(e.strokes?.length??0)&&(r+=` -`);for(let t of e.strokes??[]){if((t.points?.length??0)<2)continue;let e=t.points.map(e=>`${v(e[0])},${v(e[1])}`).join(` `),n=y(t.palette)?` ${t.palette}`:``;r+=`stroke ${t.id}${n} ${e}\n`}}),r},re=()=>{let e=document.getElementById(`helpOverlay`);if(!e)throw Error(`helpOverlay missing from DOM`);return e},ie=e=>{re().classList.toggle(`hidden`,!e)},ae=()=>!re().classList.contains(`hidden`),oe=()=>{let e=document.getElementById(`helpBtn`),t=document.getElementById(`helpClose`);e?.addEventListener(`click`,()=>ie(!0)),t?.addEventListener(`click`,()=>ie(!1)),re().addEventListener(`mousedown`,e=>{e.target===re()&&ie(!1)})},b={x:0,y:0},x=e=>e-b.x,S=e=>e-b.y,se=e=>{let t=document.getElementById(e);if(!t)throw Error(`viewport: missing #${e}`);return t},ce=()=>{let e=b.x,t=b.y;se(`canvas`).style.transform=`translate(${e}px, ${t}px)`;for(let n of[`line-layer`,`stroke-layer`,`edge-layer`])se(n).setAttribute(`transform`,`translate(${e} ${t})`);se(`ghost-line`).setAttribute(`transform`,`translate(${e} ${t})`),se(`bg-layer`).style.backgroundPosition=`${e}px ${t}px`},le=e=>{let t=e.boxes??[],n=t.find(e=>e.anchor)??t.find(e=>e.id===`b1`);if(n&&n.id){let e=document.querySelector(`.box[data-id="${n.id}"]`),t=n.x+(e?e.offsetWidth/2:0),r=n.y+(e?e.offsetHeight/2:0);b.x=window.innerWidth/2-t,b.y=window.innerHeight/2-r,ce();return}let r=[];for(let t of e.boxes??[])r.push([t.x,t.y]);for(let t of e.texts??[])r.push([t.x,t.y]);for(let t of e.lines??[]){r.push([t.x1,t.y1]),r.push([t.x2,t.y2]);for(let[e,n]of t.mids??[])r.push([e,n])}if(r.length===0)b.x=0,b.y=0;else{let e=1/0,t=1/0,n=-1/0,i=-1/0;for(let[a,o]of r)an&&(n=a),o>i&&(i=o);let a=(e+n)/2,o=(t+i)/2;b.x=window.innerWidth/2-a,b.y=window.innerHeight/2-o}ce()},ue=null,de=e=>{ue=e},fe=()=>{if(!ue)throw Error(`brush: wireBrush() not called`);return ue},C=!1,w=null,pe=1,T=()=>C,me=()=>w!==null,he={1:`#333`,2:`#fff`,3:`#b91c1c`,4:`#c2410c`,5:`#a16207`,6:`#15803d`,7:`#1d4ed8`,8:`#6d28d9`,9:`#374151`},ge=e=>`url("data:image/svg+xml;utf8,${``.replace(/#/g,`%23`)}") 2 22, crosshair`,E=null,_e=()=>{if(!C||pe===1){E&&(E.textContent=``);return}E||(E=document.createElement(`style`),E.id=`brush-cursor-dynamic`,document.head.appendChild(E));let e=ge(pe);E.textContent=`body.brush-mode,body.brush-mode #bg-layer,body.brush-mode #canvas,body.brush-mode .box,body.brush-mode .text-item { cursor: ${e}; }`},ve=e=>{C!==e&&(C=e,document.body.classList.toggle(`brush-mode`,C),_e(),fe().setStatus(C?`brush mode — drag to paint, V to exit`:`select mode`))},ye=e=>{e<1||e>9||(pe=e,C&&_e())},be=e=>Math.round(e*100)/100,xe=e=>e.map(e=>`${e[0]},${e[1]}`).join(` `),Se=(e,t)=>{let n=be(x(e)),r=be(S(t)),i=fe().mintId(),a=`http://www.w3.org/2000/svg`,o=document.createElementNS(a,`g`),s=`stroke-group`+(pe>=2?` palette-${pe}`:``);o.setAttribute(`class`,s),o.dataset.id=i;let c=document.createElementNS(a,`polyline`);c.setAttribute(`class`,`stroke-line`),c.setAttribute(`points`,`${n},${r}`),o.appendChild(c),fe().strokeLayer().appendChild(o),w={id:i,palette:pe,points:[[n,r]],polyEl:c}},Ce=(e,t)=>{if(!w)return;let n=be(x(e)),r=be(S(t)),i=w.points[w.points.length-1];Math.hypot(n-i[0],r-i[1])<2||(w.points.push([n,r]),w.polyEl.setAttribute(`points`,xe(w.points)))},we=()=>{if(!w)return;let e=i(w.points,1.5);if(e.length>=2){let t=fe().currentMap(),n={id:w.id,points:e};w.palette>=2&&(n.palette=w.palette),(t.strokes??=[]).push(n),fe().scheduleSave()}else{let e=w.polyEl.parentNode;e&&e.parentNode&&e.parentNode.removeChild(e)}w=null,fe().afterCommit()},Te=null,D=()=>{if(!Te)throw Error(`edit: wireEdit() not called`);return Te},Ee=e=>{Te=e},De=null,Oe=()=>De!==null,ke=e=>r(e.innerText??e.textContent??``,{maxLength:500}).label,Ae=(e,t)=>{if(De)return;De=e,e.contentEditable=`true`,e.textContent=t.label,e.focus();let n=document.createRange();n.selectNodeContents(e);let r=window.getSelection();r?.removeAllRanges(),r?.addRange(n);let i=n=>{e.removeEventListener(`blur`,a),e.removeEventListener(`keydown`,o),e.contentEditable=`false`,De=null;let r=ke(e);n&&r&&r!==t.label&&(t.label=r,D().scheduleSave()),e.textContent=t.label},a=()=>i(!0),o=t=>{t.key===`Enter`&&!t.shiftKey?(t.preventDefault(),e.blur()):t.key===`Escape`&&(t.preventDefault(),i(!1)),t.stopPropagation()};e.addEventListener(`blur`,a),e.addEventListener(`keydown`,o)},je=(e,t,n)=>{if(De)return;let i=n?.cancelDeletes??!1,a=e.querySelector(`.box-label`);if(!a){D().renderAll();let e=D().canvas.querySelector(`.box[data-id="${t.id}"]`);e&&je(e,t,n);return}De=e,e.contentEditable=`true`,a.textContent=t.label,e.focus();let o=document.createRange();o.selectNodeContents(a);let s=window.getSelection();s?.removeAllRanges(),s?.addRange(o);let c=n=>{e.removeEventListener(`blur`,l),e.removeEventListener(`keydown`,u),e.contentEditable=`false`,De=null;let a=r(e.innerText??e.textContent??``,{maxLength:500});a.truncated&&D().setStatus(`label truncated to 500 characters`);let o=a.label;if(!n&&i){let e=D(),n=e.getCurrentMap();n.boxes=n.boxes.filter(e=>e.id!==t.id),n.edges=n.edges.filter(e=>e.from!==t.id&&e.to!==t.id);let r=e.getCurrentPath(),i=r===`/`?`/`+t.id:r+`/`+t.id,a=e.getGraph();a.maps=a.maps.filter(e=>e.path!==i&&!e.path.startsWith(i+`/`)),e.setGraph(a),e.setCurrentMap(e.ensureMap(r)),e.selected.delete(t.id),e.scheduleSave(),e.renderAll(),e.setStatus(`cancelled`);return}n&&o&&o!==t.label&&(t.label=o,D().scheduleSave()),D().renderAll()},l=()=>c(!0),u=t=>{t.key===`Enter`&&!t.shiftKey?(t.preventDefault(),e.blur()):t.key===`Escape`&&(t.preventDefault(),c(!1)),t.stopPropagation()};e.addEventListener(`blur`,l),e.addEventListener(`keydown`,u)},Me=(e,t)=>({x:t.x,y:t.y,width:e.offsetWidth,height:e.offsetHeight}),Ne=(e,t,n)=>d(Me(e,t),n),Pe=(e,t,n,r)=>f(Me(t,e),[n,r]),Fe=(e,t,n,r,i,a)=>{let o=document.elementsFromPoint(i,a);for(let t of o){let n=t;if(n.classList?.contains(`handle`)&&n.parentElement===e){let e=n.dataset.handle;if(e)return e}}return Pe(t,e,n,r)},Ie=(e,t,n,r,i)=>p(Me(t,e),n,[r,i]),Le=null,O=null,Re=()=>{if(!Le)throw Error(`align: wireAlign() not called`);return Le},ze=e=>{Le=e},Be=()=>{let e=Re(),t=e.currentMap(),n=[];for(let r of e.selected){let i=t.boxes.find(e=>e.id===r);if(i){let t=e.canvas.querySelector(`.box[data-id="${r}"]`);t&&n.push({ref:i,width:t.offsetWidth,height:t.offsetHeight});continue}let a=(t.texts??[]).find(e=>e.id===r);if(a){let t=e.canvas.querySelector(`.text-item[data-id="${r}"]`);t&&n.push({ref:a,width:t.offsetWidth,height:t.offsetHeight})}}return n},Ve=e=>{let t=[...e].sort((e,t)=>e.ref.x-t.ref.x);for(let e=1;e{let t=[...e].sort((e,t)=>e.ref.y-t.ref.y);for(let e=1;e{if(e.length<2)return!1;if(t===`horizontal`){let t=e.reduce((e,t)=>e+t.ref.y+t.height/2,0)/e.length;for(let n of e)n.ref.y=Math.round(t-n.height/2);if(Ve(e)){let t=[...e].sort((e,t)=>e.ref.x-t.ref.x||e.ref.y-t.ref.y),n=t[0].ref.x;for(let e of t)e.ref.x=Math.round(n),n=e.ref.x+e.width+20}}else{let t=e.reduce((e,t)=>e+t.ref.x+t.width/2,0)/e.length;for(let n of e)n.ref.x=Math.round(t-n.width/2);if(He(e)){let t=[...e].sort((e,t)=>e.ref.y-t.ref.y||e.ref.x-t.ref.x),n=t[0].ref.y;for(let e of t)e.ref.y=Math.round(n),n=e.ref.y+e.height+20}}return!0},We=e=>{let t=Re();Ue(Be(),e)&&(t.renderAll(),t.scheduleSave())},Ge=`http://www.w3.org/2000/svg`,Ke=(e,t)=>{let n=document.createElementNS(Ge,`svg`);n.setAttribute(`viewBox`,`0 0 16 16`),n.setAttribute(`width`,`16`),n.setAttribute(`height`,`16`),n.setAttribute(`aria-hidden`,`true`);let r=document.createElementNS(Ge,`line`);r.setAttribute(`x1`,String(e.x1)),r.setAttribute(`y1`,String(e.y1)),r.setAttribute(`x2`,String(e.x2)),r.setAttribute(`y2`,String(e.y2)),r.setAttribute(`stroke`,`currentColor`),r.setAttribute(`stroke-width`,`1`),r.setAttribute(`stroke-dasharray`,`1.5 1.5`),r.setAttribute(`opacity`,`0.55`),n.appendChild(r);for(let e of t){let t=document.createElementNS(Ge,`rect`);t.setAttribute(`x`,String(e.x)),t.setAttribute(`y`,String(e.y)),t.setAttribute(`width`,String(e.w)),t.setAttribute(`height`,String(e.h)),t.setAttribute(`fill`,`currentColor`),n.appendChild(t)}return n},qe=()=>Ke({x1:0,y1:8,x2:16,y2:8},[{x:2,y:3,w:5,h:10},{x:9,y:5,w:5,h:6}]),Je=()=>Ke({x1:8,y1:0,x2:8,y2:16},[{x:3,y:2,w:10,h:5},{x:5,y:9,w:6,h:5}]),Ye=()=>{let e=Re();O=document.createElement(`div`),O.id=`alignToolbar`,O.style.display=`none`;let t=(e,t,n)=>{let r=document.createElement(`button`);return r.type=`button`,r.appendChild(e),r.title=t,r.setAttribute(`aria-label`,t),r.addEventListener(`mousedown`,e=>e.stopPropagation()),r.addEventListener(`touchstart`,e=>e.stopPropagation(),{passive:!0}),r.addEventListener(`click`,e=>{e.stopPropagation(),We(n)}),r};O.appendChild(t(qe(),`Align on a horizontal line`,`horizontal`)),O.appendChild(t(Je(),`Align on a vertical line`,`vertical`)),e.canvas.appendChild(O)},Xe=()=>{if(!O||!Le)return;let e=Be();if(e.length<2){O.style.display=`none`;return}O.parentNode!==Le.canvas&&Le.canvas.appendChild(O);let t=1/0,n=1/0,r=-1/0;for(let i of e)t=Math.min(t,i.ref.x),n=Math.min(n,i.ref.y),r=Math.max(r,i.ref.x+i.width);O.style.display=`flex`,O.style.left=t+(r-t)/2+`px`,O.style.top=n+`px`},Ze=e=>{let t=e.mids??[],n=[[e.x1,e.y1],...t,[e.x2,e.y2]],r=e.style??1;if(r===2&&t.length>0){let n=`M ${e.x1} ${e.y1}`;for(let e=0;e=Math.abs(o-i)?e+=` L ${a} ${i} L ${a} ${o}`:e+=` L ${r} ${o} L ${a} ${o}`}return e}let i=`M ${n[0][0]} ${n[0][1]}`;for(let e=1;e{if(!Qe)throw Error(`render: wireRender() not called`);return Qe},et=e=>{Qe=e},k=`http://www.w3.org/2000/svg`,A=()=>{let n=$e();n.canvas.innerHTML=``;let r=n.currentMap(),i=n.graph(),a=n.currentPath();for(let o of r.boxes){let r=document.createElement(`div`),s=e(o.palette),c=t(o.font);r.className=`box`+(te(i,a,o.id)?` has-submap`:``)+(s===1?``:` palette-`+s)+(c===1?``:` font-`+c),r.dataset.id=o.id,r.style.left=o.x+`px`,r.style.top=o.y+`px`;let u=document.createElement(`span`);u.className=`box-label`,u.textContent=o.label,r.appendChild(u);for(let e of l){let t=document.createElement(`div`);t.className=`handle h-`+e,t.dataset.handle=e,r.appendChild(t)}n.canvas.appendChild(r),n.attachBoxHandlers(r,o)}for(let i of r.texts){let r=document.createElement(`div`),a=e(i.palette),o=t(i.font);r.className=`text-item`+(a===1?``:` palette-`+a)+(o===1?``:` font-`+o),r.dataset.id=i.id,r.style.left=i.x+`px`,r.style.top=i.y+`px`,r.textContent=i.label,n.canvas.appendChild(r),n.attachTextHandlers(r,i)}j(),nt(),tt(),M()},tt=()=>{let t=$e();t.strokeLayer.innerHTML=``;let n=t.currentMap();for(let r of n.strokes??[]){if(!r.points||r.points.length<2)continue;let n=o(r.points),i=document.createElementNS(k,`g`),a=e(r.palette);i.setAttribute(`class`,`stroke-group`+(a===1?``:` palette-`+a)+(t.selected.has(r.id)?` selected`:``)),i.dataset.id=r.id;let s=document.createElementNS(k,`path`);s.setAttribute(`class`,`stroke-hit`),s.setAttribute(`d`,n),s.setAttribute(`fill`,`none`),s.setAttribute(`stroke`,`transparent`),s.setAttribute(`stroke-width`,`12`),i.appendChild(s);let c=document.createElementNS(k,`path`);c.setAttribute(`class`,`stroke-line`),c.setAttribute(`d`,n),c.setAttribute(`fill`,`none`),i.appendChild(c),i.addEventListener(`mousedown`,e=>{t.isBrushMode()||(e.stopPropagation(),e.shiftKey||t.selected.clear(),t.selected.add(r.id),t.selectedEdge()&&(t.setSelectedEdge(null),M()),j(),tt())}),t.strokeLayer.appendChild(i)}},nt=()=>{let t=$e();t.lineLayer.innerHTML=``;let n=t.currentMap();for(let r of n.lines){let n=document.createElementNS(k,`g`),i=e(r.palette);n.setAttribute(`class`,`line-group`+(i===1?``:` palette-`+i)+(t.selected.has(r.id)?` selected`:``)),n.dataset.id=r.id;let a=Ze(r),o=document.createElementNS(k,`path`);o.setAttribute(`class`,`line-hit`),o.setAttribute(`d`,a),o.setAttribute(`fill`,`none`),o.setAttribute(`stroke`,`transparent`),o.setAttribute(`stroke-width`,`12`),n.appendChild(o);let s=document.createElementNS(k,`path`);s.setAttribute(`class`,`line-line`),s.setAttribute(`d`,a),s.setAttribute(`fill`,`none`),n.appendChild(s);let c=document.createElementNS(k,`circle`);c.setAttribute(`class`,`line-handle`),c.setAttribute(`cx`,String(r.x1)),c.setAttribute(`cy`,String(r.y1)),c.setAttribute(`r`,`6`),c.dataset.endpoint=`1`,n.appendChild(c);let l=document.createElementNS(k,`circle`);l.setAttribute(`class`,`line-handle`),l.setAttribute(`cx`,String(r.x2)),l.setAttribute(`cy`,String(r.y2)),l.setAttribute(`r`,`6`),l.dataset.endpoint=`2`,n.appendChild(l);let u=[];for(let e=0;e<(r.mids?.length??0);e++){let[t,i]=r.mids[e],a=document.createElementNS(k,`circle`);a.setAttribute(`class`,`line-handle line-handle-mid`),a.setAttribute(`cx`,String(t)),a.setAttribute(`cy`,String(i)),a.setAttribute(`r`,`6`),a.dataset.endpoint=`m`,a.dataset.midIndex=String(e),n.appendChild(a),u.push(a)}t.attachLineHandlers(n,s,o,c,l,u,r),t.lineLayer.appendChild(n)}},j=()=>{let e=$e(),t=e.dropTargetId(),n=e.dropTargetHandle(),r=e.nearTargetId();for(let i of e.canvas.querySelectorAll(`.box`)){let a=i.dataset.id===t;i.classList.toggle(`selected`,e.selected.has(i.dataset.id??``)),i.classList.toggle(`drop-target`,a),i.classList.toggle(`proximity-target`,i.dataset.id===r);for(let e of i.querySelectorAll(`.handle`))e.classList.toggle(`target`,a&&n!==null&&e.dataset.handle===n)}for(let t of e.canvas.querySelectorAll(`.text-item`))t.classList.toggle(`selected`,e.selected.has(t.dataset.id??``));for(let t of e.lineLayer.querySelectorAll(`.line-group`))t.classList.toggle(`selected`,e.selected.has(t.dataset.id??``));for(let t of e.strokeLayer.querySelectorAll(`.stroke-group`))t.classList.toggle(`selected`,e.selected.has(t.dataset.id??``));Xe()},M=()=>{let t=$e();t.edgeLayer.innerHTML=``;let n=t.currentMap(),r=t.selectedEdge();for(let i of n.edges){let a=n.boxes.find(e=>e.id===i.from),o=n.boxes.find(e=>e.id===i.to);if(!a||!o)continue;let s=t.canvas.querySelector(`.box[data-id="${a.id}"]`),c=t.canvas.querySelector(`.box[data-id="${o.id}"]`);if(!s||!c)continue;let l=a.x+s.offsetWidth/2,u=a.y+s.offsetHeight/2,d=o.x+c.offsetWidth/2,f=o.y+c.offsetHeight/2,[p,m]=Ie(a,s,i.fromHandle,d,f),[h,g]=Ie(o,c,i.toHandle,l,u),ee=document.createElementNS(k,`g`),te=e(i.palette);ee.setAttribute(`class`,`edge-group`+(te===1?``:` palette-`+te)+(i===r?` selected`:``));let _=document.createElementNS(k,`line`);_.setAttribute(`class`,`edge-hit`),_.setAttribute(`x1`,String(p)),_.setAttribute(`y1`,String(m)),_.setAttribute(`x2`,String(h)),_.setAttribute(`y2`,String(g)),_.setAttribute(`stroke`,`transparent`),_.setAttribute(`stroke-width`,`12`),ee.appendChild(_);let v=document.createElementNS(k,`line`);v.setAttribute(`class`,`edge-line`),v.setAttribute(`x1`,String(p)),v.setAttribute(`y1`,String(m)),v.setAttribute(`x2`,String(h)),v.setAttribute(`y2`,String(g)),ee.appendChild(v),ee.addEventListener(`mousedown`,e=>{e.stopPropagation(),t.setSelectedEdge(i),t.selected.clear(),j(),M(),t.setStatus(`edge selected — press Delete to remove`)}),t.edgeLayer.appendChild(ee)}},rt=60,it=null,at=()=>{if(!it)throw Error(`render: wireProximity() not called`);return it},ot=e=>{it=e},st=(e,t)=>{let n=at(),r=null,i=1/0,a=n.link();for(let o of n.currentMap().boxes){if(a&&o.id===a.fromId)continue;let s=n.canvas.querySelector(`.box[data-id="${o.id}"]`);if(!s)continue;let c=o.x,l=o.y,u=o.x+s.offsetWidth,d=o.y+s.offsetHeight,f=Math.max(c-e,0,e-u),p=Math.max(l-t,0,t-d),m=Math.hypot(f,p);m{let e=at();e.nearTargetId()!==null&&(e.setNearTargetId(null),j())},lt=null,ut=()=>{if(!lt)throw Error(`factories: wireFactories() not called`);return lt},dt=e=>{lt=e},ft=(e,t,n)=>{let r=ut(),i=r.mintId(),a={id:i,label:`new`,x:e,y:t};r.currentMap().boxes.push(a),A();let o=r.canvas.querySelector(`.box[data-id="${i}"]`);o&&n&&(a.x=n.x-o.offsetWidth/2,a.y=n.y-o.offsetHeight/2,o.style.left=a.x+`px`,o.style.top=a.y+`px`),r.scheduleSave(),o&&(r.selected.clear(),r.selected.add(i),r.selectedEdge()&&(r.clearSelectedEdge(),M()),j(),je(o,a))},pt=(e,t)=>{let n=ut(),r=n.mintId(`t`),i={id:r,label:`text`,x:e,y:t};n.currentMap().texts.push(i),A();let a=n.canvas.querySelector(`.text-item[data-id="${r}"]`);a&&(i.x=e-a.offsetWidth/2,i.y=t-a.offsetHeight/2,a.style.left=i.x+`px`,a.style.top=i.y+`px`,n.selected.clear(),n.selected.add(r),n.selectedEdge()&&(n.clearSelectedEdge(),M()),j(),Ae(a,i)),n.scheduleSave()},mt=(e,t,n,r)=>{let i=ut(),a=i.mintId(`l`),o={id:a,x1:e,y1:t,x2:n,y2:r};i.currentMap().lines.push(o),i.selected.clear(),i.selected.add(a),i.selectedEdge()&&(i.clearSelectedEdge(),M()),A(),i.scheduleSave()},ht=()=>{let e=ut();if(e.selected.size===0){e.setStatus(`nothing selected`);return}let t=e.selected,n=e.currentMap(),r=Array.from(t).filter(e=>n.boxes.some(t=>t.id===e));n.boxes=n.boxes.filter(e=>!t.has(e.id)),n.edges=n.edges.filter(e=>!t.has(e.from)&&!t.has(e.to)),n.texts=n.texts.filter(e=>!t.has(e.id)),n.lines=n.lines.filter(e=>!t.has(e.id)),n.strokes=(n.strokes??[]).filter(e=>!t.has(e.id));let i=e.currentPath(),a=e.graph();for(let e of r){let t=i===`/`?`/`+e:i+`/`+e;a.maps=a.maps.filter(e=>e.path!==t&&!e.path.startsWith(t+`/`))}e.setGraph(a),e.setCurrentMap(e.ensureMap(i)),t.clear(),e.scheduleSave(),A()},gt=null,_t=()=>{if(!gt)throw Error(`line: wireLine() not called`);return gt},vt=e=>{gt=e},yt=!1,N=null,P=null,F=null,I=()=>yt,bt=()=>N!==null,xt=()=>{if(F)return F;let e=document.createElementNS(`http://www.w3.org/2000/svg`,`line`);return e.setAttribute(`class`,`line-preview`),e.setAttribute(`stroke`,`#07f`),e.setAttribute(`stroke-width`,`2`),e.setAttribute(`stroke-dasharray`,`5 4`),e.style.pointerEvents=`none`,_t().lineLayer().appendChild(e),F=e,e},St=()=>{F&&F.parentNode&&F.parentNode.removeChild(F),F=null},L=e=>Math.round(e*100)/100,Ct=(e,t,n)=>{let r=t-e.x,i=n-e.y,a=Math.hypot(r,i);if(a<.001)return{x:t,y:n};let o=10*Math.PI/180,s=Math.round(Math.atan2(i,r)/o)*o;return{x:L(e.x+Math.cos(s)*a),y:L(e.y+Math.sin(s)*a)}},wt=e=>{yt!==e&&(yt=e,document.body.classList.toggle(`line-mode`,yt),yt||(N=null,P=null,St()),_t().setStatus(yt?`line mode — click start, click end · L or Escape to exit`:`select mode`))},Tt=()=>{N&&(N=null,P=null,St())},Et=(e,t,n=!1)=>{let r=L(x(e)),i=L(S(t));if(!N){N={x:r,y:i},P={x:e,y:t};let n=xt();n.setAttribute(`x1`,String(r)),n.setAttribute(`y1`,String(i)),n.setAttribute(`x2`,String(r)),n.setAttribute(`y2`,String(i));return}let a=N,o=n?Ct(a,r,i):{x:r,y:i};N=null,P=null,St(),!(Math.hypot(o.x-a.x,o.y-a.y)<2)&&mt(a.x,a.y,o.x,o.y)},Dt=(e,t,n=!1)=>{if(!N||!P)return;let r=e-P.x,i=t-P.y;if(Math.hypot(r,i)<4)return;let a=L(x(e)),o=L(S(t)),s=N,c=n?Ct(s,a,o):{x:a,y:o};N=null,P=null,St(),!(Math.hypot(c.x-s.x,c.y-s.y)<2)&&mt(s.x,s.y,c.x,c.y)},Ot=(e,t,n=!1)=>{if(!N||!F)return;let r=L(x(e)),i=L(S(t)),a=n?Ct(N,r,i):{x:r,y:i};F.setAttribute(`x2`,String(a.x)),F.setAttribute(`y2`,String(a.y))},kt=null,R=null,At=e=>{kt=e},jt=()=>{if(!kt)throw Error(`clipboard: wireClipboard() not called`);return kt},Mt=()=>{let{selected:e,currentMap:t,findTextById:n,findLineById:r}=jt();if(e.size===0)return!1;let i=t(),a=[],o=[],s=[],c=[],l=new Set;for(let t of e){let e=i.boxes.find(e=>e.id===t);if(e){let t={id:e.id,label:e.label,x:e.x,y:e.y};e.palette&&(t.palette=e.palette),e.font&&(t.font=e.font),a.push(t),l.add(e.id);continue}let c=n(t);if(c){let e={id:c.id,label:c.label,x:c.x,y:c.y};c.palette&&(e.palette=c.palette),c.font&&(e.font=c.font),o.push(e);continue}let u=r(t);if(u){let e={id:u.id,x1:u.x1,y1:u.y1,x2:u.x2,y2:u.y2};u.palette&&(e.palette=u.palette),u.style&&(e.style=u.style),u.mids?.length&&(e.mids=u.mids.map(([e,t])=>[e,t])),s.push(e)}}for(let e of i.edges)l.has(e.from)&&l.has(e.to)&&c.push({from:e.from,fromHandle:e.fromHandle??``,to:e.to,toHandle:e.toHandle??``});if(!a.length&&!o.length&&!s.length)return!1;R={boxes:a,texts:o,lines:s,edges:c,pasteOffset:0};let u=[...a,...o].sort((e,t)=>e.y-t.y||e.x-t.x).map(e=>e.label);return u.length&&typeof navigator<`u`&&navigator.clipboard&&navigator.clipboard.writeText(u.join(` -`)).catch(()=>{}),!0},Nt=()=>{let{selected:e,deleteSelection:t,setStatus:n}=jt();if(!Mt()){n(`nothing to cut`);return}let r=e.size;t(),n(`cut `+r+` items`)},Pt=()=>{let{selected:e,currentMap:t,mintId:n,scheduleSave:r,renderAll:i,setStatus:a,clearSelectedEdge:o}=jt();if(!R){a(`clipboard is empty`);return}R.pasteOffset+=20;let s=R.pasteOffset,c=R.pasteOffset,l=new Map;e.clear(),o();let u=t();for(let t of R.boxes){let r=n(`b`);l.set(t.id,r);let i={id:r,label:t.label,x:t.x+s,y:t.y+c};u.boxes.push(i),e.add(r)}for(let t of R.texts){let r=n(`t`);l.set(t.id,r);let i={id:r,label:t.label,x:t.x+s,y:t.y+c};t.palette&&(i.palette=t.palette),t.font&&(i.font=t.font),u.texts.push(i),e.add(r)}for(let t of R.lines){let r=n(`l`);l.set(t.id,r);let i={id:r,x1:t.x1+s,y1:t.y1+c,x2:t.x2+s,y2:t.y2+c};t.palette&&(i.palette=t.palette),t.style&&(i.style=t.style),t.mids?.length&&(i.mids=t.mids.map(([e,t])=>[e+s,t+c])),u.lines.push(i),e.add(r)}for(let e of R.edges){let t=l.get(e.from),n=l.get(e.to);if(!t||!n)continue;let r={from:t,to:n};e.fromHandle&&(r.fromHandle=e.fromHandle),e.toHandle&&(r.toHandle=e.toHandle),u.edges.push(r)}r(),i(),a(`pasted `+e.size+` items`)},Ft=null,It=()=>{if(!Ft)throw Error(`navigation: wireNavigation() not called`);return Ft},Lt=e=>{Ft=e},Rt=e=>{let t=It().getGraph(),n=t.maps.find(t=>t.path===e);return n||(n={path:e,boxes:[],edges:[]},t.maps.push(n)),n.boxes??=[],n.edges??=[],n.texts??=[],n.lines??=[],n.strokes??=[],n},zt=()=>{let e=location.hash||``;return e.startsWith(`#`)&&(e=e.slice(1)),e?(e.startsWith(`/`)||(e=`/`+e),e):`/`},Bt=(e,t)=>{let n=t?.keepViewport??!1,r=It();r.setCurrentPath(e),r.setCurrentMap(Rt(e)),r.clearSelected(),r.clearSelectedEdge(),r.renderAll(),Ut(),n||le(Rt(e));let i=`#`+e;location.hash!==i&&history.pushState(null,``,i)},Vt=e=>{let t=It().getCurrentPath();Bt(t===`/`?`/`+e:t+`/`+e)},Ht=()=>{let e=It().getCurrentPath();if(e===`/`)return;let t=e.split(`/`).filter(Boolean);t.pop(),Bt(t.length?`/`+t.join(`/`):`/`)},Ut=()=>{let e=It(),t=e.getGraph(),n=e.getCurrentPath(),r=document.getElementById(`path`);if(!r)return;r.innerHTML=``;let i=n===`/`?[]:n.split(`/`).filter(Boolean);r.style.display=i.length===0?`none`:``;let a=document.getElementById(`toolbar`),o=document.body.classList.contains(`snapshot-mode`);if(a&&(a.style.display=i.length===0&&!o?`none`:``),i.length===0){let e=document.getElementById(`upBtn`);e&&(e.style.display=`none`);return}let s=document.createElement(`span`);s.className=`seg`,s.textContent=`/`,s.addEventListener(`click`,()=>Bt(`/`)),r.appendChild(s);let c=``,l=`/`;i.forEach((e,n)=>{if(n>0){let e=document.createElement(`span`);e.className=`sep`,e.textContent=`/`,r.appendChild(e)}c+=`/`+e;let a=c,o=((t.maps||[]).find(e=>e.path===l)?.boxes??[]).find(t=>t.id===e),s=o?.label&&o.label.trim()||e;l=a;let u=document.createElement(`span`);u.className=`seg`,u.textContent=s,u.title=e,nBt(a)):(u.style.fontWeight=`bold`,u.style.cursor=`default`),r.appendChild(u)});let u=document.getElementById(`upBtn`);u&&(u.style.display=n===`/`?`none`:``)},Wt=()=>{window.addEventListener(`hashchange`,()=>{let e=zt();e!==It().getCurrentPath()&&Bt(e)})},Gt=100,Kt=200,qt=null,z=()=>{if(!qt)throw Error(`persistence: wirePersistence() not called`);return qt},B=null,V=[],Jt=[],Yt=null,Xt=location.pathname.match(/^\/m\/([\w-]+)\/?$/),Zt=Xt?Xt[1]:null,Qt=Zt!==null,$t=e=>{qt=e},en=async()=>{let e=z(),t=null;if(Qt){document.body.classList.add(`snapshot-mode`),document.getElementById(`downloadBtn`)?.style.setProperty(`display`,``),document.getElementById(`reshareBtn`)?.style.setProperty(`display`,``);try{let e=await fetch(`/api/snapshot/`+encodeURIComponent(Zt));if(!e.ok)throw Error(`HTTP `+e.status);let n=await e.json();t=n.graph||n}catch(n){let r=n instanceof Error?n.message:String(n);e.setStatus(`snapshot `+Zt+` not loaded: `+r),t=null}}else t=await(await fetch(`/state`)).json();(!t||!t.maps||t.maps.length===0)&&(t={maps:[{path:`/`,boxes:[],edges:[]}]}),e.setGraph(t),B=JSON.stringify(t),V=[],Jt=[],e.setCurrentPath(e.readPathFromURL()),e.setStatus(Qt?`snapshot `+Zt+` — local edits only`:`loaded`)},H=()=>{z().setStatus(`saving…`),Yt&&clearTimeout(Yt),Yt=setTimeout(nn,Kt)},tn=async e=>{if(Qt){z().setStatus(`local edits only — use Download or Save as new share`);return}await fetch(`/save`,{method:`POST`,headers:{"Content-Type":`application/json`},body:e}),z().setStatus(`saved`)},nn=async()=>{let e=JSON.stringify(z().getGraph());B!==null&&e!==B&&(V.push(B),V.length>Gt&&V.shift(),Jt=[]),B=e,await tn(e)},rn=e=>{let t=z(),n=JSON.parse(e);t.setGraph(n),t.clearSelected(),t.clearSelectedEdge();let r=t.getCurrentPath(),i=n.maps.some(e=>e.path===r)?r:`/`;t.setCurrentPath(i,{keepViewport:i===r})},an=()=>{let e=z();Yt&&=(clearTimeout(Yt),null);let t=JSON.stringify(e.getGraph());if(B!==null&&t!==B&&(V.push(B),V.length>Gt&&V.shift(),Jt=[],B=t),V.length===0){e.setStatus(`nothing to undo`);return}let n=V.pop();B!==null&&Jt.push(B),B=n,rn(n),tn(n),e.setStatus(`undo (`+V.length+` left)`)},on=()=>{let e=z();if(Jt.length===0){e.setStatus(`nothing to redo`);return}let t=Jt.pop();B!==null&&V.push(B),B=t,rn(t),tn(t),e.setStatus(`redo`)},sn=()=>{let e=z(),t=e.serializeGraph(e.getGraph()),n=new Blob([t],{type:`text/plain;charset=utf-8`}),r=URL.createObjectURL(n),i=document.createElement(`a`);i.href=r,i.download=(Zt??`mindmap`)+`.flowgo`,document.body.appendChild(i),i.click(),i.remove(),setTimeout(()=>URL.revokeObjectURL(r),1e3),e.setStatus(`downloaded`)},cn=async()=>{let e=z();e.setStatus(`re-sharing…`);try{let t=await fetch(`/api/snapshot`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({graph:e.getGraph()})});if(!t.ok)throw Error(`HTTP `+t.status);let n=await t.json();if(!n.url)throw Error(`response missing url`);navigator.clipboard&&navigator.clipboard.writeText(n.url).catch(()=>{}),e.setStatus(`new share: `+n.url+` (copied)`),n.id&&history.pushState(null,``,`/m/`+n.id)}catch(t){let n=t instanceof Error?t.message:String(t);e.setStatus(`re-share failed: `+n)}},ln=null,un=()=>{if(!ln)throw Error(`clone: wireClone() not called`);return ln},dn=e=>{ln=e},fn=()=>{let{currentMap:e,selected:t,findTextById:n,findLineById:r,mintId:i}=un(),a=e(),o=new Map,s=Array.from(t),c=new Set;for(let e of s){let t=a.boxes.find(t=>t.id===e);if(t){let n=i(`b`);o.set(e,n),c.add(n);let r={id:n,label:t.label,x:t.x,y:t.y};t.palette&&(r.palette=t.palette),t.font&&(r.font=t.font),a.boxes.push(r);continue}let s=n(e);if(s){let t=i(`t`);o.set(e,t);let n={id:t,label:s.label,x:s.x,y:s.y};s.palette&&(n.palette=s.palette),s.font&&(n.font=s.font),a.texts.push(n);continue}let l=r(e);if(l){let t=i(`l`);o.set(e,t);let n={id:t,x1:l.x1,y1:l.y1,x2:l.x2,y2:l.y2};l.palette&&(n.palette=l.palette),l.style&&(n.style=l.style),l.mids?.length&&(n.mids=l.mids.map(([e,t])=>[e,t])),a.lines.push(n)}}for(let e of a.edges.slice()){let t=o.get(e.from),n=o.get(e.to);if(t&&n&&c.has(t)&&c.has(n)){let r={from:t,to:n};e.fromHandle&&(r.fromHandle=e.fromHandle),e.toHandle&&(r.toHandle=e.toHandle),a.edges.push(r)}}t.clear();for(let e of o.values())t.add(e);return o},pn=null,U=()=>{if(!pn)throw Error(`keys: wireKeys() not called`);return pn},mn=e=>{pn=e},hn=(e,t)=>((e&&e>=1&&e<=9?e:1)-1+t+9)%9+1,gn=(e,t)=>t===1?e.palette?(delete e.palette,!0):!1:e.palette===t?!1:(e.palette=t,!0),_n=e=>{let t=U(),n=t.currentMap();return n.boxes.find(t=>t.id===e)||t.findTextById(e)||n.lines.find(t=>t.id===e)||(n.strokes??[]).find(t=>t.id===e)},vn=()=>{let e=U();return e.selected.size>0||e.selectedEdge()!==null},yn=e=>{let t=U();if(!vn())return!1;let n=!1;for(let r of t.selected){let t=_n(r);t&&gn(t,hn(t.palette,e))&&(n=!0)}let r=t.selectedEdge();return r&&gn(r,hn(r.palette,e))&&(n=!0),n},bn=e=>{let t=U();if(t.selected.size===0)return!1;let n=t.currentMap(),r=!1;for(let i of t.selected){let a=n.boxes.find(e=>e.id===i)||t.findTextById(i);if(!a)continue;let o=hn(a.font,e);o===1?a.font&&(delete a.font,r=!0):a.font!==o&&(a.font=o,r=!0)}return r},xn=()=>{let e=U();if(e.selected.size!==1){e.setStatus(`anchor needs exactly one selected box`);return}let t=e.selected.values().next().value,n=e.currentMap(),r=n.boxes.find(e=>e.id===t);if(!r){e.setStatus(`anchor only applies to boxes`);return}let i=!r.anchor;for(let e of n.boxes)e.anchor&&delete e.anchor;i&&(r.anchor=!0),e.scheduleSave(),A(),e.setStatus(i?`anchored `+t:`anchor cleared`)},Sn=e=>{let t=U();if(!vn())return!1;let n=!1;for(let r of t.selected){let t=_n(r);t&&gn(t,e)&&(n=!0)}let r=t.selectedEdge();return r&&gn(r,e)&&(n=!0),n},Cn=e=>{let t=U();if(t.selected.size===0)return!1;let n=t.currentMap(),r=!1;for(let i of t.selected){let a=n.boxes.find(e=>e.id===i)||t.findTextById(i);a&&(e===1?a.font&&(delete a.font,r=!0):a.font!==e&&(a.font=e,r=!0))}return r},wn=e=>{let t=U();if(t.selected.size===0)return!1;let n=t.currentMap(),r=!1;for(let i of t.selected){let t=n.lines.find(e=>e.id===i);t&&(e===1?t.style&&(delete t.style,r=!0):t.style!==e&&(t.style=e,r=!0))}return r},Tn=()=>{document.addEventListener(`keydown`,e=>{let t=U();if(e.key===`Escape`&&ae()){ie(!1);return}if(Oe())return;let n=e.metaKey||e.ctrlKey;if(n&&!e.altKey&&(e.key===`z`||e.key===`Z`)){e.preventDefault(),e.shiftKey?on():an();return}if(n&&!e.altKey&&(e.key===`y`||e.key===`Y`)){e.preventDefault(),on();return}if(n&&!e.altKey&&!e.shiftKey&&(e.key===`a`||e.key===`A`)){e.preventDefault();let n=t.currentMap();t.selected.clear();for(let e of n.boxes)t.selected.add(e.id);for(let e of n.texts??[])t.selected.add(e.id);for(let e of n.lines??[])t.selected.add(e.id);t.selectedEdge()&&(t.setSelectedEdge(null),M()),j(),t.setStatus(`selected `+t.selected.size+` items`);return}if(n&&!e.altKey&&!e.shiftKey&&(e.key===`c`||e.key===`C`)){if(window.getSelection&&String(window.getSelection()))return;e.preventDefault(),Mt()?t.setStatus(`copied `+t.selected.size+` items`):t.setStatus(`nothing to copy`);return}if(n&&!e.altKey&&!e.shiftKey&&(e.key===`x`||e.key===`X`)){e.preventDefault(),Nt();return}if(n&&!e.altKey&&!e.shiftKey&&(e.key===`v`||e.key===`V`)){e.preventDefault(),Pt();return}if(!n&&!e.altKey&&(e.key===`t`||e.key===`T`)){e.preventDefault(),pt(x(t.lastCursor.x),S(t.lastCursor.y));return}if(!n&&!e.altKey&&(e.key===`l`||e.key===`L`)){e.preventDefault(),wt(!I());return}if(!n&&!e.altKey&&(e.key===`b`||e.key===`B`)){e.preventDefault(),ve(!0);return}if(!n&&!e.altKey&&(e.key===`v`||e.key===`V`)){e.preventDefault(),ve(!1),wt(!1);return}if(!n&&!e.altKey&&(e.key===`a`||e.key===`A`)){e.preventDefault(),xn();return}if(!n&&!e.altKey&&!e.shiftKey&&/^[1-9]$/.test(e.key)){let n=parseInt(e.key,10);if(T()){e.preventDefault(),ye(n);return}if(!vn())return;Sn(n)&&(e.preventDefault(),t.scheduleSave(),A());return}if(!n&&!e.altKey&&e.shiftKey&&/^Digit[1-9]$/.test(e.code)){if(t.selected.size===0)return;let n=parseInt(e.code.slice(5),10),r=Cn(n),i=wn(n);(r||i)&&(e.preventDefault(),t.scheduleSave(),A());return}if(!n&&!e.altKey&&e.shiftKey&&(e.key===`+`||e.key===`*`||e.key===`_`||e.key===`-`)){if(t.selected.size===0)return;bn(e.key===`_`||e.key===`-`?-1:1)&&(e.preventDefault(),t.scheduleSave(),A());return}if(!n&&!e.altKey&&(e.key===`+`||e.key===`=`||e.key===`-`)){if(!vn())return;yn(e.key===`-`?-1:1)&&(e.preventDefault(),t.scheduleSave(),A());return}if(!n&&!e.altKey&&!e.shiftKey&&e.key===`Enter`){if(t.selected.size!==1)return;let n=t.selected.values().next().value,r=t.currentMap(),i=r.boxes.find(e=>e.id===n);if(i){let r=t.canvas.querySelector(`.box[data-id="${n}"]`);r&&(e.preventDefault(),je(r,i));return}let a=(r.texts??[]).find(e=>e.id===n);if(a){let r=t.canvas.querySelector(`.text-item[data-id="${n}"]`);r&&(e.preventDefault(),Ae(r,a))}return}if(e.key===`Escape`){if(T()){ve(!1);return}if(I()){bt()?Tt():wt(!1);return}let e=t.link();e&&(e.handleEl.classList.remove(`active`),t.ghostLine.style.display=`none`,t.clearLink(),t.setDropTargetId(null),t.setDropTargetHandle(null),j(),t.clearProximity()),t.selected.clear(),t.setSelectedEdge(null),j(),M()}if(e.key===`Delete`||e.key===`Backspace`){let n=t.selectedEdge();if(n){e.preventDefault();let r=t.currentMap(),i=r.edges.indexOf(n);i>=0&&r.edges.splice(i,1),t.setSelectedEdge(null),t.scheduleSave(),M(),t.setStatus(`edge removed`);return}t.selected.size>0&&(e.preventDefault(),ht())}})},En=null,Dn=()=>{if(!En)throw Error(`mouse: wireMouse() not called`);return En},On=e=>{En=e},kn=(e,t)=>{let n=Dn(),r=document.elementsFromPoint(e,t);for(let e of r){if(!e||e===n.ghostLine)continue;let t=e.closest?.(`.box`);if(t)return t}return null},An=e=>{let t=Dn();if(t.lastCursor.x=e.clientX,t.lastCursor.y=e.clientY,me()){Ce(e.clientX,e.clientY);return}if(I()){Ot(e.clientX,e.clientY,e.shiftKey);return}let n=t.pan();if(n){b.x=n.startVX+(e.clientX-n.downX),b.y=n.startVY+(e.clientY-n.downY),ce();return}let r=t.drag();if(r){let t=e.clientX-r.downX,n=e.clientY-r.downY;if(!r.active&&Math.hypot(t,n)>4){r.active=!0;for(let e of r.movers)e.el?.classList?.add(`dragging`)}if(r.active){for(let i of r.movers)i.apply(t,n,e);M()}return}let i=t.band();if(i){let t=Math.min(i.startX,e.clientX),n=Math.min(i.startY,e.clientY),r=Math.abs(e.clientX-i.startX),a=Math.abs(e.clientY-i.startY);i.el.style.left=t+`px`,i.el.style.top=n+`px`,i.el.style.width=r+`px`,i.el.style.height=a+`px`;return}let a=t.link();if(a){t.ghostLine.setAttribute(`x2`,String(x(e.clientX))),t.ghostLine.setAttribute(`y2`,String(S(e.clientY)));let n=kn(e.clientX,e.clientY),r=n&&n.dataset.id!==a.fromId?n.dataset.id??null:null,i=null;if(r&&n){let o=t.currentMap().boxes.find(e=>e.id===r);o&&(i=Fe(n,o,a.startX,a.startY,e.clientX,e.clientY))}let o=r!==t.dropTargetId(),s=i!==t.dropTargetHandle();(o||s)&&(t.setDropTargetId(r),t.setDropTargetHandle(i),j()),st(x(e.clientX),S(e.clientY));return}st(x(e.clientX),S(e.clientY))},jn=e=>{let t=Dn();if(me()){we();return}if(I()){Dt(e.clientX,e.clientY,e.shiftKey);return}if(t.pan()){t.setPan(null),document.body.classList.remove(`panning`);return}let n=t.drag();if(n){let e=n.active;for(let e of n.movers)e.el?.classList?.remove(`dragging`);let r=n.primaryId;t.setDrag(null),e?t.scheduleSave():(t.selected.clear(),r&&t.selected.add(r),t.selectedEdge()&&(t.setSelectedEdge(null),M()),j());return}let r=t.band();if(r){let n=Math.min(r.startX,e.clientX),i=Math.min(r.startY,e.clientY),a=Math.max(r.startX,e.clientX),o=Math.max(r.startY,e.clientY);if(a-n>2||o-i>2){let e=x(n),r=S(i),s=x(a),c=S(o),l=t.currentMap();for(let n of l.boxes){let i=t.canvas.querySelector(`.box[data-id="${n.id}"]`);if(!i)continue;let a=n.x+i.offsetWidth,o=n.y+i.offsetHeight;n.xe&&n.yr&&t.selected.add(n.id)}for(let n of l.texts){let i=t.canvas.querySelector(`.text-item[data-id="${n.id}"]`);if(!i)continue;let a=n.x+i.offsetWidth,o=n.y+i.offsetHeight;n.xe&&n.yr&&t.selected.add(n.id)}for(let n of l.lines){let i=[n.x1,n.x2],a=[n.y1,n.y2];for(let[e,t]of n.mids??[])i.push(e),a.push(t);let o=Math.min(...i),l=Math.min(...a),u=Math.max(...i),d=Math.max(...a);oe&&lr&&t.selected.add(n.id)}j(),t.selected.size>0&&t.setStatus(t.selected.size+` selected`)}r.el.remove(),t.setBand(null);return}let i=t.link();if(i){i.handleEl.classList.remove(`active`),t.ghostLine.style.display=`none`;let n=kn(e.clientX,e.clientY);if(n&&n.dataset.id!==i.fromId){let r=n.dataset.id,a=t.currentMap(),o=Fe(n,a.boxes.find(e=>e.id===r),i.startX,i.startY,e.clientX,e.clientY),s={from:i.fromId,to:r};i.fromHandle&&(s.fromHandle=i.fromHandle),o&&(s.toHandle=o),a.edges=h(a.edges,s),t.scheduleSave(),M()}else{let n=t.mintId(),r=x(e.clientX),a=S(e.clientY),o={id:n,label:`new`,x:r,y:a},s=t.currentMap();s.boxes.push(o),A();let c=t.canvas.querySelector(`.box[data-id="${n}"]`);if(c){o.x=r-c.offsetWidth/2,o.y=a-c.offsetHeight/2,c.style.left=o.x+`px`,c.style.top=o.y+`px`;let e=Pe(o,c,i.startX,i.startY),l={from:i.fromId,to:n};i.fromHandle&&(l.fromHandle=i.fromHandle),e&&(l.toHandle=e),s.edges=h(s.edges,l),M(),t.selected.clear(),t.selected.add(n),j(),je(c,o,{cancelDeletes:!0})}t.scheduleSave()}t.setLink(null),(t.dropTargetId()||t.dropTargetHandle())&&(t.setDropTargetId(null),t.setDropTargetHandle(null),j()),ct()}},Mn=e=>{e.button===2&&(e.preventDefault(),Dn().setPan({downX:e.clientX,downY:e.clientY,startVX:b.x,startVY:b.y}),document.body.classList.add(`panning`))},Nn=e=>{let t=Dn();if(e.button!==0)return;if(T()){e.preventDefault(),e.stopPropagation(),Se(e.clientX,e.clientY);return}if(I()){e.preventDefault(),e.stopPropagation(),Et(e.clientX,e.clientY,e.shiftKey);return}e.shiftKey||t.selected.clear(),t.selectedEdge()&&(t.setSelectedEdge(null),M()),j();let n=document.createElement(`div`);n.className=`selection-band`,n.style.left=e.clientX+`px`,n.style.top=e.clientY+`px`,n.style.width=`0px`,n.style.height=`0px`,document.body.appendChild(n),t.setBand({startX:e.clientX,startY:e.clientY,el:n})},Pn=(e,t,n,r,i,a)=>{let o=i-n,s=a-r,c=o*o+s*s,l=c===0?0:Math.max(0,Math.min(1,((e-n)*o+(t-r)*s)/c)),u=n+l*o,d=r+l*s,f=e-u,p=t-d;return{d2:f*f+p*p,t:l}},Fn=14,In=(e,t,n)=>{let r=null,i=0,a=Fn*Fn;for(let o of e.lines){let e=[[o.x1,o.y1],...o.mids??[],[o.x2,o.y2]];for(let s=0;s{let t=Dn();if(T())return;if(I()){let n=x(e.clientX),r=S(e.clientY);In(t.currentMap(),n,r)&&(Tt(),t.scheduleSave(),A());return}let n=x(e.clientX),r=S(e.clientY);ft(n,r,{x:n,y:r})},Rn=()=>{document.addEventListener(`mousemove`,An),document.addEventListener(`mouseup`,jn),document.addEventListener(`mousedown`,Mn),window.addEventListener(`contextmenu`,e=>e.preventDefault()),window.addEventListener(`auxclick`,e=>{e.button===1&&e.preventDefault()});let e=document.getElementById(`bg-layer`);e&&(e.addEventListener(`mousedown`,Nn),e.addEventListener(`dblclick`,Ln))},zn=typeof navigator<`u`&&/Mac|iPhone|iPad|iPod/i.test(navigator.platform||navigator.userAgent||``),Bn=e=>zn?e.metaKey:e.ctrlKey,W=e=>Math.round(e/20)*20,Vn=e=>{let t=e.mids??[],n=[[e.x1,e.y1],...t,[e.x2,e.y2]],r=e.style??1;if(r===2&&t.length>0){let n=`M ${e.x1} ${e.y1}`;for(let e=0;e=Math.abs(o-i)?e+=` L ${a} ${i} L ${a} ${o}`:e+=` L ${r} ${o} L ${a} ${o}`}return e}let i=`M ${n[0][0]} ${n[0][1]}`;for(let e=1;e{let n=e.x,r=e.y;return{el:t,apply(i,a,o){let s=n+i,c=r+a;o?.shiftKey&&(s=W(s),c=W(c)),e.x=s,e.y=c,t.style.left=e.x+`px`,t.style.top=e.y+`px`}}},Un=(e,t)=>{let n=e.x,r=e.y;return{el:t,apply(i,a,o){let s=n+i,c=r+a;o?.shiftKey&&(s=W(s),c=W(c)),e.x=s,e.y=c,t.style.left=e.x+`px`,t.style.top=e.y+`px`}}},Wn=(e,t,n,r,i,a,o)=>{let s=e.x1,c=e.y1,l=e.x2,u=e.y2,d=(e.mids??[]).map(([e,t])=>[e,t]);return{el:t,apply(t,f,p){let m=t,h=f;if(p?.shiftKey&&(m=W(s+t)-s,h=W(c+f)-c),e.x1=s+m,e.y1=c+h,e.x2=l+m,e.y2=u+h,d.length>0){e.mids||=[];for(let t=0;t{let r=typeof t==`object`?t.mid:-1,i=t===1?e.x1:t===2?e.x2:e.mids?.[r]?.[0]??0,a=t===1?e.y1:t===2?e.y2:e.mids?.[r]?.[1]??0;return{el:n.g,apply(o,s,c){let l=i+o,u=a+s;c?.shiftKey&&(l=W(l),u=W(u)),t===1?(e.x1=l,e.y1=u):t===2?(e.x2=l,e.y2=u):e.mids&&e.mids[r]&&(e.mids[r]=[l,u]);let d=Vn(e);n.line.setAttribute(`d`,d),n.hit.setAttribute(`d`,d);let f=t===1?n.h1:t===2?n.h2:n.midHandles[r]??null;f&&(f.setAttribute(`cx`,String(l)),f.setAttribute(`cy`,String(u)))}}},Kn=(e,t,n,r)=>{let i=e.points.map(([e,t])=>[e,t]);return{el:t,apply(t,a,s){let c=t,l=a;if(s?.shiftKey&&i.length>0){let e=i[0];c=W(e[0]+t)-e[0],l=W(e[1]+a)-e[1]}for(let t=0;t{if(!qn)throw Error(`attach: wireAttach() not called`);return qn},Jn=e=>{qn=e},Yn=()=>{let e=G(),t=[],n=e.currentMap();for(let r of e.selected){let i=n.boxes.find(e=>e.id===r);if(i){let n=e.canvas.querySelector(`.box[data-id="${r}"]`);n&&t.push(Hn(i,n));continue}let a=e.findTextById(r);if(a){let n=e.canvas.querySelector(`.text-item[data-id="${r}"]`);n&&t.push(Un(a,n));continue}let o=e.findLineById(r);if(o){let n=e.lineLayer.querySelector(`.line-group[data-id="${r}"]`);if(n){let e=n.querySelector(`.line-line`),r=n.querySelector(`.line-hit`),i=n.querySelector(`.line-handle[data-endpoint="1"]`),a=n.querySelector(`.line-handle[data-endpoint="2"]`),s=Array.from(n.querySelectorAll(`.line-handle[data-endpoint="m"]`));t.push(Wn(o,n,e,r,i,a,s))}continue}let s=e.findStrokeById(r);if(s){let n=e.strokeLayer.querySelector(`.stroke-group[data-id="${r}"]`);if(n){let e=n.querySelector(`.stroke-hit`),r=n.querySelector(`.stroke-line`);t.push(Kn(s,n,e,r))}}}return t},Xn=(e,t)=>{e.addEventListener(`mousedown`,n=>{let r=G();if(e.isContentEditable||n.button!==0)return;n.preventDefault(),n.stopPropagation(),r.selected.has(t.id)||(n.shiftKey||r.selected.clear(),r.selected.add(t.id),r.selectedEdge()&&(r.setSelectedEdge(null),M()),j());let i=t.id;if(n.altKey){let e=r.cloneSelection();e.has(t.id)&&(i=e.get(t.id))}r.setDrag({movers:Yn(),primaryId:i,downX:n.clientX,downY:n.clientY,active:!1})}),e.addEventListener(`dblclick`,n=>{let r=G();e.isContentEditable||(n.preventDefault(),n.stopPropagation(),r.selected.clear(),r.selected.add(t.id),j(),Ae(e,t))})},Zn=(e,t,n,r,i,a,o)=>{n.addEventListener(`mousedown`,e=>{let t=G();if(e.button!==0)return;e.preventDefault(),e.stopPropagation(),t.selected.has(o.id)||(e.shiftKey||t.selected.clear(),t.selected.add(o.id),t.selectedEdge()&&(t.setSelectedEdge(null),M()),j());let n=o.id;if(e.altKey){let e=t.cloneSelection();e.has(o.id)&&(n=e.get(o.id))}t.setDrag({movers:Yn(),primaryId:n,downX:e.clientX,downY:e.clientY,active:!1})}),n.addEventListener(`dblclick`,e=>{e.preventDefault(),e.stopPropagation();let t=G(),n=x(e.clientX),r=S(e.clientY);o.mids||=[];let i=[[o.x1,o.y1],...o.mids,[o.x2,o.y2]],a=0,s=1/0;for(let e=0;e{let l=G();s.button===0&&(s.preventDefault(),s.stopPropagation(),l.selected.clear(),l.selected.add(o.id),l.selectedEdge()&&(l.setSelectedEdge(null),M()),j(),l.setDrag({movers:[Gn(o,c,{g:e,line:t,hit:n,h1:r,h2:i,midHandles:a})],primaryId:o.id,downX:s.clientX,downY:s.clientY,active:!1}))});for(let s=0;s{let c=G();s.button===0&&(s.preventDefault(),s.stopPropagation(),c.selected.clear(),c.selected.add(o.id),c.selectedEdge()&&(c.setSelectedEdge(null),M()),j(),c.setDrag({movers:[Gn(o,{mid:l},{g:e,line:t,hit:n,h1:r,h2:i,midHandles:a})],primaryId:o.id,downX:s.clientX,downY:s.clientY,active:!1}))}),c.addEventListener(`dblclick`,e=>{e.preventDefault(),e.stopPropagation(),o.mids&&l{e.addEventListener(`mousedown`,n=>{let r=G();if(e.isContentEditable)return;if(n.button===1||n.button===0&&Bn(n)){n.preventDefault(),n.stopPropagation(),Vt(t.id);return}if(n.button!==0)return;let i=n.target;if(i.classList.contains(`handle`)){n.preventDefault(),n.stopPropagation();let a=i.dataset.handle,o=r.currentMap(),s=null,c=null,l=``;for(let e=o.edges.length-1;e>=0;e--){let n=o.edges[e];if(n.from===t.id&&n.fromHandle===a){s=n,c=n.to,l=n.toHandle??``;break}if(n.to===t.id&&n.toHandle===a){s=n,c=n.from,l=n.fromHandle??``;break}}if(s&&c){let a=o.edges.indexOf(s);a>=0&&o.edges.splice(a,1);let u=o.boxes.find(e=>e.id===c),d=r.canvas.querySelector(`.box[data-id="${c}"]`);if(!u||!d){o.edges.push(s),M();return}let f=t.x+e.offsetWidth/2,p=t.y+e.offsetHeight/2,m=l||Pe(u,d,f,p),[h,g]=Ne(d,u,m);r.setLink({fromId:c,fromHandle:m,startX:h,startY:g,handleEl:i,rerouting:!0}),i.classList.add(`active`),r.ghostLine.setAttribute(`x1`,String(h)),r.ghostLine.setAttribute(`y1`,String(g)),r.ghostLine.setAttribute(`x2`,String(x(n.clientX))),r.ghostLine.setAttribute(`y2`,String(S(n.clientY))),r.ghostLine.style.display=``,M(),r.setStatus(`re-routing edge — drop on a box, or in empty space`);return}let[u,d]=Ne(e,t,a);r.setLink({fromId:t.id,fromHandle:a,startX:u,startY:d,handleEl:i}),i.classList.add(`active`),r.ghostLine.setAttribute(`x1`,String(u)),r.ghostLine.setAttribute(`y1`,String(d)),r.ghostLine.setAttribute(`x2`,String(x(n.clientX))),r.ghostLine.setAttribute(`y2`,String(S(n.clientY))),r.ghostLine.style.display=``,r.setStatus(`drop on a box to connect, or release to cancel`);return}n.preventDefault(),n.stopPropagation(),r.selected.has(t.id)||(n.shiftKey||r.selected.clear(),r.selected.add(t.id),r.selectedEdge()&&(r.setSelectedEdge(null),M()),j());let a=t.id;if(n.altKey){let e=r.cloneSelection();e.has(t.id)&&(a=e.get(t.id))}r.setDrag({movers:Yn(),primaryId:a,downX:n.clientX,downY:n.clientY,active:!1})}),e.addEventListener(`dblclick`,n=>{let r=G();e.isContentEditable||(n.preventDefault(),n.stopPropagation(),r.selected.clear(),r.selected.add(t.id),r.selectedEdge()&&(r.setSelectedEdge(null),M()),j(),je(e,t))})},$n=(e,t,n)=>e!==null&&e.id===t.id&&t.time-e.time<=n?{kind:`double`,nextLastTap:null}:{kind:`single`,nextLastTap:t},er=(e,t,n,r,i)=>Math.hypot(n-e,r-t)>i,tr=300,nr=``,rr=500,ir=4,K=null,ar=null,or=()=>{ar!==null&&(clearTimeout(ar),ar=null)},sr=()=>document.getElementById(`deleteZone`),cr=e=>{let t=sr();return t?e<=t.getBoundingClientRect().bottom:!1},lr=e=>{sr()?.classList.toggle(`armed`,e)},ur=null,dr=()=>{if(!ur)throw Error(`touch: wireTouch() not called`);return ur},fr=e=>{ur=e},pr=(e,t)=>{if(!(e instanceof Element))return null;let n=e.closest(`.handle`);if(n){let e=n.parentElement?.closest?.(`.box`)??null;if(e&&!e.isContentEditable){let r=e.dataset.id,i=n.dataset.handle;if(r&&i&&t.has(r))return{kind:`handle`,boxEl:e,boxId:r,handleEl:n,code:i}}}let r=e.closest(`.box`);if(r&&!r.isContentEditable){let e=r.dataset.id;if(e)return{kind:`box`,el:r,id:e}}let i=e.closest(`.text-item`);if(i&&!i.isContentEditable){let e=i.dataset.id;if(e)return{kind:`text`,el:i,id:e}}let a=e.closest(`.line-handle`);if(a){let e=a.closest(`.line-group`)?.dataset?.id,n=a.dataset.endpoint;if(e&&t.has(e)&&(n===`1`||n===`2`||n===`m`)){let t;if(n===`1`)t=1;else if(n===`2`)t=2;else{let e=parseInt(a.dataset.midIndex??`0`,10);t={mid:Number.isFinite(e)?e:0}}return{kind:`line-endpoint`,lineId:e,endpoint:t}}}let o=e.closest(`.line-group`);if(o){let e=o.dataset.id;if(e)return{kind:`line`,id:e}}let s=e.closest(`.stroke-group`);if(s){let e=s.dataset.id;if(e)return{kind:`stroke`,id:e}}return e.closest(`#bg-layer`)||e.closest(`#bg-svg`)||e.closest(`#edges`)?{kind:`bg`}:null},mr=()=>{let e=dr();if(or(),me()){we();return}if(bt()){Tt();return}e.pan()&&(e.setPan(null),document.body.classList.remove(`panning`));let t=e.drag();if(t){for(let e of t.movers)e.el.classList?.remove(`dragging`);e.setDrag(null),document.body.classList.remove(`dragging`),lr(!1)}let n=e.link();n&&(n.handleEl.classList.remove(`active`),e.ghostLine.style.display=`none`,e.setLink(null),(e.dropTargetId()||e.dropTargetHandle())&&(e.setDropTargetId(null),e.setDropTargetHandle(null),j()),ct())},hr=e=>{if(e.touches.length!==1){mr();return}if(T()){let t=e.touches[0];e.preventDefault(),Se(t.clientX,t.clientY);return}if(I()){let t=e.touches[0];e.preventDefault(),Et(t.clientX,t.clientY);return}let t=document.querySelector(`[contenteditable="true"]`);t&&!t.contains(e.target)&&t.blur(),document.body.classList.contains(`panning`)||(document.body.classList.remove(`dragging`),lr(!1));let n=dr(),r=e.touches[0],i=pr(e.target,n.selected);if(i){if(i.kind===`bg`){e.preventDefault(),n.setPan({downX:r.clientX,downY:r.clientY,startVX:b.x,startVY:b.y}),document.body.classList.add(`panning`);return}if(i.kind===`handle`){e.preventDefault();let t=n.currentMap(),a=t.boxes.find(e=>e.id===i.boxId);if(!a)return;let o=null,s=null,c=``;for(let e=t.edges.length-1;e>=0;e--){let n=t.edges[e];if(n.from===i.boxId&&n.fromHandle===i.code){o=n,s=n.to,c=n.toHandle??``;break}if(n.to===i.boxId&&n.toHandle===i.code){o=n,s=n.from,c=n.fromHandle??``;break}}if(o&&s){let e=t.edges.indexOf(o);e>=0&&t.edges.splice(e,1);let l=t.boxes.find(e=>e.id===s),u=n.canvas.querySelector(`.box[data-id="${s}"]`);if(!l||!u){t.edges.push(o),M();return}let d=a.x+i.boxEl.offsetWidth/2,f=a.y+i.boxEl.offsetHeight/2,p=c||Pe(l,u,d,f),[m,h]=Ne(u,l,p);n.setLink({fromId:s,fromHandle:p,startX:m,startY:h,handleEl:i.handleEl,rerouting:!0}),i.handleEl.classList.add(`active`),n.ghostLine.setAttribute(`x1`,String(m)),n.ghostLine.setAttribute(`y1`,String(h)),n.ghostLine.setAttribute(`x2`,String(x(r.clientX))),n.ghostLine.setAttribute(`y2`,String(S(r.clientY))),n.ghostLine.style.display=``,M();return}let[l,u]=Ne(i.boxEl,a,i.code),d=i.handleEl.getBoundingClientRect(),f=x(d.left+d.width/2),p=S(d.top+d.height/2);n.setLink({fromId:i.boxId,fromHandle:i.code,startX:l,startY:u,handleEl:i.handleEl}),i.handleEl.classList.add(`active`),n.ghostLine.setAttribute(`x1`,String(f)),n.ghostLine.setAttribute(`y1`,String(p)),n.ghostLine.setAttribute(`x2`,String(x(r.clientX))),n.ghostLine.setAttribute(`y2`,String(S(r.clientY))),n.ghostLine.style.display=``;return}if(i.kind===`line-endpoint`){let t=i.lineId,a=i.endpoint,o=document.querySelector(`.line-group[data-id="${t}"]`),s=n.currentMap().lines.find(e=>e.id===t);if(!o||!s)return;let c=o.querySelector(`.line-line`),l=o.querySelector(`.line-hit`),u=o.querySelector(`.line-handle[data-endpoint="1"]`),d=o.querySelector(`.line-handle[data-endpoint="2"]`),f=Array.from(o.querySelectorAll(`.line-handle[data-endpoint="m"]`));if(!c||!l||!u||!d)return;e.preventDefault(),n.selected.clear(),n.selected.add(t),n.selectedEdge()&&(n.setSelectedEdge(null),M()),j(),n.setDrag({movers:[Gn(s,a,{g:o,line:c,hit:l,h1:u,h2:d,midHandles:f})],primaryId:t,downX:r.clientX,downY:r.clientY,active:!1});return}if(e.preventDefault(),n.selected.has(i.id)||(n.selected.clear(),n.selected.add(i.id),n.selectedEdge()&&(n.setSelectedEdge(null),M()),j()),n.setDrag({movers:Yn(),primaryId:i.id,downX:r.clientX,downY:r.clientY,active:!1}),i.kind===`box`){let e=i.id;ar=window.setTimeout(()=>{ar=null;let t=n.drag();if(!(!t||t.active)){t.longPressFired=!0;for(let e of t.movers)e.el.classList?.remove(`dragging`);n.setDrag(null),document.body.classList.remove(`dragging`),lr(!1),K=null,Vt(e)}},rr)}}},gr=e=>{let t=dr();if(e.touches.length!==1){mr();return}let n=e.touches[0];if(!n)return;if(me()){e.preventDefault(),Ce(n.clientX,n.clientY);return}if(I()&&bt()){e.preventDefault(),Ot(n.clientX,n.clientY);return}let r=t.pan();if(r){e.preventDefault(),b.x=r.startVX+(n.clientX-r.downX),b.y=r.startVY+(n.clientY-r.downY),ce();return}let i=t.drag();if(i){e.preventDefault();let t=n.clientX-i.downX,r=n.clientY-i.downY;if(!i.active&&er(i.downX,i.downY,n.clientX,n.clientY,ir)){i.active=!0,or(),K=null;for(let e of i.movers)e.el.classList?.add(`dragging`);document.body.classList.add(`dragging`)}if(i.active){for(let e of i.movers)e.apply(t,r,null);M(),lr(cr(n.clientY))}return}let a=t.link();if(a){e.preventDefault();let r=x(n.clientX),i=S(n.clientY);t.ghostLine.setAttribute(`x2`,String(r)),t.ghostLine.setAttribute(`y2`,String(i));let o=kn(n.clientX,n.clientY),s=o&&o.dataset.id!==a.fromId?o.dataset.id??null:null,c=null;if(s&&o){let e=t.currentMap().boxes.find(e=>e.id===s);e&&(c=Fe(o,e,a.startX,a.startY,n.clientX,n.clientY))}(s!==t.dropTargetId()||c!==t.dropTargetHandle())&&(t.setDropTargetId(s),t.setDropTargetHandle(c),j()),st(r,i)}},_r=e=>{let t=dr();if(or(),me()){we();return}if(I()&&bt()){let t=e.changedTouches[0];t&&Dt(t.clientX,t.clientY);return}let n=t.pan();if(n){let r=e.changedTouches[0]??null,i=r!==null&&er(n.downX,n.downY,r.clientX,r.clientY,ir);if(t.setPan(null),document.body.classList.remove(`panning`),!i&&r){let e=$n(K,{id:nr,time:performance.now()},tr);if(K=e.nextLastTap,e.kind===`double`){let e=x(r.clientX),t=S(r.clientY);ft(e,t,{x:e,y:t})}else (t.selected.size>0||t.selectedEdge())&&(t.selected.clear(),t.selectedEdge()&&(t.setSelectedEdge(null),M()),j())}else K=null;return}let r=t.link();if(r){vr(r,e.changedTouches[0]??null);return}let i=t.drag();if(!i)return;let a=i.active,o=i.longPressFired===!0;for(let e of i.movers)e.el.classList?.remove(`dragging`);let s=i.primaryId;t.setDrag(null),document.body.classList.remove(`dragging`);let c=sr()?.classList.contains(`armed`)??!1;if(lr(!1),o)return;if(a){if(c){ht(),K=null;return}t.scheduleSave(),K=null;return}if(!s)return;let l=$n(K,{id:s,time:performance.now()},tr);if(K=l.nextLastTap,l.kind===`double`){let e=t.currentMap().boxes.find(e=>e.id===s);if(e){let n=document.querySelector(`#canvas .box[data-id="${s}"]`);n&&(t.selected.clear(),t.selected.add(s),t.selectedEdge()&&(t.setSelectedEdge(null),M()),j(),je(n,e));return}let n=t.findTextById(s);if(n){let e=document.querySelector(`#canvas .text-item[data-id="${s}"]`);e&&(t.selected.clear(),t.selected.add(s),j(),Ae(e,n));return}return}t.selected.clear(),t.selected.add(s),t.selectedEdge()&&(t.setSelectedEdge(null),M()),j()},vr=(e,t)=>{let n=dr();if(e.handleEl.classList.remove(`active`),n.ghostLine.style.display=`none`,!t){n.setLink(null),(n.dropTargetId()||n.dropTargetHandle())&&(n.setDropTargetId(null),n.setDropTargetHandle(null),j()),ct();return}let r=kn(t.clientX,t.clientY);if(r&&r.dataset.id!==e.fromId){let i=r.dataset.id,a=n.currentMap(),o=Fe(r,a.boxes.find(e=>e.id===i),e.startX,e.startY,t.clientX,t.clientY),s={from:e.fromId,to:i};e.fromHandle&&(s.fromHandle=e.fromHandle),o&&(s.toHandle=o),a.edges=h(a.edges,s),n.scheduleSave(),M()}else{let r=n.mintId(),i=x(t.clientX),a=S(t.clientY),o={id:r,label:`new`,x:i,y:a},s=n.currentMap();s.boxes.push(o),A();let c=n.canvas.querySelector(`.box[data-id="${r}"]`);if(c){o.x=i-c.offsetWidth/2,o.y=a-c.offsetHeight/2,c.style.left=o.x+`px`,c.style.top=o.y+`px`;let t=Pe(o,c,e.startX,e.startY),l={from:e.fromId,to:r};e.fromHandle&&(l.fromHandle=e.fromHandle),t&&(l.toHandle=t),s.edges=h(s.edges,l),M(),n.selected.clear(),n.selected.add(r),j(),je(c,o,{cancelDeletes:!0})}n.scheduleSave()}n.setLink(null),(n.dropTargetId()||n.dropTargetHandle())&&(n.setDropTargetId(null),n.setDropTargetHandle(null),j()),ct()},yr=e=>{mr(),K=null},br=()=>{document.addEventListener(`touchstart`,hr,{passive:!1}),document.addEventListener(`touchmove`,gr,{passive:!1}),document.addEventListener(`touchend`,_r),document.addEventListener(`touchcancel`,yr)},xr=()=>T()?`brush`:I()?`line`:`cursor`,Sr=e=>{ve(e===`brush`),wt(e===`line`)},Cr=`http://www.w3.org/2000/svg`,wr=(e,t)=>{let n=document.createElementNS(Cr,`svg`);return n.setAttribute(`width`,String(e)),n.setAttribute(`height`,String(e)),n.setAttribute(`viewBox`,`0 0 ${e} ${e}`),n.setAttribute(`fill`,`none`),n.setAttribute(`stroke`,`currentColor`),n.setAttribute(`stroke-width`,`1.6`),n.setAttribute(`stroke-linecap`,`round`),n.setAttribute(`stroke-linejoin`,`round`),t(n),n},Tr=()=>wr(20,e=>{let t=document.createElementNS(Cr,`path`);t.setAttribute(`d`,`M4 3 L4 16 L8 12 L11 18 L13 17 L10 11 L16 11 Z`),t.setAttribute(`fill`,`currentColor`),t.setAttribute(`stroke`,`currentColor`),e.appendChild(t)}),Er=()=>wr(20,e=>{let t=document.createElementNS(Cr,`path`);t.setAttribute(`d`,`M3 17 L5 16 L14 7 L12 5 L3 14 Z`),t.setAttribute(`fill`,`currentColor`),e.appendChild(t);let n=document.createElementNS(Cr,`path`);n.setAttribute(`d`,`M13 6 L16 3 L18 5 L15 8 Z`),n.setAttribute(`fill`,`#a60`),n.setAttribute(`stroke`,`currentColor`),e.appendChild(n)}),Dr=()=>wr(20,e=>{let t=document.createElementNS(Cr,`line`);t.setAttribute(`x1`,`4`),t.setAttribute(`y1`,`16`),t.setAttribute(`x2`,`16`),t.setAttribute(`y2`,`4`),e.appendChild(t);for(let[t,n]of[[4,16],[16,4]]){let r=document.createElementNS(Cr,`circle`);r.setAttribute(`cx`,String(t)),r.setAttribute(`cy`,String(n)),r.setAttribute(`r`,`2`),r.setAttribute(`fill`,`currentColor`),e.appendChild(r)}}),Or=()=>{if(document.getElementById(`modeBar`))return;let e=document.createElement(`div`);e.id=`modeBar`;let t=[],n=(n,i,a)=>{let o=document.createElement(`button`);o.type=`button`,o.dataset.mode=n,o.title=i,o.setAttribute(`aria-label`,i),o.appendChild(a),o.addEventListener(`mousedown`,e=>e.stopPropagation()),o.addEventListener(`touchstart`,e=>e.stopPropagation(),{passive:!0});let s=!1,c=e=>{e.stopPropagation(),e.preventDefault(),!s&&(s=!0,setTimeout(()=>{s=!1},0),Sr(n),r())};return o.addEventListener(`pointerup`,c),o.addEventListener(`click`,c),e.appendChild(o),t.push({mode:n,el:o}),o};n(`cursor`,`Cursor`,Tr()),n(`brush`,`Brush`,Er()),n(`line`,`Line`,Dr());let r=()=>{let e=xr();for(let{mode:n,el:r}of t)r.classList.toggle(`active`,n===e),r.setAttribute(`aria-pressed`,n===e?`true`:`false`)};r(),new MutationObserver(r).observe(document.body,{attributes:!0,attributeFilter:[`class`]}),document.body.appendChild(e)},q={maps:[]},kr=`/`,J={boxes:[],edges:[]},Y=new Set,Ar={x:window.innerWidth/2,y:window.innerHeight/2},jr=null,Mr=null,Nr=null,X=null,Z=null,Pr=null,Fr=null,Ir=null,Q=document.getElementById(`canvas`),Lr=document.getElementById(`edge-layer`),Rr=document.getElementById(`line-layer`),zr=document.getElementById(`stroke-layer`),Br=document.getElementById(`ghost-line`);function Vr(e){return s(e||`b`,c(J.boxes,J.texts||[],J.lines||[],J.strokes||[]))}var Hr=e=>J.texts.find(t=>t.id===e),Ur=e=>J.lines.find(t=>t.id===e),Wr=e=>(J.strokes||[]).find(t=>t.id===e),Gr=()=>le(J);function $(e){}function Kr(){let e=fn();return A(),j(),H(),e}et({canvas:Q,lineLayer:Rr,strokeLayer:zr,edgeLayer:Lr,currentMap:()=>J,graph:()=>q,currentPath:()=>kr,selected:Y,selectedEdge:()=>Z,setSelectedEdge:e=>{Z=e},dropTargetId:()=>Pr,dropTargetHandle:()=>Fr,nearTargetId:()=>Ir,attachBoxHandlers:Qn,attachTextHandlers:Xn,attachLineHandlers:Zn,isBrushMode:()=>T(),setStatus:$}),ot({canvas:Q,currentMap:()=>J,link:()=>X,nearTargetId:()=>Ir,setNearTargetId:e=>{Ir=e}}),Lt({getGraph:()=>q,getCurrentPath:()=>kr,setCurrentPath:e=>{kr=e},setCurrentMap:e=>{J=e},clearSelected:()=>Y.clear(),clearSelectedEdge:()=>{Z=null},renderAll:()=>A()}),Wt(),Jn({canvas:Q,lineLayer:Rr,strokeLayer:zr,ghostLine:Br,currentMap:()=>J,findTextById:Hr,findLineById:Ur,findStrokeById:Wr,selected:Y,selectedEdge:()=>Z,setSelectedEdge:e=>{Z=e},setDrag:e=>{Nr=e},setLink:e=>{X=e},cloneSelection:Kr,setStatus:$}),dt({canvas:Q,currentMap:()=>J,setCurrentMap:e=>{J=e},graph:()=>q,setGraph:e=>{q=e},currentPath:()=>kr,ensureMap:Rt,selected:Y,selectedEdge:()=>Z,clearSelectedEdge:()=>{Z=null},mintId:Vr,scheduleSave:()=>H(),setStatus:$}),Ee({canvas:Q,getCurrentMap:()=>J,setCurrentMap:e=>{J=e},getCurrentPath:()=>kr,getGraph:()=>q,setGraph:e=>{q=e},ensureMap:Rt,selected:Y,scheduleSave:()=>H(),renderAll:()=>A(),setStatus:$}),$t({getGraph:()=>q,setGraph:e=>{q=e},serializeGraph:ne,setCurrentPath:(e,t)=>Bt(e,t),getCurrentPath:()=>kr,readPathFromURL:zt,setStatus:$,clearSelected:()=>Y.clear(),clearSelectedEdge:()=>{Z=null}}),dn({currentMap:()=>J,selected:Y,findTextById:Hr,findLineById:Ur,mintId:Vr}),ze({canvas:Q,currentMap:()=>J,selected:Y,scheduleSave:()=>H(),renderAll:()=>A()}),Ye(),At({selected:Y,currentMap:()=>J,findTextById:Hr,findLineById:Ur,mintId:Vr,scheduleSave:()=>H(),renderAll:()=>A(),deleteSelection:()=>ht(),setStatus:$,clearSelectedEdge:()=>{Z=null}}),de({mintId:()=>Vr(`s`),strokeLayer:()=>zr,currentMap:()=>J,scheduleSave:()=>H(),afterCommit:()=>tt(),setStatus:$}),vt({lineLayer:()=>Rr,setStatus:$}),On({canvas:Q,ghostLine:Br,currentMap:()=>J,mintId:()=>Vr(),selected:Y,lastCursor:Ar,drag:()=>Nr,setDrag:e=>{Nr=e},link:()=>X,setLink:e=>{X=e},pan:()=>Mr,setPan:e=>{Mr=e},band:()=>jr,setBand:e=>{jr=e},selectedEdge:()=>Z,setSelectedEdge:e=>{Z=e},dropTargetId:()=>Pr,setDropTargetId:e=>{Pr=e},dropTargetHandle:()=>Fr,setDropTargetHandle:e=>{Fr=e},scheduleSave:()=>H(),setStatus:$}),Rn(),fr({canvas:Q,ghostLine:Br,currentMap:()=>J,findTextById:Hr,mintId:()=>Vr(),selected:Y,drag:()=>Nr,setDrag:e=>{Nr=e},pan:()=>Mr,setPan:e=>{Mr=e},link:()=>X,setLink:e=>{X=e},dropTargetId:()=>Pr,setDropTargetId:e=>{Pr=e},dropTargetHandle:()=>Fr,setDropTargetHandle:e=>{Fr=e},selectedEdge:()=>Z,setSelectedEdge:e=>{Z=e},scheduleSave:()=>H()}),br(),mn({canvas:Q,ghostLine:Br,currentMap:()=>J,findTextById:Hr,selected:Y,selectedEdge:()=>Z,setSelectedEdge:e=>{Z=e},link:()=>X,clearLink:()=>{X=null},setDropTargetId:e=>{Pr=e},setDropTargetHandle:e=>{Fr=e},clearProximity:()=>ct(),lastCursor:Ar,scheduleSave:()=>H(),setStatus:$}),Tn(),oe();var qr=window.matchMedia(`(pointer: coarse)`),Jr=()=>document.body.classList.toggle(`touch-input`,qr.matches);qr.addEventListener(`change`,Jr),Jr(),Or(),document.getElementById(`upBtn`).addEventListener(`click`,Ht),document.getElementById(`downloadBtn`).addEventListener(`click`,sn),document.getElementById(`reshareBtn`).addEventListener(`click`,cn),window.addEventListener(`mousedown`,e=>{e.button===1&&e.preventDefault()},!0),window.addEventListener(`resize`,()=>Gr()),en(),fetch(`/version`).then(e=>e.ok?e.text():``).then(e=>{let t=e.trim();t&&(document.getElementById(`version`).textContent=`flowgo `+t)}).catch(()=>{}); +`);for(let t of e.strokes??[]){if((t.points?.length??0)<2)continue;let e=t.points.map(e=>`${v(e[0])},${v(e[1])}`).join(` `),n=y(t.palette)?` ${t.palette}`:``;r+=`stroke ${t.id}${n} ${e}\n`}}),r},re=()=>{let e=document.getElementById(`helpOverlay`);if(!e)throw Error(`helpOverlay missing from DOM`);return e},ie=e=>{re().classList.toggle(`hidden`,!e)},ae=()=>!re().classList.contains(`hidden`),oe=()=>{let e=document.getElementById(`helpBtn`),t=document.getElementById(`helpClose`);e?.addEventListener(`click`,()=>ie(!0)),t?.addEventListener(`click`,()=>ie(!1)),re().addEventListener(`mousedown`,e=>{e.target===re()&&ie(!1)})},b={x:0,y:0},x=e=>e-b.x,S=e=>e-b.y,se=e=>{let t=document.getElementById(e);if(!t)throw Error(`viewport: missing #${e}`);return t},ce=()=>{let e=b.x,t=b.y;se(`canvas`).style.transform=`translate(${e}px, ${t}px)`;for(let n of[`line-layer`,`stroke-layer`,`edge-layer`])se(n).setAttribute(`transform`,`translate(${e} ${t})`);se(`ghost-line`).setAttribute(`transform`,`translate(${e} ${t})`),se(`bg-layer`).style.backgroundPosition=`${e}px ${t}px`},le=e=>{let t=e.boxes??[],n=t.find(e=>e.anchor)??t.find(e=>e.id===`b1`);if(n&&n.id){let e=document.querySelector(`.box[data-id="${n.id}"]`),t=n.x+(e?e.offsetWidth/2:0),r=n.y+(e?e.offsetHeight/2:0);b.x=window.innerWidth/2-t,b.y=window.innerHeight/2-r,ce();return}let r=[];for(let t of e.boxes??[])r.push([t.x,t.y]);for(let t of e.texts??[])r.push([t.x,t.y]);for(let t of e.lines??[]){r.push([t.x1,t.y1]),r.push([t.x2,t.y2]);for(let[e,n]of t.mids??[])r.push([e,n])}if(r.length===0)b.x=0,b.y=0;else{let e=1/0,t=1/0,n=-1/0,i=-1/0;for(let[a,o]of r)an&&(n=a),o>i&&(i=o);let a=(e+n)/2,o=(t+i)/2;b.x=window.innerWidth/2-a,b.y=window.innerHeight/2-o}ce()},ue=null,de=e=>{ue=e},C=()=>{if(!ue)throw Error(`mutations: wireMutations() not called`);ue.scheduleSave()},fe=()=>C(),pe=()=>C(),me=()=>C(),he=()=>C(),ge=()=>C(),w=()=>C(),_e=()=>C(),ve=null,ye=e=>{ve=e},be=()=>{if(!ve)throw Error(`brush: wireBrush() not called`);return ve},xe=!1,T=null,Se=1,E=()=>xe,Ce=()=>T!==null,we={1:`#333`,2:`#fff`,3:`#b91c1c`,4:`#c2410c`,5:`#a16207`,6:`#15803d`,7:`#1d4ed8`,8:`#6d28d9`,9:`#374151`},Te=e=>`url("data:image/svg+xml;utf8,${``.replace(/#/g,`%23`)}") 2 22, crosshair`,D=null,Ee=()=>{if(!xe||Se===1){D&&(D.textContent=``);return}D||(D=document.createElement(`style`),D.id=`brush-cursor-dynamic`,document.head.appendChild(D));let e=Te(Se);D.textContent=`body.brush-mode,body.brush-mode #bg-layer,body.brush-mode #canvas,body.brush-mode .box,body.brush-mode .text-item { cursor: ${e}; }`},De=e=>{xe!==e&&(xe=e,document.body.classList.toggle(`brush-mode`,xe),Ee(),be().setStatus(xe?`brush mode — drag to paint, V to exit`:`select mode`))},Oe=e=>{e<1||e>9||(Se=e,xe&&Ee())},ke=e=>Math.round(e*100)/100,Ae=e=>e.map(e=>`${e[0]},${e[1]}`).join(` `),je=(e,t)=>{let n=ke(x(e)),r=ke(S(t)),i=be().mintId(),a=`http://www.w3.org/2000/svg`,o=document.createElementNS(a,`g`),s=`stroke-group`+(Se>=2?` palette-${Se}`:``);o.setAttribute(`class`,s),o.dataset.id=i;let c=document.createElementNS(a,`polyline`);c.setAttribute(`class`,`stroke-line`),c.setAttribute(`points`,`${n},${r}`),o.appendChild(c),be().strokeLayer().appendChild(o),T={id:i,palette:Se,points:[[n,r]],polyEl:c}},Me=(e,t)=>{if(!T)return;let n=ke(x(e)),r=ke(S(t)),i=T.points[T.points.length-1];Math.hypot(n-i[0],r-i[1])<2||(T.points.push([n,r]),T.polyEl.setAttribute(`points`,Ae(T.points)))},Ne=()=>{if(!T)return;let e=i(T.points,1.5);if(e.length>=2){let t=be().currentMap(),n={id:T.id,points:e};T.palette>=2&&(n.palette=T.palette),(t.strokes??=[]).push(n),ge()}else{let e=T.polyEl.parentNode;e&&e.parentNode&&e.parentNode.removeChild(e)}T=null,be().afterCommit()},Pe=null,Fe=()=>{if(!Pe)throw Error(`edit: wireEdit() not called`);return Pe},Ie=e=>{Pe=e},Le=null,Re=()=>Le!==null,ze=e=>r(e.innerText??e.textContent??``,{maxLength:500}).label,Be=(e,t)=>{if(Le)return;Le=e,e.contentEditable=`true`,e.textContent=t.label,e.focus();let n=document.createRange();n.selectNodeContents(e);let r=window.getSelection();r?.removeAllRanges(),r?.addRange(n);let i=n=>{e.removeEventListener(`blur`,a),e.removeEventListener(`keydown`,o),e.contentEditable=`false`,Le=null;let r=ze(e);n&&r&&r!==t.label&&(t.label=r,me()),e.textContent=t.label},a=()=>i(!0),o=t=>{t.key===`Enter`&&!t.shiftKey?(t.preventDefault(),e.blur()):t.key===`Escape`&&(t.preventDefault(),i(!1)),t.stopPropagation()};e.addEventListener(`blur`,a),e.addEventListener(`keydown`,o)},O=(e,t,n)=>{if(Le)return;let i=n?.cancelDeletes??!1,a=e.querySelector(`.box-label`);if(!a){Fe().renderAll();let e=Fe().canvas.querySelector(`.box[data-id="${t.id}"]`);e&&O(e,t,n);return}Le=e,e.contentEditable=`true`,a.textContent=t.label,e.focus();let o=document.createRange();o.selectNodeContents(a);let s=window.getSelection();s?.removeAllRanges(),s?.addRange(o);let c=n=>{e.removeEventListener(`blur`,l),e.removeEventListener(`keydown`,u),e.contentEditable=`false`,Le=null;let a=r(e.innerText??e.textContent??``,{maxLength:500});a.truncated&&Fe().setStatus(`label truncated to 500 characters`);let o=a.label;if(!n&&i){let e=Fe(),n=e.getCurrentMap();n.boxes=n.boxes.filter(e=>e.id!==t.id),n.edges=n.edges.filter(e=>e.from!==t.id&&e.to!==t.id);let r=e.getCurrentPath(),i=r===`/`?`/`+t.id:r+`/`+t.id,a=e.getGraph();a.maps=a.maps.filter(e=>e.path!==i&&!e.path.startsWith(i+`/`)),e.setGraph(a),e.setCurrentMap(e.ensureMap(r)),e.selected.delete(t.id),_e(),e.renderAll(),e.setStatus(`cancelled`);return}n&&o&&o!==t.label&&(t.label=o,fe()),Fe().renderAll()},l=()=>c(!0),u=t=>{t.key===`Enter`&&!t.shiftKey?(t.preventDefault(),e.blur()):t.key===`Escape`&&(t.preventDefault(),c(!1)),t.stopPropagation()};e.addEventListener(`blur`,l),e.addEventListener(`keydown`,u)},Ve=(e,t)=>({x:t.x,y:t.y,width:e.offsetWidth,height:e.offsetHeight}),He=(e,t,n)=>d(Ve(e,t),n),Ue=(e,t,n,r)=>f(Ve(t,e),[n,r]),We=(e,t,n,r,i,a)=>{let o=document.elementsFromPoint(i,a);for(let t of o){let n=t;if(n.classList?.contains(`handle`)&&n.parentElement===e){let e=n.dataset.handle;if(e)return e}}return Ue(t,e,n,r)},Ge=(e,t,n,r,i)=>p(Ve(t,e),n,[r,i]),Ke=null,k=null,qe=()=>{if(!Ke)throw Error(`align: wireAlign() not called`);return Ke},Je=e=>{Ke=e},Ye=()=>{let e=qe(),t=e.currentMap(),n=[];for(let r of e.selected){let i=t.boxes.find(e=>e.id===r);if(i){let t=e.canvas.querySelector(`.box[data-id="${r}"]`);t&&n.push({ref:i,width:t.offsetWidth,height:t.offsetHeight});continue}let a=(t.texts??[]).find(e=>e.id===r);if(a){let t=e.canvas.querySelector(`.text-item[data-id="${r}"]`);t&&n.push({ref:a,width:t.offsetWidth,height:t.offsetHeight})}}return n},Xe=e=>{let t=[...e].sort((e,t)=>e.ref.x-t.ref.x);for(let e=1;e{let t=[...e].sort((e,t)=>e.ref.y-t.ref.y);for(let e=1;e{if(e.length<2)return!1;if(t===`horizontal`){let t=e.reduce((e,t)=>e+t.ref.y+t.height/2,0)/e.length;for(let n of e)n.ref.y=Math.round(t-n.height/2);if(Xe(e)){let t=[...e].sort((e,t)=>e.ref.x-t.ref.x||e.ref.y-t.ref.y),n=t[0].ref.x;for(let e of t)e.ref.x=Math.round(n),n=e.ref.x+e.width+20}}else{let t=e.reduce((e,t)=>e+t.ref.x+t.width/2,0)/e.length;for(let n of e)n.ref.x=Math.round(t-n.width/2);if(Ze(e)){let t=[...e].sort((e,t)=>e.ref.y-t.ref.y||e.ref.x-t.ref.x),n=t[0].ref.y;for(let e of t)e.ref.y=Math.round(n),n=e.ref.y+e.height+20}}return!0},$e=e=>{let t=qe();Qe(Ye(),e)&&(t.renderAll(),w())},et=`http://www.w3.org/2000/svg`,tt=(e,t)=>{let n=document.createElementNS(et,`svg`);n.setAttribute(`viewBox`,`0 0 16 16`),n.setAttribute(`width`,`16`),n.setAttribute(`height`,`16`),n.setAttribute(`aria-hidden`,`true`);let r=document.createElementNS(et,`line`);r.setAttribute(`x1`,String(e.x1)),r.setAttribute(`y1`,String(e.y1)),r.setAttribute(`x2`,String(e.x2)),r.setAttribute(`y2`,String(e.y2)),r.setAttribute(`stroke`,`currentColor`),r.setAttribute(`stroke-width`,`1`),r.setAttribute(`stroke-dasharray`,`1.5 1.5`),r.setAttribute(`opacity`,`0.55`),n.appendChild(r);for(let e of t){let t=document.createElementNS(et,`rect`);t.setAttribute(`x`,String(e.x)),t.setAttribute(`y`,String(e.y)),t.setAttribute(`width`,String(e.w)),t.setAttribute(`height`,String(e.h)),t.setAttribute(`fill`,`currentColor`),n.appendChild(t)}return n},nt=()=>tt({x1:0,y1:8,x2:16,y2:8},[{x:2,y:3,w:5,h:10},{x:9,y:5,w:5,h:6}]),rt=()=>tt({x1:8,y1:0,x2:8,y2:16},[{x:3,y:2,w:10,h:5},{x:5,y:9,w:6,h:5}]),it=()=>{let e=qe();k=document.createElement(`div`),k.id=`alignToolbar`,k.style.display=`none`;let t=(e,t,n)=>{let r=document.createElement(`button`);return r.type=`button`,r.appendChild(e),r.title=t,r.setAttribute(`aria-label`,t),r.addEventListener(`mousedown`,e=>e.stopPropagation()),r.addEventListener(`touchstart`,e=>e.stopPropagation(),{passive:!0}),r.addEventListener(`click`,e=>{e.stopPropagation(),$e(n)}),r};k.appendChild(t(nt(),`Align on a horizontal line`,`horizontal`)),k.appendChild(t(rt(),`Align on a vertical line`,`vertical`)),e.canvas.appendChild(k)},at=()=>{if(!k||!Ke)return;let e=Ye();if(e.length<2){k.style.display=`none`;return}k.parentNode!==Ke.canvas&&Ke.canvas.appendChild(k);let t=1/0,n=1/0,r=-1/0;for(let i of e)t=Math.min(t,i.ref.x),n=Math.min(n,i.ref.y),r=Math.max(r,i.ref.x+i.width);k.style.display=`flex`,k.style.left=t+(r-t)/2+`px`,k.style.top=n+`px`},ot=e=>{let t=e.mids??[],n=[[e.x1,e.y1],...t,[e.x2,e.y2]],r=e.style??1;if(r===2&&t.length>0){let n=`M ${e.x1} ${e.y1}`;for(let e=0;e=Math.abs(o-i)?e+=` L ${a} ${i} L ${a} ${o}`:e+=` L ${r} ${o} L ${a} ${o}`}return e}let i=`M ${n[0][0]} ${n[0][1]}`;for(let e=1;e{if(!st)throw Error(`render: wireRender() not called`);return st},lt=e=>{st=e},A=`http://www.w3.org/2000/svg`,j=()=>{let n=ct();n.canvas.innerHTML=``;let r=n.currentMap(),i=n.graph(),a=n.currentPath();for(let o of r.boxes){let r=document.createElement(`div`),s=e(o.palette),c=t(o.font);r.className=`box`+(te(i,a,o.id)?` has-submap`:``)+(s===1?``:` palette-`+s)+(c===1?``:` font-`+c),r.dataset.id=o.id,r.style.left=o.x+`px`,r.style.top=o.y+`px`;let u=document.createElement(`span`);u.className=`box-label`,u.textContent=o.label,r.appendChild(u);for(let e of l){let t=document.createElement(`div`);t.className=`handle h-`+e,t.dataset.handle=e,r.appendChild(t)}n.canvas.appendChild(r),n.attachBoxHandlers(r,o)}for(let i of r.texts){let r=document.createElement(`div`),a=e(i.palette),o=t(i.font);r.className=`text-item`+(a===1?``:` palette-`+a)+(o===1?``:` font-`+o),r.dataset.id=i.id,r.style.left=i.x+`px`,r.style.top=i.y+`px`,r.textContent=i.label,n.canvas.appendChild(r),n.attachTextHandlers(r,i)}M(),dt(),ut(),N()},ut=()=>{let t=ct();t.strokeLayer.innerHTML=``;let n=t.currentMap();for(let r of n.strokes??[]){if(!r.points||r.points.length<2)continue;let n=o(r.points),i=document.createElementNS(A,`g`),a=e(r.palette);i.setAttribute(`class`,`stroke-group`+(a===1?``:` palette-`+a)+(t.selected.has(r.id)?` selected`:``)),i.dataset.id=r.id;let s=document.createElementNS(A,`path`);s.setAttribute(`class`,`stroke-hit`),s.setAttribute(`d`,n),s.setAttribute(`fill`,`none`),s.setAttribute(`stroke`,`transparent`),s.setAttribute(`stroke-width`,`12`),i.appendChild(s);let c=document.createElementNS(A,`path`);c.setAttribute(`class`,`stroke-line`),c.setAttribute(`d`,n),c.setAttribute(`fill`,`none`),i.appendChild(c),i.addEventListener(`mousedown`,e=>{t.isBrushMode()||(e.stopPropagation(),e.shiftKey||t.selected.clear(),t.selected.add(r.id),t.selectedEdge()&&(t.setSelectedEdge(null),N()),M(),ut())}),t.strokeLayer.appendChild(i)}},dt=()=>{let t=ct();t.lineLayer.innerHTML=``;let n=t.currentMap();for(let r of n.lines){let n=document.createElementNS(A,`g`),i=e(r.palette);n.setAttribute(`class`,`line-group`+(i===1?``:` palette-`+i)+(t.selected.has(r.id)?` selected`:``)),n.dataset.id=r.id;let a=ot(r),o=document.createElementNS(A,`path`);o.setAttribute(`class`,`line-hit`),o.setAttribute(`d`,a),o.setAttribute(`fill`,`none`),o.setAttribute(`stroke`,`transparent`),o.setAttribute(`stroke-width`,`12`),n.appendChild(o);let s=document.createElementNS(A,`path`);s.setAttribute(`class`,`line-line`),s.setAttribute(`d`,a),s.setAttribute(`fill`,`none`),n.appendChild(s);let c=document.createElementNS(A,`circle`);c.setAttribute(`class`,`line-handle`),c.setAttribute(`cx`,String(r.x1)),c.setAttribute(`cy`,String(r.y1)),c.setAttribute(`r`,`6`),c.dataset.endpoint=`1`,n.appendChild(c);let l=document.createElementNS(A,`circle`);l.setAttribute(`class`,`line-handle`),l.setAttribute(`cx`,String(r.x2)),l.setAttribute(`cy`,String(r.y2)),l.setAttribute(`r`,`6`),l.dataset.endpoint=`2`,n.appendChild(l);let u=[];for(let e=0;e<(r.mids?.length??0);e++){let[t,i]=r.mids[e],a=document.createElementNS(A,`circle`);a.setAttribute(`class`,`line-handle line-handle-mid`),a.setAttribute(`cx`,String(t)),a.setAttribute(`cy`,String(i)),a.setAttribute(`r`,`6`),a.dataset.endpoint=`m`,a.dataset.midIndex=String(e),n.appendChild(a),u.push(a)}t.attachLineHandlers(n,s,o,c,l,u,r),t.lineLayer.appendChild(n)}},M=()=>{let e=ct(),t=e.dropTargetId(),n=e.dropTargetHandle(),r=e.nearTargetId();for(let i of e.canvas.querySelectorAll(`.box`)){let a=i.dataset.id===t;i.classList.toggle(`selected`,e.selected.has(i.dataset.id??``)),i.classList.toggle(`drop-target`,a),i.classList.toggle(`proximity-target`,i.dataset.id===r);for(let e of i.querySelectorAll(`.handle`))e.classList.toggle(`target`,a&&n!==null&&e.dataset.handle===n)}for(let t of e.canvas.querySelectorAll(`.text-item`))t.classList.toggle(`selected`,e.selected.has(t.dataset.id??``));for(let t of e.lineLayer.querySelectorAll(`.line-group`))t.classList.toggle(`selected`,e.selected.has(t.dataset.id??``));for(let t of e.strokeLayer.querySelectorAll(`.stroke-group`))t.classList.toggle(`selected`,e.selected.has(t.dataset.id??``));at()},N=()=>{let t=ct();t.edgeLayer.innerHTML=``;let n=t.currentMap(),r=t.selectedEdge();for(let i of n.edges){let a=n.boxes.find(e=>e.id===i.from),o=n.boxes.find(e=>e.id===i.to);if(!a||!o)continue;let s=t.canvas.querySelector(`.box[data-id="${a.id}"]`),c=t.canvas.querySelector(`.box[data-id="${o.id}"]`);if(!s||!c)continue;let l=a.x+s.offsetWidth/2,u=a.y+s.offsetHeight/2,d=o.x+c.offsetWidth/2,f=o.y+c.offsetHeight/2,[p,m]=Ge(a,s,i.fromHandle,d,f),[h,g]=Ge(o,c,i.toHandle,l,u),ee=document.createElementNS(A,`g`),te=e(i.palette);ee.setAttribute(`class`,`edge-group`+(te===1?``:` palette-`+te)+(i===r?` selected`:``));let _=document.createElementNS(A,`line`);_.setAttribute(`class`,`edge-hit`),_.setAttribute(`x1`,String(p)),_.setAttribute(`y1`,String(m)),_.setAttribute(`x2`,String(h)),_.setAttribute(`y2`,String(g)),_.setAttribute(`stroke`,`transparent`),_.setAttribute(`stroke-width`,`12`),ee.appendChild(_);let v=document.createElementNS(A,`line`);v.setAttribute(`class`,`edge-line`),v.setAttribute(`x1`,String(p)),v.setAttribute(`y1`,String(m)),v.setAttribute(`x2`,String(h)),v.setAttribute(`y2`,String(g)),ee.appendChild(v),ee.addEventListener(`mousedown`,e=>{e.stopPropagation(),t.setSelectedEdge(i),t.selected.clear(),M(),N(),t.setStatus(`edge selected — press Delete to remove`)}),t.edgeLayer.appendChild(ee)}},ft=60,pt=null,mt=()=>{if(!pt)throw Error(`render: wireProximity() not called`);return pt},ht=e=>{pt=e},gt=(e,t)=>{let n=mt(),r=null,i=1/0,a=n.link();for(let o of n.currentMap().boxes){if(a&&o.id===a.fromId)continue;let s=n.canvas.querySelector(`.box[data-id="${o.id}"]`);if(!s)continue;let c=o.x,l=o.y,u=o.x+s.offsetWidth,d=o.y+s.offsetHeight,f=Math.max(c-e,0,e-u),p=Math.max(l-t,0,t-d),m=Math.hypot(f,p);m{let e=mt();e.nearTargetId()!==null&&(e.setNearTargetId(null),M())},vt=null,yt=()=>{if(!vt)throw Error(`factories: wireFactories() not called`);return vt},bt=e=>{vt=e},xt=(e,t,n)=>{let r=yt(),i=r.mintId(),a={id:i,label:`new`,x:e,y:t};r.currentMap().boxes.push(a),j();let o=r.canvas.querySelector(`.box[data-id="${i}"]`);o&&n&&(a.x=n.x-o.offsetWidth/2,a.y=n.y-o.offsetHeight/2,o.style.left=a.x+`px`,o.style.top=a.y+`px`),fe(),o&&(r.selected.clear(),r.selected.add(i),r.selectedEdge()&&(r.clearSelectedEdge(),N()),M(),O(o,a))},St=(e,t)=>{let n=yt(),r=n.mintId(`t`),i={id:r,label:`text`,x:e,y:t};n.currentMap().texts.push(i),j();let a=n.canvas.querySelector(`.text-item[data-id="${r}"]`);a&&(i.x=e-a.offsetWidth/2,i.y=t-a.offsetHeight/2,a.style.left=i.x+`px`,a.style.top=i.y+`px`,n.selected.clear(),n.selected.add(r),n.selectedEdge()&&(n.clearSelectedEdge(),N()),M(),Be(a,i)),me()},Ct=(e,t,n,r)=>{let i=yt(),a=i.mintId(`l`),o={id:a,x1:e,y1:t,x2:n,y2:r};i.currentMap().lines.push(o),i.selected.clear(),i.selected.add(a),i.selectedEdge()&&(i.clearSelectedEdge(),N()),j(),he()},wt=()=>{let e=yt();if(e.selected.size===0){e.setStatus(`nothing selected`);return}let t=e.selected,n=e.currentMap(),r=Array.from(t).filter(e=>n.boxes.some(t=>t.id===e));n.boxes=n.boxes.filter(e=>!t.has(e.id)),n.edges=n.edges.filter(e=>!t.has(e.from)&&!t.has(e.to)),n.texts=n.texts.filter(e=>!t.has(e.id)),n.lines=n.lines.filter(e=>!t.has(e.id)),n.strokes=(n.strokes??[]).filter(e=>!t.has(e.id));let i=e.currentPath(),a=e.graph();for(let e of r){let t=i===`/`?`/`+e:i+`/`+e;a.maps=a.maps.filter(e=>e.path!==t&&!e.path.startsWith(t+`/`))}e.setGraph(a),e.setCurrentMap(e.ensureMap(i)),t.clear(),_e(),j()},Tt=null,Et=()=>{if(!Tt)throw Error(`line: wireLine() not called`);return Tt},Dt=e=>{Tt=e},Ot=!1,P=null,F=null,I=null,L=()=>Ot,kt=()=>P!==null,At=()=>{if(I)return I;let e=document.createElementNS(`http://www.w3.org/2000/svg`,`line`);return e.setAttribute(`class`,`line-preview`),e.setAttribute(`stroke`,`#07f`),e.setAttribute(`stroke-width`,`2`),e.setAttribute(`stroke-dasharray`,`5 4`),e.style.pointerEvents=`none`,Et().lineLayer().appendChild(e),I=e,e},jt=()=>{I&&I.parentNode&&I.parentNode.removeChild(I),I=null},R=e=>Math.round(e*100)/100,Mt=(e,t,n)=>{let r=t-e.x,i=n-e.y,a=Math.hypot(r,i);if(a<.001)return{x:t,y:n};let o=10*Math.PI/180,s=Math.round(Math.atan2(i,r)/o)*o;return{x:R(e.x+Math.cos(s)*a),y:R(e.y+Math.sin(s)*a)}},Nt=e=>{Ot!==e&&(Ot=e,document.body.classList.toggle(`line-mode`,Ot),Ot||(P=null,F=null,jt()),Et().setStatus(Ot?`line mode — click start, click end · L or Escape to exit`:`select mode`))},Pt=()=>{P&&(P=null,F=null,jt())},Ft=(e,t,n=!1)=>{let r=R(x(e)),i=R(S(t));if(!P){P={x:r,y:i},F={x:e,y:t};let n=At();n.setAttribute(`x1`,String(r)),n.setAttribute(`y1`,String(i)),n.setAttribute(`x2`,String(r)),n.setAttribute(`y2`,String(i));return}let a=P,o=n?Mt(a,r,i):{x:r,y:i};P=null,F=null,jt(),!(Math.hypot(o.x-a.x,o.y-a.y)<2)&&Ct(a.x,a.y,o.x,o.y)},It=(e,t,n=!1)=>{if(!P||!F)return;let r=e-F.x,i=t-F.y;if(Math.hypot(r,i)<4)return;let a=R(x(e)),o=R(S(t)),s=P,c=n?Mt(s,a,o):{x:a,y:o};P=null,F=null,jt(),!(Math.hypot(c.x-s.x,c.y-s.y)<2)&&Ct(s.x,s.y,c.x,c.y)},Lt=(e,t,n=!1)=>{if(!P||!I)return;let r=R(x(e)),i=R(S(t)),a=n?Mt(P,r,i):{x:r,y:i};I.setAttribute(`x2`,String(a.x)),I.setAttribute(`y2`,String(a.y))},Rt=null,z=null,zt=e=>{Rt=e},Bt=()=>{if(!Rt)throw Error(`clipboard: wireClipboard() not called`);return Rt},Vt=()=>{let{selected:e,currentMap:t,findTextById:n,findLineById:r}=Bt();if(e.size===0)return!1;let i=t(),a=[],o=[],s=[],c=[],l=new Set;for(let t of e){let e=i.boxes.find(e=>e.id===t);if(e){let t={id:e.id,label:e.label,x:e.x,y:e.y};e.palette&&(t.palette=e.palette),e.font&&(t.font=e.font),a.push(t),l.add(e.id);continue}let c=n(t);if(c){let e={id:c.id,label:c.label,x:c.x,y:c.y};c.palette&&(e.palette=c.palette),c.font&&(e.font=c.font),o.push(e);continue}let u=r(t);if(u){let e={id:u.id,x1:u.x1,y1:u.y1,x2:u.x2,y2:u.y2};u.palette&&(e.palette=u.palette),u.style&&(e.style=u.style),u.mids?.length&&(e.mids=u.mids.map(([e,t])=>[e,t])),s.push(e)}}for(let e of i.edges)l.has(e.from)&&l.has(e.to)&&c.push({from:e.from,fromHandle:e.fromHandle??``,to:e.to,toHandle:e.toHandle??``});if(!a.length&&!o.length&&!s.length)return!1;z={boxes:a,texts:o,lines:s,edges:c,pasteOffset:0};let u=[...a,...o].sort((e,t)=>e.y-t.y||e.x-t.x).map(e=>e.label);return u.length&&typeof navigator<`u`&&navigator.clipboard&&navigator.clipboard.writeText(u.join(` +`)).catch(()=>{}),!0},Ht=()=>{let{selected:e,deleteSelection:t,setStatus:n}=Bt();if(!Vt()){n(`nothing to cut`);return}let r=e.size;t(),n(`cut `+r+` items`)},Ut=()=>{let{selected:e,currentMap:t,mintId:n,renderAll:r,setStatus:i,clearSelectedEdge:a}=Bt();if(!z){i(`clipboard is empty`);return}z.pasteOffset+=20;let o=z.pasteOffset,s=z.pasteOffset,c=new Map;e.clear(),a();let l=t();for(let t of z.boxes){let r=n(`b`);c.set(t.id,r);let i={id:r,label:t.label,x:t.x+o,y:t.y+s};l.boxes.push(i),e.add(r)}for(let t of z.texts){let r=n(`t`);c.set(t.id,r);let i={id:r,label:t.label,x:t.x+o,y:t.y+s};t.palette&&(i.palette=t.palette),t.font&&(i.font=t.font),l.texts.push(i),e.add(r)}for(let t of z.lines){let r=n(`l`);c.set(t.id,r);let i={id:r,x1:t.x1+o,y1:t.y1+s,x2:t.x2+o,y2:t.y2+s};t.palette&&(i.palette=t.palette),t.style&&(i.style=t.style),t.mids?.length&&(i.mids=t.mids.map(([e,t])=>[e+o,t+s])),l.lines.push(i),e.add(r)}for(let e of z.edges){let t=c.get(e.from),n=c.get(e.to);if(!t||!n)continue;let r={from:t,to:n};e.fromHandle&&(r.fromHandle=e.fromHandle),e.toHandle&&(r.toHandle=e.toHandle),l.edges.push(r)}w(),r(),i(`pasted `+e.size+` items`)},Wt=null,Gt=()=>{if(!Wt)throw Error(`navigation: wireNavigation() not called`);return Wt},Kt=e=>{Wt=e},qt=e=>{let t=Gt().getGraph(),n=t.maps.find(t=>t.path===e);return n||(n={path:e,boxes:[],edges:[]},t.maps.push(n)),n.boxes??=[],n.edges??=[],n.texts??=[],n.lines??=[],n.strokes??=[],n},Jt=()=>{let e=location.hash||``;return e.startsWith(`#`)&&(e=e.slice(1)),e?(e.startsWith(`/`)||(e=`/`+e),e):`/`},Yt=(e,t)=>{let n=t?.keepViewport??!1,r=Gt();r.setCurrentPath(e),r.setCurrentMap(qt(e)),r.clearSelected(),r.clearSelectedEdge(),r.renderAll(),Qt(),n||le(qt(e));let i=`#`+e;location.hash!==i&&history.pushState(null,``,i)},Xt=e=>{let t=Gt().getCurrentPath();Yt(t===`/`?`/`+e:t+`/`+e)},Zt=()=>{let e=Gt().getCurrentPath();if(e===`/`)return;let t=e.split(`/`).filter(Boolean);t.pop(),Yt(t.length?`/`+t.join(`/`):`/`)},Qt=()=>{let e=Gt(),t=e.getGraph(),n=e.getCurrentPath(),r=document.getElementById(`path`);if(!r)return;r.innerHTML=``;let i=n===`/`?[]:n.split(`/`).filter(Boolean);r.style.display=i.length===0?`none`:``;let a=document.getElementById(`toolbar`),o=document.body.classList.contains(`snapshot-mode`);if(a&&(a.style.display=i.length===0&&!o?`none`:``),i.length===0){let e=document.getElementById(`upBtn`);e&&(e.style.display=`none`);return}let s=document.createElement(`span`);s.className=`seg`,s.textContent=`/`,s.addEventListener(`click`,()=>Yt(`/`)),r.appendChild(s);let c=``,l=`/`;i.forEach((e,n)=>{if(n>0){let e=document.createElement(`span`);e.className=`sep`,e.textContent=`/`,r.appendChild(e)}c+=`/`+e;let a=c,o=((t.maps||[]).find(e=>e.path===l)?.boxes??[]).find(t=>t.id===e),s=o?.label&&o.label.trim()||e;l=a;let u=document.createElement(`span`);u.className=`seg`,u.textContent=s,u.title=e,nYt(a)):(u.style.fontWeight=`bold`,u.style.cursor=`default`),r.appendChild(u)});let u=document.getElementById(`upBtn`);u&&(u.style.display=n===`/`?`none`:``)},$t=()=>{window.addEventListener(`hashchange`,()=>{let e=Jt();e!==Gt().getCurrentPath()&&Yt(e)})},en=100,tn=200,nn=null,B=()=>{if(!nn)throw Error(`persistence: wirePersistence() not called`);return nn},V=null,H=[],rn=[],an=null,on=location.pathname.match(/^\/m\/([\w-]+)\/?$/),sn=on?on[1]:null,cn=sn!==null,ln=e=>{nn=e},un=async()=>{let e=B(),t=null;if(cn){document.body.classList.add(`snapshot-mode`),document.getElementById(`downloadBtn`)?.style.setProperty(`display`,``),document.getElementById(`reshareBtn`)?.style.setProperty(`display`,``);try{let e=await fetch(`/api/snapshot/`+encodeURIComponent(sn));if(!e.ok)throw Error(`HTTP `+e.status);let n=await e.json();t=n.graph||n}catch(n){let r=n instanceof Error?n.message:String(n);e.setStatus(`snapshot `+sn+` not loaded: `+r),t=null}}else t=await(await fetch(`/state`)).json();(!t||!t.maps||t.maps.length===0)&&(t={maps:[{path:`/`,boxes:[],edges:[]}]}),e.setGraph(t),V=JSON.stringify(t),H=[],rn=[],e.setCurrentPath(e.readPathFromURL()),e.setStatus(cn?`snapshot `+sn+` — local edits only`:`loaded`)},dn=()=>{B().setStatus(`saving…`),an&&clearTimeout(an),an=setTimeout(pn,tn)},fn=async e=>{if(cn){B().setStatus(`local edits only — use Download or Save as new share`);return}await fetch(`/save`,{method:`POST`,headers:{"Content-Type":`application/json`},body:e}),B().setStatus(`saved`)},pn=async()=>{let e=JSON.stringify(B().getGraph());V!==null&&e!==V&&(H.push(V),H.length>en&&H.shift(),rn=[]),V=e,await fn(e)},mn=e=>{let t=B(),n=JSON.parse(e);t.setGraph(n),t.clearSelected(),t.clearSelectedEdge();let r=t.getCurrentPath(),i=n.maps.some(e=>e.path===r)?r:`/`;t.setCurrentPath(i,{keepViewport:i===r})},hn=()=>{let e=B();an&&=(clearTimeout(an),null);let t=JSON.stringify(e.getGraph());if(V!==null&&t!==V&&(H.push(V),H.length>en&&H.shift(),rn=[],V=t),H.length===0){e.setStatus(`nothing to undo`);return}let n=H.pop();V!==null&&rn.push(V),V=n,mn(n),fn(n),e.setStatus(`undo (`+H.length+` left)`)},gn=()=>{let e=B();if(rn.length===0){e.setStatus(`nothing to redo`);return}let t=rn.pop();V!==null&&H.push(V),V=t,mn(t),fn(t),e.setStatus(`redo`)},_n=()=>{let e=B(),t=e.serializeGraph(e.getGraph()),n=new Blob([t],{type:`text/plain;charset=utf-8`}),r=URL.createObjectURL(n),i=document.createElement(`a`);i.href=r,i.download=(sn??`mindmap`)+`.flowgo`,document.body.appendChild(i),i.click(),i.remove(),setTimeout(()=>URL.revokeObjectURL(r),1e3),e.setStatus(`downloaded`)},vn=async()=>{let e=B();e.setStatus(`re-sharing…`);try{let t=await fetch(`/api/snapshot`,{method:`POST`,headers:{"Content-Type":`application/json`},body:JSON.stringify({graph:e.getGraph()})});if(!t.ok)throw Error(`HTTP `+t.status);let n=await t.json();if(!n.url)throw Error(`response missing url`);navigator.clipboard&&navigator.clipboard.writeText(n.url).catch(()=>{}),e.setStatus(`new share: `+n.url+` (copied)`),n.id&&history.pushState(null,``,`/m/`+n.id)}catch(t){let n=t instanceof Error?t.message:String(t);e.setStatus(`re-share failed: `+n)}},yn=null,bn=()=>{if(!yn)throw Error(`clone: wireClone() not called`);return yn},xn=e=>{yn=e},Sn=()=>{let{currentMap:e,selected:t,findTextById:n,findLineById:r,mintId:i}=bn(),a=e(),o=new Map,s=Array.from(t),c=new Set;for(let e of s){let t=a.boxes.find(t=>t.id===e);if(t){let n=i(`b`);o.set(e,n),c.add(n);let r={id:n,label:t.label,x:t.x,y:t.y};t.palette&&(r.palette=t.palette),t.font&&(r.font=t.font),a.boxes.push(r);continue}let s=n(e);if(s){let t=i(`t`);o.set(e,t);let n={id:t,label:s.label,x:s.x,y:s.y};s.palette&&(n.palette=s.palette),s.font&&(n.font=s.font),a.texts.push(n);continue}let l=r(e);if(l){let t=i(`l`);o.set(e,t);let n={id:t,x1:l.x1,y1:l.y1,x2:l.x2,y2:l.y2};l.palette&&(n.palette=l.palette),l.style&&(n.style=l.style),l.mids?.length&&(n.mids=l.mids.map(([e,t])=>[e,t])),a.lines.push(n)}}for(let e of a.edges.slice()){let t=o.get(e.from),n=o.get(e.to);if(t&&n&&c.has(t)&&c.has(n)){let r={from:t,to:n};e.fromHandle&&(r.fromHandle=e.fromHandle),e.toHandle&&(r.toHandle=e.toHandle),a.edges.push(r)}}t.clear();for(let e of o.values())t.add(e);return o},Cn=null,U=()=>{if(!Cn)throw Error(`keys: wireKeys() not called`);return Cn},wn=e=>{Cn=e},Tn=(e,t)=>((e&&e>=1&&e<=9?e:1)-1+t+9)%9+1,En=(e,t)=>t===1?e.palette?(delete e.palette,!0):!1:e.palette===t?!1:(e.palette=t,!0),Dn=e=>{let t=U(),n=t.currentMap();return n.boxes.find(t=>t.id===e)||t.findTextById(e)||n.lines.find(t=>t.id===e)||(n.strokes??[]).find(t=>t.id===e)},On=()=>{let e=U();return e.selected.size>0||e.selectedEdge()!==null},kn=e=>{let t=U();if(!On())return!1;let n=!1;for(let r of t.selected){let t=Dn(r);t&&En(t,Tn(t.palette,e))&&(n=!0)}let r=t.selectedEdge();return r&&En(r,Tn(r.palette,e))&&(n=!0),n},An=e=>{let t=U();if(t.selected.size===0)return!1;let n=t.currentMap(),r=!1;for(let i of t.selected){let a=n.boxes.find(e=>e.id===i)||t.findTextById(i);if(!a)continue;let o=Tn(a.font,e);o===1?a.font&&(delete a.font,r=!0):a.font!==o&&(a.font=o,r=!0)}return r},jn=()=>{let e=U();if(e.selected.size!==1){e.setStatus(`anchor needs exactly one selected box`);return}let t=e.selected.values().next().value,n=e.currentMap(),r=n.boxes.find(e=>e.id===t);if(!r){e.setStatus(`anchor only applies to boxes`);return}let i=!r.anchor;for(let e of n.boxes)e.anchor&&delete e.anchor;i&&(r.anchor=!0),fe(),j(),e.setStatus(i?`anchored `+t:`anchor cleared`)},Mn=e=>{let t=U();if(!On())return!1;let n=!1;for(let r of t.selected){let t=Dn(r);t&&En(t,e)&&(n=!0)}let r=t.selectedEdge();return r&&En(r,e)&&(n=!0),n},Nn=e=>{let t=U();if(t.selected.size===0)return!1;let n=t.currentMap(),r=!1;for(let i of t.selected){let a=n.boxes.find(e=>e.id===i)||t.findTextById(i);a&&(e===1?a.font&&(delete a.font,r=!0):a.font!==e&&(a.font=e,r=!0))}return r},Pn=e=>{let t=U();if(t.selected.size===0)return!1;let n=t.currentMap(),r=!1;for(let i of t.selected){let t=n.lines.find(e=>e.id===i);t&&(e===1?t.style&&(delete t.style,r=!0):t.style!==e&&(t.style=e,r=!0))}return r},Fn=()=>{document.addEventListener(`keydown`,e=>{let t=U();if(e.key===`Escape`&&ae()){ie(!1);return}if(Re())return;let n=e.metaKey||e.ctrlKey;if(n&&!e.altKey&&(e.key===`z`||e.key===`Z`)){e.preventDefault(),e.shiftKey?gn():hn();return}if(n&&!e.altKey&&(e.key===`y`||e.key===`Y`)){e.preventDefault(),gn();return}if(n&&!e.altKey&&!e.shiftKey&&(e.key===`a`||e.key===`A`)){e.preventDefault();let n=t.currentMap();t.selected.clear();for(let e of n.boxes)t.selected.add(e.id);for(let e of n.texts??[])t.selected.add(e.id);for(let e of n.lines??[])t.selected.add(e.id);t.selectedEdge()&&(t.setSelectedEdge(null),N()),M(),t.setStatus(`selected `+t.selected.size+` items`);return}if(n&&!e.altKey&&!e.shiftKey&&(e.key===`c`||e.key===`C`)){if(window.getSelection&&String(window.getSelection()))return;e.preventDefault(),Vt()?t.setStatus(`copied `+t.selected.size+` items`):t.setStatus(`nothing to copy`);return}if(n&&!e.altKey&&!e.shiftKey&&(e.key===`x`||e.key===`X`)){e.preventDefault(),Ht();return}if(n&&!e.altKey&&!e.shiftKey&&(e.key===`v`||e.key===`V`)){e.preventDefault(),Ut();return}if(!n&&!e.altKey&&(e.key===`t`||e.key===`T`)){e.preventDefault(),St(x(t.lastCursor.x),S(t.lastCursor.y));return}if(!n&&!e.altKey&&(e.key===`l`||e.key===`L`)){e.preventDefault(),Nt(!L());return}if(!n&&!e.altKey&&(e.key===`b`||e.key===`B`)){e.preventDefault(),De(!0);return}if(!n&&!e.altKey&&(e.key===`v`||e.key===`V`)){e.preventDefault(),De(!1),Nt(!1);return}if(!n&&!e.altKey&&(e.key===`a`||e.key===`A`)){e.preventDefault(),jn();return}if(!n&&!e.altKey&&!e.shiftKey&&/^[1-9]$/.test(e.key)){let t=parseInt(e.key,10);if(E()){e.preventDefault(),Oe(t);return}if(!On())return;Mn(t)&&(e.preventDefault(),w(),j());return}if(!n&&!e.altKey&&e.shiftKey&&/^Digit[1-9]$/.test(e.code)){if(t.selected.size===0)return;let n=parseInt(e.code.slice(5),10),r=Nn(n),i=Pn(n);(r||i)&&(e.preventDefault(),w(),j());return}if(!n&&!e.altKey&&e.shiftKey&&(e.key===`+`||e.key===`*`||e.key===`_`||e.key===`-`)){if(t.selected.size===0)return;An(e.key===`_`||e.key===`-`?-1:1)&&(e.preventDefault(),w(),j());return}if(!n&&!e.altKey&&(e.key===`+`||e.key===`=`||e.key===`-`)){if(!On())return;kn(e.key===`-`?-1:1)&&(e.preventDefault(),w(),j());return}if(!n&&!e.altKey&&!e.shiftKey&&e.key===`Enter`){if(t.selected.size!==1)return;let n=t.selected.values().next().value,r=t.currentMap(),i=r.boxes.find(e=>e.id===n);if(i){let r=t.canvas.querySelector(`.box[data-id="${n}"]`);r&&(e.preventDefault(),O(r,i));return}let a=(r.texts??[]).find(e=>e.id===n);if(a){let r=t.canvas.querySelector(`.text-item[data-id="${n}"]`);r&&(e.preventDefault(),Be(r,a))}return}if(e.key===`Escape`){if(E()){De(!1);return}if(L()){kt()?Pt():Nt(!1);return}let e=t.link();e&&(e.handleEl.classList.remove(`active`),t.ghostLine.style.display=`none`,t.clearLink(),t.setDropTargetId(null),t.setDropTargetHandle(null),M(),t.clearProximity()),t.selected.clear(),t.setSelectedEdge(null),M(),N()}if(e.key===`Delete`||e.key===`Backspace`){let n=t.selectedEdge();if(n){e.preventDefault();let r=t.currentMap(),i=r.edges.indexOf(n);i>=0&&r.edges.splice(i,1),t.setSelectedEdge(null),pe(),N(),t.setStatus(`edge removed`);return}t.selected.size>0&&(e.preventDefault(),wt())}})},In=null,Ln=()=>{if(!In)throw Error(`mouse: wireMouse() not called`);return In},Rn=e=>{In=e},zn=(e,t)=>{let n=Ln(),r=document.elementsFromPoint(e,t);for(let e of r){if(!e||e===n.ghostLine)continue;let t=e.closest?.(`.box`);if(t)return t}return null},Bn=e=>{let t=Ln();if(t.lastCursor.x=e.clientX,t.lastCursor.y=e.clientY,Ce()){Me(e.clientX,e.clientY);return}if(L()){Lt(e.clientX,e.clientY,e.shiftKey);return}let n=t.pan();if(n){b.x=n.startVX+(e.clientX-n.downX),b.y=n.startVY+(e.clientY-n.downY),ce();return}let r=t.drag();if(r){let t=e.clientX-r.downX,n=e.clientY-r.downY;if(!r.active&&Math.hypot(t,n)>4){r.active=!0;for(let e of r.movers)e.el?.classList?.add(`dragging`)}if(r.active){for(let i of r.movers)i.apply(t,n,e);N()}return}let i=t.band();if(i){let t=Math.min(i.startX,e.clientX),n=Math.min(i.startY,e.clientY),r=Math.abs(e.clientX-i.startX),a=Math.abs(e.clientY-i.startY);i.el.style.left=t+`px`,i.el.style.top=n+`px`,i.el.style.width=r+`px`,i.el.style.height=a+`px`;return}let a=t.link();if(a){t.ghostLine.setAttribute(`x2`,String(x(e.clientX))),t.ghostLine.setAttribute(`y2`,String(S(e.clientY)));let n=zn(e.clientX,e.clientY),r=n&&n.dataset.id!==a.fromId?n.dataset.id??null:null,i=null;if(r&&n){let o=t.currentMap().boxes.find(e=>e.id===r);o&&(i=We(n,o,a.startX,a.startY,e.clientX,e.clientY))}let o=r!==t.dropTargetId(),s=i!==t.dropTargetHandle();(o||s)&&(t.setDropTargetId(r),t.setDropTargetHandle(i),M()),gt(x(e.clientX),S(e.clientY));return}gt(x(e.clientX),S(e.clientY))},Vn=e=>{let t=Ln();if(Ce()){Ne();return}if(L()){It(e.clientX,e.clientY,e.shiftKey);return}if(t.pan()){t.setPan(null),document.body.classList.remove(`panning`);return}let n=t.drag();if(n){let e=n.active;for(let e of n.movers)e.el?.classList?.remove(`dragging`);let r=n.primaryId;t.setDrag(null),e?w():(t.selected.clear(),r&&t.selected.add(r),t.selectedEdge()&&(t.setSelectedEdge(null),N()),M());return}let r=t.band();if(r){let n=Math.min(r.startX,e.clientX),i=Math.min(r.startY,e.clientY),a=Math.max(r.startX,e.clientX),o=Math.max(r.startY,e.clientY);if(a-n>2||o-i>2){let e=x(n),r=S(i),s=x(a),c=S(o),l=t.currentMap();for(let n of l.boxes){let i=t.canvas.querySelector(`.box[data-id="${n.id}"]`);if(!i)continue;let a=n.x+i.offsetWidth,o=n.y+i.offsetHeight;n.xe&&n.yr&&t.selected.add(n.id)}for(let n of l.texts){let i=t.canvas.querySelector(`.text-item[data-id="${n.id}"]`);if(!i)continue;let a=n.x+i.offsetWidth,o=n.y+i.offsetHeight;n.xe&&n.yr&&t.selected.add(n.id)}for(let n of l.lines){let i=[n.x1,n.x2],a=[n.y1,n.y2];for(let[e,t]of n.mids??[])i.push(e),a.push(t);let o=Math.min(...i),l=Math.min(...a),u=Math.max(...i),d=Math.max(...a);oe&&lr&&t.selected.add(n.id)}M(),t.selected.size>0&&t.setStatus(t.selected.size+` selected`)}r.el.remove(),t.setBand(null);return}let i=t.link();if(i){i.handleEl.classList.remove(`active`),t.ghostLine.style.display=`none`;let n=zn(e.clientX,e.clientY);if(n&&n.dataset.id!==i.fromId){let r=n.dataset.id,a=t.currentMap(),o=We(n,a.boxes.find(e=>e.id===r),i.startX,i.startY,e.clientX,e.clientY),s={from:i.fromId,to:r};i.fromHandle&&(s.fromHandle=i.fromHandle),o&&(s.toHandle=o),a.edges=h(a.edges,s),pe(),N()}else{let n=t.mintId(),r=x(e.clientX),a=S(e.clientY),o={id:n,label:`new`,x:r,y:a},s=t.currentMap();s.boxes.push(o),j();let c=t.canvas.querySelector(`.box[data-id="${n}"]`);if(c){o.x=r-c.offsetWidth/2,o.y=a-c.offsetHeight/2,c.style.left=o.x+`px`,c.style.top=o.y+`px`;let e=Ue(o,c,i.startX,i.startY),l={from:i.fromId,to:n};i.fromHandle&&(l.fromHandle=i.fromHandle),e&&(l.toHandle=e),s.edges=h(s.edges,l),N(),t.selected.clear(),t.selected.add(n),M(),O(c,o,{cancelDeletes:!0})}w()}t.setLink(null),(t.dropTargetId()||t.dropTargetHandle())&&(t.setDropTargetId(null),t.setDropTargetHandle(null),M()),_t()}},Hn=e=>{e.button===2&&(e.preventDefault(),Ln().setPan({downX:e.clientX,downY:e.clientY,startVX:b.x,startVY:b.y}),document.body.classList.add(`panning`))},Un=e=>{let t=Ln();if(e.button!==0)return;if(E()){e.preventDefault(),e.stopPropagation(),je(e.clientX,e.clientY);return}if(L()){e.preventDefault(),e.stopPropagation(),Ft(e.clientX,e.clientY,e.shiftKey);return}e.shiftKey||t.selected.clear(),t.selectedEdge()&&(t.setSelectedEdge(null),N()),M();let n=document.createElement(`div`);n.className=`selection-band`,n.style.left=e.clientX+`px`,n.style.top=e.clientY+`px`,n.style.width=`0px`,n.style.height=`0px`,document.body.appendChild(n),t.setBand({startX:e.clientX,startY:e.clientY,el:n})},Wn=(e,t,n,r,i,a)=>{let o=i-n,s=a-r,c=o*o+s*s,l=c===0?0:Math.max(0,Math.min(1,((e-n)*o+(t-r)*s)/c)),u=n+l*o,d=r+l*s,f=e-u,p=t-d;return{d2:f*f+p*p,t:l}},Gn=14,Kn=(e,t,n)=>{let r=null,i=0,a=Gn*Gn;for(let o of e.lines){let e=[[o.x1,o.y1],...o.mids??[],[o.x2,o.y2]];for(let s=0;s{let t=Ln();if(E())return;if(L()){let n=x(e.clientX),r=S(e.clientY);Kn(t.currentMap(),n,r)&&(Pt(),he(),j());return}let n=x(e.clientX),r=S(e.clientY);xt(n,r,{x:n,y:r})},Jn=()=>{document.addEventListener(`mousemove`,Bn),document.addEventListener(`mouseup`,Vn),document.addEventListener(`mousedown`,Hn),window.addEventListener(`contextmenu`,e=>e.preventDefault()),window.addEventListener(`auxclick`,e=>{e.button===1&&e.preventDefault()});let e=document.getElementById(`bg-layer`);e&&(e.addEventListener(`mousedown`,Un),e.addEventListener(`dblclick`,qn))},Yn=typeof navigator<`u`&&/Mac|iPhone|iPad|iPod/i.test(navigator.platform||navigator.userAgent||``),Xn=e=>Yn?e.metaKey:e.ctrlKey,W=e=>Math.round(e/20)*20,Zn=e=>{let t=e.mids??[],n=[[e.x1,e.y1],...t,[e.x2,e.y2]],r=e.style??1;if(r===2&&t.length>0){let n=`M ${e.x1} ${e.y1}`;for(let e=0;e=Math.abs(o-i)?e+=` L ${a} ${i} L ${a} ${o}`:e+=` L ${r} ${o} L ${a} ${o}`}return e}let i=`M ${n[0][0]} ${n[0][1]}`;for(let e=1;e{let n=e.x,r=e.y;return{el:t,apply(i,a,o){let s=n+i,c=r+a;o?.shiftKey&&(s=W(s),c=W(c)),e.x=s,e.y=c,t.style.left=e.x+`px`,t.style.top=e.y+`px`}}},$n=(e,t)=>{let n=e.x,r=e.y;return{el:t,apply(i,a,o){let s=n+i,c=r+a;o?.shiftKey&&(s=W(s),c=W(c)),e.x=s,e.y=c,t.style.left=e.x+`px`,t.style.top=e.y+`px`}}},er=(e,t,n,r,i,a,o)=>{let s=e.x1,c=e.y1,l=e.x2,u=e.y2,d=(e.mids??[]).map(([e,t])=>[e,t]);return{el:t,apply(t,f,p){let m=t,h=f;if(p?.shiftKey&&(m=W(s+t)-s,h=W(c+f)-c),e.x1=s+m,e.y1=c+h,e.x2=l+m,e.y2=u+h,d.length>0){e.mids||=[];for(let t=0;t{let r=typeof t==`object`?t.mid:-1,i=t===1?e.x1:t===2?e.x2:e.mids?.[r]?.[0]??0,a=t===1?e.y1:t===2?e.y2:e.mids?.[r]?.[1]??0;return{el:n.g,apply(o,s,c){let l=i+o,u=a+s;c?.shiftKey&&(l=W(l),u=W(u)),t===1?(e.x1=l,e.y1=u):t===2?(e.x2=l,e.y2=u):e.mids&&e.mids[r]&&(e.mids[r]=[l,u]);let d=Zn(e);n.line.setAttribute(`d`,d),n.hit.setAttribute(`d`,d);let f=t===1?n.h1:t===2?n.h2:n.midHandles[r]??null;f&&(f.setAttribute(`cx`,String(l)),f.setAttribute(`cy`,String(u)))}}},nr=(e,t,n,r)=>{let i=e.points.map(([e,t])=>[e,t]);return{el:t,apply(t,a,s){let c=t,l=a;if(s?.shiftKey&&i.length>0){let e=i[0];c=W(e[0]+t)-e[0],l=W(e[1]+a)-e[1]}for(let t=0;t{if(!rr)throw Error(`attach: wireAttach() not called`);return rr},ir=e=>{rr=e},ar=()=>{let e=G(),t=[],n=e.currentMap();for(let r of e.selected){let i=n.boxes.find(e=>e.id===r);if(i){let n=e.canvas.querySelector(`.box[data-id="${r}"]`);n&&t.push(Qn(i,n));continue}let a=e.findTextById(r);if(a){let n=e.canvas.querySelector(`.text-item[data-id="${r}"]`);n&&t.push($n(a,n));continue}let o=e.findLineById(r);if(o){let n=e.lineLayer.querySelector(`.line-group[data-id="${r}"]`);if(n){let e=n.querySelector(`.line-line`),r=n.querySelector(`.line-hit`),i=n.querySelector(`.line-handle[data-endpoint="1"]`),a=n.querySelector(`.line-handle[data-endpoint="2"]`),s=Array.from(n.querySelectorAll(`.line-handle[data-endpoint="m"]`));t.push(er(o,n,e,r,i,a,s))}continue}let s=e.findStrokeById(r);if(s){let n=e.strokeLayer.querySelector(`.stroke-group[data-id="${r}"]`);if(n){let e=n.querySelector(`.stroke-hit`),r=n.querySelector(`.stroke-line`);t.push(nr(s,n,e,r))}}}return t},or=(e,t)=>{e.addEventListener(`mousedown`,n=>{let r=G();if(e.isContentEditable||n.button!==0)return;n.preventDefault(),n.stopPropagation(),r.selected.has(t.id)||(n.shiftKey||r.selected.clear(),r.selected.add(t.id),r.selectedEdge()&&(r.setSelectedEdge(null),N()),M());let i=t.id;if(n.altKey){let e=r.cloneSelection();e.has(t.id)&&(i=e.get(t.id))}r.setDrag({movers:ar(),primaryId:i,downX:n.clientX,downY:n.clientY,active:!1})}),e.addEventListener(`dblclick`,n=>{let r=G();e.isContentEditable||(n.preventDefault(),n.stopPropagation(),r.selected.clear(),r.selected.add(t.id),M(),Be(e,t))})},sr=(e,t,n,r,i,a,o)=>{n.addEventListener(`mousedown`,e=>{let t=G();if(e.button!==0)return;e.preventDefault(),e.stopPropagation(),t.selected.has(o.id)||(e.shiftKey||t.selected.clear(),t.selected.add(o.id),t.selectedEdge()&&(t.setSelectedEdge(null),N()),M());let n=o.id;if(e.altKey){let e=t.cloneSelection();e.has(o.id)&&(n=e.get(o.id))}t.setDrag({movers:ar(),primaryId:n,downX:e.clientX,downY:e.clientY,active:!1})}),n.addEventListener(`dblclick`,e=>{e.preventDefault(),e.stopPropagation();let t=G(),n=x(e.clientX),r=S(e.clientY);o.mids||=[];let i=[[o.x1,o.y1],...o.mids,[o.x2,o.y2]],a=0,s=1/0;for(let e=0;e{let l=G();s.button===0&&(s.preventDefault(),s.stopPropagation(),l.selected.clear(),l.selected.add(o.id),l.selectedEdge()&&(l.setSelectedEdge(null),N()),M(),l.setDrag({movers:[tr(o,c,{g:e,line:t,hit:n,h1:r,h2:i,midHandles:a})],primaryId:o.id,downX:s.clientX,downY:s.clientY,active:!1}))});for(let s=0;s{let c=G();s.button===0&&(s.preventDefault(),s.stopPropagation(),c.selected.clear(),c.selected.add(o.id),c.selectedEdge()&&(c.setSelectedEdge(null),N()),M(),c.setDrag({movers:[tr(o,{mid:l},{g:e,line:t,hit:n,h1:r,h2:i,midHandles:a})],primaryId:o.id,downX:s.clientX,downY:s.clientY,active:!1}))}),c.addEventListener(`dblclick`,e=>{e.preventDefault(),e.stopPropagation(),o.mids&&l{e.addEventListener(`mousedown`,n=>{let r=G();if(e.isContentEditable)return;if(n.button===1||n.button===0&&Xn(n)){n.preventDefault(),n.stopPropagation(),Xt(t.id);return}if(n.button!==0)return;let i=n.target;if(i.classList.contains(`handle`)){n.preventDefault(),n.stopPropagation();let a=i.dataset.handle,o=r.currentMap(),s=null,c=null,l=``;for(let e=o.edges.length-1;e>=0;e--){let n=o.edges[e];if(n.from===t.id&&n.fromHandle===a){s=n,c=n.to,l=n.toHandle??``;break}if(n.to===t.id&&n.toHandle===a){s=n,c=n.from,l=n.fromHandle??``;break}}if(s&&c){let a=o.edges.indexOf(s);a>=0&&o.edges.splice(a,1);let u=o.boxes.find(e=>e.id===c),d=r.canvas.querySelector(`.box[data-id="${c}"]`);if(!u||!d){o.edges.push(s),N();return}let f=t.x+e.offsetWidth/2,p=t.y+e.offsetHeight/2,m=l||Ue(u,d,f,p),[h,g]=He(d,u,m);r.setLink({fromId:c,fromHandle:m,startX:h,startY:g,handleEl:i,rerouting:!0}),i.classList.add(`active`),r.ghostLine.setAttribute(`x1`,String(h)),r.ghostLine.setAttribute(`y1`,String(g)),r.ghostLine.setAttribute(`x2`,String(x(n.clientX))),r.ghostLine.setAttribute(`y2`,String(S(n.clientY))),r.ghostLine.style.display=``,N(),r.setStatus(`re-routing edge — drop on a box, or in empty space`);return}let[u,d]=He(e,t,a);r.setLink({fromId:t.id,fromHandle:a,startX:u,startY:d,handleEl:i}),i.classList.add(`active`),r.ghostLine.setAttribute(`x1`,String(u)),r.ghostLine.setAttribute(`y1`,String(d)),r.ghostLine.setAttribute(`x2`,String(x(n.clientX))),r.ghostLine.setAttribute(`y2`,String(S(n.clientY))),r.ghostLine.style.display=``,r.setStatus(`drop on a box to connect, or release to cancel`);return}n.preventDefault(),n.stopPropagation(),r.selected.has(t.id)||(n.shiftKey||r.selected.clear(),r.selected.add(t.id),r.selectedEdge()&&(r.setSelectedEdge(null),N()),M());let a=t.id;if(n.altKey){let e=r.cloneSelection();e.has(t.id)&&(a=e.get(t.id))}r.setDrag({movers:ar(),primaryId:a,downX:n.clientX,downY:n.clientY,active:!1})}),e.addEventListener(`dblclick`,n=>{let r=G();e.isContentEditable||(n.preventDefault(),n.stopPropagation(),r.selected.clear(),r.selected.add(t.id),r.selectedEdge()&&(r.setSelectedEdge(null),N()),M(),O(e,t))})},lr=(e,t,n)=>e!==null&&e.id===t.id&&t.time-e.time<=n?{kind:`double`,nextLastTap:null}:{kind:`single`,nextLastTap:t},ur=(e,t,n,r,i)=>Math.hypot(n-e,r-t)>i,dr=300,fr=``,pr=500,mr=4,K=null,hr=null,gr=()=>{hr!==null&&(clearTimeout(hr),hr=null)},_r=()=>document.getElementById(`deleteZone`),vr=e=>{let t=_r();return t?e<=t.getBoundingClientRect().bottom:!1},yr=e=>{_r()?.classList.toggle(`armed`,e)},br=null,xr=()=>{if(!br)throw Error(`touch: wireTouch() not called`);return br},Sr=e=>{br=e},Cr=(e,t)=>{if(!(e instanceof Element))return null;let n=e.closest(`.handle`);if(n){let e=n.parentElement?.closest?.(`.box`)??null;if(e&&!e.isContentEditable){let r=e.dataset.id,i=n.dataset.handle;if(r&&i&&t.has(r))return{kind:`handle`,boxEl:e,boxId:r,handleEl:n,code:i}}}let r=e.closest(`.box`);if(r&&!r.isContentEditable){let e=r.dataset.id;if(e)return{kind:`box`,el:r,id:e}}let i=e.closest(`.text-item`);if(i&&!i.isContentEditable){let e=i.dataset.id;if(e)return{kind:`text`,el:i,id:e}}let a=e.closest(`.line-handle`);if(a){let e=a.closest(`.line-group`)?.dataset?.id,n=a.dataset.endpoint;if(e&&t.has(e)&&(n===`1`||n===`2`||n===`m`)){let t;if(n===`1`)t=1;else if(n===`2`)t=2;else{let e=parseInt(a.dataset.midIndex??`0`,10);t={mid:Number.isFinite(e)?e:0}}return{kind:`line-endpoint`,lineId:e,endpoint:t}}}let o=e.closest(`.line-group`);if(o){let e=o.dataset.id;if(e)return{kind:`line`,id:e}}let s=e.closest(`.stroke-group`);if(s){let e=s.dataset.id;if(e)return{kind:`stroke`,id:e}}return e.closest(`#bg-layer`)||e.closest(`#bg-svg`)||e.closest(`#edges`)?{kind:`bg`}:null},wr=()=>{let e=xr();if(gr(),Ce()){Ne();return}if(kt()){Pt();return}e.pan()&&(e.setPan(null),document.body.classList.remove(`panning`));let t=e.drag();if(t){for(let e of t.movers)e.el.classList?.remove(`dragging`);e.setDrag(null),document.body.classList.remove(`dragging`),yr(!1)}let n=e.link();n&&(n.handleEl.classList.remove(`active`),e.ghostLine.style.display=`none`,e.setLink(null),(e.dropTargetId()||e.dropTargetHandle())&&(e.setDropTargetId(null),e.setDropTargetHandle(null),M()),_t())},Tr=e=>{if(e.touches.length!==1){wr();return}if(E()){let t=e.touches[0];e.preventDefault(),je(t.clientX,t.clientY);return}if(L()){let t=e.touches[0];e.preventDefault(),Ft(t.clientX,t.clientY);return}let t=document.querySelector(`[contenteditable="true"]`);t&&!t.contains(e.target)&&t.blur(),document.body.classList.contains(`panning`)||(document.body.classList.remove(`dragging`),yr(!1));let n=xr(),r=e.touches[0],i=Cr(e.target,n.selected);if(i){if(i.kind===`bg`){e.preventDefault(),n.setPan({downX:r.clientX,downY:r.clientY,startVX:b.x,startVY:b.y}),document.body.classList.add(`panning`);return}if(i.kind===`handle`){e.preventDefault();let t=n.currentMap(),a=t.boxes.find(e=>e.id===i.boxId);if(!a)return;let o=null,s=null,c=``;for(let e=t.edges.length-1;e>=0;e--){let n=t.edges[e];if(n.from===i.boxId&&n.fromHandle===i.code){o=n,s=n.to,c=n.toHandle??``;break}if(n.to===i.boxId&&n.toHandle===i.code){o=n,s=n.from,c=n.fromHandle??``;break}}if(o&&s){let e=t.edges.indexOf(o);e>=0&&t.edges.splice(e,1);let l=t.boxes.find(e=>e.id===s),u=n.canvas.querySelector(`.box[data-id="${s}"]`);if(!l||!u){t.edges.push(o),N();return}let d=a.x+i.boxEl.offsetWidth/2,f=a.y+i.boxEl.offsetHeight/2,p=c||Ue(l,u,d,f),[m,h]=He(u,l,p);n.setLink({fromId:s,fromHandle:p,startX:m,startY:h,handleEl:i.handleEl,rerouting:!0}),i.handleEl.classList.add(`active`),n.ghostLine.setAttribute(`x1`,String(m)),n.ghostLine.setAttribute(`y1`,String(h)),n.ghostLine.setAttribute(`x2`,String(x(r.clientX))),n.ghostLine.setAttribute(`y2`,String(S(r.clientY))),n.ghostLine.style.display=``,N();return}let[l,u]=He(i.boxEl,a,i.code),d=i.handleEl.getBoundingClientRect(),f=x(d.left+d.width/2),p=S(d.top+d.height/2);n.setLink({fromId:i.boxId,fromHandle:i.code,startX:l,startY:u,handleEl:i.handleEl}),i.handleEl.classList.add(`active`),n.ghostLine.setAttribute(`x1`,String(f)),n.ghostLine.setAttribute(`y1`,String(p)),n.ghostLine.setAttribute(`x2`,String(x(r.clientX))),n.ghostLine.setAttribute(`y2`,String(S(r.clientY))),n.ghostLine.style.display=``;return}if(i.kind===`line-endpoint`){let t=i.lineId,a=i.endpoint,o=document.querySelector(`.line-group[data-id="${t}"]`),s=n.currentMap().lines.find(e=>e.id===t);if(!o||!s)return;let c=o.querySelector(`.line-line`),l=o.querySelector(`.line-hit`),u=o.querySelector(`.line-handle[data-endpoint="1"]`),d=o.querySelector(`.line-handle[data-endpoint="2"]`),f=Array.from(o.querySelectorAll(`.line-handle[data-endpoint="m"]`));if(!c||!l||!u||!d)return;e.preventDefault(),n.selected.clear(),n.selected.add(t),n.selectedEdge()&&(n.setSelectedEdge(null),N()),M(),n.setDrag({movers:[tr(s,a,{g:o,line:c,hit:l,h1:u,h2:d,midHandles:f})],primaryId:t,downX:r.clientX,downY:r.clientY,active:!1});return}if(e.preventDefault(),n.selected.has(i.id)||(n.selected.clear(),n.selected.add(i.id),n.selectedEdge()&&(n.setSelectedEdge(null),N()),M()),n.setDrag({movers:ar(),primaryId:i.id,downX:r.clientX,downY:r.clientY,active:!1}),i.kind===`box`){let e=i.id;hr=window.setTimeout(()=>{hr=null;let t=n.drag();if(!(!t||t.active)){t.longPressFired=!0;for(let e of t.movers)e.el.classList?.remove(`dragging`);n.setDrag(null),document.body.classList.remove(`dragging`),yr(!1),K=null,Xt(e)}},pr)}}},Er=e=>{let t=xr();if(e.touches.length!==1){wr();return}let n=e.touches[0];if(!n)return;if(Ce()){e.preventDefault(),Me(n.clientX,n.clientY);return}if(L()&&kt()){e.preventDefault(),Lt(n.clientX,n.clientY);return}let r=t.pan();if(r){e.preventDefault(),b.x=r.startVX+(n.clientX-r.downX),b.y=r.startVY+(n.clientY-r.downY),ce();return}let i=t.drag();if(i){e.preventDefault();let t=n.clientX-i.downX,r=n.clientY-i.downY;if(!i.active&&ur(i.downX,i.downY,n.clientX,n.clientY,mr)){i.active=!0,gr(),K=null;for(let e of i.movers)e.el.classList?.add(`dragging`);document.body.classList.add(`dragging`)}if(i.active){for(let e of i.movers)e.apply(t,r,null);N(),yr(vr(n.clientY))}return}let a=t.link();if(a){e.preventDefault();let r=x(n.clientX),i=S(n.clientY);t.ghostLine.setAttribute(`x2`,String(r)),t.ghostLine.setAttribute(`y2`,String(i));let o=zn(n.clientX,n.clientY),s=o&&o.dataset.id!==a.fromId?o.dataset.id??null:null,c=null;if(s&&o){let e=t.currentMap().boxes.find(e=>e.id===s);e&&(c=We(o,e,a.startX,a.startY,n.clientX,n.clientY))}(s!==t.dropTargetId()||c!==t.dropTargetHandle())&&(t.setDropTargetId(s),t.setDropTargetHandle(c),M()),gt(r,i)}},Dr=e=>{let t=xr();if(gr(),Ce()){Ne();return}if(L()&&kt()){let t=e.changedTouches[0];t&&It(t.clientX,t.clientY);return}let n=t.pan();if(n){let r=e.changedTouches[0]??null,i=r!==null&&ur(n.downX,n.downY,r.clientX,r.clientY,mr);if(t.setPan(null),document.body.classList.remove(`panning`),!i&&r){let e=lr(K,{id:fr,time:performance.now()},dr);if(K=e.nextLastTap,e.kind===`double`){let e=x(r.clientX),t=S(r.clientY);xt(e,t,{x:e,y:t})}else (t.selected.size>0||t.selectedEdge())&&(t.selected.clear(),t.selectedEdge()&&(t.setSelectedEdge(null),N()),M())}else K=null;return}let r=t.link();if(r){Or(r,e.changedTouches[0]??null);return}let i=t.drag();if(!i)return;let a=i.active,o=i.longPressFired===!0;for(let e of i.movers)e.el.classList?.remove(`dragging`);let s=i.primaryId;t.setDrag(null),document.body.classList.remove(`dragging`);let c=_r()?.classList.contains(`armed`)??!1;if(yr(!1),o)return;if(a){if(c){wt(),K=null;return}w(),K=null;return}if(!s)return;let l=lr(K,{id:s,time:performance.now()},dr);if(K=l.nextLastTap,l.kind===`double`){let e=t.currentMap().boxes.find(e=>e.id===s);if(e){let n=document.querySelector(`#canvas .box[data-id="${s}"]`);n&&(t.selected.clear(),t.selected.add(s),t.selectedEdge()&&(t.setSelectedEdge(null),N()),M(),O(n,e));return}let n=t.findTextById(s);if(n){let e=document.querySelector(`#canvas .text-item[data-id="${s}"]`);e&&(t.selected.clear(),t.selected.add(s),M(),Be(e,n));return}return}t.selected.clear(),t.selected.add(s),t.selectedEdge()&&(t.setSelectedEdge(null),N()),M()},Or=(e,t)=>{let n=xr();if(e.handleEl.classList.remove(`active`),n.ghostLine.style.display=`none`,!t){n.setLink(null),(n.dropTargetId()||n.dropTargetHandle())&&(n.setDropTargetId(null),n.setDropTargetHandle(null),M()),_t();return}let r=zn(t.clientX,t.clientY);if(r&&r.dataset.id!==e.fromId){let i=r.dataset.id,a=n.currentMap(),o=We(r,a.boxes.find(e=>e.id===i),e.startX,e.startY,t.clientX,t.clientY),s={from:e.fromId,to:i};e.fromHandle&&(s.fromHandle=e.fromHandle),o&&(s.toHandle=o),a.edges=h(a.edges,s),pe(),N()}else{let r=n.mintId(),i=x(t.clientX),a=S(t.clientY),o={id:r,label:`new`,x:i,y:a},s=n.currentMap();s.boxes.push(o),j();let c=n.canvas.querySelector(`.box[data-id="${r}"]`);if(c){o.x=i-c.offsetWidth/2,o.y=a-c.offsetHeight/2,c.style.left=o.x+`px`,c.style.top=o.y+`px`;let t=Ue(o,c,e.startX,e.startY),l={from:e.fromId,to:r};e.fromHandle&&(l.fromHandle=e.fromHandle),t&&(l.toHandle=t),s.edges=h(s.edges,l),N(),n.selected.clear(),n.selected.add(r),M(),O(c,o,{cancelDeletes:!0})}w()}n.setLink(null),(n.dropTargetId()||n.dropTargetHandle())&&(n.setDropTargetId(null),n.setDropTargetHandle(null),M()),_t()},kr=e=>{wr(),K=null},Ar=()=>{document.addEventListener(`touchstart`,Tr,{passive:!1}),document.addEventListener(`touchmove`,Er,{passive:!1}),document.addEventListener(`touchend`,Dr),document.addEventListener(`touchcancel`,kr)},jr=()=>E()?`brush`:L()?`line`:`cursor`,Mr=e=>{De(e===`brush`),Nt(e===`line`)},Nr=`http://www.w3.org/2000/svg`,Pr=(e,t)=>{let n=document.createElementNS(Nr,`svg`);return n.setAttribute(`width`,String(e)),n.setAttribute(`height`,String(e)),n.setAttribute(`viewBox`,`0 0 ${e} ${e}`),n.setAttribute(`fill`,`none`),n.setAttribute(`stroke`,`currentColor`),n.setAttribute(`stroke-width`,`1.6`),n.setAttribute(`stroke-linecap`,`round`),n.setAttribute(`stroke-linejoin`,`round`),t(n),n},Fr=()=>Pr(20,e=>{let t=document.createElementNS(Nr,`path`);t.setAttribute(`d`,`M4 3 L4 16 L8 12 L11 18 L13 17 L10 11 L16 11 Z`),t.setAttribute(`fill`,`currentColor`),t.setAttribute(`stroke`,`currentColor`),e.appendChild(t)}),Ir=()=>Pr(20,e=>{let t=document.createElementNS(Nr,`path`);t.setAttribute(`d`,`M3 17 L5 16 L14 7 L12 5 L3 14 Z`),t.setAttribute(`fill`,`currentColor`),e.appendChild(t);let n=document.createElementNS(Nr,`path`);n.setAttribute(`d`,`M13 6 L16 3 L18 5 L15 8 Z`),n.setAttribute(`fill`,`#a60`),n.setAttribute(`stroke`,`currentColor`),e.appendChild(n)}),Lr=()=>Pr(20,e=>{let t=document.createElementNS(Nr,`line`);t.setAttribute(`x1`,`4`),t.setAttribute(`y1`,`16`),t.setAttribute(`x2`,`16`),t.setAttribute(`y2`,`4`),e.appendChild(t);for(let[t,n]of[[4,16],[16,4]]){let r=document.createElementNS(Nr,`circle`);r.setAttribute(`cx`,String(t)),r.setAttribute(`cy`,String(n)),r.setAttribute(`r`,`2`),r.setAttribute(`fill`,`currentColor`),e.appendChild(r)}}),Rr=()=>{if(document.getElementById(`modeBar`))return;let e=document.createElement(`div`);e.id=`modeBar`;let t=[],n=(n,i,a)=>{let o=document.createElement(`button`);o.type=`button`,o.dataset.mode=n,o.title=i,o.setAttribute(`aria-label`,i),o.appendChild(a),o.addEventListener(`mousedown`,e=>e.stopPropagation()),o.addEventListener(`touchstart`,e=>e.stopPropagation(),{passive:!0});let s=!1,c=e=>{e.stopPropagation(),e.preventDefault(),!s&&(s=!0,setTimeout(()=>{s=!1},0),Mr(n),r())};return o.addEventListener(`pointerup`,c),o.addEventListener(`click`,c),e.appendChild(o),t.push({mode:n,el:o}),o};n(`cursor`,`Cursor`,Fr()),n(`brush`,`Brush`,Ir()),n(`line`,`Line`,Lr());let r=()=>{let e=jr();for(let{mode:n,el:r}of t)r.classList.toggle(`active`,n===e),r.setAttribute(`aria-pressed`,n===e?`true`:`false`)};r(),new MutationObserver(r).observe(document.body,{attributes:!0,attributeFilter:[`class`]}),document.body.appendChild(e)},q={maps:[]},zr=`/`,J={boxes:[],edges:[]},Y=new Set,Br={x:window.innerWidth/2,y:window.innerHeight/2},Vr=null,Hr=null,Ur=null,X=null,Z=null,Wr=null,Gr=null,Kr=null,Q=document.getElementById(`canvas`),qr=document.getElementById(`edge-layer`),Jr=document.getElementById(`line-layer`),Yr=document.getElementById(`stroke-layer`),Xr=document.getElementById(`ghost-line`);function Zr(e){return s(e||`b`,c(J.boxes,J.texts||[],J.lines||[],J.strokes||[]))}var Qr=e=>J.texts.find(t=>t.id===e),$r=e=>J.lines.find(t=>t.id===e),ei=e=>(J.strokes||[]).find(t=>t.id===e),ti=()=>le(J);function $(e){}function ni(){let e=Sn();return j(),M(),w(),e}lt({canvas:Q,lineLayer:Jr,strokeLayer:Yr,edgeLayer:qr,currentMap:()=>J,graph:()=>q,currentPath:()=>zr,selected:Y,selectedEdge:()=>Z,setSelectedEdge:e=>{Z=e},dropTargetId:()=>Wr,dropTargetHandle:()=>Gr,nearTargetId:()=>Kr,attachBoxHandlers:cr,attachTextHandlers:or,attachLineHandlers:sr,isBrushMode:()=>E(),setStatus:$}),ht({canvas:Q,currentMap:()=>J,link:()=>X,nearTargetId:()=>Kr,setNearTargetId:e=>{Kr=e}}),Kt({getGraph:()=>q,getCurrentPath:()=>zr,setCurrentPath:e=>{zr=e},setCurrentMap:e=>{J=e},clearSelected:()=>Y.clear(),clearSelectedEdge:()=>{Z=null},renderAll:()=>j()}),$t(),ir({canvas:Q,lineLayer:Jr,strokeLayer:Yr,ghostLine:Xr,currentMap:()=>J,findTextById:Qr,findLineById:$r,findStrokeById:ei,selected:Y,selectedEdge:()=>Z,setSelectedEdge:e=>{Z=e},setDrag:e=>{Ur=e},setLink:e=>{X=e},cloneSelection:ni,setStatus:$}),bt({canvas:Q,currentMap:()=>J,setCurrentMap:e=>{J=e},graph:()=>q,setGraph:e=>{q=e},currentPath:()=>zr,ensureMap:qt,selected:Y,selectedEdge:()=>Z,clearSelectedEdge:()=>{Z=null},mintId:Zr,setStatus:$}),Ie({canvas:Q,getCurrentMap:()=>J,setCurrentMap:e=>{J=e},getCurrentPath:()=>zr,getGraph:()=>q,setGraph:e=>{q=e},ensureMap:qt,selected:Y,renderAll:()=>j(),setStatus:$}),ln({getGraph:()=>q,setGraph:e=>{q=e},serializeGraph:ne,setCurrentPath:(e,t)=>Yt(e,t),getCurrentPath:()=>zr,readPathFromURL:Jt,setStatus:$,clearSelected:()=>Y.clear(),clearSelectedEdge:()=>{Z=null}}),de({scheduleSave:()=>dn()}),xn({currentMap:()=>J,selected:Y,findTextById:Qr,findLineById:$r,mintId:Zr}),Je({canvas:Q,currentMap:()=>J,selected:Y,renderAll:()=>j()}),it(),zt({selected:Y,currentMap:()=>J,findTextById:Qr,findLineById:$r,mintId:Zr,renderAll:()=>j(),deleteSelection:()=>wt(),setStatus:$,clearSelectedEdge:()=>{Z=null}}),ye({mintId:()=>Zr(`s`),strokeLayer:()=>Yr,currentMap:()=>J,afterCommit:()=>ut(),setStatus:$}),Dt({lineLayer:()=>Jr,setStatus:$}),Rn({canvas:Q,ghostLine:Xr,currentMap:()=>J,mintId:()=>Zr(),selected:Y,lastCursor:Br,drag:()=>Ur,setDrag:e=>{Ur=e},link:()=>X,setLink:e=>{X=e},pan:()=>Hr,setPan:e=>{Hr=e},band:()=>Vr,setBand:e=>{Vr=e},selectedEdge:()=>Z,setSelectedEdge:e=>{Z=e},dropTargetId:()=>Wr,setDropTargetId:e=>{Wr=e},dropTargetHandle:()=>Gr,setDropTargetHandle:e=>{Gr=e},setStatus:$}),Jn(),Sr({canvas:Q,ghostLine:Xr,currentMap:()=>J,findTextById:Qr,mintId:()=>Zr(),selected:Y,drag:()=>Ur,setDrag:e=>{Ur=e},pan:()=>Hr,setPan:e=>{Hr=e},link:()=>X,setLink:e=>{X=e},dropTargetId:()=>Wr,setDropTargetId:e=>{Wr=e},dropTargetHandle:()=>Gr,setDropTargetHandle:e=>{Gr=e},selectedEdge:()=>Z,setSelectedEdge:e=>{Z=e}}),Ar(),wn({canvas:Q,ghostLine:Xr,currentMap:()=>J,findTextById:Qr,selected:Y,selectedEdge:()=>Z,setSelectedEdge:e=>{Z=e},link:()=>X,clearLink:()=>{X=null},setDropTargetId:e=>{Wr=e},setDropTargetHandle:e=>{Gr=e},clearProximity:()=>_t(),lastCursor:Br,setStatus:$}),Fn(),oe();var ri=window.matchMedia(`(pointer: coarse)`),ii=()=>document.body.classList.toggle(`touch-input`,ri.matches);ri.addEventListener(`change`,ii),ii(),Rr(),document.getElementById(`upBtn`).addEventListener(`click`,Zt),document.getElementById(`downloadBtn`).addEventListener(`click`,_n),document.getElementById(`reshareBtn`).addEventListener(`click`,vn),window.addEventListener(`mousedown`,e=>{e.button===1&&e.preventDefault()},!0),window.addEventListener(`resize`,()=>ti()),un(),fetch(`/version`).then(e=>e.ok?e.text():``).then(e=>{let t=e.trim();t&&(document.getElementById(`version`).textContent=`flowgo `+t)}).catch(()=>{});
diff --git a/src/editor/align.ts b/src/editor/align.ts index 3134c32..6e4bfe0 100644 --- a/src/editor/align.ts +++ b/src/editor/align.ts @@ -13,6 +13,8 @@ // updateSelectionToolbar() is fired from applyClasses() in render.ts // so selection changes immediately reposition / hide the toolbar. +import { mutatedCurrentMap } from "./mutations.ts"; + interface BoxLike { id: string; x: number; y: number; } interface TextLike { id: string; x: number; y: number; } @@ -25,7 +27,6 @@ interface AlignBindings { readonly canvas: HTMLElement; readonly currentMap: () => CurrentMap; readonly selected: Set; - readonly scheduleSave: () => void; readonly renderAll: () => void; } @@ -143,7 +144,7 @@ export const applyAlign = (axis: "horizontal" | "vertical"): void => { const items = collectAlignable(); if (!alignItems(items, axis)) return; w.renderAll(); - w.scheduleSave(); + mutatedCurrentMap(); }; // Build SVG nodes through createElementNS so every element lands in diff --git a/src/editor/attach.ts b/src/editor/attach.ts index f26ee80..45db1dd 100644 --- a/src/editor/attach.ts +++ b/src/editor/attach.ts @@ -12,7 +12,7 @@ import { renderEdges, renderLines, } from "./render.ts"; -import { scheduleSave } from "./persistence.ts"; +import { mutatedLine } from "./mutations.ts"; import { makeBoxMover, makeLineEndpointMover, @@ -283,7 +283,7 @@ export const attachLineHandlers = ( w.setSelectedEdge(null); renderEdges(); } - scheduleSave(); + mutatedLine(); // Full re-render so the new mid handle is wired up. renderLines(); applyClasses(); @@ -352,7 +352,7 @@ export const attachLineHandlers = ( l.mids.splice(idx, 1); if (l.mids.length === 0) delete l.mids; } - scheduleSave(); + mutatedLine(); renderLines(); applyClasses(); }); diff --git a/src/editor/brush.ts b/src/editor/brush.ts index 5f28d9f..a4fa379 100644 --- a/src/editor/brush.ts +++ b/src/editor/brush.ts @@ -4,6 +4,7 @@ // dispatches mousedown/move/up to the start/extend/finish trio. import { simplifyStroke } from "../index.ts"; +import { mutatedStroke } from "./mutations.ts"; import { toDataX, toDataY } from "./viewport.ts"; interface ActiveStroke { @@ -17,7 +18,6 @@ interface BrushBindings { readonly mintId: () => string; readonly strokeLayer: () => SVGGElement; readonly currentMap: () => { strokes?: Array }; - readonly scheduleSave: () => void; readonly afterCommit: () => void; // call renderStrokes readonly setStatus: (s: string) => void; } @@ -157,7 +157,7 @@ export const finishStroke = (): void => { }; if (active.palette >= 2) stroke.palette = active.palette; (m.strokes ??= []).push(stroke); - must().scheduleSave(); + mutatedStroke(); } else { const g = active.polyEl.parentNode; if (g && g.parentNode) g.parentNode.removeChild(g); diff --git a/src/editor/clipboard.test.ts b/src/editor/clipboard.test.ts index 82bd8d5..242ac5d 100644 --- a/src/editor/clipboard.test.ts +++ b/src/editor/clipboard.test.ts @@ -5,6 +5,7 @@ import { pasteSelection, wireClipboard, } from "./clipboard.ts"; +import { wireMutations } from "./mutations.ts"; interface Box { id: string; @@ -54,6 +55,7 @@ const makeState = (): State => ({ const wire = (s: State): void => { let n = 0; + wireMutations({ scheduleSave: () => {} }); wireClipboard({ selected: s.selected, currentMap: () => ({ @@ -65,7 +67,6 @@ const wire = (s: State): void => { findTextById: (id) => s.texts.find((t) => t.id === id), findLineById: (id) => s.lines.find((l) => l.id === id), mintId: (p) => `${p}_new${++n}`, - scheduleSave: () => {}, renderAll: () => {}, deleteSelection: () => { s.boxes = s.boxes.filter((b) => !s.selected.has(b.id)); diff --git a/src/editor/clipboard.ts b/src/editor/clipboard.ts index d606987..a836bdb 100644 --- a/src/editor/clipboard.ts +++ b/src/editor/clipboard.ts @@ -5,6 +5,8 @@ // copied box set, mirroring the existing semantics. Each paste shifts // by 20px so repeated paste presses cascade rather than stack. +import { mutatedCurrentMap } from "./mutations.ts"; + interface BoxLike { id: string; label: string; @@ -62,7 +64,6 @@ interface ClipboardBindings { readonly findTextById: (id: string) => TextLike | undefined; readonly findLineById: (id: string) => LineLike | undefined; readonly mintId: (prefix: string) => string; - readonly scheduleSave: () => void; readonly renderAll: () => void; readonly deleteSelection: () => void; readonly setStatus: (s: string) => void; @@ -153,7 +154,7 @@ export const cutSelection = (): void => { export const pasteSelection = (): void => { const { - selected, currentMap, mintId, scheduleSave, renderAll, + selected, currentMap, mintId, renderAll, setStatus, clearSelectedEdge, } = must(); if (!buffer) { @@ -207,7 +208,7 @@ export const pasteSelection = (): void => { if (ed.toHandle) newEdge.toHandle = ed.toHandle; map.edges.push(newEdge); } - scheduleSave(); + mutatedCurrentMap(); renderAll(); setStatus("pasted " + selected.size + " items"); }; diff --git a/src/editor/edit.ts b/src/editor/edit.ts index d033342..33d684f 100644 --- a/src/editor/edit.ts +++ b/src/editor/edit.ts @@ -10,6 +10,7 @@ // of that span. import { MAX_LABEL_LEN, normalizeLabel } from "../graph/label.ts"; +import { mutatedBox, mutatedDoc, mutatedText } from "./mutations.ts"; interface BoxLike { id: string; @@ -37,7 +38,6 @@ interface EditBindings { readonly setGraph: (g: { maps: { path: string }[] }) => void; readonly ensureMap: (path: string) => ReturnType; readonly selected: Set; - readonly scheduleSave: () => void; readonly renderAll: () => void; readonly setStatus: (s: string) => void; } @@ -88,7 +88,7 @@ export const startTextEdit = (el: HTMLElement, t: TextLike): void => { const newLabel = readEditableText(el); if (commit && newLabel && newLabel !== t.label) { t.label = newLabel; - must().scheduleSave(); + mutatedText(); } el.textContent = t.label; }; @@ -177,14 +177,14 @@ export const startEdit = ( w.setGraph(g); w.setCurrentMap(w.ensureMap(cur)); w.selected.delete(b.id); - w.scheduleSave(); + mutatedDoc(); w.renderAll(); w.setStatus("cancelled"); return; } if (commit && newLabel && newLabel !== b.label) { b.label = newLabel; - must().scheduleSave(); + mutatedBox(); } // Rebuild the affected box from state. Trying to surgically // pluck out only the stray nodes the contenteditable inserted diff --git a/src/editor/factories.ts b/src/editor/factories.ts index 720abf7..9424be4 100644 --- a/src/editor/factories.ts +++ b/src/editor/factories.ts @@ -15,6 +15,12 @@ // the removed boxes). import { startEdit, startTextEdit } from "./edit.ts"; +import { + mutatedBox, + mutatedDoc, + mutatedLine, + mutatedText, +} from "./mutations.ts"; import { applyClasses, renderAll, renderEdges } from "./render.ts"; interface BoxLike { @@ -60,7 +66,6 @@ interface FactoryBindings { readonly selectedEdge: () => unknown; readonly clearSelectedEdge: () => void; readonly mintId: (prefix?: string) => string; - readonly scheduleSave: () => void; readonly setStatus: (s: string) => void; } @@ -91,7 +96,7 @@ export const createBoxAt = ( el.style.left = b.x + "px"; el.style.top = b.y + "px"; } - w.scheduleSave(); + mutatedBox(); if (el) { w.selected.clear(); w.selected.add(id); @@ -125,7 +130,7 @@ export const createTextAt = (cx: number, cy: number): void => { applyClasses(); startTextEdit(el, t); } - w.scheduleSave(); + mutatedText(); }; export const createLineSegment = ( @@ -145,7 +150,7 @@ export const createLineSegment = ( renderEdges(); } renderAll(); - w.scheduleSave(); + mutatedLine(); }; export const deleteSelection = (): void => { @@ -175,6 +180,6 @@ export const deleteSelection = (): void => { w.setGraph(g); w.setCurrentMap(w.ensureMap(cur)); sel.clear(); - w.scheduleSave(); + mutatedDoc(); renderAll(); }; diff --git a/src/editor/keys.ts b/src/editor/keys.ts index 5957d8b..b3e0348 100644 --- a/src/editor/keys.ts +++ b/src/editor/keys.ts @@ -10,6 +10,11 @@ import { isHelpOpen, setHelpOpen } from "./help.ts"; import { isEditing, startEdit, startTextEdit } from "./edit.ts"; +import { + mutatedBox, + mutatedCurrentMap, + mutatedEdge, +} from "./mutations.ts"; import { undo, redo } from "./persistence.ts"; import { isBrushMode, @@ -97,7 +102,6 @@ interface KeysBindings { readonly setDropTargetHandle: (h: string | null) => void; readonly clearProximity: () => void; readonly lastCursor: { x: number; y: number }; - readonly scheduleSave: () => void; readonly setStatus: (s: string) => void; } @@ -215,7 +219,7 @@ const toggleAnchor = (): void => { if (b.anchor) delete b.anchor; } if (turningOn) target.anchor = true; - w.scheduleSave(); + mutatedBox(); renderAll(); w.setStatus(turningOn ? "anchored " + id : "anchor cleared"); }; @@ -383,7 +387,7 @@ export const attachKeyboardListener = (): void => { if (!hasAnySelection()) return; if (applyPalette(palette)) { e.preventDefault(); - w.scheduleSave(); + mutatedCurrentMap(); renderAll(); } return; @@ -397,7 +401,7 @@ export const attachKeyboardListener = (): void => { const styleChanged = applyLineStyle(n); if (fontChanged || styleChanged) { e.preventDefault(); - w.scheduleSave(); + mutatedCurrentMap(); renderAll(); } return; @@ -417,7 +421,7 @@ export const attachKeyboardListener = (): void => { const dir = e.key === "_" || e.key === "-" ? -1 : 1; if (stepFont(dir as 1 | -1)) { e.preventDefault(); - w.scheduleSave(); + mutatedCurrentMap(); renderAll(); } return; @@ -429,7 +433,7 @@ export const attachKeyboardListener = (): void => { const dir = e.key === "-" ? -1 : 1; if (stepPalette(dir as 1 | -1)) { e.preventDefault(); - w.scheduleSave(); + mutatedCurrentMap(); renderAll(); } return; @@ -503,7 +507,7 @@ export const attachKeyboardListener = (): void => { const idx = map.edges.indexOf(sel); if (idx >= 0) map.edges.splice(idx, 1); w.setSelectedEdge(null); - w.scheduleSave(); + mutatedEdge(); renderEdges(); w.setStatus("edge removed"); return; diff --git a/src/editor/main.ts b/src/editor/main.ts index b1454a7..110d0f6 100644 --- a/src/editor/main.ts +++ b/src/editor/main.ts @@ -41,6 +41,7 @@ import { scheduleSave, wirePersistence, } from "./persistence.ts"; +import { mutatedCurrentMap, wireMutations } from "./mutations.ts"; import { cloneSelection as cloneSelectionPure, wireClone, @@ -127,7 +128,7 @@ function cloneSelection() { const idMap = cloneSelectionPure(); renderAll(); applyClasses(); - scheduleSave(); + mutatedCurrentMap(); return idMap; } @@ -205,7 +206,6 @@ wireFactories({ selectedEdge: () => selectedEdge, clearSelectedEdge: () => { selectedEdge = null; }, mintId: uid, - scheduleSave: () => scheduleSave(), setStatus, }); @@ -218,7 +218,6 @@ wireEdit({ setGraph: (g) => { graph = g; }, ensureMap, selected, - scheduleSave: () => scheduleSave(), renderAll: () => renderAll(), setStatus, }); @@ -235,6 +234,11 @@ wirePersistence({ clearSelectedEdge: () => { selectedEdge = null; }, }); +// Every mutation funnels through mutations.ts. The default wiring +// just calls scheduleSave; downstream consumers can swap it without +// touching the 26 call sites. +wireMutations({ scheduleSave: () => scheduleSave() }); + wireClone({ currentMap: () => state, selected, @@ -247,7 +251,6 @@ wireAlign({ canvas, currentMap: () => state, selected, - scheduleSave: () => scheduleSave(), renderAll: () => renderAll(), }); attachAlignToolbar(); @@ -258,7 +261,6 @@ wireClipboard({ findTextById, findLineById, mintId: uid, - scheduleSave: () => scheduleSave(), renderAll: () => renderAll(), deleteSelection: () => deleteSelection(), setStatus, @@ -269,7 +271,6 @@ wireBrush({ mintId: () => uid("s"), strokeLayer: () => strokeLayer, currentMap: () => state, - scheduleSave: () => scheduleSave(), afterCommit: () => renderStrokes(), setStatus, }); @@ -300,7 +301,6 @@ wireMouse({ setDropTargetId: (id) => { dropTargetId = id; }, dropTargetHandle: () => dropTargetHandle, setDropTargetHandle: (h) => { dropTargetHandle = h; }, - scheduleSave: () => scheduleSave(), setStatus, }); attachMouseListeners(); @@ -324,7 +324,6 @@ wireTouch({ setDropTargetHandle: (h) => { dropTargetHandle = h; }, selectedEdge: () => selectedEdge, setSelectedEdge: (e) => { selectedEdge = e; }, - scheduleSave: () => scheduleSave(), }); attachTouchListeners(); @@ -342,7 +341,6 @@ wireKeys({ setDropTargetHandle: (h) => { dropTargetHandle = h; }, clearProximity: () => clearProximity(), lastCursor, - scheduleSave: () => scheduleSave(), setStatus, }); attachKeyboardListener(); diff --git a/src/editor/mouse.ts b/src/editor/mouse.ts index a002ea4..c812d6b 100644 --- a/src/editor/mouse.ts +++ b/src/editor/mouse.ts @@ -22,6 +22,11 @@ import { startEdit } from "./edit.ts"; import { nearestHandle, pickTargetHandle } from "./anchors.ts"; import { addOrReplaceEdge as addOrReplaceEdgePure } from "../graph/edge.ts"; import { createBoxAt } from "./factories.ts"; +import { + mutatedCurrentMap, + mutatedEdge, + mutatedLine, +} from "./mutations.ts"; interface BoxLike { id: string; @@ -107,7 +112,6 @@ interface MouseBindings { readonly setDropTargetId: (id: string | null) => void; readonly dropTargetHandle: () => string | null; readonly setDropTargetHandle: (h: string | null) => void; - readonly scheduleSave: () => void; readonly setStatus: (s: string) => void; } @@ -243,7 +247,7 @@ const onMouseUp = (e: MouseEvent): void => { const primaryId = drag.primaryId; w.setDrag(null); if (wasActive) { - w.scheduleSave(); + mutatedCurrentMap(); } else { // Single-click without movement: collapse selection to just this item. w.selected.clear(); @@ -331,7 +335,7 @@ const onMouseUp = (e: MouseEvent): void => { if (link.fromHandle) newEdge.fromHandle = link.fromHandle; if (toCode) newEdge.toHandle = toCode; map.edges = addOrReplaceEdgePure(map.edges, newEdge); - w.scheduleSave(); + mutatedEdge(); renderEdges(); } else { // Dropped in empty space: spawn a new box at the cursor and @@ -360,7 +364,7 @@ const onMouseUp = (e: MouseEvent): void => { applyClasses(); startEdit(newEl, newBox, { cancelDeletes: true }); } - w.scheduleSave(); + mutatedCurrentMap(); } w.setLink(null); if (w.dropTargetId() || w.dropTargetHandle()) { @@ -490,7 +494,7 @@ const onBgDblClick = (e: MouseEvent): void => { const hit = tryInsertMidNearPoint(w.currentMap(), cx, cy); if (hit) { cancelPendingLine(); - w.scheduleSave(); + mutatedLine(); renderAll(); } return; diff --git a/src/editor/mutations.ts b/src/editor/mutations.ts new file mode 100644 index 0000000..70e35e8 --- /dev/null +++ b/src/editor/mutations.ts @@ -0,0 +1,41 @@ +// Chokepoint for "the live graph just mutated; persist." Every +// mutation seam in the editor calls one of the typed mutator +// functions below instead of scheduleSave() directly. +// +// Today every mutator funnels into the wired scheduleSave — +// behaviour is identical to calling scheduleSave() at the call site. +// The typed surface exists so that downstream wiring can hook on +// the right kind of change without a 30-site audit later. + +interface MutationBindings { + readonly scheduleSave: () => void; +} + +let bindings: MutationBindings | null = null; + +export const wireMutations = (b: MutationBindings): void => { + bindings = b; +}; + +const fire = (): void => { + if (!bindings) throw new Error("mutations: wireMutations() not called"); + bindings.scheduleSave(); +}; + +// One function per kind on the current map. The function shapes +// reserve room for downstream wiring that wants to scope a diff to +// a specific entity; today they all just fire scheduleSave. + +export const mutatedBox = (): void => fire(); +export const mutatedEdge = (): void => fire(); +export const mutatedText = (): void => fire(); +export const mutatedLine = (): void => fire(); +export const mutatedStroke = (): void => fire(); + +// The current map changed in a way that spans multiple kinds or +// touches the whole map (paste, align, multi-select palette change). +export const mutatedCurrentMap = (): void => fire(); + +// The document structure changed (maps added/removed via box +// deletion, or anything that affects more than one map at once). +export const mutatedDoc = (): void => fire(); diff --git a/src/editor/touch.ts b/src/editor/touch.ts index 1fbc2a1..2f1e546 100644 --- a/src/editor/touch.ts +++ b/src/editor/touch.ts @@ -53,6 +53,7 @@ import { handleAnchor, nearestHandle, pickTargetHandle } from "./anchors.ts"; import { addOrReplaceEdge as addOrReplaceEdgePure } from "../graph/edge.ts"; import { findBoxAt } from "./mouse.ts"; import { createBoxAt, deleteSelection } from "./factories.ts"; +import { mutatedCurrentMap, mutatedEdge } from "./mutations.ts"; import { classifyTap, movedBeyond, type TapRecord } from "./gestures.ts"; import { makeLineEndpointMover, type Mover } from "./movers.ts"; @@ -125,7 +126,6 @@ interface TouchBindings { readonly setDropTargetHandle: (h: string | null) => void; readonly selectedEdge: () => EdgeLike | null; readonly setSelectedEdge: (e: EdgeLike | null) => void; - readonly scheduleSave: () => void; } // Double-tap window — taps on the same target within this many ms @@ -762,7 +762,7 @@ const onTouchEnd = (e: TouchEvent): void => { lastTap = null; return; } - w.scheduleSave(); + mutatedCurrentMap(); lastTap = null; return; } @@ -856,7 +856,7 @@ const finalizeLink = (link: LinkState, t: Touch | null): void => { if (link.fromHandle) newEdge.fromHandle = link.fromHandle; if (toCode) newEdge.toHandle = toCode; map.edges = addOrReplaceEdgePure(map.edges, newEdge); - w.scheduleSave(); + mutatedEdge(); renderEdges(); } else { const newId = w.mintId(); @@ -883,7 +883,7 @@ const finalizeLink = (link: LinkState, t: Touch | null): void => { applyClasses(); startEdit(newEl, newBox, { cancelDeletes: true }); } - w.scheduleSave(); + mutatedCurrentMap(); } w.setLink(null); if (w.dropTargetId() || w.dropTargetHandle()) {