diff --git a/.claude/project/followups/INDEX.md b/.claude/project/followups/INDEX.md index d56520f..ea9544b 100644 --- a/.claude/project/followups/INDEX.md +++ b/.claude/project/followups/INDEX.md @@ -2,26 +2,27 @@ Auto-generated by `atomic followups render`. Do not edit. -Open: 10 β€’ Stale: 6 β€’ Last rendered: 2026-09-08 +Open: 11 β€’ Stale: 8 β€’ Last rendered: 2026-10-27 ## πŸ“‹ plans (1) - [usage-index-back-reference](usage-index-back-reference.md) β€” Usage index: derived back-reference of store/entity touchpoints β†’ docs/spec/process-flows.md -## 🟑 risks (5) +## 🟑 risks (6) -- [dfd-polish-round3-cytoscape-typing](dfd-polish-round3-cytoscape-typing.md) β€” Type the cytoscape cy binding (retire the TS2339 Core defect) (91d, **stale**) -- [dfd-store-clusters-f-15](dfd-store-clusters-f-15.md) β€” test-graph-search.ts intermittent stall in page.evaluate (0d) -- [dict-side-nav-scrollspy-timing](dict-side-nav-scrollspy-timing.md) β€” Side-nav scrollspy test: scrollIntoView β†’ waitForFunction implicit layout assumption (102d) -- [render-perf-elk-web-worker](render-perf-elk-web-worker.md) β€” ELK web worker β€” needs separate worker build entrypoint to activate under Bun (90d, **stale**) -- [unified-app-polish-flow-modal-light-mode](unified-app-polish-flow-modal-light-mode.md) β€” Flow-view dialogs may render dark in light mode (92d, **stale**) +- [dfd-polish-round3-cytoscape-typing](dfd-polish-round3-cytoscape-typing.md) β€” Type the cytoscape cy binding (retire the TS2339 Core defect) (140d, **stale**) +- [dfd-store-clusters-f-15](dfd-store-clusters-f-15.md) β€” test-graph-search.ts intermittent stall in page.evaluate (49d) +- [dict-side-nav-scrollspy-timing](dict-side-nav-scrollspy-timing.md) β€” Side-nav scrollspy test: scrollIntoView β†’ waitForFunction implicit layout assumption (151d, **stale**) +- [render-perf-elk-web-worker](render-perf-elk-web-worker.md) β€” ELK web worker β€” needs separate worker build entrypoint to activate under Bun (139d, **stale**) +- [unified-app-polish-flow-modal-light-mode](unified-app-polish-flow-modal-light-mode.md) β€” Flow-view dialogs may render dark in light mode (141d, **stale**) +- [viewer-qol-f-1](viewer-qol-f-1.md) β€” Bundle may ship React dev build / StrictMode double effects (0d) ## πŸ”΅ nits (4) -- [dfd-cold-deeplink-fallback](dfd-cold-deeplink-fallback.md) β€” DFD cold deep-link (#view=flow&dfd=) falls back to context diagram on fresh load (85d, **stale**) -- [parse-ts-preexisting-tsc-errors](parse-ts-preexisting-tsc-errors.md) β€” Fix 6 pre-existing tsc errors in src/parse.ts (101d) -- [render-perf-flow-index](render-perf-flow-index.md) β€” Index flow validate/parse O(n^2) lookups (findings must stay identical) (90d, **stale**) -- [unified-app-polish-stack-entities-dfd](unified-app-polish-stack-entities-dfd.md) β€” Stack entities in the DFD to reclaim visual real-estate (92d, **stale**) +- [dfd-cold-deeplink-fallback](dfd-cold-deeplink-fallback.md) β€” DFD cold deep-link (#view=flow&dfd=) falls back to context diagram on fresh load (134d, **stale**) +- [parse-ts-preexisting-tsc-errors](parse-ts-preexisting-tsc-errors.md) β€” Fix 6 pre-existing tsc errors in src/parse.ts (150d, **stale**) +- [render-perf-flow-index](render-perf-flow-index.md) β€” Index flow validate/parse O(n^2) lookups (findings must stay identical) (139d, **stale**) +- [unified-app-polish-stack-entities-dfd](unified-app-polish-stack-entities-dfd.md) β€” Stack entities in the DFD to reclaim visual real-estate (141d, **stale**) ## ❓ questions (0) diff --git a/.claude/project/followups/viewer-qol-f-1.md b/.claude/project/followups/viewer-qol-f-1.md new file mode 100644 index 0000000..a5d1886 --- /dev/null +++ b/.claude/project/followups/viewer-qol-f-1.md @@ -0,0 +1,14 @@ +--- +id: viewer-qol-f-1 +title: Bundle may ship React dev build / StrictMode double effects +created: "2026-10-27" +origin: | + docs/spec/viewer-qol.md, iter 10 implementer probe +kind: finding +severity: risk +review_by: "2026-12-26" +status: open +file: src/app/main.tsx:1 +--- + +dist/static/index.js may ship React dev-only code: effects disconnect and reconnect about 20 ms after load (StrictMode double-run suspected) in both dev serve and the static export. Observed by the CP5 implementer while probing scroll restore; not proven. Check whether build:bundle's --define of process.env.NODE_ENV reaches the HTML-import build and whether in main.tsx should be dev-only. diff --git a/.claude/rules/wiki/frontend.md b/.claude/rules/wiki/frontend.md index 57cf5c2..38b5d78 100644 --- a/.claude/rules/wiki/frontend.md +++ b/.claude/rules/wiki/frontend.md @@ -4,7 +4,7 @@ paths: - "src/app/**" --- -Domain: frontend. React 19 unified SPA (Graph/Dictionary/Flows views); shell (`App.tsx`) owns state + composition; change-review overlay (`ChangesPanel`, `ChangeBadge`/`ChangeDot`, `logic/change-marks.ts`) marks graph, Dictionary, flow nodes, flow index and level-menu rows, with removed items in warning red; modals stack above the global findings banner; `flowview=`/`collapse=` hash params; views own cy/SVG lifecycle +Domain: frontend. React 19 unified SPA (Graph/Dictionary/Flows views); shell (`App.tsx`) owns state + composition; change-review overlay (`ChangesPanel`, `ChangeBadge`/`ChangeDot`, `logic/change-marks.ts`) marks graph, Dictionary, flow nodes, flow index and level-menu rows, with removed items in warning red; modals stack above the global findings banner; `flowview=`/`collapse=` hash params; views own cy/SVG lifecycle; FAB Settings modal + Export HTML with saved view state; client Mermaid rendering Map: - docs/wiki/frontend.md diff --git a/.claude/rules/wiki/generators.md b/.claude/rules/wiki/generators.md index 878f04c..524cd90 100644 --- a/.claude/rules/wiki/generators.md +++ b/.claude/rules/wiki/generators.md @@ -4,7 +4,7 @@ paths: - "src/generators/**" --- -Domain: generators. Unified static HTML export via `generateApp` (single file β€” graph + dict + flows), now embedding `window.__FLOW_CLUSTERS__`; sole static generator +Domain: generators. Unified static HTML export via `generateApp` (single file β€” graph + dict + flows), embedding `window.__FLOW_CLUSTERS__` and the Mermaid runtime; `exportModel` (`export-model.ts`) is the shared export path for CLI and server Map: - docs/wiki/generators.md diff --git a/.claude/rules/wiki/parser.md b/.claude/rules/wiki/parser.md index 78b7cd7..f2829b8 100644 --- a/.claude/rules/wiki/parser.md +++ b/.claude/rules/wiki/parser.md @@ -6,6 +6,7 @@ paths: - "src/model/model-index.ts" - "src/model/change-review.ts" - "src/model/change-store.ts" + - "src/model/markdown-mermaid.ts" --- Domain: parser. `ignatius.yml` config loading β†’ ParseResult: {model, globalErrors}; nodes, edges, cardinality + classification derivation; wiki-link inline rule + two-pass body rendering (`linkStateNeutral`); `buildModelIndex` lookup maps; change review: `change-review.ts` diffs model + flows against the approved baseline kept by `change-store.ts` and cross-checks agent change notes diff --git a/.claude/rules/wiki/server.md b/.claude/rules/wiki/server.md index d496e45..38f64d7 100644 --- a/.claude/rules/wiki/server.md +++ b/.claude/rules/wiki/server.md @@ -4,7 +4,7 @@ paths: - "src/server/**" --- -Domain: server. Bun.serve with `/api/model` (carries the change-review `diff`), `POST /api/approve`, `/api/flow` (returns `clusters`), `/events` SSE live-reload driven by fs.watch plus a `baseline.json` stat poll; `/dict`, `/flow`, `/flow-dict` redirect to SPA hash routes +Domain: server. Bun.serve with `/api/model` (carries the change-review `diff`), `POST /api/approve`, `/api/flow` (returns `clusters`), `POST /api/export` (HTML export with client view state), `/static/mermaid.js`, `/events` SSE live-reload driven by fs.watch plus a `baseline.json` stat poll; `/dict`, `/flow`, `/flow-dict` redirect to SPA hash routes Map: - docs/wiki/server.md diff --git a/.claude/rules/wiki/theme.md b/.claude/rules/wiki/theme.md index 47ab832..d767529 100644 --- a/.claude/rules/wiki/theme.md +++ b/.claude/rules/wiki/theme.md @@ -4,7 +4,7 @@ paths: - "src/theme/**" --- -Domain: theme. ThemeConfig + Branding types, default palettes, flow-kind colors + per-kind store-cap symbols (`FLOW_STORE_KIND_SYMBOLS`), dark/light merging +Domain: theme. ThemeConfig + Branding types, default palettes, flow-kind colors + per-kind store-cap symbols (`FLOW_STORE_KIND_SYMBOLS`), dark/light merging; `mermaid-theme.ts` maps `theme.mermaid` to Mermaid theme variables Map: - docs/wiki/theme.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 36c63d7..2d07c2f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,8 @@ on: jobs: test: - runs-on: ubuntu-latest + # Playwright 1.60 cannot install Chromium on ubuntu-latest's 26.04 image. + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 diff --git a/README.md b/README.md index 13edb48..0614486 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ Edit any file in the folder and the graph reloads in the browser without a refre | [Bidirectional predicates](docs/guides/predicates.md) | Forward and reverse edge labels with hover-swap | | [Process flows](docs/guides/flows.md) | Data flow diagrams: processes, externals, stores, sub-DFDs | | [Validation and findings](docs/guides/validation.md) | The linter, severity tiers, and where findings surface | -| [Themes and branding](docs/guides/themes-and-branding.md) | Color palettes, the light/dark toggle, logo and copyright | +| [Themes and branding](docs/guides/themes-and-branding.md) | Color palettes, light and dark themes, logo and copyright | | [The modeling skill](docs/guides/modeling-skill.md) | `/ignatius-modeling` Q&A authoring in Claude Code | | [Building from source](docs/guides/building-from-source.md) | Build stages, project layout, and tests | diff --git a/bun.lock b/bun.lock index 95c26eb..98ff462 100644 --- a/bun.lock +++ b/bun.lock @@ -15,6 +15,7 @@ "cytoscape-navigator": "^2.0.2", "elkjs": "^0.11.0", "markdown-it": "^14.1.0", + "mermaid": "~12.2.0", "react": "^19", "react-dom": "^19", "shiki": "^4.4.3", @@ -31,10 +32,30 @@ }, }, "packages": { + "@antfu/install-pkg": ["@antfu/install-pkg@2.1.0", "", { "dependencies": { "package-manager-detector": "^1.8.0", "tinyexec": "^1.3.1" } }, "sha512-sdg9NxU3zR4Mnawfbc/x6GB5Wf17WYud5qOuEuxXjaKpYpMkISSJEjItGebXJ2bQ4DIcly4NYH23mtkGJjvKUw=="], + + "@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.2", "", {}, "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA=="], + + "@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@13.2.0", "", { "dependencies": { "@chevrotain/gast": "13.2.0", "@chevrotain/types": "13.2.0" } }, "sha512-bA9dvWlhAUcHkNwU+8w9yiUxJPlj/Lm3VqS7MD+NbIR/7LmpNLuK/AWNFdCo/T3SK5ND0/UHf3IJvl/Jn/i6yg=="], + + "@chevrotain/gast": ["@chevrotain/gast@13.2.0", "", { "dependencies": { "@chevrotain/types": "13.2.0" } }, "sha512-bCARdAsG+eEbOveSXcOAC7pVxA04I1ionzeMOwIYf5NX7SaS7zgn4Dgug2BSqoL+iyKeMNSorQjsFSHJHsCNwg=="], + + "@chevrotain/regexp-to-ast": ["@chevrotain/regexp-to-ast@13.2.0", "", {}, "sha512-kxpUkLtiaK09ph+dib+Yvm+3sWSnuwlo25gqfKRgTPNmF6H7uW4gVkMjaRdtnN783S9sM6vUNWh9CsxYX4QwKw=="], + + "@chevrotain/types": ["@chevrotain/types@13.2.0", "", {}, "sha512-7qrYHRlKfWf2AvBfJi5eVOrKAkbxvMFsy/iIHTGIiApNcEecCaVIdR+mIQhyCBsTDJxBz4JWXHMo6krbhR9hVg=="], + + "@chevrotain/utils": ["@chevrotain/utils@13.2.0", "", {}, "sha512-SzKYY68rseOrb9KILVzPsl6SjkQtdWw01SRGB5aCVI5kFcHltQsOYV3JB0Ffst72DgkvHCyDQ0NLwxtmCCf97Q=="], + "@clack/core": ["@clack/core@1.4.0", "", { "dependencies": { "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-7Wctjq6f7c1CPz8sPpkwUnz8yRgVANkpNupb81q432FjcJg4l+Sw7XANdNSdWfAKq0IHI0JTcUeK5dxs/HrGPw=="], "@clack/prompts": ["@clack/prompts@1.5.0", "", { "dependencies": { "@clack/core": "1.4.0", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-wKh+wTjmrUoUdkZg8KpJO5X+p9PWV+KE9mePseq9UYWkukgTKsGS47RRL2HstwVcvDQH+PenrPJWII8+MfiiyA=="], + "@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="], + + "@iconify/utils": ["@iconify/utils@3.1.7", "", { "dependencies": { "@antfu/install-pkg": "^2.0.1", "@iconify/types": "^2.0.0", "import-meta-resolve": "^4.2.0" } }, "sha512-JZHlwdID+dy+lTgbYC8NEC4zeugqeYsc6jewvzb4c58kHauJn+X7rNwQjxz5p2qSjqaEeQoLkCIQ9v/H4PK0/w=="], + + "@mermaid-js/parser": ["@mermaid-js/parser@2.0.1", "", { "dependencies": { "@chevrotain/types": "~13.2.0" } }, "sha512-j37dj/h6p6pots1L7pLPPNuABEEu9WP4OYg9pW1g+nYG331j0+3canE9OagtYdytYvdSAMG+2fKU5cxep6LgjA=="], + "@shikijs/core": ["@shikijs/core@4.4.3", "", { "dependencies": { "@shikijs/primitive": "4.4.3", "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5", "hast-util-to-html": "^9.0.5" } }, "sha512-QCR4q2ZO/ILJEuwiBMel4wdcTDb1JGwfjKTxPDF6x8ixOaluPrVqIn06C99AcRPhmYlBR56d/Fb+GN58GzExpg=="], "@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.4.3", "", { "dependencies": { "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-FbOjFJp9VLdo1Wevs10BBtVxiTWwNLqZh5Gkhjgda/ioL15YOgeSl9n+6XMa3qRlPQzfhFNe641SrynFHYG0nQ=="], @@ -55,6 +76,70 @@ "@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="], + "@types/d3": ["@types/d3@7.4.3", "", { "dependencies": { "@types/d3-array": "*", "@types/d3-axis": "*", "@types/d3-brush": "*", "@types/d3-chord": "*", "@types/d3-color": "*", "@types/d3-contour": "*", "@types/d3-delaunay": "*", "@types/d3-dispatch": "*", "@types/d3-drag": "*", "@types/d3-dsv": "*", "@types/d3-ease": "*", "@types/d3-fetch": "*", "@types/d3-force": "*", "@types/d3-format": "*", "@types/d3-geo": "*", "@types/d3-hierarchy": "*", "@types/d3-interpolate": "*", "@types/d3-path": "*", "@types/d3-polygon": "*", "@types/d3-quadtree": "*", "@types/d3-random": "*", "@types/d3-scale": "*", "@types/d3-scale-chromatic": "*", "@types/d3-selection": "*", "@types/d3-shape": "*", "@types/d3-time": "*", "@types/d3-time-format": "*", "@types/d3-timer": "*", "@types/d3-transition": "*", "@types/d3-zoom": "*" } }, "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww=="], + + "@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="], + + "@types/d3-axis": ["@types/d3-axis@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw=="], + + "@types/d3-brush": ["@types/d3-brush@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A=="], + + "@types/d3-chord": ["@types/d3-chord@3.0.6", "", {}, "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg=="], + + "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], + + "@types/d3-contour": ["@types/d3-contour@3.0.6", "", { "dependencies": { "@types/d3-array": "*", "@types/geojson": "*" } }, "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg=="], + + "@types/d3-delaunay": ["@types/d3-delaunay@6.0.4", "", {}, "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw=="], + + "@types/d3-dispatch": ["@types/d3-dispatch@3.0.7", "", {}, "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA=="], + + "@types/d3-drag": ["@types/d3-drag@3.0.7", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ=="], + + "@types/d3-dsv": ["@types/d3-dsv@3.0.7", "", {}, "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g=="], + + "@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="], + + "@types/d3-fetch": ["@types/d3-fetch@3.0.7", "", { "dependencies": { "@types/d3-dsv": "*" } }, "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA=="], + + "@types/d3-force": ["@types/d3-force@3.0.10", "", {}, "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw=="], + + "@types/d3-format": ["@types/d3-format@3.0.4", "", {}, "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g=="], + + "@types/d3-geo": ["@types/d3-geo@3.1.1", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-65Emv9fQiQQqphLlRkuQ5ypPsOmWPhtBGCMv61JDPEPMvsx+gzhGf74yw1a78xFKPj6zw4AgQICJoQv0vK9M2w=="], + + "@types/d3-hierarchy": ["@types/d3-hierarchy@3.1.7", "", {}, "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg=="], + + "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="], + + "@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="], + + "@types/d3-polygon": ["@types/d3-polygon@3.0.2", "", {}, "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA=="], + + "@types/d3-quadtree": ["@types/d3-quadtree@3.0.6", "", {}, "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg=="], + + "@types/d3-random": ["@types/d3-random@3.0.4", "", {}, "sha512-UHYId5WTCx4L4YNel7NU00XUXXgvgpgZOvp10PuvsQENjMDXhh2RyFc0KBjO7B45ne4Ha1yVH7ii0vnzKkuzWA=="], + + "@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="], + + "@types/d3-scale-chromatic": ["@types/d3-scale-chromatic@3.1.0", "", {}, "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ=="], + + "@types/d3-selection": ["@types/d3-selection@3.0.12", "", {}, "sha512-Qe/KWYhEiIIxGs7HrAAjMfShxKldx19SJtr5zu53f3afPsdZNz7HHtdTLXo/kqeiWNXVycI24kSnfzBYkTzpgw=="], + + "@types/d3-shape": ["@types/d3-shape@3.2.0", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-kVd74ta9eof3eJOvbNd1vGKS/XERRyQbT26Og63hIsvDO84cjD5gEOhsXf26w3FSoNlPVz84DOFcKv/oou+fMw=="], + + "@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="], + + "@types/d3-time-format": ["@types/d3-time-format@4.0.3", "", {}, "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg=="], + + "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="], + + "@types/d3-transition": ["@types/d3-transition@3.0.9", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg=="], + + "@types/d3-zoom": ["@types/d3-zoom@3.0.9", "", { "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, "sha512-0sE1406XBYJGiqD3AusTl9ZqC//2mIXix51tbom25gDCA8ri4xnSZg28CaSE8Srl6FClqABUKfsn0qghgdepMA=="], + + "@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="], + "@types/hast": ["@types/hast@3.0.5", "", { "dependencies": { "@types/unist": "*" } }, "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g=="], "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], @@ -65,10 +150,14 @@ "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], "@ungap/structured-clone": ["@ungap/structured-clone@1.3.3", "", {}, "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg=="], + "@upsetjs/venn.js": ["@upsetjs/venn.js@2.0.0", "", { "optionalDependencies": { "d3-selection": "^3.0.0", "d3-transition": "^3.0.1" } }, "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw=="], + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], "bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], @@ -79,30 +168,110 @@ "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], + "chevrotain": ["chevrotain@13.2.0", "", { "dependencies": { "@chevrotain/cst-dts-gen": "13.2.0", "@chevrotain/gast": "13.2.0", "@chevrotain/regexp-to-ast": "13.2.0", "@chevrotain/types": "13.2.0", "@chevrotain/utils": "13.2.0" } }, "sha512-GTouk5YzgBxlTPCaDn5q0q2EJKkrYmnPF+HmF8v0Qo4r3iyPG7HJivkN1wthCXZcm3sIcZFpGTRovLPNdJNRCA=="], + "citty": ["citty@0.2.2", "", {}, "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w=="], "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], + "commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], + "cose-base": ["cose-base@2.2.0", "", { "dependencies": { "layout-base": "^2.0.0" } }, "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g=="], "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], "cytoscape": ["cytoscape@3.33.4", "", {}, "sha512-HIN5Pmd9MrX9BkV7tDwnOcEJCSFvCpc8X97h3f508J6I5FsqAY65wKOCvgH2CuP42CaahWaz4tuh32SOOIH7ww=="], + "cytoscape-cose-bilkent": ["cytoscape-cose-bilkent@4.1.0", "", { "dependencies": { "cose-base": "^1.0.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ=="], + "cytoscape-elk": ["cytoscape-elk@2.3.0", "", { "dependencies": { "elkjs": "^0.9.3" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-1h2ZmPOy5HD2+mrfF3P2ICxfnDyPCWg/xLVs7fIjTOzdQu51ydrMtm6Sb7KnhFwLBzhGIVYI2Gbns0njggBarQ=="], "cytoscape-fcose": ["cytoscape-fcose@2.2.0", "", { "dependencies": { "cose-base": "^2.2.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ=="], "cytoscape-navigator": ["cytoscape-navigator@2.0.2", "", { "peerDependencies": { "cytoscape": "^2.6.0 || ^3.0.0" } }, "sha512-TZFBLFWEMW858UOt4rzusOjtDj7YT5vNx2uCwpUuicUYbaWCHHcUROBZWO+hiuSPWpVhvGLFlOq3NBcAVYOAgw=="], + "d3": ["d3@7.9.0", "", { "dependencies": { "d3-array": "3", "d3-axis": "3", "d3-brush": "3", "d3-chord": "3", "d3-color": "3", "d3-contour": "4", "d3-delaunay": "6", "d3-dispatch": "3", "d3-drag": "3", "d3-dsv": "3", "d3-ease": "3", "d3-fetch": "3", "d3-force": "3", "d3-format": "3", "d3-geo": "3", "d3-hierarchy": "3", "d3-interpolate": "3", "d3-path": "3", "d3-polygon": "3", "d3-quadtree": "3", "d3-random": "3", "d3-scale": "4", "d3-scale-chromatic": "3", "d3-selection": "3", "d3-shape": "3", "d3-time": "3", "d3-time-format": "4", "d3-timer": "3", "d3-transition": "3", "d3-zoom": "3" } }, "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA=="], + + "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], + + "d3-axis": ["d3-axis@3.0.0", "", {}, "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw=="], + + "d3-brush": ["d3-brush@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "3", "d3-transition": "3" } }, "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ=="], + + "d3-chord": ["d3-chord@3.0.1", "", { "dependencies": { "d3-path": "1 - 3" } }, "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g=="], + + "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], + + "d3-contour": ["d3-contour@4.0.2", "", { "dependencies": { "d3-array": "^3.2.0" } }, "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA=="], + + "d3-delaunay": ["d3-delaunay@6.0.4", "", { "dependencies": { "delaunator": "5" } }, "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A=="], + + "d3-dispatch": ["d3-dispatch@3.0.1", "", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="], + + "d3-drag": ["d3-drag@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-selection": "3" } }, "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg=="], + + "d3-dsv": ["d3-dsv@3.0.1", "", { "dependencies": { "commander": "7", "iconv-lite": "0.6", "rw": "1" }, "bin": { "csv2json": "bin/dsv2json.js", "csv2tsv": "bin/dsv2dsv.js", "dsv2dsv": "bin/dsv2dsv.js", "dsv2json": "bin/dsv2json.js", "json2csv": "bin/json2dsv.js", "json2dsv": "bin/json2dsv.js", "json2tsv": "bin/json2dsv.js", "tsv2csv": "bin/dsv2dsv.js", "tsv2json": "bin/dsv2json.js" } }, "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q=="], + + "d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="], + + "d3-fetch": ["d3-fetch@3.0.1", "", { "dependencies": { "d3-dsv": "1 - 3" } }, "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw=="], + + "d3-force": ["d3-force@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-quadtree": "1 - 3", "d3-timer": "1 - 3" } }, "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg=="], + + "d3-format": ["d3-format@3.1.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="], + + "d3-geo": ["d3-geo@3.1.1", "", { "dependencies": { "d3-array": "2.5.0 - 3" } }, "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q=="], + + "d3-hierarchy": ["d3-hierarchy@3.1.2", "", {}, "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA=="], + + "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], + + "d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="], + + "d3-polygon": ["d3-polygon@3.0.1", "", {}, "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg=="], + + "d3-quadtree": ["d3-quadtree@3.0.1", "", {}, "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw=="], + + "d3-random": ["d3-random@3.0.1", "", {}, "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ=="], + + "d3-sankey": ["d3-sankey@0.12.3", "", { "dependencies": { "d3-array": "1 - 2", "d3-shape": "^1.2.0" } }, "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ=="], + + "d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="], + + "d3-scale-chromatic": ["d3-scale-chromatic@3.1.0", "", { "dependencies": { "d3-color": "1 - 3", "d3-interpolate": "1 - 3" } }, "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ=="], + + "d3-selection": ["d3-selection@3.0.0", "", {}, "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ=="], + + "d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="], + + "d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="], + + "d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="], + + "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], + + "d3-transition": ["d3-transition@3.0.1", "", { "dependencies": { "d3-color": "1 - 3", "d3-dispatch": "1 - 3", "d3-ease": "1 - 3", "d3-interpolate": "1 - 3", "d3-timer": "1 - 3" }, "peerDependencies": { "d3-selection": "2 - 3" } }, "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w=="], + + "d3-zoom": ["d3-zoom@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "2 - 3", "d3-transition": "2 - 3" } }, "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw=="], + + "dagre-d3-es": ["dagre-d3-es@7.0.14", "", { "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" } }, "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg=="], + + "dayjs": ["dayjs@1.11.23", "", {}, "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ=="], + + "delaunator": ["delaunator@5.1.0", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ=="], + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + "dompurify": ["dompurify@3.4.17", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-B/pSlYVHQ58kqISD/yDjuJ1yfzI34m1gF4lAod2bwSRhiN4K5Cl3H8Af7hqB2PmTL911tdEudnnXn5FnhRoz0w=="], + "elkjs": ["elkjs@0.11.1", "", {}, "sha512-zxxR9k+rx5ktMwT/FwyLdPCrq7xN6e4VGGHH8hA01vVYKjTFik7nHOxBnAYtrgYUB1RpAiLvA1/U2YraWxyKKg=="], "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + "es-toolkit": ["es-toolkit@1.53.0", "", {}, "sha512-3+AYXp9fmJ/UhHBMYj2/U+RaUouMD90SzoItyDOB2Za7W6dB7fQApFGnZPl2MF6K8/Tv1koh1JyirSx/kju6Pw=="], + "fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="], "fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="], @@ -111,22 +280,40 @@ "fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + "hachure-fill": ["hachure-fill@0.5.2", "", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="], + "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], + "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + + "import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="], + + "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], + + "katex": ["katex@0.18.11", "", { "dependencies": { "commander": "^15.0.0" }, "bin": { "katex": "cli.js" } }, "sha512-yvyz/2VMKqoho/35Hat5w37wIEdWJ5ElVMidTAlWrxr5uW4aBGsEpveTmKh18/zw08v/7lGswCPon4OhqmqrTA=="], + + "khroma": ["khroma@2.1.0", "", {}, "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="], + "layout-base": ["layout-base@2.0.1", "", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="], "linkify-it": ["linkify-it@5.0.1", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg=="], + "lodash-es": ["lodash-es@4.18.1", "", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="], + "markdown-it": ["markdown-it@14.2.0", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.1", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ=="], + "marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="], + "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="], "mdurl": ["mdurl@2.0.0", "", {}, "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="], + "mermaid": ["mermaid@12.2.0", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.2", "@iconify/utils": "^3.0.2", "@mermaid-js/parser": "^2.0.1", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", "chevrotain": "~13.2.0", "cytoscape": "^3.34.0", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", "dayjs": "^1.11.21", "dompurify": "^3.4.12", "elkjs": "^0.9.3", "es-toolkit": "^1.45.1", "katex": "^0.18.0", "khroma": "^2.1.0", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" } }, "sha512-w7nr/SFrLO7BaVTs3dQTvYSV6nYSBj0Yy9DjBhsIJtAcHqRzXSq4Pg32ujHZ/AdWsAMNEvvhbm5dt9E2tOiHzw=="], + "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], @@ -141,10 +328,18 @@ "oniguruma-to-es": ["oniguruma-to-es@4.3.6", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="], + "package-manager-detector": ["package-manager-detector@1.9.0", "", {}, "sha512-zAMfbta9nPA8AKkA4WdmNAT8R8s6Dk0/xwPsqNnRidMFcLo1YkLuPDTgs2f64XdzS8vbLNnoJ+uUIkgbz7oRHA=="], + + "path-data-parser": ["path-data-parser@0.1.0", "", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="], + "playwright": ["playwright@1.60.0", "", { "dependencies": { "playwright-core": "1.60.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA=="], "playwright-core": ["playwright-core@1.60.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA=="], + "points-on-curve": ["points-on-curve@0.2.0", "", {}, "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="], + + "points-on-path": ["points-on-path@0.2.1", "", { "dependencies": { "path-data-parser": "0.1.0", "points-on-curve": "0.2.0" } }, "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g=="], + "property-information": ["property-information@7.2.0", "", {}, "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg=="], "punycode.js": ["punycode.js@2.3.1", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="], @@ -159,6 +354,14 @@ "regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="], + "robust-predicates": ["robust-predicates@3.0.3", "", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="], + + "roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="], + + "rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], "shiki": ["shiki@4.4.3", "", { "dependencies": { "@shikijs/core": "4.4.3", "@shikijs/engine-javascript": "4.4.3", "@shikijs/engine-oniguruma": "4.4.3", "@shikijs/langs": "4.4.3", "@shikijs/themes": "4.4.3", "@shikijs/types": "4.4.3", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.5" } }, "sha512-Mb/GvXPHBAXdgGIcnfU5L3ldpn1XcxrGkPHwqgRx17/I2XRfqlFKk2vGkHWINn1kdXvzJZeuO3is6I9KLPFm0g=="], @@ -169,8 +372,14 @@ "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], + "stylis": ["stylis@4.4.0", "", {}, "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA=="], + + "tinyexec": ["tinyexec@1.3.1", "", {}, "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA=="], + "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], + "ts-dedent": ["ts-dedent@2.3.0", "", {}, "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg=="], + "uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="], "undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], @@ -185,6 +394,8 @@ "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], + "uuid": ["uuid@14.0.3", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-Sc5u1VVln93y1Xnz4R+CunOZc7R9LUcTmhj7Rc7Eg3HVcf7rVn2FOoa812fe0W8p3jD7xeoGSYbfqCGGH2B6xw=="], + "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], @@ -197,6 +408,24 @@ "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + "cytoscape-cose-bilkent/cose-base": ["cose-base@1.0.3", "", { "dependencies": { "layout-base": "^1.0.0" } }, "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg=="], + "cytoscape-elk/elkjs": ["elkjs@0.9.3", "", {}, "sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ=="], + + "d3-dsv/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], + + "d3-sankey/d3-array": ["d3-array@2.12.1", "", { "dependencies": { "internmap": "^1.0.0" } }, "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ=="], + + "d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="], + + "mermaid/cytoscape": ["cytoscape@3.34.4", "", {}, "sha512-msY99tMY8iWTg+sIPGfSqDcPNOt4xcj+v/K/7qUFaCUxxlqnMkUYLBjRUzn9W5CU/R2hrmtJa5+UtovrEM2ctg=="], + + "mermaid/elkjs": ["elkjs@0.9.3", "", {}, "sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ=="], + + "cytoscape-cose-bilkent/cose-base/layout-base": ["layout-base@1.0.2", "", {}, "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="], + + "d3-sankey/d3-array/internmap": ["internmap@1.0.1", "", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="], + + "d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="], } } diff --git a/docs/design/viewer-qol.md b/docs/design/viewer-qol.md new file mode 100644 index 0000000..9dc8e01 --- /dev/null +++ b/docs/design/viewer-qol.md @@ -0,0 +1,97 @@ +# Viewer quality of life + + +## Problem + + +Requests from the model owner after daily use of the viewer: + +1. Rendered markdown looks compressed. The global reset zeroes every margin, and the four wrapper classes (`.doc-body`, `.dict-entity-body`, `.flow-node-body`, `.dict-group-desc`) each restore a different, partial set of rules. `.doc-body` has no rule for `h1`, `h3`–`h6`, `pre`, `blockquote` or `hr`, and its `h2` borrows the uppercase section-label style. +2. View preferences are scattered across seven `ignatius-*` localStorage keys with no place to change them, and the open rule for example tables (`rows <= 3`) cannot be overridden. +3. The dictionary sidebar opens only from the FAB menu ("Toggle sidebar"). There is no affordance on screen and no close control in the panel. +4. Bodies cannot carry diagrams. A process body that explains how the process transforms its inputs has only prose and tables to do it with. +5. `ignatius export` writes the model with default view state. What the user arranged (hidden entities, positions, layout mode, lens, theme) is lost. +6. The dictionary scroll position resets when the model reloads over SSE, when the view is shown again after a detour, and on page refresh. The sidebar scroll position has the same problem. +7. `Modal` focuses its close button on mount but does not keep focus inside, so Tab walks into the page behind it. +8. Reviewing the branch, the requester asked for keys for the previous layout, layout reset, copy link and hiding a whole group, and a Hide / Show button per group in the Groups dialog. + + +## Goals / Non-goals + + +Goals: + +- Each item fixed in the existing structure: `Modal.tsx` for every modal, one shared markdown class for every markdown site, one settings store. +- Mermaid diagrams render in every markdown body, themed from the model's palette by default and overridable in `ignatius.yml`. +- Models without a mermaid fence pay nothing for Mermaid: no download in live mode, no bytes in an export. +- The modeling skill teaches logic flows inside a process body as a complement to the DFD, not a replacement. + +Non-goals: + +- Migrating the existing `ignatius-*` localStorage keys into the settings store. They keep working as they are. +- Per-column hiding. The only hide the viewer has is graph entity hide; export carries that. +- Exporting from an already-exported static file. Export needs the server's bundle and model. +- Mermaid in the DFD canvas or the graph. Diagrams live in markdown bodies only. + + +## Approach + + +### Markdown typography + +Every markdown site adds the class `md` next to its existing wrapper class. One `.md` block in `styles.css` owns vertical rhythm: line height, paragraph and list spacing, a heading scale, `pre`, `blockquote`, `hr`, `img`, tables, and first/last-child margin trimming. Wrapper classes keep only what is specific to their place (font size, border, colour). Per-wrapper `p`/`ul`/`h*` rules that `.md` replaces are removed so one rule decides each element. + +### Settings + +`ignatius-settings` holds one JSON object. A module-level store in `useSettings.ts` reads it once when the module loads and merges it over defaults (unknown or wrong-typed fields fall back to the default). `useSettings()` reads the store through `useSyncExternalStore`; an update merges, writes, and notifies subscribers. No provider is needed, so the flow dialogs, which render in their own React root, see the same settings as the app shell. A Settings modal, opened from a FAB item present in every view, lists the toggles. First setting: `alwaysOpenExamples` (default `false`): when on, every example `
` renders open regardless of row count. The theme lives here too, under Appearance, so the header loses its theme button and the viewer has one place for preferences; it keeps its own `ignatius-theme` key. + +### Sidebar tab + +A tab fixed to the right edge, visible in the Dictionary view while the sidebar is closed. At rest only a narrow lip shows: an accent edge with a grip. The reader sees something is there, with no clipped label. On hover or keyboard focus the tab slides fully in and the icon and label appear. It holds a three-line icon over the word "Sidebar" and opens the sidebar on click. The panel gains a close button in its header. Open state persists in `ignatius-dict-nav-open`. Like the panel, the tab is hidden at `max-width: 768px` and in print. + +### Scroll memory + +`.dict-view` and `.dict-nav-panel` save `scrollTop` to sessionStorage on scroll (one write per animation frame). They restore it on mount, when the Dictionary view is shown again, and after a model reload replaces the content. sessionStorage, because the ask is "across refreshes" of one tab; a second tab on another model starts at the top. + +Mermaid diagrams render asynchronously and keep growing the page after a restore, so the restore holds: it re-applies the saved position as content grows until the user scrolls, a scroll it did not make moves the container, or about 2 s pass. A process deep link wins by effect order: App's process scroll runs in a `requestAnimationFrame` registered from a passive effect, after the Dictionary's layout-effect restore, and its scroll ends the hold. + +### Mermaid + +```mermaid +flowchart LR + F["```mermaid fence"] --> P["markdown-it fence rule
(server / CLI)"] + P --> D["div.mermaid-diagram
escaped source"] + D --> O["MutationObserver
at app root"] + O --> L{"mermaid loaded?"} + L -->|no| S["load mermaid.js
(live: script src; export: inlined)"] + L -->|yes| R["mermaid.render"] + S --> R + R --> V["inline SVG"] +``` + +- **Parse side.** Both markdown-it instances (`src/model/parse.ts`, `src/flows/flow-markdown.ts`) emit a ```` ```mermaid ```` fence as `
` holding the HTML-escaped source. Every body type gets it with no per-site work. +- **Browser side.** One `MutationObserver` on the app root finds unrendered `.mermaid-diagram` nodes in any view or modal and renders them. Mermaid is initialised with `startOnLoad: false`, `securityLevel: 'strict'`, `theme: 'base'`, `look: 'classic'`, `flowchart.wrappingWidth: 200` (Mermaid 12's 120 px splits short bold labels), and the resolved `themeVariables` and `themeCSS` for the current mode; a theme-mode switch re-renders. A render error replaces the diagram with the source in a `pre` plus the error message, styled as a warning. +- **Bundle.** Mermaid minified is 5.3 MB against the app's 4.5 MB. It builds as a separate `dist/static/mermaid.js` that sets `window.__IGNATIUS_MERMAID__`. Live mode loads it with a ` in the JS bundle to prevent premature `; + // Every rendered body (entity, group, flow node, cluster, change note) is in the injected JSON. + if (injection.includes('class=\\"mermaid-diagram\\"')) { + injection += ``; + } + + if (opts.viewState) { + injection += ``; + } + // Inline the CSS (function replacement: $ in cssContent must not be interpolated). let html = htmlTemplate.replace( /]*href="[^"]*index-[^"]+\.css"[^>]*>/, diff --git a/src/generators/embedded-bundle.ts b/src/generators/embedded-bundle.ts index 2855271..c42096f 100644 --- a/src/generators/embedded-bundle.ts +++ b/src/generators/embedded-bundle.ts @@ -9,7 +9,8 @@ * `bun build --compile` can resolve them at compile time. * * The stable names (index.html, index.js, index.css) are produced by the - * build:bundle script, which copies the hashed outputs to fixed names. + * build:bundle script, which copies the hashed outputs to fixed names; + * mermaid.js is built under its stable name directly. */ import { Glob } from 'bun'; @@ -18,9 +19,12 @@ import { join } from 'node:path'; import bundleHtmlPath from '../../dist/static/index.html' with { type: 'file' }; import bundleJsPath from '../../dist/static/index.js' with { type: 'file' }; import bundleCssPath from '../../dist/static/index.css' with { type: 'file' }; +import mermaidJsPath from '../../dist/static/mermaid.js' with { type: 'file' }; + +export { mermaidJsPath }; /** - * The raw content of the three compiled bundle assets. + * The raw content of the compiled bundle assets. * * WHY here (not graph.ts): this is the canonical producer of BundleContent β€” * the embedded binary reads it via loadEmbeddedBundle; the disk-read path @@ -34,6 +38,8 @@ export type BundleContent = { cssContent: string; /** Raw JS content (the full bundled module, unescaped). */ jsContent: string; + /** Raw Mermaid sidecar (dist/static/mermaid.js), inlined only into exports that have a diagram. */ + mermaidJs: string; }; /** @@ -81,8 +87,9 @@ export async function loadBundleFromDir(bundleDir: string): Promise { const htmlFile = Bun.file(bundleHtmlPath); const jsFile = Bun.file(bundleJsPath); const cssFile = Bun.file(bundleCssPath); + const mermaidFile = Bun.file(mermaidJsPath); - const [htmlExists, jsExists, cssExists] = await Promise.all([ - htmlFile.exists(), jsFile.exists(), cssFile.exists(), + const [htmlExists, jsExists, cssExists, mermaidExists] = await Promise.all([ + htmlFile.exists(), jsFile.exists(), cssFile.exists(), mermaidFile.exists(), ]); - if (!htmlExists || !jsExists || !cssExists) { + if (!htmlExists || !jsExists || !cssExists || !mermaidExists) { throw new Error( - 'Bundle missing β€” dist/static/{index.html,index.js,index.css} not found.\n' + + 'Bundle missing β€” dist/static/{index.html,index.js,index.css,mermaid.js} not found.\n' + 'Run: bun run build:bundle\n' + 'Or: bun run build:cli (full build with stable names + compile)', ); } - const [htmlTemplate, jsContent, cssContent] = await Promise.all([ - htmlFile.text(), jsFile.text(), cssFile.text(), + const [htmlTemplate, jsContent, cssContent, mermaidJs] = await Promise.all([ + htmlFile.text(), jsFile.text(), cssFile.text(), mermaidFile.text(), ]); - return { htmlTemplate, cssContent, jsContent }; + return { htmlTemplate, cssContent, jsContent, mermaidJs }; } diff --git a/src/generators/export-model.ts b/src/generators/export-model.ts new file mode 100644 index 0000000..de73dc0 --- /dev/null +++ b/src/generators/export-model.ts @@ -0,0 +1,82 @@ +import { existsSync } from 'node:fs'; +import { join } from 'node:path'; +import { parseModels, type Model } from '../model/parse'; +import { validateModel, type EntityError, type GlobalError } from '../model/validate'; +import { parseFlows, type FlowModel } from '../flows/flow-parse'; +import { isRecord } from '../flows/flow-markdown'; +import { validateFlows, type FlowError } from '../flows/flow-validate'; +import { readReview } from '../model/change-store'; +import { layoutFingerprint } from '../model/layout-fingerprint'; +import { buildFlowLayoutKeys } from '../flows/flow-fingerprint'; +import { FLOW_LAYOUT_STORAGE_KEY, GRAPH_LAYOUT_STORAGE_KEY } from '../app/logic/storage-keys'; +import { generateApp, type GenerateAppOpts, type ViewState } from './app'; +import type { BundleContent } from './embedded-bundle'; + +export type ModelExport = { + html: string; + model: Model; + globalErrors: GlobalError[]; + entityErrors: EntityError[]; + flowErrors: FlowError[]; +}; + +function keepEntries(serialized: string, keep: (entryKey: string) => boolean): string | null { + let parsed: unknown; + try { + parsed = JSON.parse(serialized); + } catch { + return null; + } + if (!isRecord(parsed)) return null; + const kept = Object.entries(parsed).filter(([entryKey]) => keep(entryKey)); + return kept.length > 0 ? JSON.stringify(Object.fromEntries(kept)) : null; +} + +/** Reduce the layout stores, shared by every model served on the same origin, to this model's entries. + * Flow entries are keyed `::`. */ +export function scopeViewState(viewState: ViewState, layoutKey: string, flowLayoutKeys: string[]): ViewState { + const ownFlowKeys = new Set(flowLayoutKeys); + const keepers: Record boolean> = { + [GRAPH_LAYOUT_STORAGE_KEY]: entryKey => entryKey === layoutKey, + [FLOW_LAYOUT_STORAGE_KEY]: entryKey => ownFlowKeys.has(entryKey.split('::')[0] ?? ''), + }; + const storage: Record = {}; + for (const [key, value] of Object.entries(viewState.storage)) { + const keep = keepers[key]; + const scoped = keep ? keepEntries(value, keep) : value; + if (scoped !== null) storage[key] = scoped; + } + return { storage, hash: viewState.hash }; +} + +/** Parse and validate the model root at `dir`, with its flows when present, and render the single-file export. */ +export async function exportModel( + dir: string, + bundle: BundleContent, + opts: Omit = {}, +): Promise { + const { model, globalErrors: parseGlobalErrors } = await parseModels(dir); + const validation = validateModel(model); + const globalErrors = [...parseGlobalErrors, ...validation.globalErrors]; + + let flowModel: FlowModel | null = null; + let flowErrors: FlowError[] = []; + if (existsSync(join(dir, 'flows'))) { + const parsed = await parseFlows(dir); + globalErrors.push(...parsed.globalErrors); + flowModel = parsed.flowModel; + flowErrors = validateFlows(flowModel, model, model._meta?.flowRules).flowErrors; + } + + const review = await readReview(dir, model).catch((err: unknown) => { + console.error(`ignatius: could not build the change review: ${err instanceof Error ? err.message : String(err)}`); + return null; + }); + const viewState = opts.viewState && scopeViewState( + opts.viewState, + layoutFingerprint(model), + flowModel ? Object.values(buildFlowLayoutKeys(flowModel)) : [], + ); + const html = await generateApp(model, flowModel, bundle, { ...opts, review, viewState }); + return { html, model, globalErrors, entityErrors: validation.entityErrors, flowErrors }; +} diff --git a/src/model/markdown-mermaid.ts b/src/model/markdown-mermaid.ts new file mode 100644 index 0000000..463126d --- /dev/null +++ b/src/model/markdown-mermaid.ts @@ -0,0 +1,23 @@ +interface FenceToken { + info: string; + content: string; +} + +type RenderRule = (tokens: FenceToken[], idx: number, options: unknown, env: unknown, self: unknown) => string; + +interface MarkdownItLike { + renderer: { rules: { fence?: RenderRule } }; + utils: { escapeHtml(str: string): string }; +} + +/** markdown-it plugin: render `mermaid` fences as diagram placeholders, every other fence as before. */ +export function mermaidFencePlugin(md: MarkdownItLike): void { + const fallback = md.renderer.rules.fence; + md.renderer.rules.fence = (tokens, idx, options, env, self) => { + const token = tokens[idx]; + if (token && token.info.trim().split(/\s+/)[0] === 'mermaid') { + return `
${md.utils.escapeHtml(token.content)}
\n`; + } + return fallback ? fallback(tokens, idx, options, env, self) : ''; + }; +} diff --git a/src/model/parse.ts b/src/model/parse.ts index 5d6a135..27170d5 100644 --- a/src/model/parse.ts +++ b/src/model/parse.ts @@ -6,10 +6,12 @@ import { defaultTheme, mergeTheme, type ThemeConfig } from '../theme/theme-defau import { defaultBranding, mergeBranding, inlineBrandingLogos, type Branding } from '../theme/branding-defaults'; import { wikiLinkPlugin, type WikiLinkEnv } from './wikilink'; import { highlightCodeFence } from './markdown-highlight'; +import { mermaidFencePlugin } from './markdown-mermaid'; import type { GlobalError } from './validate'; const md = new MarkdownIt({ highlight: highlightCodeFence }); md.use(wikiLinkPlugin); +md.use(mermaidFencePlugin); function isRecord(v: unknown): v is Record { return typeof v === 'object' && v !== null; diff --git a/src/model/validate.ts b/src/model/validate.ts index 732d1c8..a397e81 100644 --- a/src/model/validate.ts +++ b/src/model/validate.ts @@ -10,6 +10,7 @@ */ import type { Model, ModelNode, ModelEdge, SubtypeCluster } from './parse'; +import { mermaidKeyMessage } from '../theme/theme-defaults'; // Type-only import from flow-validate. Both directions are import type (erased at runtime) // so there is no runtime circular dependency β€” flow-validate.ts imports type RuleId from here. import type { FlowError } from '../flows/flow-validate'; @@ -29,6 +30,7 @@ export type RuleId = | 'config.index_file_ext' | 'config.index_file_path' | 'config.index_file_entity' + | 'config.mermaid_theme_invalid' // router index drift | 'index.stale' | 'index.orphaned' @@ -194,6 +196,11 @@ export const RULES: Record = { explanation: 'A file under `data/` matches the configured `index_file` name but declares an `entity` field. Its frontmatter is read to detect this, and once detected the entity is dropped from the model. Rename the entity file or change `index_file` in `ignatius.yml`.', class: 'B', }, + 'config.mermaid_theme_invalid': { + title: 'Invalid Mermaid theme value', + explanation: 'A part of `ignatius.yml` that reaches the Mermaid diagram stylesheet is invalid. `theme.mermaid`, `theme.mermaid.dark` and `theme.mermaid.light` must be mappings. A value in `dark` or `light` must be a number or a string made only of letters, digits, spaces and `# % ( ) , . ; - "`. `stroke_width` must be 0-8, `radius` 0-32, `shadow` a boolean, and `node_fills` 1-12 `#rrggbb` colors. Other keys under `theme.mermaid` are unknown. The `theme.dark`/`theme.light` `background`, `surface`, `text` and `textMuted` colors must use the same characters as mode-block values. Values reach the stylesheet as written, so anything else could inject CSS. The part is ignored and diagrams use the default for it.', + class: 'A', + }, 'index.stale': { title: 'Router digest is out of date', explanation: 'A generated router\'s stored digest no longer matches the folder it describes β€” the model changed since `ignatius index` last ran. Run `ignatius index` to regenerate.', @@ -576,6 +583,15 @@ export function validateModel(model: Model): ValidationResult { ); } + for (const key of model.theme.invalidMermaidKeys ?? []) { + entityErrors.push({ + ruleId: 'config.mermaid_theme_invalid', + entityId: 'ignatius.yml', + severity: 'warning', + message: mermaidKeyMessage(key), + }); + } + // Edge rules β€” Class B (unknown_target) strips edge from cleanedModel const strippedEdgeTargets = new Set(); // track stripped edge signatures const cleanedEdges: typeof model.edges = []; diff --git a/src/router/agents.ts b/src/router/agents.ts index 77eb758..d7de45f 100644 --- a/src/router/agents.ts +++ b/src/router/agents.ts @@ -111,7 +111,7 @@ export function buildClaudeShim(): string { ].join('\n'); } -function slugify(name: string): string { +export function slugify(name: string): string { return name .toLowerCase() .replace(/[^a-z0-9]+/g, '-') diff --git a/src/server/server.ts b/src/server/server.ts index 5b92aa2..e545ec8 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -5,7 +5,11 @@ import { layoutFingerprint } from '../model/layout-fingerprint'; import { parseFlows } from '../flows/flow-parse'; import { validateFlows } from '../flows/flow-validate'; import { buildFlowLayoutKeys } from '../flows/flow-fingerprint'; +import { isRecord } from '../flows/flow-markdown'; import { approve, baselinePath, buildReview, ensureBaseline } from '../model/change-store'; +import { exportModel } from '../generators/export-model'; +import type { ViewState } from '../generators/app'; +import { slugify } from '../router/agents'; import { resolve, normalize, isAbsolute, join } from 'path'; import { watch, watchFile, unwatchFile, existsSync, type Stats } from 'fs'; @@ -48,6 +52,36 @@ function isSameOrigin(req: Request): boolean { } } +const MAX_EXPORT_BODY_BYTES = 5 * 1024 * 1024; + +// Counts bytes as they arrive, so a chunked body with no content-length cannot be buffered past the cap. +async function readTextCapped(req: Request, maxBytes: number): Promise { + if (req.body === null) return ''; + const reader = req.body.getReader(); + const decoder = new TextDecoder(); + let bytes = 0; + let text = ''; + for (;;) { + const { done, value } = await reader.read(); + if (done) return text + decoder.decode(); + bytes += value.byteLength; + if (bytes > maxBytes) { + await reader.cancel(); + return null; + } + text += decoder.decode(value, { stream: true }); + } +} + +// The body comes from the page's localStorage, so anything outside the viewer's own keys is dropped. +function parseViewState(body: unknown): ViewState | null { + if (!isRecord(body) || !isRecord(body.storage)) return null; + const entries = Object.entries(body.storage).filter( + (entry): entry is [string, string] => entry[0].startsWith('ignatius-') && typeof entry[1] === 'string', + ); + return { storage: Object.fromEntries(entries), hash: typeof body.hash === 'string' ? body.hash : '' }; +} + export type ServeHandle = { server: ReturnType; stop: (force?: boolean) => void; @@ -120,6 +154,42 @@ export function serveCommand(modelsDir: string, opts: { port?: number } = {}): S broadcast('model-changed'); return Response.json(counts); }, + '/api/export': async (req: Request) => { + if (req.method !== 'POST') { + return new Response('Method Not Allowed', { status: 405, headers: { Allow: 'POST' } }); + } + if (!isSameOrigin(req) || !isLocalHost(req)) return new Response('Forbidden', { status: 403 }); + const text = await readTextCapped(req, MAX_EXPORT_BODY_BYTES); + if (text === null) return new Response('Export request over 5 MB', { status: 413 }); + let body: unknown; + try { + body = JSON.parse(text); + } catch { + return new Response('Export request is not valid JSON', { status: 400 }); + } + const viewState = parseViewState(body); + if (viewState === null) return new Response('Export request needs { storage, hash }', { status: 400 }); + + let bundle: import('../generators/embedded-bundle').BundleContent; + try { + const { loadEmbeddedBundle } = await import('../generators/embedded-bundle'); + bundle = await loadEmbeddedBundle(); + } catch { + return new Response('Export bundle not built: run bun run build:bundle', { status: 500 }); + } + const themeMode = viewState.storage['ignatius-theme'] === 'light' ? 'light' : 'dark'; + try { + const { html, model } = await exportModel(modelsDir, bundle, { themeMode, viewState }); + return new Response(html, { + headers: { + 'Content-Type': 'text/html; charset=utf-8', + 'Content-Disposition': `attachment; filename="${slugify(model._meta?.name ?? '')}.html"`, + }, + }); + } catch (err) { + return new Response(`Export failed: ${err instanceof Error ? err.message : String(err)}`, { status: 500 }); + } + }, '/flow': () => Response.redirect('/#view=flow', 302), '/api/flow': async () => { // Guard: if no flows/ directory, return an empty-state payload (200), not 500. @@ -151,6 +221,15 @@ export function serveCommand(modelsDir: string, opts: { port?: number } = {}): S headers: { Location: '/#view=dict' }, }); }, + '/static/mermaid.js': async () => { + // Imported on demand so the server still starts from source before dist/static is built. + try { + const { mermaidJsPath } = await import('../generators/embedded-bundle'); + return new Response(Bun.file(mermaidJsPath), { headers: { 'Content-Type': 'text/javascript' } }); + } catch { + return new Response('mermaid.js not built: run bun run build:bundle', { status: 404 }); + } + }, '/api/asset': async (req) => { const url = new URL(req.url); const rawPath = url.searchParams.get('path'); diff --git a/src/theme/mermaid-theme.ts b/src/theme/mermaid-theme.ts new file mode 100644 index 0000000..164e1b6 --- /dev/null +++ b/src/theme/mermaid-theme.ts @@ -0,0 +1,316 @@ +import { blendHex } from '../app/logic/color'; +import { + defaultFlowKinds, + defaultTheme, + DIAGRAM_PALETTE_KEYS, + SAFE_MERMAID_VALUE, + semanticColors, + type ThemeConfig, + type ThemeMode, + type ThemePalette, +} from './theme-defaults'; + +/** `theme.mermaid` in ignatius.yml: node style keys, plus Mermaid `themeVariables` per mode under Mermaid's own key names. */ +export type MermaidThemeConfig = { + stroke_width?: number; + radius?: number; + shadow?: boolean; + node_fills?: string[]; + dark?: Record; + light?: Record; +}; + +export type MermaidThemeVariables = Record>; + +/** What `mermaid.initialize` takes for one theme mode. */ +export type MermaidStyle = { themeVariables: MermaidThemeVariables; themeCSS: string }; + +/** The entity colours at full strength, bright enough for black labels in both modes. */ +export const DEFAULT_MERMAID_FILLS: readonly string[] = [ + semanticColors.dark.independent.fg, + semanticColors.dark.dependent.fg, + semanticColors.dark.subtype.fg, + semanticColors.dark.associative.fg, + defaultFlowKinds.dark.queue.fg, +]; +const DEFAULT_MERMAID_STROKE_WIDTH = 3; +const DEFAULT_MERMAID_RADIUS = 8; + +/** Keep in step with `body` in src/app/styles.css. */ +const BODY_FONT = '-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif'; +const WHITE = '#ffffff'; +const BLACK = '#000000'; +const OUTLINE = BLACK; + +// Every node outline except the start dot and the ER attribute rows, which share the `.node` group. +const NODE_SHAPE = ':is(rect, polygon, path, circle):not(.state-start):not(:is(.row-rect-odd, .row-rect-even, .divider) *)'; + +function relativeLuminance(hex: string): number { + const m = /^#([0-9a-f]{6})$/i.exec(hex.trim()); + const raw = m?.[1]; + if (raw === undefined) return 0.5; + const [r = 0, g = 0, b = 0] = [0, 2, 4].map(i => { + const v = Number.parseInt(raw.slice(i, i + 2), 16) / 255; + return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4; + }); + return 0.2126 * r + 0.7152 * g + 0.0722 * b; +} + +function contrastRatio(a: string, b: string): number { + const la = relativeLuminance(a); + const lb = relativeLuminance(b); + return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05); +} + +function labelOn(fill: string): string { + return contrastRatio(BLACK, fill) >= contrastRatio(WHITE, fill) ? BLACK : WHITE; +} + +/** The one label colour for a slot drawn on any of `fills`: the one whose worst contrast is best. */ +function labelOnAll(fills: readonly string[]): string { + const worst = (ink: string) => Math.min(...fills.map(fill => contrastRatio(ink, fill))); + return worst(BLACK) >= worst(WHITE) ? BLACK : WHITE; +} + +function erRowEven(fills: readonly string[]): string { + return blendHex(WHITE, fills[0] ?? WHITE, 0.12); +} + +function nodeFills(theme: ThemeConfig): readonly string[] { + const fills = theme.mermaid?.node_fills; + return fills && fills.length > 0 ? fills : DEFAULT_MERMAID_FILLS; +} + +// Mermaid writes the palette into the diagram's